diff --git a/Cargo.lock b/Cargo.lock index 9ffe92754..12a3ca94e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,9 +165,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-config" -version = "1.6.2" +version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fcc63c9860579e4cb396239570e979376e70aab79e496621748a09913f8b36" +checksum = "02a18fd934af6ae7ca52410d4548b98eb895aab0f1ea417d168d85db1434a141" dependencies = [ "aws-credential-types", "aws-runtime", @@ -254,9 +254,9 @@ dependencies = [ [[package]] name = "aws-sdk-ec2" -version = "1.124.0" +version = "1.134.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6746a315a5446304942f057e6a072347dad558d23bfbda64c42b9a236f824013" +checksum = "a9a84e95f739e79d157409fa00e41008dabd181022193dabfabc68ddccbd6055" dependencies = [ "aws-credential-types", "aws-runtime", @@ -271,16 +271,15 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sso" -version = "1.65.0" +version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8efec445fb78df585327094fcef4cad895b154b58711e504db7a93c41aa27151" +checksum = "83447efb7179d8e2ad2afb15ceb9c113debbc2ecdf109150e338e2e28b86190b" dependencies = [ "aws-credential-types", "aws-runtime", @@ -294,16 +293,15 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ssooidc" -version = "1.66.0" +version = "1.71.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e49cca619c10e7b002dc8e66928ceed66ab7f56c1a3be86c5437bf2d8d89bba" +checksum = "c5f9bfbbda5e2b9fe330de098f14558ee8b38346408efe9f2e9cee82dc1636a4" dependencies = [ "aws-credential-types", "aws-runtime", @@ -317,16 +315,15 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sts" -version = "1.66.0" +version = "1.71.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7420479eac0a53f776cc8f0d493841ffe58ad9d9783f3947be7265784471b47a" +checksum = "e17b984a66491ec08b4f4097af8911251db79296b3e4a763060b45805746264f" dependencies = [ "aws-credential-types", "aws-runtime", @@ -341,7 +338,6 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] @@ -419,7 +415,7 @@ dependencies = [ "hyper-util", "pin-project-lite", "rustls 0.21.12", - "rustls 0.23.26", + "rustls 0.23.27", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", @@ -651,7 +647,7 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "pin-project-lite", - "rustls 0.23.26", + "rustls 0.23.27", "rustls-pemfile 2.2.0", "rustls-pki-types", "tokio", @@ -795,9 +791,9 @@ dependencies = [ [[package]] name = "bollard" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ccca1260af6a459d75994ad5acc1651bcabcbdbc41467cc9786519ab854c30" +checksum = "af706e9dc793491dd382c99c22fde6e9934433d4cc0d6a4b34eb2cdc57a5c917" dependencies = [ "base64 0.22.1", "bollard-stubs", @@ -828,20 +824,21 @@ dependencies = [ [[package]] name = "bollard-stubs" -version = "1.47.1-rc.27.3.1" +version = "1.48.2-rc.28.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f179cfbddb6e77a5472703d4b30436bff32929c0aa8a9008ecf23d1d3cdd0da" +checksum = "79cdf0fccd5341b38ae0be74b74410bdd5eceeea8876dc149a13edfe57e3b259" dependencies = [ "serde", + "serde_json", "serde_repr", "serde_with", ] [[package]] name = "bson" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af8113ff51309e2779e8785a246c10fb783e8c2452f134d6257fd71cc03ccd6c" +checksum = "7969a9ba84b0ff843813e7249eed1678d9b6607ce5a3b8f0a47af3fcf7978e6e" dependencies = [ "ahash", "base64 0.22.1", @@ -893,7 +890,7 @@ dependencies = [ [[package]] name = "cache" -version = "1.17.5" +version = "1.18.0" dependencies = [ "anyhow", "tokio", @@ -996,9 +993,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.37" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" +checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" dependencies = [ "clap_builder", "clap_derive", @@ -1006,9 +1003,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.37" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" +checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" dependencies = [ "anstream", "anstyle", @@ -1060,7 +1057,7 @@ dependencies = [ [[package]] name = "command" -version = "1.17.5" +version = "1.18.0" dependencies = [ "anyhow", "formatting", @@ -1523,9 +1520,9 @@ dependencies = [ [[package]] name = "english-to-cron" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a13a7d5e0ab3872c3ee478366eae624d89ab953d30276b0eee08169774ceb73" +checksum = "e26fb7377cbec9a94f60428e6e6afbe10c699a14639b4d3d4b67b25c0bbe0806" dependencies = [ "regex", ] @@ -1544,7 +1541,7 @@ dependencies = [ [[package]] name = "environment_file" -version = "1.17.5" +version = "1.18.0" dependencies = [ "thiserror 2.0.12", ] @@ -1624,7 +1621,7 @@ dependencies = [ [[package]] name = "formatting" -version = "1.17.5" +version = "1.18.0" dependencies = [ "serror", ] @@ -1786,7 +1783,7 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "git" -version = "1.17.5" +version = "1.18.0" dependencies = [ "anyhow", "cache", @@ -2160,7 +2157,7 @@ dependencies = [ "http 1.3.1", "hyper 1.6.0", "hyper-util", - "rustls 0.23.26", + "rustls 0.23.27", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", @@ -2523,7 +2520,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "1.17.5" +version = "1.18.0" dependencies = [ "anyhow", "clap", @@ -2539,7 +2536,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "1.17.5" +version = "1.18.0" dependencies = [ "anyhow", "async_timing_util", @@ -2551,6 +2548,7 @@ dependencies = [ "derive_variants", "envy", "futures", + "indexmap 2.9.0", "mongo_indexed", "partial_derive2", "reqwest", @@ -2570,7 +2568,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "1.17.5" +version = "1.18.0" dependencies = [ "anyhow", "arc-swap", @@ -2599,6 +2597,7 @@ dependencies = [ "git", "hex", "hmac", + "indexmap 2.9.0", "jsonwebtoken", "komodo_client", "logger", @@ -2608,7 +2607,6 @@ dependencies = [ "nom_pem", "octorust", "openidconnect", - "ordered_hash_map", "partial_derive2", "periphery_client", "rand 0.9.1", @@ -2616,7 +2614,7 @@ dependencies = [ "reqwest", "resolver_api", "response", - "rustls 0.23.26", + "rustls 0.23.27", "serde", "serde_json", "serde_yaml", @@ -2639,7 +2637,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "1.17.5" +version = "1.18.0" dependencies = [ "anyhow", "async_timing_util", @@ -2667,7 +2665,7 @@ dependencies = [ "resolver_api", "response", "run_command", - "rustls 0.23.26", + "rustls 0.23.27", "serde", "serde_json", "serde_yaml", @@ -2681,6 +2679,21 @@ dependencies = [ "uuid", ] +[[package]] +name = "komodo_util" +version = "1.18.0" +dependencies = [ + "anyhow", + "dotenvy", + "envy", + "futures-util", + "mungos", + "serde", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2757,7 +2770,7 @@ dependencies = [ [[package]] name = "logger" -version = "1.17.5" +version = "1.18.0" dependencies = [ "anyhow", "komodo_client", @@ -3512,13 +3525,13 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "periphery_client" -version = "1.17.5" +version = "1.18.0" dependencies = [ "anyhow", "komodo_client", "reqwest", "resolver_api", - "rustls 0.23.26", + "rustls 0.23.27", "serde", "serde_json", "serde_qs", @@ -3718,7 +3731,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.26", + "rustls 0.23.27", "socket2", "thiserror 2.0.12", "tokio", @@ -3737,7 +3750,7 @@ dependencies = [ "rand 0.9.1", "ring", "rustc-hash 2.1.1", - "rustls 0.23.26", + "rustls 0.23.27", "rustls-pki-types", "slab", "thiserror 2.0.12", @@ -3921,7 +3934,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.26", + "rustls 0.23.27", "rustls-native-certs 0.8.1", "rustls-pemfile 2.2.0", "rustls-pki-types", @@ -4040,7 +4053,7 @@ dependencies = [ [[package]] name = "response" -version = "1.17.5" +version = "1.18.0" dependencies = [ "anyhow", "axum", @@ -4175,16 +4188,16 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.26" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.1", + "rustls-webpki 0.103.3", "subtle", "zeroize", ] @@ -4252,9 +4265,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.1" +version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ "aws-lc-rs", "ring", @@ -4854,9 +4867,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.35.0" +version = "0.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b897c8ea620e181c7955369a31be5f48d9a9121cb59fd33ecef9ff2a34323422" +checksum = "79251336d17c72d9762b8b54be4befe38d2db56fbbc0241396d70f173c39d47a" dependencies = [ "libc", "memchr", @@ -5016,9 +5029,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.2" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", @@ -5059,7 +5072,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.26", + "rustls 0.23.27", "tokio", ] @@ -5083,7 +5096,7 @@ checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" dependencies = [ "futures-util", "log", - "rustls 0.23.26", + "rustls 0.23.27", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", @@ -5225,9 +5238,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" +checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" dependencies = [ "bitflags 2.9.0", "bytes", @@ -5367,7 +5380,7 @@ dependencies = [ "httparse", "log", "rand 0.9.1", - "rustls 0.23.26", + "rustls 0.23.27", "rustls-pki-types", "sha1", "thiserror 2.0.12", @@ -5502,9 +5515,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ "getrandom 0.3.2", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index de8e7ab52..286170620 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "1.17.5" +version = "1.18.0" edition = "2024" authors = ["mbecker20 "] license = "GPL-3.0-or-later" @@ -45,7 +45,7 @@ svi = "1.0.1" # ASYNC reqwest = { version = "0.12.15", default-features = false, features = ["json", "stream", "rustls-tls-native-roots"] } -tokio = { version = "1.44.2", features = ["full"] } +tokio = { version = "1.45.1", features = ["full"] } tokio-util = { version = "0.7.15", features = ["io", "codec"] } tokio-stream = { version = "0.1.17", features = ["sync"] } pin-project-lite = "0.2.16" @@ -56,12 +56,12 @@ arc-swap = "1.7.1" # SERVER tokio-tungstenite = { version = "0.26.2", features = ["rustls-tls-native-roots"] } axum-extra = { version = "0.10.1", features = ["typed-header"] } -tower-http = { version = "0.6.2", features = ["fs", "cors"] } +tower-http = { version = "0.6.4", features = ["fs", "cors"] } axum-server = { version = "0.7.2", features = ["tls-rustls"] } axum = { version = "0.8.4", features = ["ws", "json", "macros"] } # SER/DE -ordered_hash_map = { version = "0.4.0", features = ["serde"] } +indexmap = { version = "2.9.0", features = ["serde"] } serde = { version = "1.0.219", features = ["derive"] } strum = { version = "0.27.1", features = ["derive"] } serde_json = "1.0.140" @@ -83,19 +83,19 @@ opentelemetry = "0.29.1" tracing = "0.1.41" # CONFIG -clap = { version = "4.5.37", features = ["derive"] } +clap = { version = "4.5.38", features = ["derive"] } dotenvy = "0.15.7" envy = "0.4.2" # CRYPTO / AUTH -uuid = { version = "1.16.0", features = ["v4", "fast-rng", "serde"] } +uuid = { version = "1.17.0", features = ["v4", "fast-rng", "serde"] } jsonwebtoken = { version = "9.3.1", default-features = false } openidconnect = "4.0.0" urlencoding = "2.1.3" nom_pem = "4.0.0" bcrypt = "0.17.0" base64 = "0.22.1" -rustls = "0.23.26" +rustls = "0.23.27" hmac = "0.12.1" sha2 = "0.10.9" rand = "0.9.1" @@ -103,16 +103,16 @@ hex = "0.4.3" # SYSTEM portable-pty = "0.9.0" -bollard = "0.18.1" -sysinfo = "0.35.0" +bollard = "0.19.0" +sysinfo = "0.35.1" # CLOUD -aws-config = "1.6.2" -aws-sdk-ec2 = "1.124.0" +aws-config = "1.6.3" +aws-sdk-ec2 = "1.134.0" aws-credential-types = "1.2.3" ## CRON -english-to-cron = "0.1.4" +english-to-cron = "0.1.6" chrono-tz = "0.10.3" chrono = "0.4.41" croner = "2.1.0" @@ -126,4 +126,4 @@ wildcard = "0.3.0" colored = "3.0.0" regex = "1.11.1" bytes = "1.10.1" -bson = "2.14.0" +bson = "2.15.0" diff --git a/bin/binaries.Dockerfile b/bin/binaries.Dockerfile index d3294100b..81693856c 100644 --- a/bin/binaries.Dockerfile +++ b/bin/binaries.Dockerfile @@ -1,7 +1,7 @@ -## Builds the Komodo Core and Periphery binaries +## Builds the Komodo Core, Periphery, and Util binaries ## for a specific architecture. -FROM rust:1.86.0-bullseye AS builder +FROM rust:1.87.0-bullseye AS builder WORKDIR /builder COPY Cargo.toml Cargo.lock ./ @@ -10,17 +10,20 @@ COPY ./client/core/rs ./client/core/rs COPY ./client/periphery ./client/periphery COPY ./bin/core ./bin/core COPY ./bin/periphery ./bin/periphery +COPY ./bin/util ./bin/util # Compile bin RUN \ cargo build -p komodo_core --release && \ - cargo build -p komodo_periphery --release + cargo build -p komodo_periphery --release && \ + cargo build -p komodo_util --release # Copy just the binaries to scratch image FROM scratch COPY --from=builder /builder/target/release/core /core COPY --from=builder /builder/target/release/periphery /periphery +COPY --from=builder /builder/target/release/util /util LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo LABEL org.opencontainers.image.description="Komodo Binaries" diff --git a/bin/core/Cargo.toml b/bin/core/Cargo.toml index 00fb1d931..86cb130d8 100644 --- a/bin/core/Cargo.toml +++ b/bin/core/Cargo.toml @@ -39,7 +39,6 @@ svi.workspace = true # external aws-credential-types.workspace = true tokio-tungstenite.workspace = true -ordered_hash_map.workspace = true english-to-cron.workspace = true openidconnect.workspace = true jsonwebtoken.workspace = true @@ -54,6 +53,7 @@ serde_json.workspace = true serde_yaml.workspace = true typeshare.workspace = true chrono-tz.workspace = true +indexmap.workspace = true octorust.workspace = true wildcard.workspace = true arc-swap.workspace = true diff --git a/bin/core/aio.Dockerfile b/bin/core/aio.Dockerfile index ed330b1a7..36a6bafd2 100644 --- a/bin/core/aio.Dockerfile +++ b/bin/core/aio.Dockerfile @@ -1,7 +1,7 @@ ## All in one, multi stage compile + runtime Docker build for your architecture. # Build Core -FROM rust:1.86.0-bullseye AS core-builder +FROM rust:1.87.0-bullseye AS core-builder WORKDIR /builder COPY Cargo.toml Cargo.lock ./ diff --git a/bin/core/src/alert/mod.rs b/bin/core/src/alert/mod.rs index 16e0ba2e3..46aec7f13 100644 --- a/bin/core/src/alert/mod.rs +++ b/bin/core/src/alert/mod.rs @@ -130,13 +130,15 @@ pub async fn send_alert_to_alerter( ) }) } - AlerterEndpoint::Ntfy(NtfyAlerterEndpoint { url }) => { - ntfy::send_alert(url, alert).await.with_context(|| { - format!( - "Failed to send alert to ntfy Alerter {}", - alerter.name - ) - }) + AlerterEndpoint::Ntfy(NtfyAlerterEndpoint { url, email }) => { + ntfy::send_alert(url, email.as_deref(), alert) + .await + .with_context(|| { + format!( + "Failed to send alert to ntfy Alerter {}", + alerter.name + ) + }) } AlerterEndpoint::Pushover(PushoverAlerterEndpoint { url }) => { pushover::send_alert(url, alert).await.with_context(|| { diff --git a/bin/core/src/alert/ntfy.rs b/bin/core/src/alert/ntfy.rs index 0d0865076..b82e8b7be 100644 --- a/bin/core/src/alert/ntfy.rs +++ b/bin/core/src/alert/ntfy.rs @@ -5,6 +5,7 @@ use super::*; #[instrument(level = "debug")] pub async fn send_alert( url: &str, + email: Option<&str>, alert: &Alert, ) -> anyhow::Result<()> { let level = fmt_level(alert.level); @@ -224,22 +225,27 @@ pub async fn send_alert( }; if !content.is_empty() { - send_message(url, content).await?; + send_message(url, email, content).await?; } Ok(()) } async fn send_message( url: &str, + email: Option<&str>, content: String, ) -> anyhow::Result<()> { - let response = http_client() + let mut request = http_client() .post(url) .header("Title", "ntfy Alert") - .body(content) - .send() - .await - .context("Failed to send message")?; + .body(content); + + if let Some(email) = email { + request = request.header("X-Email", email); + } + + let response = + request.send().await.context("Failed to send message")?; let status = response.status(); if status.is_success() { diff --git a/bin/core/src/api/execute/action.rs b/bin/core/src/api/execute/action.rs index 944fad349..1f1484331 100644 --- a/bin/core/src/api/execute/action.rs +++ b/bin/core/src/api/execute/action.rs @@ -39,7 +39,8 @@ use crate::{ random_string, update::update_update, }, - resource::{self, refresh_action_state_cache}, + permission::get_check_permissions, + resource::refresh_action_state_cache, state::{action_states, db_client}, }; @@ -71,10 +72,10 @@ impl Resolve for RunAction { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let mut action = resource::get_check_permissions::( + let mut action = get_check_permissions::( &self.action, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; diff --git a/bin/core/src/api/execute/alerter.rs b/bin/core/src/api/execute/alerter.rs index 96c872c69..47239897c 100644 --- a/bin/core/src/api/execute/alerter.rs +++ b/bin/core/src/api/execute/alerter.rs @@ -12,7 +12,7 @@ use resolver_api::Resolve; use crate::{ alert::send_alert_to_alerter, helpers::update::update_update, - resource::get_check_permissions, + permission::get_check_permissions, }; use super::ExecuteArgs; @@ -26,7 +26,7 @@ impl Resolve for TestAlerter { let alerter = get_check_permissions::( &self.alerter, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; diff --git a/bin/core/src/api/execute/build.rs b/bin/core/src/api/execute/build.rs index cf5178f43..6fcb8f1d0 100644 --- a/bin/core/src/api/execute/build.rs +++ b/bin/core/src/api/execute/build.rs @@ -48,6 +48,7 @@ use crate::{ registry_token, update::{init_execution_update, update_update}, }, + permission::get_check_permissions, resource::{self, refresh_build_state_cache}, state::{action_states, db_client}, }; @@ -80,10 +81,10 @@ impl Resolve for RunBuild { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let mut build = resource::get_check_permissions::( + let mut build = get_check_permissions::( &self.build, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -513,10 +514,10 @@ impl Resolve for CancelBuild { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let build = resource::get_check_permissions::( + let build = get_check_permissions::( &self.build, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; diff --git a/bin/core/src/api/execute/deployment.rs b/bin/core/src/api/execute/deployment.rs index e8e7da213..9c0d17ac5 100644 --- a/bin/core/src/api/execute/deployment.rs +++ b/bin/core/src/api/execute/deployment.rs @@ -34,6 +34,7 @@ use crate::{ update::update_update, }, monitor::update_cache_for_server, + permission::get_check_permissions, resource, state::action_states, }; @@ -68,10 +69,10 @@ async fn setup_deployment_execution( deployment: &str, user: &User, ) -> anyhow::Result<(Deployment, Server)> { - let deployment = resource::get_check_permissions::( + let deployment = get_check_permissions::( deployment, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; diff --git a/bin/core/src/api/execute/mod.rs b/bin/core/src/api/execute/mod.rs index 06c5d8fe8..d3016591c 100644 --- a/bin/core/src/api/execute/mod.rs +++ b/bin/core/src/api/execute/mod.rs @@ -12,6 +12,7 @@ use komodo_client::{ api::execute::*, entities::{ Operation, + permission::PermissionLevel, update::{Log, Update}, user::User, }, @@ -86,18 +87,6 @@ pub enum ExecuteRequest { PruneBuildx(PruneBuildx), PruneSystem(PruneSystem), - // ==== DEPLOYMENT ==== - Deploy(Deploy), - BatchDeploy(BatchDeploy), - PullDeployment(PullDeployment), - StartDeployment(StartDeployment), - RestartDeployment(RestartDeployment), - PauseDeployment(PauseDeployment), - UnpauseDeployment(UnpauseDeployment), - StopDeployment(StopDeployment), - DestroyDeployment(DestroyDeployment), - BatchDestroyDeployment(BatchDestroyDeployment), - // ==== STACK ==== DeployStack(DeployStack), BatchDeployStack(BatchDeployStack), @@ -113,6 +102,18 @@ pub enum ExecuteRequest { DestroyStack(DestroyStack), BatchDestroyStack(BatchDestroyStack), + // ==== DEPLOYMENT ==== + Deploy(Deploy), + BatchDeploy(BatchDeploy), + PullDeployment(PullDeployment), + StartDeployment(StartDeployment), + RestartDeployment(RestartDeployment), + PauseDeployment(PauseDeployment), + UnpauseDeployment(UnpauseDeployment), + StopDeployment(StopDeployment), + DestroyDeployment(DestroyDeployment), + BatchDestroyDeployment(BatchDestroyDeployment), + // ==== BUILD ==== RunBuild(RunBuild), BatchRunBuild(BatchRunBuild), @@ -298,6 +299,7 @@ async fn batch_execute( pattern, Default::default(), user, + PermissionLevel::Execute.into(), &[], ) .await?; diff --git a/bin/core/src/api/execute/procedure.rs b/bin/core/src/api/execute/procedure.rs index b7f5f9b5e..f4f539cf8 100644 --- a/bin/core/src/api/execute/procedure.rs +++ b/bin/core/src/api/execute/procedure.rs @@ -21,7 +21,8 @@ use tokio::sync::Mutex; use crate::{ alert::send_alerts, helpers::{procedure::execute_procedure, update::update_update}, - resource::{self, refresh_procedure_state_cache}, + permission::get_check_permissions, + resource::refresh_procedure_state_cache, state::{action_states, db_client}, }; @@ -70,10 +71,10 @@ fn resolve_inner( >, > { Box::pin(async move { - let procedure = resource::get_check_permissions::( + let procedure = get_check_permissions::( &procedure, &user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; diff --git a/bin/core/src/api/execute/repo.rs b/bin/core/src/api/execute/repo.rs index ccf3cb695..b9fa20a4b 100644 --- a/bin/core/src/api/execute/repo.rs +++ b/bin/core/src/api/execute/repo.rs @@ -41,6 +41,7 @@ use crate::{ query::get_variables_and_secrets, update::update_update, }, + permission::get_check_permissions, resource::{self, refresh_repo_state_cache}, state::{action_states, db_client}, }; @@ -73,10 +74,10 @@ impl Resolve for CloneRepo { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let mut repo = resource::get_check_permissions::( + let mut repo = get_check_permissions::( &self.repo, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -185,10 +186,10 @@ impl Resolve for PullRepo { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let mut repo = resource::get_check_permissions::( + let mut repo = get_check_permissions::( &self.repo, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -340,10 +341,10 @@ impl Resolve for BuildRepo { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let mut repo = resource::get_check_permissions::( + let mut repo = get_check_permissions::( &self.repo, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -651,10 +652,10 @@ impl Resolve for CancelRepoBuild { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let repo = resource::get_check_permissions::( + let repo = get_check_permissions::( &self.repo, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; diff --git a/bin/core/src/api/execute/server.rs b/bin/core/src/api/execute/server.rs index 2e29c71d1..262d025db 100644 --- a/bin/core/src/api/execute/server.rs +++ b/bin/core/src/api/execute/server.rs @@ -15,7 +15,7 @@ use resolver_api::Resolve; use crate::{ helpers::{periphery_client, update::update_update}, monitor::update_cache_for_server, - resource, + permission::get_check_permissions, state::action_states, }; @@ -27,10 +27,10 @@ impl Resolve for StartContainer { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -81,10 +81,10 @@ impl Resolve for RestartContainer { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -137,10 +137,10 @@ impl Resolve for PauseContainer { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -191,10 +191,10 @@ impl Resolve for UnpauseContainer { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -247,10 +247,10 @@ impl Resolve for StopContainer { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -309,10 +309,10 @@ impl Resolve for DestroyContainer { signal, time, } = self; - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -365,10 +365,10 @@ impl Resolve for StartAllContainers { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -415,10 +415,10 @@ impl Resolve for RestartAllContainers { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -467,10 +467,10 @@ impl Resolve for PauseAllContainers { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -517,10 +517,10 @@ impl Resolve for UnpauseAllContainers { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -569,10 +569,10 @@ impl Resolve for StopAllContainers { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -619,10 +619,10 @@ impl Resolve for PruneContainers { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -675,10 +675,10 @@ impl Resolve for DeleteNetwork { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -726,10 +726,10 @@ impl Resolve for PruneNetworks { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -780,10 +780,10 @@ impl Resolve for DeleteImage { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -828,10 +828,10 @@ impl Resolve for PruneImages { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -880,10 +880,10 @@ impl Resolve for DeleteVolume { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -931,10 +931,10 @@ impl Resolve for PruneVolumes { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -983,10 +983,10 @@ impl Resolve for PruneDockerBuilders { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -1035,10 +1035,10 @@ impl Resolve for PruneBuildx { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -1087,10 +1087,10 @@ impl Resolve for PruneSystem { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; diff --git a/bin/core/src/api/execute/stack.rs b/bin/core/src/api/execute/stack.rs index c6c7fa62a..853cb6786 100644 --- a/bin/core/src/api/execute/stack.rs +++ b/bin/core/src/api/execute/stack.rs @@ -29,6 +29,7 @@ use crate::{ update::{add_update_without_send, update_update}, }, monitor::update_cache_for_server, + permission::get_check_permissions, resource, stack::{execute::execute_compose, get_stack_and_server}, state::{action_states, db_client}, @@ -69,7 +70,7 @@ impl Resolve for DeployStack { let (mut stack, server) = get_stack_and_server( &self.stack, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), true, ) .await?; @@ -320,10 +321,10 @@ impl Resolve for DeployStackIfChanged { self, ExecuteArgs { user, update }: &ExecuteArgs, ) -> serror::Result { - let stack = resource::get_check_permissions::( + let stack = get_check_permissions::( &self.stack, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; RefreshStackCache { @@ -402,11 +403,8 @@ impl Resolve for BatchPullStack { ExecuteArgs { user, .. }: &ExecuteArgs, ) -> serror::Result { Ok( - super::batch_execute::( - &self.pattern, - user, - ) - .await?, + super::batch_execute::(&self.pattern, user) + .await?, ) } } @@ -498,7 +496,7 @@ impl Resolve for PullStack { let (stack, server) = get_stack_and_server( &self.stack, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), true, ) .await?; diff --git a/bin/core/src/api/execute/sync.rs b/bin/core/src/api/execute/sync.rs index 44dd8f022..27ed6f20e 100644 --- a/bin/core/src/api/execute/sync.rs +++ b/bin/core/src/api/execute/sync.rs @@ -29,7 +29,7 @@ use resolver_api::Resolve; use crate::{ api::write::WriteArgs, helpers::{query::get_id_to_tags, update::update_update}, - resource, + permission::get_check_permissions, state::{action_states, db_client}, sync::{ AllResourcesById, ResourceSyncTrait, @@ -54,9 +54,11 @@ impl Resolve for RunSync { resource_type: match_resource_type, resources: match_resources, } = self; - let sync = resource::get_check_permissions::< - entities::sync::ResourceSync, - >(&sync, user, PermissionLevel::Execute) + let sync = get_check_permissions::( + &sync, + user, + PermissionLevel::Execute.into(), + ) .await?; // get the action state for the sync (or insert default). diff --git a/bin/core/src/api/read/action.rs b/bin/core/src/api/read/action.rs index b9428562f..9dccb24ae 100644 --- a/bin/core/src/api/read/action.rs +++ b/bin/core/src/api/read/action.rs @@ -12,6 +12,7 @@ use resolver_api::Resolve; use crate::{ helpers::query::get_all_tags, + permission::get_check_permissions, resource, state::{action_state_cache, action_states}, }; @@ -24,10 +25,10 @@ impl Resolve for GetAction { ReadArgs { user }: &ReadArgs, ) -> serror::Result { Ok( - resource::get_check_permissions::( + get_check_permissions::( &self.action, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?, ) @@ -45,8 +46,13 @@ impl Resolve for ListActions { get_all_tags(None).await? }; Ok( - resource::list_for_user::(self.query, user, &all_tags) - .await?, + resource::list_for_user::( + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, + ) + .await?, ) } } @@ -63,7 +69,10 @@ impl Resolve for ListFullActions { }; Ok( resource::list_full_for_user::( - self.query, user, &all_tags, + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, ) .await?, ) @@ -75,10 +84,10 @@ impl Resolve for GetActionActionState { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let action = resource::get_check_permissions::( + let action = get_check_permissions::( &self.action, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let action_state = action_states() @@ -99,6 +108,7 @@ impl Resolve for GetActionsSummary { let actions = resource::list_full_for_user::( Default::default(), user, + PermissionLevel::Read.into(), &[], ) .await diff --git a/bin/core/src/api/read/alert.rs b/bin/core/src/api/read/alert.rs index 574869430..35f6cdb9d 100644 --- a/bin/core/src/api/read/alert.rs +++ b/bin/core/src/api/read/alert.rs @@ -16,7 +16,7 @@ use mungos::{ use resolver_api::Resolve; use crate::{ - config::core_config, resource::get_resource_ids_for_user, + config::core_config, permission::get_resource_ids_for_user, state::db_client, }; diff --git a/bin/core/src/api/read/alerter.rs b/bin/core/src/api/read/alerter.rs index 1e6b66741..253f78b98 100644 --- a/bin/core/src/api/read/alerter.rs +++ b/bin/core/src/api/read/alerter.rs @@ -11,7 +11,8 @@ use mungos::mongodb::bson::doc; use resolver_api::Resolve; use crate::{ - helpers::query::get_all_tags, resource, state::db_client, + helpers::query::get_all_tags, permission::get_check_permissions, + resource, state::db_client, }; use super::ReadArgs; @@ -22,10 +23,10 @@ impl Resolve for GetAlerter { ReadArgs { user }: &ReadArgs, ) -> serror::Result { Ok( - resource::get_check_permissions::( + get_check_permissions::( &self.alerter, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?, ) @@ -43,8 +44,13 @@ impl Resolve for ListAlerters { get_all_tags(None).await? }; Ok( - resource::list_for_user::(self.query, user, &all_tags) - .await?, + resource::list_for_user::( + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, + ) + .await?, ) } } @@ -61,7 +67,10 @@ impl Resolve for ListFullAlerters { }; Ok( resource::list_full_for_user::( - self.query, user, &all_tags, + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, ) .await?, ) diff --git a/bin/core/src/api/read/build.rs b/bin/core/src/api/read/build.rs index 4fa7ac1f5..3305e1031 100644 --- a/bin/core/src/api/read/build.rs +++ b/bin/core/src/api/read/build.rs @@ -22,6 +22,7 @@ use resolver_api::Resolve; use crate::{ config::core_config, helpers::query::get_all_tags, + permission::get_check_permissions, resource, state::{ action_states, build_state_cache, db_client, github_client, @@ -36,10 +37,10 @@ impl Resolve for GetBuild { ReadArgs { user }: &ReadArgs, ) -> serror::Result { Ok( - resource::get_check_permissions::( + get_check_permissions::( &self.build, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?, ) @@ -57,8 +58,13 @@ impl Resolve for ListBuilds { get_all_tags(None).await? }; Ok( - resource::list_for_user::(self.query, user, &all_tags) - .await?, + resource::list_for_user::( + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, + ) + .await?, ) } } @@ -75,7 +81,10 @@ impl Resolve for ListFullBuilds { }; Ok( resource::list_full_for_user::( - self.query, user, &all_tags, + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, ) .await?, ) @@ -87,10 +96,10 @@ impl Resolve for GetBuildActionState { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let build = resource::get_check_permissions::( + let build = get_check_permissions::( &self.build, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let action_state = action_states() @@ -111,6 +120,7 @@ impl Resolve for GetBuildsSummary { let builds = resource::list_full_for_user::( Default::default(), user, + PermissionLevel::Read.into(), &[], ) .await @@ -218,10 +228,10 @@ impl Resolve for ListBuildVersions { patch, limit, } = self; - let build = resource::get_check_permissions::( + let build = get_check_permissions::( &build, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; @@ -274,7 +284,10 @@ impl Resolve for ListCommonBuildExtraArgs { get_all_tags(None).await? }; let builds = resource::list_full_for_user::( - self.query, user, &all_tags, + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, ) .await .context("failed to get resources matching query")?; @@ -306,10 +319,10 @@ impl Resolve for GetBuildWebhookEnabled { }); }; - let build = resource::get_check_permissions::( + let build = get_check_permissions::( &self.build, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; diff --git a/bin/core/src/api/read/builder.rs b/bin/core/src/api/read/builder.rs index b387ab8a5..ba2e551b5 100644 --- a/bin/core/src/api/read/builder.rs +++ b/bin/core/src/api/read/builder.rs @@ -11,7 +11,8 @@ use mungos::mongodb::bson::doc; use resolver_api::Resolve; use crate::{ - helpers::query::get_all_tags, resource, state::db_client, + helpers::query::get_all_tags, permission::get_check_permissions, + resource, state::db_client, }; use super::ReadArgs; @@ -22,10 +23,10 @@ impl Resolve for GetBuilder { ReadArgs { user }: &ReadArgs, ) -> serror::Result { Ok( - resource::get_check_permissions::( + get_check_permissions::( &self.builder, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?, ) @@ -43,8 +44,13 @@ impl Resolve for ListBuilders { get_all_tags(None).await? }; Ok( - resource::list_for_user::(self.query, user, &all_tags) - .await?, + resource::list_for_user::( + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, + ) + .await?, ) } } @@ -61,7 +67,10 @@ impl Resolve for ListFullBuilders { }; Ok( resource::list_full_for_user::( - self.query, user, &all_tags, + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, ) .await?, ) diff --git a/bin/core/src/api/read/deployment.rs b/bin/core/src/api/read/deployment.rs index 83f5ec00b..0796bc31d 100644 --- a/bin/core/src/api/read/deployment.rs +++ b/bin/core/src/api/read/deployment.rs @@ -8,19 +8,22 @@ use komodo_client::{ Deployment, DeploymentActionState, DeploymentConfig, DeploymentListItem, DeploymentState, }, - docker::container::ContainerStats, + docker::container::{Container, ContainerStats}, permission::PermissionLevel, - server::Server, + server::{Server, ServerState}, update::Log, }, }; -use periphery_client::api; +use periphery_client::api::{self, container::InspectContainer}; use resolver_api::Resolve; use crate::{ helpers::{periphery_client, query::get_all_tags}, + permission::get_check_permissions, resource, - state::{action_states, deployment_status_cache}, + state::{ + action_states, deployment_status_cache, server_status_cache, + }, }; use super::ReadArgs; @@ -31,10 +34,10 @@ impl Resolve for GetDeployment { ReadArgs { user }: &ReadArgs, ) -> serror::Result { Ok( - resource::get_check_permissions::( + get_check_permissions::( &self.deployment, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?, ) @@ -53,7 +56,10 @@ impl Resolve for ListDeployments { }; let only_update_available = self.query.specific.update_available; let deployments = resource::list_for_user::( - self.query, user, &all_tags, + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, ) .await?; let deployments = if only_update_available { @@ -80,7 +86,10 @@ impl Resolve for ListFullDeployments { }; Ok( resource::list_full_for_user::( - self.query, user, &all_tags, + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, ) .await?, ) @@ -92,10 +101,10 @@ impl Resolve for GetDeploymentContainer { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let deployment = resource::get_check_permissions::( + let deployment = get_check_permissions::( &self.deployment, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let status = deployment_status_cache() @@ -126,10 +135,10 @@ impl Resolve for GetDeploymentLog { name, config: DeploymentConfig { server_id, .. }, .. - } = resource::get_check_permissions::( + } = get_check_permissions::( &deployment, user, - PermissionLevel::Read, + PermissionLevel::Read.logs(), ) .await?; if server_id.is_empty() { @@ -164,10 +173,10 @@ impl Resolve for SearchDeploymentLog { name, config: DeploymentConfig { server_id, .. }, .. - } = resource::get_check_permissions::( + } = get_check_permissions::( &deployment, user, - PermissionLevel::Read, + PermissionLevel::Read.logs(), ) .await?; if server_id.is_empty() { @@ -188,6 +197,50 @@ impl Resolve for SearchDeploymentLog { } } +impl Resolve for InspectDeploymentContainer { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> serror::Result { + let InspectDeploymentContainer { deployment } = self; + let Deployment { + name, + config: DeploymentConfig { server_id, .. }, + .. + } = get_check_permissions::( + &deployment, + user, + PermissionLevel::Read.inspect(), + ) + .await?; + if server_id.is_empty() { + return Err( + anyhow!( + "Cannot inspect deployment, not attached to any server" + ) + .into(), + ); + } + let server = resource::get::(&server_id).await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if cache.state != ServerState::Ok { + return Err( + anyhow!( + "Cannot inspect container: server is {:?}", + cache.state + ) + .into(), + ); + } + let res = periphery_client(&server)? + .request(InspectContainer { name }) + .await?; + Ok(res) + } +} + impl Resolve for GetDeploymentStats { async fn resolve( self, @@ -197,10 +250,10 @@ impl Resolve for GetDeploymentStats { name, config: DeploymentConfig { server_id, .. }, .. - } = resource::get_check_permissions::( + } = get_check_permissions::( &self.deployment, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; if server_id.is_empty() { @@ -222,10 +275,10 @@ impl Resolve for GetDeploymentActionState { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let deployment = resource::get_check_permissions::( + let deployment = get_check_permissions::( &self.deployment, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let action_state = action_states() @@ -246,6 +299,7 @@ impl Resolve for GetDeploymentsSummary { let deployments = resource::list_full_for_user::( Default::default(), user, + PermissionLevel::Read.into(), &[], ) .await @@ -289,7 +343,10 @@ impl Resolve for ListCommonDeploymentExtraArgs { get_all_tags(None).await? }; let deployments = resource::list_full_for_user::( - self.query, user, &all_tags, + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, ) .await .context("failed to get resources matching query")?; diff --git a/bin/core/src/api/read/mod.rs b/bin/core/src/api/read/mod.rs index a52e85176..48c8c0392 100644 --- a/bin/core/src/api/read/mod.rs +++ b/bin/core/src/api/read/mod.rs @@ -11,6 +11,7 @@ use komodo_client::{ build::Build, builder::{Builder, BuilderConfig}, config::{DockerRegistry, GitProvider}, + permission::PermissionLevel, repo::Repo, server::Server, sync::ResourceSync, @@ -71,7 +72,7 @@ enum ReadRequest { // ==== USER ==== GetUsername(GetUsername), - GetPermissionLevel(GetPermissionLevel), + GetPermission(GetPermission), FindUser(FindUser), ListUsers(ListUsers), ListApiKeys(ListApiKeys), @@ -123,6 +124,25 @@ enum ReadRequest { ListComposeProjects(ListComposeProjects), ListTerminals(ListTerminals), + // ==== SERVER STATS ==== + GetSystemInformation(GetSystemInformation), + GetSystemStats(GetSystemStats), + ListSystemProcesses(ListSystemProcesses), + + // ==== STACK ==== + GetStacksSummary(GetStacksSummary), + GetStack(GetStack), + GetStackActionState(GetStackActionState), + GetStackWebhooksEnabled(GetStackWebhooksEnabled), + GetStackLog(GetStackLog), + SearchStackLog(SearchStackLog), + InspectStackContainer(InspectStackContainer), + ListStacks(ListStacks), + ListFullStacks(ListFullStacks), + ListStackServices(ListStackServices), + ListCommonStackExtraArgs(ListCommonStackExtraArgs), + ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs), + // ==== DEPLOYMENT ==== GetDeploymentsSummary(GetDeploymentsSummary), GetDeployment(GetDeployment), @@ -131,6 +151,7 @@ enum ReadRequest { GetDeploymentStats(GetDeploymentStats), GetDeploymentLog(GetDeploymentLog), SearchDeploymentLog(SearchDeploymentLog), + InspectDeploymentContainer(InspectDeploymentContainer), ListDeployments(ListDeployments), ListFullDeployments(ListFullDeployments), ListCommonDeploymentExtraArgs(ListCommonDeploymentExtraArgs), @@ -162,19 +183,6 @@ enum ReadRequest { ListResourceSyncs(ListResourceSyncs), ListFullResourceSyncs(ListFullResourceSyncs), - // ==== STACK ==== - GetStacksSummary(GetStacksSummary), - GetStack(GetStack), - GetStackActionState(GetStackActionState), - GetStackWebhooksEnabled(GetStackWebhooksEnabled), - GetStackLog(GetStackLog), - SearchStackLog(SearchStackLog), - ListStacks(ListStacks), - ListFullStacks(ListFullStacks), - ListStackServices(ListStackServices), - ListCommonStackExtraArgs(ListCommonStackExtraArgs), - ListCommonStackBuildExtraArgs(ListCommonStackBuildExtraArgs), - // ==== BUILDER ==== GetBuildersSummary(GetBuildersSummary), GetBuilder(GetBuilder), @@ -203,11 +211,6 @@ enum ReadRequest { ListAlerts(ListAlerts), GetAlert(GetAlert), - // ==== SERVER STATS ==== - GetSystemInformation(GetSystemInformation), - GetSystemStats(GetSystemStats), - ListSystemProcesses(ListSystemProcesses), - // ==== VARIABLE ==== GetVariable(GetVariable), ListVariables(ListVariables), @@ -396,16 +399,19 @@ impl Resolve for ListGitProvidersFromConfig { resource::list_full_for_user::( Default::default(), user, + PermissionLevel::Read.into(), &[] ), resource::list_full_for_user::( Default::default(), user, + PermissionLevel::Read.into(), &[] ), resource::list_full_for_user::( Default::default(), user, + PermissionLevel::Read.into(), &[] ), )?; diff --git a/bin/core/src/api/read/permission.rs b/bin/core/src/api/read/permission.rs index eb934b914..3a2d36b0e 100644 --- a/bin/core/src/api/read/permission.rs +++ b/bin/core/src/api/read/permission.rs @@ -1,7 +1,7 @@ use anyhow::{Context, anyhow}; use komodo_client::{ api::read::{ - GetPermissionLevel, GetPermissionLevelResponse, ListPermissions, + GetPermission, GetPermissionResponse, ListPermissions, ListPermissionsResponse, ListUserTargetPermissions, ListUserTargetPermissionsResponse, }, @@ -35,13 +35,13 @@ impl Resolve for ListPermissions { } } -impl Resolve for GetPermissionLevel { +impl Resolve for GetPermission { async fn resolve( self, ReadArgs { user }: &ReadArgs, - ) -> serror::Result { + ) -> serror::Result { if user.admin { - return Ok(PermissionLevel::Write); + return Ok(PermissionLevel::Write.all()); } Ok(get_user_permission_on_target(user, &self.target).await?) } diff --git a/bin/core/src/api/read/procedure.rs b/bin/core/src/api/read/procedure.rs index 7ecb044b1..cafda2144 100644 --- a/bin/core/src/api/read/procedure.rs +++ b/bin/core/src/api/read/procedure.rs @@ -10,6 +10,7 @@ use resolver_api::Resolve; use crate::{ helpers::query::get_all_tags, + permission::get_check_permissions, resource, state::{action_states, procedure_state_cache}, }; @@ -22,10 +23,10 @@ impl Resolve for GetProcedure { ReadArgs { user }: &ReadArgs, ) -> serror::Result { Ok( - resource::get_check_permissions::( + get_check_permissions::( &self.procedure, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?, ) @@ -44,7 +45,10 @@ impl Resolve for ListProcedures { }; Ok( resource::list_for_user::( - self.query, user, &all_tags, + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, ) .await?, ) @@ -63,7 +67,10 @@ impl Resolve for ListFullProcedures { }; Ok( resource::list_full_for_user::( - self.query, user, &all_tags, + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, ) .await?, ) @@ -78,6 +85,7 @@ impl Resolve for GetProceduresSummary { let procedures = resource::list_full_for_user::( Default::default(), user, + PermissionLevel::Read.into(), &[], ) .await @@ -120,10 +128,10 @@ impl Resolve for GetProcedureActionState { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let procedure = resource::get_check_permissions::( + let procedure = get_check_permissions::( &self.procedure, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let action_state = action_states() diff --git a/bin/core/src/api/read/repo.rs b/bin/core/src/api/read/repo.rs index ae691c89d..1b99a1f52 100644 --- a/bin/core/src/api/read/repo.rs +++ b/bin/core/src/api/read/repo.rs @@ -12,6 +12,7 @@ use resolver_api::Resolve; use crate::{ config::core_config, helpers::query::get_all_tags, + permission::get_check_permissions, resource, state::{action_states, github_client, repo_state_cache}, }; @@ -24,10 +25,10 @@ impl Resolve for GetRepo { ReadArgs { user }: &ReadArgs, ) -> serror::Result { Ok( - resource::get_check_permissions::( + get_check_permissions::( &self.repo, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?, ) @@ -45,8 +46,13 @@ impl Resolve for ListRepos { get_all_tags(None).await? }; Ok( - resource::list_for_user::(self.query, user, &all_tags) - .await?, + resource::list_for_user::( + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, + ) + .await?, ) } } @@ -63,7 +69,10 @@ impl Resolve for ListFullRepos { }; Ok( resource::list_full_for_user::( - self.query, user, &all_tags, + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, ) .await?, ) @@ -75,10 +84,10 @@ impl Resolve for GetRepoActionState { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let repo = resource::get_check_permissions::( + let repo = get_check_permissions::( &self.repo, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let action_state = action_states() @@ -99,6 +108,7 @@ impl Resolve for GetReposSummary { let repos = resource::list_full_for_user::( Default::default(), user, + PermissionLevel::Read.into(), &[], ) .await @@ -160,10 +170,10 @@ impl Resolve for GetRepoWebhooksEnabled { }); }; - let repo = resource::get_check_permissions::( + let repo = get_check_permissions::( &self.repo, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index 785442c02..610eeadf1 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -51,6 +51,7 @@ use crate::{ periphery_client, query::{get_all_tags, get_system_info}, }, + permission::get_check_permissions, resource, stack::compose_container_match_regex, state::{action_states, db_client, server_status_cache}, @@ -66,6 +67,7 @@ impl Resolve for GetServersSummary { let servers = resource::list_for_user::( Default::default(), user, + PermissionLevel::Read.into(), &[], ) .await?; @@ -93,10 +95,10 @@ impl Resolve for GetPeripheryVersion { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let version = server_status_cache() @@ -114,10 +116,10 @@ impl Resolve for GetServer { ReadArgs { user }: &ReadArgs, ) -> serror::Result { Ok( - resource::get_check_permissions::( + get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?, ) @@ -135,8 +137,13 @@ impl Resolve for ListServers { get_all_tags(None).await? }; Ok( - resource::list_for_user::(self.query, user, &all_tags) - .await?, + resource::list_for_user::( + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, + ) + .await?, ) } } @@ -153,7 +160,10 @@ impl Resolve for ListFullServers { }; Ok( resource::list_full_for_user::( - self.query, user, &all_tags, + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, ) .await?, ) @@ -165,10 +175,10 @@ impl Resolve for GetServerState { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let status = server_status_cache() @@ -187,10 +197,10 @@ impl Resolve for GetServerActionState { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let action_state = action_states() @@ -208,10 +218,10 @@ impl Resolve for GetSystemInformation { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; get_system_info(&server).await.map_err(Into::into) @@ -223,10 +233,10 @@ impl Resolve for GetSystemStats { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let status = @@ -255,10 +265,10 @@ impl Resolve for ListSystemProcesses { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.processes(), ) .await?; let mut lock = processes_cache().lock().await; @@ -294,10 +304,10 @@ impl Resolve for GetHistoricalServerStats { granularity, page, } = self; - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &server, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let granularity = @@ -342,10 +352,10 @@ impl Resolve for ListDockerContainers { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let cache = server_status_cache() @@ -367,6 +377,7 @@ impl Resolve for ListAllDockerContainers { let servers = resource::list_for_user::( Default::default(), user, + PermissionLevel::Read.into(), &[], ) .await? @@ -400,6 +411,7 @@ impl Resolve for GetDockerContainersSummary { let servers = resource::list_full_for_user::( Default::default(), user, + PermissionLevel::Read.into(), &[], ) .await @@ -436,10 +448,10 @@ impl Resolve for InspectDockerContainer { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.inspect(), ) .await?; let cache = server_status_cache() @@ -476,10 +488,10 @@ impl Resolve for GetContainerLog { tail, timestamps, } = self; - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &server, user, - PermissionLevel::Read, + PermissionLevel::Read.logs(), ) .await?; let res = periphery_client(&server)? @@ -507,10 +519,10 @@ impl Resolve for SearchContainerLog { invert, timestamps, } = self; - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &server, user, - PermissionLevel::Read, + PermissionLevel::Read.logs(), ) .await?; let res = periphery_client(&server)? @@ -532,10 +544,10 @@ impl Resolve for GetResourceMatchingContainer { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; // first check deployments @@ -593,10 +605,10 @@ impl Resolve for ListDockerNetworks { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let cache = server_status_cache() @@ -615,10 +627,10 @@ impl Resolve for InspectDockerNetwork { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let cache = server_status_cache() @@ -645,10 +657,10 @@ impl Resolve for ListDockerImages { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let cache = server_status_cache() @@ -667,10 +679,10 @@ impl Resolve for InspectDockerImage { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let cache = server_status_cache() @@ -694,10 +706,10 @@ impl Resolve for ListDockerImageHistory { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result> { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let cache = server_status_cache() @@ -724,10 +736,10 @@ impl Resolve for ListDockerVolumes { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let cache = server_status_cache() @@ -746,10 +758,10 @@ impl Resolve for InspectDockerVolume { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let cache = server_status_cache() @@ -773,10 +785,10 @@ impl Resolve for ListComposeProjects { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let cache = server_status_cache() @@ -832,10 +844,10 @@ impl Resolve for ListTerminals { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Read, + PermissionLevel::Read.terminal(), ) .await?; let cache = terminals_cache().get_or_insert(server.id.clone()); diff --git a/bin/core/src/api/read/stack.rs b/bin/core/src/api/read/stack.rs index 462de4c93..008e64224 100644 --- a/bin/core/src/api/read/stack.rs +++ b/bin/core/src/api/read/stack.rs @@ -1,25 +1,32 @@ use std::collections::HashSet; -use anyhow::Context; +use anyhow::{Context, anyhow}; use komodo_client::{ api::read::*, entities::{ config::core::CoreConfig, + docker::container::Container, permission::PermissionLevel, + server::{Server, ServerState}, stack::{Stack, StackActionState, StackListItem, StackState}, }, }; -use periphery_client::api::compose::{ - GetComposeLog, GetComposeLogSearch, +use periphery_client::api::{ + compose::{GetComposeLog, GetComposeLogSearch}, + container::InspectContainer, }; use resolver_api::Resolve; use crate::{ config::core_config, helpers::{periphery_client, query::get_all_tags}, + permission::get_check_permissions, resource, stack::get_stack_and_server, - state::{action_states, github_client, stack_status_cache}, + state::{ + action_states, github_client, server_status_cache, + stack_status_cache, + }, }; use super::ReadArgs; @@ -30,10 +37,10 @@ impl Resolve for GetStack { ReadArgs { user }: &ReadArgs, ) -> serror::Result { Ok( - resource::get_check_permissions::( + get_check_permissions::( &self.stack, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?, ) @@ -45,10 +52,10 @@ impl Resolve for ListStackServices { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let stack = resource::get_check_permissions::( + let stack = get_check_permissions::( &self.stack, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; @@ -75,9 +82,13 @@ impl Resolve for GetStackLog { tail, timestamps, } = self; - let (stack, server) = - get_stack_and_server(&stack, user, PermissionLevel::Read, true) - .await?; + let (stack, server) = get_stack_and_server( + &stack, + user, + PermissionLevel::Read.logs(), + true, + ) + .await?; let res = periphery_client(&server)? .request(GetComposeLog { project: stack.project_name(false), @@ -104,9 +115,13 @@ impl Resolve for SearchStackLog { invert, timestamps, } = self; - let (stack, server) = - get_stack_and_server(&stack, user, PermissionLevel::Read, true) - .await?; + let (stack, server) = get_stack_and_server( + &stack, + user, + PermissionLevel::Read.logs(), + true, + ) + .await?; let res = periphery_client(&server)? .request(GetComposeLogSearch { project: stack.project_name(false), @@ -122,6 +137,60 @@ impl Resolve for SearchStackLog { } } +impl Resolve for InspectStackContainer { + async fn resolve( + self, + ReadArgs { user }: &ReadArgs, + ) -> serror::Result { + let InspectStackContainer { stack, service } = self; + let stack = get_check_permissions::( + &stack, + user, + PermissionLevel::Read.inspect(), + ) + .await?; + if stack.config.server_id.is_empty() { + return Err( + anyhow!("Cannot inspect stack, not attached to any server") + .into(), + ); + } + let server = + resource::get::(&stack.config.server_id).await?; + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if cache.state != ServerState::Ok { + return Err( + anyhow!( + "Cannot inspect container: server is {:?}", + cache.state + ) + .into(), + ); + } + let services = &stack_status_cache() + .get(&stack.id) + .await + .unwrap_or_default() + .curr + .services; + let Some(name) = services + .into_iter() + .find(|s| s.service == service) + .and_then(|s| s.container.as_ref().map(|c| c.name.clone())) + else { + return Err(anyhow!( + "No service found matching '{service}'. Was the stack last deployed manually?" + ).into()); + }; + let res = periphery_client(&server)? + .request(InspectContainer { name }) + .await?; + Ok(res) + } +} + impl Resolve for ListCommonStackExtraArgs { async fn resolve( self, @@ -133,7 +202,10 @@ impl Resolve for ListCommonStackExtraArgs { get_all_tags(None).await? }; let stacks = resource::list_full_for_user::( - self.query, user, &all_tags, + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, ) .await .context("failed to get resources matching query")?; @@ -164,7 +236,10 @@ impl Resolve for ListCommonStackBuildExtraArgs { get_all_tags(None).await? }; let stacks = resource::list_full_for_user::( - self.query, user, &all_tags, + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, ) .await .context("failed to get resources matching query")?; @@ -195,9 +270,13 @@ impl Resolve for ListStacks { get_all_tags(None).await? }; let only_update_available = self.query.specific.update_available; - let stacks = - resource::list_for_user::(self.query, user, &all_tags) - .await?; + let stacks = resource::list_for_user::( + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, + ) + .await?; let stacks = if only_update_available { stacks .into_iter() @@ -228,7 +307,10 @@ impl Resolve for ListFullStacks { }; Ok( resource::list_full_for_user::( - self.query, user, &all_tags, + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, ) .await?, ) @@ -240,10 +322,10 @@ impl Resolve for GetStackActionState { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let stack = resource::get_check_permissions::( + let stack = get_check_permissions::( &self.stack, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let action_state = action_states() @@ -264,6 +346,7 @@ impl Resolve for GetStacksSummary { let stacks = resource::list_full_for_user::( Default::default(), user, + PermissionLevel::Read.into(), &[], ) .await @@ -302,10 +385,10 @@ impl Resolve for GetStackWebhooksEnabled { }); }; - let stack = resource::get_check_permissions::( + let stack = get_check_permissions::( &self.stack, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; diff --git a/bin/core/src/api/read/sync.rs b/bin/core/src/api/read/sync.rs index 5db3a987c..3e2674064 100644 --- a/bin/core/src/api/read/sync.rs +++ b/bin/core/src/api/read/sync.rs @@ -14,6 +14,7 @@ use resolver_api::Resolve; use crate::{ config::core_config, helpers::query::get_all_tags, + permission::get_check_permissions, resource, state::{action_states, github_client}, }; @@ -26,10 +27,10 @@ impl Resolve for GetResourceSync { ReadArgs { user }: &ReadArgs, ) -> serror::Result { Ok( - resource::get_check_permissions::( + get_check_permissions::( &self.sync, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?, ) @@ -48,7 +49,10 @@ impl Resolve for ListResourceSyncs { }; Ok( resource::list_for_user::( - self.query, user, &all_tags, + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, ) .await?, ) @@ -67,7 +71,10 @@ impl Resolve for ListFullResourceSyncs { }; Ok( resource::list_full_for_user::( - self.query, user, &all_tags, + self.query, + user, + PermissionLevel::Read.into(), + &all_tags, ) .await?, ) @@ -79,10 +86,10 @@ impl Resolve for GetResourceSyncActionState { self, ReadArgs { user }: &ReadArgs, ) -> serror::Result { - let sync = resource::get_check_permissions::( + let sync = get_check_permissions::( &self.sync, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; let action_state = action_states() @@ -104,6 +111,7 @@ impl Resolve for GetResourceSyncsSummary { resource::list_full_for_user::( Default::default(), user, + PermissionLevel::Read.into(), &[], ) .await @@ -160,10 +168,10 @@ impl Resolve for GetSyncWebhooksEnabled { }); }; - let sync = resource::get_check_permissions::( + let sync = get_check_permissions::( &self.sync, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; diff --git a/bin/core/src/api/read/toml.rs b/bin/core/src/api/read/toml.rs index aa96e7ea0..802fcdb3e 100644 --- a/bin/core/src/api/read/toml.rs +++ b/bin/core/src/api/read/toml.rs @@ -20,12 +20,14 @@ use crate::{ helpers::query::{ get_all_tags, get_id_to_tags, get_user_user_group_ids, }, + permission::get_check_permissions, resource, state::db_client, sync::{ AllResourcesById, - toml::{TOML_PRETTY_OPTIONS, ToToml, convert_resource}, - user_groups::convert_user_groups, + toml::{ToToml, convert_resource}, + user_groups::{convert_user_groups, user_group_to_toml}, + variables::variable_to_toml, }, }; @@ -45,6 +47,7 @@ async fn get_all_targets( resource::list_for_user::( ResourceQuery::builder().tags(tags).build(), user, + PermissionLevel::Read.into(), &all_tags, ) .await? @@ -55,6 +58,7 @@ async fn get_all_targets( resource::list_for_user::( ResourceQuery::builder().tags(tags).build(), user, + PermissionLevel::Read.into(), &all_tags, ) .await? @@ -65,6 +69,7 @@ async fn get_all_targets( resource::list_for_user::( ResourceQuery::builder().tags(tags).build(), user, + PermissionLevel::Read.into(), &all_tags, ) .await? @@ -75,6 +80,7 @@ async fn get_all_targets( resource::list_for_user::( ResourceQuery::builder().tags(tags).build(), user, + PermissionLevel::Read.into(), &all_tags, ) .await? @@ -85,6 +91,7 @@ async fn get_all_targets( resource::list_for_user::( ResourceQuery::builder().tags(tags).build(), user, + PermissionLevel::Read.into(), &all_tags, ) .await? @@ -95,6 +102,7 @@ async fn get_all_targets( resource::list_for_user::( ResourceQuery::builder().tags(tags).build(), user, + PermissionLevel::Read.into(), &all_tags, ) .await? @@ -105,6 +113,7 @@ async fn get_all_targets( resource::list_for_user::( ResourceQuery::builder().tags(tags).build(), user, + PermissionLevel::Read.into(), &all_tags, ) .await? @@ -115,6 +124,7 @@ async fn get_all_targets( resource::list_for_user::( ResourceQuery::builder().tags(tags).build(), user, + PermissionLevel::Read.into(), &all_tags, ) .await? @@ -125,6 +135,7 @@ async fn get_all_targets( resource::list_for_user::( ResourceQuery::builder().tags(tags).build(), user, + PermissionLevel::Read.into(), &all_tags, ) .await? @@ -135,6 +146,7 @@ async fn get_all_targets( resource::list_full_for_user::( ResourceQuery::builder().tags(tags).build(), user, + PermissionLevel::Read.into(), &all_tags, ) .await? @@ -198,10 +210,10 @@ impl Resolve for ExportResourcesToToml { for target in targets { match target { ResourceTarget::Alerter(id) => { - let alerter = resource::get_check_permissions::( + let alerter = get_check_permissions::( &id, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; res.alerters.push(convert_resource::( @@ -212,10 +224,10 @@ impl Resolve for ExportResourcesToToml { )) } ResourceTarget::ResourceSync(id) => { - let sync = resource::get_check_permissions::( + let sync = get_check_permissions::( &id, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; if sync.config.file_contents.is_empty() @@ -231,10 +243,10 @@ impl Resolve for ExportResourcesToToml { } } ResourceTarget::Server(id) => { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &id, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; res.servers.push(convert_resource::( @@ -245,13 +257,12 @@ impl Resolve for ExportResourcesToToml { )) } ResourceTarget::Builder(id) => { - let mut builder = - resource::get_check_permissions::( - &id, - user, - PermissionLevel::Read, - ) - .await?; + let mut builder = get_check_permissions::( + &id, + user, + PermissionLevel::Read.into(), + ) + .await?; Builder::replace_ids(&mut builder, &all); res.builders.push(convert_resource::( builder, @@ -261,10 +272,10 @@ impl Resolve for ExportResourcesToToml { )) } ResourceTarget::Build(id) => { - let mut build = resource::get_check_permissions::( + let mut build = get_check_permissions::( &id, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; Build::replace_ids(&mut build, &all); @@ -276,10 +287,10 @@ impl Resolve for ExportResourcesToToml { )) } ResourceTarget::Deployment(id) => { - let mut deployment = resource::get_check_permissions::< - Deployment, - >( - &id, user, PermissionLevel::Read + let mut deployment = get_check_permissions::( + &id, + user, + PermissionLevel::Read.into(), ) .await?; Deployment::replace_ids(&mut deployment, &all); @@ -291,10 +302,10 @@ impl Resolve for ExportResourcesToToml { )) } ResourceTarget::Repo(id) => { - let mut repo = resource::get_check_permissions::( + let mut repo = get_check_permissions::( &id, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; Repo::replace_ids(&mut repo, &all); @@ -306,10 +317,10 @@ impl Resolve for ExportResourcesToToml { )) } ResourceTarget::Stack(id) => { - let mut stack = resource::get_check_permissions::( + let mut stack = get_check_permissions::( &id, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; Stack::replace_ids(&mut stack, &all); @@ -321,10 +332,10 @@ impl Resolve for ExportResourcesToToml { )) } ResourceTarget::Procedure(id) => { - let mut procedure = resource::get_check_permissions::< - Procedure, - >( - &id, user, PermissionLevel::Read + let mut procedure = get_check_permissions::( + &id, + user, + PermissionLevel::Read.into(), ) .await?; Procedure::replace_ids(&mut procedure, &all); @@ -336,10 +347,10 @@ impl Resolve for ExportResourcesToToml { )); } ResourceTarget::Action(id) => { - let mut action = resource::get_check_permissions::( + let mut action = get_check_permissions::( &id, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; Action::replace_ids(&mut action, &all); @@ -490,22 +501,14 @@ fn serialize_resources_toml( if !toml.is_empty() { toml.push_str("\n\n##\n\n"); } - toml.push_str("[[variable]]\n"); - toml.push_str( - &toml_pretty::to_string(variable, TOML_PRETTY_OPTIONS) - .context("failed to serialize variables to toml")?, - ); + toml.push_str(&variable_to_toml(variable)?); } - for user_group in &resources.user_groups { + for user_group in resources.user_groups { if !toml.is_empty() { toml.push_str("\n\n##\n\n"); } - toml.push_str("[[user_group]]\n"); - toml.push_str( - &toml_pretty::to_string(user_group, TOML_PRETTY_OPTIONS) - .context("failed to serialize user_groups to toml")?, - ); + toml.push_str(&user_group_to_toml(user_group)?); } Ok(toml) diff --git a/bin/core/src/api/read/update.rs b/bin/core/src/api/read/update.rs index beecab433..f1176037b 100644 --- a/bin/core/src/api/read/update.rs +++ b/bin/core/src/api/read/update.rs @@ -27,7 +27,11 @@ use mungos::{ }; use resolver_api::Resolve; -use crate::{config::core_config, resource, state::db_client}; +use crate::{ + config::core_config, + permission::{get_check_permissions, get_resource_ids_for_user}, + state::db_client, +}; use super::ReadArgs; @@ -41,18 +45,17 @@ impl Resolve for ListUpdates { let query = if user.admin || core_config().transparent_mode { self.query } else { - let server_query = - resource::get_resource_ids_for_user::(user) - .await? - .map(|ids| { - doc! { - "target.type": "Server", "target.id": { "$in": ids } - } - }) - .unwrap_or_else(|| doc! { "target.type": "Server" }); + let server_query = get_resource_ids_for_user::(user) + .await? + .map(|ids| { + doc! { + "target.type": "Server", "target.id": { "$in": ids } + } + }) + .unwrap_or_else(|| doc! { "target.type": "Server" }); let deployment_query = - resource::get_resource_ids_for_user::(user) + get_resource_ids_for_user::(user) .await? .map(|ids| { doc! { @@ -61,38 +64,35 @@ impl Resolve for ListUpdates { }) .unwrap_or_else(|| doc! { "target.type": "Deployment" }); - let stack_query = - resource::get_resource_ids_for_user::(user) - .await? - .map(|ids| { - doc! { - "target.type": "Stack", "target.id": { "$in": ids } - } - }) - .unwrap_or_else(|| doc! { "target.type": "Stack" }); + let stack_query = get_resource_ids_for_user::(user) + .await? + .map(|ids| { + doc! { + "target.type": "Stack", "target.id": { "$in": ids } + } + }) + .unwrap_or_else(|| doc! { "target.type": "Stack" }); - let build_query = - resource::get_resource_ids_for_user::(user) - .await? - .map(|ids| { - doc! { - "target.type": "Build", "target.id": { "$in": ids } - } - }) - .unwrap_or_else(|| doc! { "target.type": "Build" }); + let build_query = get_resource_ids_for_user::(user) + .await? + .map(|ids| { + doc! { + "target.type": "Build", "target.id": { "$in": ids } + } + }) + .unwrap_or_else(|| doc! { "target.type": "Build" }); - let repo_query = - resource::get_resource_ids_for_user::(user) - .await? - .map(|ids| { - doc! { - "target.type": "Repo", "target.id": { "$in": ids } - } - }) - .unwrap_or_else(|| doc! { "target.type": "Repo" }); + let repo_query = get_resource_ids_for_user::(user) + .await? + .map(|ids| { + doc! { + "target.type": "Repo", "target.id": { "$in": ids } + } + }) + .unwrap_or_else(|| doc! { "target.type": "Repo" }); let procedure_query = - resource::get_resource_ids_for_user::(user) + get_resource_ids_for_user::(user) .await? .map(|ids| { doc! { @@ -101,47 +101,43 @@ impl Resolve for ListUpdates { }) .unwrap_or_else(|| doc! { "target.type": "Procedure" }); - let action_query = - resource::get_resource_ids_for_user::(user) - .await? - .map(|ids| { - doc! { - "target.type": "Action", "target.id": { "$in": ids } - } - }) - .unwrap_or_else(|| doc! { "target.type": "Action" }); - - let builder_query = - resource::get_resource_ids_for_user::(user) - .await? - .map(|ids| { - doc! { - "target.type": "Builder", "target.id": { "$in": ids } - } - }) - .unwrap_or_else(|| doc! { "target.type": "Builder" }); - - let alerter_query = - resource::get_resource_ids_for_user::(user) - .await? - .map(|ids| { - doc! { - "target.type": "Alerter", "target.id": { "$in": ids } - } - }) - .unwrap_or_else(|| doc! { "target.type": "Alerter" }); - - let resource_sync_query = - resource::get_resource_ids_for_user::( - user, - ) + let action_query = get_resource_ids_for_user::(user) .await? .map(|ids| { doc! { - "target.type": "ResourceSync", "target.id": { "$in": ids } + "target.type": "Action", "target.id": { "$in": ids } } }) - .unwrap_or_else(|| doc! { "target.type": "ResourceSync" }); + .unwrap_or_else(|| doc! { "target.type": "Action" }); + + let builder_query = get_resource_ids_for_user::(user) + .await? + .map(|ids| { + doc! { + "target.type": "Builder", "target.id": { "$in": ids } + } + }) + .unwrap_or_else(|| doc! { "target.type": "Builder" }); + + let alerter_query = get_resource_ids_for_user::(user) + .await? + .map(|ids| { + doc! { + "target.type": "Alerter", "target.id": { "$in": ids } + } + }) + .unwrap_or_else(|| doc! { "target.type": "Alerter" }); + + let resource_sync_query = get_resource_ids_for_user::< + ResourceSync, + >(user) + .await? + .map(|ids| { + doc! { + "target.type": "ResourceSync", "target.id": { "$in": ids } + } + }) + .unwrap_or_else(|| doc! { "target.type": "ResourceSync" }); let mut query = self.query.unwrap_or_default(); query.extend(doc! { @@ -233,82 +229,82 @@ impl Resolve for GetUpdate { ); } ResourceTarget::Server(id) => { - resource::get_check_permissions::( + get_check_permissions::( id, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; } ResourceTarget::Deployment(id) => { - resource::get_check_permissions::( + get_check_permissions::( id, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; } ResourceTarget::Build(id) => { - resource::get_check_permissions::( + get_check_permissions::( id, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; } ResourceTarget::Repo(id) => { - resource::get_check_permissions::( + get_check_permissions::( id, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; } ResourceTarget::Builder(id) => { - resource::get_check_permissions::( + get_check_permissions::( id, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; } ResourceTarget::Alerter(id) => { - resource::get_check_permissions::( + get_check_permissions::( id, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; } ResourceTarget::Procedure(id) => { - resource::get_check_permissions::( + get_check_permissions::( id, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; } ResourceTarget::Action(id) => { - resource::get_check_permissions::( + get_check_permissions::( id, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; } ResourceTarget::ResourceSync(id) => { - resource::get_check_permissions::( + get_check_permissions::( id, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; } ResourceTarget::Stack(id) => { - resource::get_check_permissions::( + get_check_permissions::( id, user, - PermissionLevel::Read, + PermissionLevel::Read.into(), ) .await?; } diff --git a/bin/core/src/api/terminal.rs b/bin/core/src/api/terminal.rs index c79bccea0..4f55a5936 100644 --- a/bin/core/src/api/terminal.rs +++ b/bin/core/src/api/terminal.rs @@ -10,7 +10,8 @@ use serror::Json; use uuid::Uuid; use crate::{ - auth::auth_request, helpers::periphery_client, resource, + auth::auth_request, helpers::periphery_client, + permission::get_check_permissions, }; pub fn router() -> Router { @@ -45,10 +46,10 @@ async fn execute_inner( info!("/terminal request | user: {}", user.username); let res = async { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &server, &user, - PermissionLevel::Write, + PermissionLevel::Read.terminal(), ) .await?; diff --git a/bin/core/src/api/write/action.rs b/bin/core/src/api/write/action.rs index c262e754b..cab3d7fa9 100644 --- a/bin/core/src/api/write/action.rs +++ b/bin/core/src/api/write/action.rs @@ -6,7 +6,7 @@ use komodo_client::{ }; use resolver_api::Resolve; -use crate::resource; +use crate::{permission::get_check_permissions, resource}; use super::WriteArgs; @@ -29,13 +29,12 @@ impl Resolve for CopyAction { self, WriteArgs { user }: &WriteArgs, ) -> serror::Result { - let Action { config, .. } = - resource::get_check_permissions::( - &self.id, - user, - PermissionLevel::Write, - ) - .await?; + let Action { config, .. } = get_check_permissions::( + &self.id, + user, + PermissionLevel::Write.into(), + ) + .await?; Ok( resource::create::(&self.name, config.into(), user) .await?, diff --git a/bin/core/src/api/write/alerter.rs b/bin/core/src/api/write/alerter.rs index e3c1deee5..470ef5612 100644 --- a/bin/core/src/api/write/alerter.rs +++ b/bin/core/src/api/write/alerter.rs @@ -6,7 +6,7 @@ use komodo_client::{ }; use resolver_api::Resolve; -use crate::resource; +use crate::{permission::get_check_permissions, resource}; use super::WriteArgs; @@ -29,13 +29,12 @@ impl Resolve for CopyAlerter { self, WriteArgs { user }: &WriteArgs, ) -> serror::Result { - let Alerter { config, .. } = - resource::get_check_permissions::( - &self.id, - user, - PermissionLevel::Write, - ) - .await?; + let Alerter { config, .. } = get_check_permissions::( + &self.id, + user, + PermissionLevel::Write.into(), + ) + .await?; Ok( resource::create::(&self.name, config.into(), user) .await?, diff --git a/bin/core/src/api/write/build.rs b/bin/core/src/api/write/build.rs index 4e0f609b3..e9889ee60 100644 --- a/bin/core/src/api/write/build.rs +++ b/bin/core/src/api/write/build.rs @@ -36,6 +36,7 @@ use crate::{ query::get_server_with_state, update::{add_update, make_update}, }, + permission::get_check_permissions, resource, state::{db_client, github_client}, }; @@ -61,13 +62,12 @@ impl Resolve for CopyBuild { self, WriteArgs { user }: &WriteArgs, ) -> serror::Result { - let Build { mut config, .. } = - resource::get_check_permissions::( - &self.id, - user, - PermissionLevel::Write, - ) - .await?; + let Build { mut config, .. } = get_check_permissions::( + &self.id, + user, + PermissionLevel::Read.into(), + ) + .await?; // reset version to 0.0.0 config.version = Default::default(); Ok( @@ -107,10 +107,10 @@ impl Resolve for RenameBuild { impl Resolve for WriteBuildFileContents { #[instrument(name = "WriteBuildFileContents", skip(args))] async fn resolve(self, args: &WriteArgs) -> serror::Result { - let build = resource::get_check_permissions::( + let build = get_check_permissions::( &self.build, &args.user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; @@ -294,10 +294,10 @@ impl Resolve for RefreshBuildCache { ) -> serror::Result { // Even though this is a write request, this doesn't change any config. Anyone that can execute the // build should be able to do this. - let build = resource::get_check_permissions::( + let build = get_check_permissions::( &self.build, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -493,10 +493,10 @@ impl Resolve for CreateBuildWebhook { let WriteArgs { user } = args; - let build = resource::get_check_permissions::( + let build = get_check_permissions::( &self.build, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; @@ -606,10 +606,10 @@ impl Resolve for DeleteBuildWebhook { ); }; - let build = resource::get_check_permissions::( + let build = get_check_permissions::( &self.build, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; diff --git a/bin/core/src/api/write/builder.rs b/bin/core/src/api/write/builder.rs index 337d98506..560551f6c 100644 --- a/bin/core/src/api/write/builder.rs +++ b/bin/core/src/api/write/builder.rs @@ -6,7 +6,7 @@ use komodo_client::{ }; use resolver_api::Resolve; -use crate::resource; +use crate::{permission::get_check_permissions, resource}; use super::WriteArgs; @@ -29,13 +29,12 @@ impl Resolve for CopyBuilder { self, WriteArgs { user }: &WriteArgs, ) -> serror::Result { - let Builder { config, .. } = - resource::get_check_permissions::( - &self.id, - user, - PermissionLevel::Write, - ) - .await?; + let Builder { config, .. } = get_check_permissions::( + &self.id, + user, + PermissionLevel::Write.into(), + ) + .await?; Ok( resource::create::(&self.name, config.into(), user) .await?, diff --git a/bin/core/src/api/write/deployment.rs b/bin/core/src/api/write/deployment.rs index 69a02726a..55938d952 100644 --- a/bin/core/src/api/write/deployment.rs +++ b/bin/core/src/api/write/deployment.rs @@ -11,7 +11,7 @@ use komodo_client::{ komodo_timestamp, permission::PermissionLevel, server::{Server, ServerState}, - to_komodo_name, + to_docker_compatible_name, update::Update, }, }; @@ -25,6 +25,7 @@ use crate::{ query::get_deployment_state, update::{add_update, make_update}, }, + permission::get_check_permissions, resource, state::{action_states, db_client, server_status_cache}, }; @@ -51,10 +52,10 @@ impl Resolve for CopyDeployment { WriteArgs { user }: &WriteArgs, ) -> serror::Result { let Deployment { config, .. } = - resource::get_check_permissions::( + get_check_permissions::( &self.id, user, - PermissionLevel::Write, + PermissionLevel::Read.into(), ) .await?; Ok( @@ -70,10 +71,10 @@ impl Resolve for CreateDeploymentFromContainer { self, WriteArgs { user }: &WriteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Write, + PermissionLevel::Read.inspect().attach(), ) .await?; let cache = server_status_cache() @@ -188,10 +189,10 @@ impl Resolve for RenameDeployment { self, WriteArgs { user }: &WriteArgs, ) -> serror::Result { - let deployment = resource::get_check_permissions::( + let deployment = get_check_permissions::( &self.id, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; @@ -206,7 +207,7 @@ impl Resolve for RenameDeployment { let _action_guard = action_state.update(|state| state.renaming = true)?; - let name = to_komodo_name(&self.name); + let name = to_docker_compatible_name(&self.name); let container_state = get_deployment_state(&deployment).await?; diff --git a/bin/core/src/api/write/mod.rs b/bin/core/src/api/write/mod.rs index db26457fd..a489a712e 100644 --- a/bin/core/src/api/write/mod.rs +++ b/bin/core/src/api/write/mod.rs @@ -69,6 +69,7 @@ pub enum WriteRequest { AddUserToUserGroup(AddUserToUserGroup), RemoveUserFromUserGroup(RemoveUserFromUserGroup), SetUsersInUserGroup(SetUsersInUserGroup), + SetEveryoneUserGroup(SetEveryoneUserGroup), // ==== PERMISSIONS ==== UpdateUserAdmin(UpdateUserAdmin), @@ -89,6 +90,17 @@ pub enum WriteRequest { DeleteTerminal(DeleteTerminal), DeleteAllTerminals(DeleteAllTerminals), + // ==== STACK ==== + CreateStack(CreateStack), + CopyStack(CopyStack), + DeleteStack(DeleteStack), + UpdateStack(UpdateStack), + RenameStack(RenameStack), + WriteStackFileContents(WriteStackFileContents), + RefreshStackCache(RefreshStackCache), + CreateStackWebhook(CreateStackWebhook), + DeleteStackWebhook(DeleteStackWebhook), + // ==== DEPLOYMENT ==== CreateDeployment(CreateDeployment), CopyDeployment(CopyDeployment), @@ -158,17 +170,6 @@ pub enum WriteRequest { CreateSyncWebhook(CreateSyncWebhook), DeleteSyncWebhook(DeleteSyncWebhook), - // ==== STACK ==== - CreateStack(CreateStack), - CopyStack(CopyStack), - DeleteStack(DeleteStack), - UpdateStack(UpdateStack), - RenameStack(RenameStack), - WriteStackFileContents(WriteStackFileContents), - RefreshStackCache(RefreshStackCache), - CreateStackWebhook(CreateStackWebhook), - DeleteStackWebhook(DeleteStackWebhook), - // ==== TAG ==== CreateTag(CreateTag), DeleteTag(DeleteTag), diff --git a/bin/core/src/api/write/permissions.rs b/bin/core/src/api/write/permissions.rs index b9b6c5223..ed641ab63 100644 --- a/bin/core/src/api/write/permissions.rs +++ b/bin/core/src/api/write/permissions.rs @@ -11,7 +11,7 @@ use komodo_client::{ use mungos::{ by_id::{find_one_by_id, update_one_by_id}, mongodb::{ - bson::{Document, doc, oid::ObjectId}, + bson::{Document, doc, oid::ObjectId, to_bson}, options::UpdateOptions, }, }; @@ -65,6 +65,10 @@ impl Resolve for UpdateUserBasePermissions { self, WriteArgs { user: admin }: &WriteArgs, ) -> serror::Result { + if !admin.admin { + return Err(anyhow!("this method is admin only").into()); + } + let UpdateUserBasePermissions { user_id, enabled, @@ -72,10 +76,6 @@ impl Resolve for UpdateUserBasePermissions { create_builds, } = self; - if !admin.admin { - return Err(anyhow!("this method is admin only").into()); - } - let user = find_one_by_id(&db_client().users, &user_id) .await .context("failed to query mongo for user")? @@ -122,16 +122,16 @@ impl Resolve for UpdatePermissionOnResourceType { self, WriteArgs { user: admin }: &WriteArgs, ) -> serror::Result { - let UpdatePermissionOnResourceType { + if !admin.admin { + return Err(anyhow!("this method is admin only").into()); + } + + let Self { user_target, resource_type, permission, } = self; - if !admin.admin { - return Err(anyhow!("this method is admin only").into()); - } - // Some extra checks if user target is an actual User if let UserTarget::User(user_id) = &user_target { let user = get_user(user_id).await?; @@ -153,9 +153,11 @@ impl Resolve for UpdatePermissionOnResourceType { let id = ObjectId::from_str(&user_target_id) .context("id is not ObjectId")?; - let field = format!("all.{resource_type}"); let filter = doc! { "_id": id }; - let update = doc! { "$set": { &field: permission.as_ref() } }; + let field = format!("all.{resource_type}"); + let set = + to_bson(&permission).context("permission is not Bson")?; + let update = doc! { "$set": { &field: &set } }; match user_target_variant { UserTargetVariant::User => { @@ -164,7 +166,7 @@ impl Resolve for UpdatePermissionOnResourceType { .update_one(filter, update) .await .with_context(|| { - format!("failed to set {field}: {permission} on db") + format!("failed to set {field}: {set} on db") })?; } UserTargetVariant::UserGroup => { @@ -173,7 +175,7 @@ impl Resolve for UpdatePermissionOnResourceType { .update_one(filter, update) .await .with_context(|| { - format!("failed to set {field}: {permission} on db") + format!("failed to set {field}: {set} on db") })?; } } @@ -188,19 +190,22 @@ impl Resolve for UpdatePermissionOnTarget { self, WriteArgs { user: admin }: &WriteArgs, ) -> serror::Result { + if !admin.admin { + return Err(anyhow!("this method is admin only").into()); + } + let UpdatePermissionOnTarget { user_target, resource_target, permission, } = self; - if !admin.admin { - return Err(anyhow!("this method is admin only").into()); - } - - // Some extra checks if user target is an actual User + // Some extra checks relevant if user target is an actual User if let UserTarget::User(user_id) = &user_target { let user = get_user(user_id).await?; + if !user.enabled { + return Err(anyhow!("user not enabled").into()); + } if user.admin { return Err( anyhow!( @@ -209,9 +214,6 @@ impl Resolve for UpdatePermissionOnTarget { .into(), ); } - if !user.enabled { - return Err(anyhow!("user not enabled").into()); - } } let (user_target_variant, user_target_id) = @@ -223,6 +225,9 @@ impl Resolve for UpdatePermissionOnTarget { let (user_target_variant, resource_variant) = (user_target_variant.as_ref(), resource_variant.as_ref()); + let specific = to_bson(&permission.specific) + .context("permission.specific is not valid Bson")?; + db_client() .permissions .update_one( @@ -238,7 +243,8 @@ impl Resolve for UpdatePermissionOnTarget { "user_target.id": user_target_id, "resource_target.type": resource_variant, "resource_target.id": resource_id, - "level": permission.as_ref(), + "level": permission.level.as_ref(), + "specific": specific } }, ) diff --git a/bin/core/src/api/write/procedure.rs b/bin/core/src/api/write/procedure.rs index 89c6539e1..e98ead1c7 100644 --- a/bin/core/src/api/write/procedure.rs +++ b/bin/core/src/api/write/procedure.rs @@ -6,7 +6,7 @@ use komodo_client::{ }; use resolver_api::Resolve; -use crate::resource; +use crate::{permission::get_check_permissions, resource}; use super::WriteArgs; @@ -30,10 +30,10 @@ impl Resolve for CopyProcedure { WriteArgs { user }: &WriteArgs, ) -> serror::Result { let Procedure { config, .. } = - resource::get_check_permissions::( + get_check_permissions::( &self.id, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; Ok( diff --git a/bin/core/src/api/write/repo.rs b/bin/core/src/api/write/repo.rs index 99f271848..15497ae43 100644 --- a/bin/core/src/api/write/repo.rs +++ b/bin/core/src/api/write/repo.rs @@ -10,7 +10,7 @@ use komodo_client::{ permission::PermissionLevel, repo::{PartialRepoConfig, Repo, RepoInfo}, server::Server, - to_komodo_name, + to_path_compatible_name, update::{Log, Update}, }, }; @@ -28,6 +28,7 @@ use crate::{ git_token, periphery_client, update::{add_update, make_update}, }, + permission::get_check_permissions, resource, state::{action_states, db_client, github_client}, }; @@ -50,13 +51,12 @@ impl Resolve for CopyRepo { self, WriteArgs { user }: &WriteArgs, ) -> serror::Result { - let Repo { config, .. } = - resource::get_check_permissions::( - &self.id, - user, - PermissionLevel::Write, - ) - .await?; + let Repo { config, .. } = get_check_permissions::( + &self.id, + user, + PermissionLevel::Read.into(), + ) + .await?; Ok( resource::create::(&self.name, config.into(), user) .await?, @@ -87,10 +87,10 @@ impl Resolve for RenameRepo { self, WriteArgs { user }: &WriteArgs, ) -> serror::Result { - let repo = resource::get_check_permissions::( + let repo = get_check_permissions::( &self.id, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; @@ -111,7 +111,7 @@ impl Resolve for RenameRepo { let _action_guard = action_state.update(|state| state.renaming = true)?; - let name = to_komodo_name(&self.name); + let name = to_path_compatible_name(&self.name); let mut update = make_update(&repo, Operation::RenameRepo, user); @@ -131,7 +131,7 @@ impl Resolve for RenameRepo { let log = match periphery_client(&server)? .request(api::git::RenameRepo { - curr_name: to_komodo_name(&repo.name), + curr_name: to_path_compatible_name(&repo.name), new_name: name.clone(), }) .await @@ -169,10 +169,10 @@ impl Resolve for RefreshRepoCache { ) -> serror::Result { // Even though this is a write request, this doesn't change any config. Anyone that can execute the // repo should be able to do this. - let repo = resource::get_check_permissions::( + let repo = get_check_permissions::( &self.repo, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -257,10 +257,10 @@ impl Resolve for CreateRepoWebhook { ); }; - let repo = resource::get_check_permissions::( + let repo = get_check_permissions::( &self.repo, &args.user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; @@ -380,10 +380,10 @@ impl Resolve for DeleteRepoWebhook { ); }; - let repo = resource::get_check_permissions::( + let repo = get_check_permissions::( &self.repo, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; diff --git a/bin/core/src/api/write/server.rs b/bin/core/src/api/write/server.rs index ecbce29ea..e2150db04 100644 --- a/bin/core/src/api/write/server.rs +++ b/bin/core/src/api/write/server.rs @@ -6,6 +6,7 @@ use komodo_client::{ NoData, Operation, permission::PermissionLevel, server::Server, + to_docker_compatible_name, update::{Update, UpdateStatus}, }, }; @@ -17,6 +18,7 @@ use crate::{ periphery_client, update::{add_update, make_update, update_update}, }, + permission::get_check_permissions, resource, }; @@ -68,10 +70,10 @@ impl Resolve for CreateNetwork { self, WriteArgs { user }: &WriteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; @@ -84,7 +86,7 @@ impl Resolve for CreateNetwork { match periphery .request(api::network::CreateNetwork { - name: self.name, + name: to_docker_compatible_name(&self.name), driver: None, }) .await @@ -109,10 +111,10 @@ impl Resolve for CreateTerminal { self, WriteArgs { user }: &WriteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Write, + PermissionLevel::Write.terminal(), ) .await?; @@ -137,10 +139,10 @@ impl Resolve for DeleteTerminal { self, WriteArgs { user }: &WriteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Write, + PermissionLevel::Write.terminal(), ) .await?; @@ -163,10 +165,10 @@ impl Resolve for DeleteAllTerminals { self, WriteArgs { user }: &WriteArgs, ) -> serror::Result { - let server = resource::get_check_permissions::( + let server = get_check_permissions::( &self.server, user, - PermissionLevel::Write, + PermissionLevel::Write.terminal(), ) .await?; diff --git a/bin/core/src/api/write/stack.rs b/bin/core/src/api/write/stack.rs index 329d93760..a7b133fed 100644 --- a/bin/core/src/api/write/stack.rs +++ b/bin/core/src/api/write/stack.rs @@ -30,6 +30,7 @@ use crate::{ query::get_server_with_state, update::{add_update, make_update}, }, + permission::get_check_permissions, resource, stack::{ get_stack_and_server, @@ -60,13 +61,12 @@ impl Resolve for CopyStack { self, WriteArgs { user }: &WriteArgs, ) -> serror::Result { - let Stack { config, .. } = - resource::get_check_permissions::( - &self.id, - user, - PermissionLevel::Write, - ) - .await?; + let Stack { config, .. } = get_check_permissions::( + &self.id, + user, + PermissionLevel::Read.into(), + ) + .await?; Ok( resource::create::(&self.name, config.into(), user) .await?, @@ -115,7 +115,7 @@ impl Resolve for WriteStackFileContents { let (mut stack, server) = get_stack_and_server( &stack, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), true, ) .await?; @@ -229,10 +229,10 @@ impl Resolve for RefreshStackCache { ) -> serror::Result { // Even though this is a write request, this doesn't change any config. Anyone that can execute the // stack should be able to do this. - let stack = resource::get_check_permissions::( + let stack = get_check_permissions::( &self.stack, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; @@ -432,10 +432,10 @@ impl Resolve for CreateStackWebhook { ); }; - let stack = resource::get_check_permissions::( + let stack = get_check_permissions::( &self.stack, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; @@ -552,10 +552,10 @@ impl Resolve for DeleteStackWebhook { ); }; - let stack = resource::get_check_permissions::( + let stack = get_check_permissions::( &self.stack, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; diff --git a/bin/core/src/api/write/sync.rs b/bin/core/src/api/write/sync.rs index a1c9ce581..382843131 100644 --- a/bin/core/src/api/write/sync.rs +++ b/bin/core/src/api/write/sync.rs @@ -24,7 +24,7 @@ use komodo_client::{ PartialResourceSyncConfig, ResourceSync, ResourceSyncInfo, SyncDeployUpdate, }, - to_komodo_name, + to_path_compatible_name, update::{Log, Update}, user::sync_user, }, @@ -48,6 +48,7 @@ use crate::{ query::get_id_to_tags, update::{add_update, make_update, update_update}, }, + permission::get_check_permissions, resource, state::{db_client, github_client}, sync::{ @@ -78,10 +79,10 @@ impl Resolve for CopyResourceSync { WriteArgs { user }: &WriteArgs, ) -> serror::Result { let ResourceSync { config, .. } = - resource::get_check_permissions::( + get_check_permissions::( &self.id, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; Ok( @@ -134,10 +135,10 @@ impl Resolve for RenameResourceSync { impl Resolve for WriteSyncFileContents { #[instrument(name = "WriteSyncFileContents", skip(args))] async fn resolve(self, args: &WriteArgs) -> serror::Result { - let sync = resource::get_check_permissions::( + let sync = get_check_permissions::( &self.sync, &args.user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; @@ -178,7 +179,7 @@ async fn write_sync_file_contents_on_host( let root = core_config() .sync_directory - .join(to_komodo_name(&sync.name)); + .join(to_path_compatible_name(&sync.name)); let file_path = file_path.parse::().context("Invalid file path")?; let resource_path = resource_path @@ -345,9 +346,11 @@ impl Resolve for CommitSync { async fn resolve(self, args: &WriteArgs) -> serror::Result { let WriteArgs { user } = args; - let sync = resource::get_check_permissions::< - entities::sync::ResourceSync, - >(&self.sync, user, PermissionLevel::Write) + let sync = get_check_permissions::( + &self.sync, + user, + PermissionLevel::Write.into(), + ) .await?; let file_contents_empty = sync.config.file_contents_empty(); @@ -411,7 +414,7 @@ impl Resolve for CommitSync { }; let file_path = core_config() .sync_directory - .join(to_komodo_name(&sync.name)) + .join(to_path_compatible_name(&sync.name)) .join(&resource_path); if let Some(parent) = file_path.parent() { fs::create_dir_all(parent) @@ -514,10 +517,13 @@ impl Resolve for RefreshResourceSyncPending { ) -> serror::Result { // Even though this is a write request, this doesn't change any config. Anyone that can execute the // sync should be able to do this. - let mut sync = resource::get_check_permissions::< - entities::sync::ResourceSync, - >(&self.sync, user, PermissionLevel::Execute) - .await?; + let mut sync = + get_check_permissions::( + &self.sync, + user, + PermissionLevel::Execute.into(), + ) + .await?; if !sync.config.managed && !sync.config.files_on_host @@ -864,10 +870,10 @@ impl Resolve for CreateSyncWebhook { ); }; - let sync = resource::get_check_permissions::( + let sync = get_check_permissions::( &self.sync, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; @@ -984,10 +990,10 @@ impl Resolve for DeleteSyncWebhook { ); }; - let sync = resource::get_check_permissions::( + let sync = get_check_permissions::( &self.sync, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; diff --git a/bin/core/src/api/write/tag.rs b/bin/core/src/api/write/tag.rs index 7605d0f1d..7c3079d4a 100644 --- a/bin/core/src/api/write/tag.rs +++ b/bin/core/src/api/write/tag.rs @@ -30,6 +30,7 @@ use resolver_api::Resolve; use crate::{ helpers::query::{get_tag, get_tag_check_owner}, + permission::get_check_permissions, resource, state::db_client, }; @@ -150,94 +151,94 @@ impl Resolve for UpdateTagsOnResource { return Err(anyhow!("Invalid target type: System").into()); } ResourceTarget::Build(id) => { - resource::get_check_permissions::( + get_check_permissions::( &id, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; resource::update_tags::(&id, self.tags, args).await?; } ResourceTarget::Builder(id) => { - resource::get_check_permissions::( + get_check_permissions::( &id, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; resource::update_tags::(&id, self.tags, args).await? } ResourceTarget::Deployment(id) => { - resource::get_check_permissions::( + get_check_permissions::( &id, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; resource::update_tags::(&id, self.tags, args) .await? } ResourceTarget::Server(id) => { - resource::get_check_permissions::( + get_check_permissions::( &id, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; resource::update_tags::(&id, self.tags, args).await? } ResourceTarget::Repo(id) => { - resource::get_check_permissions::( + get_check_permissions::( &id, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; resource::update_tags::(&id, self.tags, args).await? } ResourceTarget::Alerter(id) => { - resource::get_check_permissions::( + get_check_permissions::( &id, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; resource::update_tags::(&id, self.tags, args).await? } ResourceTarget::Procedure(id) => { - resource::get_check_permissions::( + get_check_permissions::( &id, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; resource::update_tags::(&id, self.tags, args) .await? } ResourceTarget::Action(id) => { - resource::get_check_permissions::( + get_check_permissions::( &id, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; resource::update_tags::(&id, self.tags, args).await? } ResourceTarget::ResourceSync(id) => { - resource::get_check_permissions::( + get_check_permissions::( &id, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; resource::update_tags::(&id, self.tags, args) .await? } ResourceTarget::Stack(id) => { - resource::get_check_permissions::( + get_check_permissions::( &id, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; resource::update_tags::(&id, self.tags, args).await? diff --git a/bin/core/src/api/write/user_group.rs b/bin/core/src/api/write/user_group.rs index 7b982a6e4..3b7d9a680 100644 --- a/bin/core/src/api/write/user_group.rs +++ b/bin/core/src/api/write/user_group.rs @@ -2,10 +2,7 @@ use std::{collections::HashMap, str::FromStr}; use anyhow::{Context, anyhow}; use komodo_client::{ - api::write::{ - AddUserToUserGroup, CreateUserGroup, DeleteUserGroup, - RemoveUserFromUserGroup, RenameUserGroup, SetUsersInUserGroup, - }, + api::write::*, entities::{komodo_timestamp, user_group::UserGroup}, }; use mungos::{ @@ -20,6 +17,7 @@ use crate::state::db_client; use super::WriteArgs; impl Resolve for CreateUserGroup { + #[instrument(name = "CreateUserGroup", skip(admin), fields(admin = admin.username))] async fn resolve( self, WriteArgs { user: admin }: &WriteArgs, @@ -28,11 +26,12 @@ impl Resolve for CreateUserGroup { return Err(anyhow!("This call is admin-only").into()); } let user_group = UserGroup { + name: self.name, id: Default::default(), + everyone: Default::default(), users: Default::default(), all: Default::default(), updated_at: komodo_timestamp(), - name: self.name, }; let db = db_client(); let id = db @@ -53,6 +52,7 @@ impl Resolve for CreateUserGroup { } impl Resolve for RenameUserGroup { + #[instrument(name = "RenameUserGroup", skip(admin), fields(admin = admin.username))] async fn resolve( self, WriteArgs { user: admin }: &WriteArgs, @@ -78,6 +78,7 @@ impl Resolve for RenameUserGroup { } impl Resolve for DeleteUserGroup { + #[instrument(name = "DeleteUserGroup", skip(admin), fields(admin = admin.username))] async fn resolve( self, WriteArgs { user: admin }: &WriteArgs, @@ -110,6 +111,7 @@ impl Resolve for DeleteUserGroup { } impl Resolve for AddUserToUserGroup { + #[instrument(name = "AddUserToUserGroup", skip(admin), fields(admin = admin.username))] async fn resolve( self, WriteArgs { user: admin }: &WriteArgs, @@ -153,6 +155,7 @@ impl Resolve for AddUserToUserGroup { } impl Resolve for RemoveUserFromUserGroup { + #[instrument(name = "RemoveUserFromUserGroup", skip(admin), fields(admin = admin.username))] async fn resolve( self, WriteArgs { user: admin }: &WriteArgs, @@ -196,6 +199,7 @@ impl Resolve for RemoveUserFromUserGroup { } impl Resolve for SetUsersInUserGroup { + #[instrument(name = "SetUsersInUserGroup", skip(admin), fields(admin = admin.username))] async fn resolve( self, WriteArgs { user: admin }: &WriteArgs, @@ -240,3 +244,33 @@ impl Resolve for SetUsersInUserGroup { Ok(res) } } + +impl Resolve for SetEveryoneUserGroup { + #[instrument(name = "SetEveryoneUserGroup", skip(admin), fields(admin = admin.username))] + async fn resolve( + self, + WriteArgs { user: admin }: &WriteArgs, + ) -> serror::Result { + if !admin.admin { + return Err(anyhow!("This call is admin-only").into()); + } + + let db = db_client(); + + let filter = match ObjectId::from_str(&self.user_group) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": &self.user_group }, + }; + db.user_groups + .update_one(filter.clone(), doc! { "$set": { "everyone": self.everyone } }) + .await + .context("failed to set everyone on user group")?; + let res = db + .user_groups + .find_one(filter) + .await + .context("failed to query db for UserGroups")? + .context("no user group with given id")?; + Ok(res) + } +} diff --git a/bin/core/src/config.rs b/bin/core/src/config.rs index 0115a8a4a..d4ae79df6 100644 --- a/bin/core/src/config.rs +++ b/bin/core/src/config.rs @@ -199,6 +199,7 @@ pub fn core_config() -> &'static CoreConfig { .komodo_logging_opentelemetry_service_name .unwrap_or(config.logging.opentelemetry_service_name), }, + pretty_startup_config: env.komodo_pretty_startup_config.unwrap_or(config.pretty_startup_config), ssl_enabled: env.komodo_ssl_enabled.unwrap_or(config.ssl_enabled), ssl_key_file: env.komodo_ssl_key_file.unwrap_or(config.ssl_key_file), ssl_cert_file: env.komodo_ssl_cert_file.unwrap_or(config.ssl_cert_file), diff --git a/bin/core/src/helpers/matcher.rs b/bin/core/src/helpers/matcher.rs new file mode 100644 index 000000000..2654151b4 --- /dev/null +++ b/bin/core/src/helpers/matcher.rs @@ -0,0 +1,32 @@ +use anyhow::Context; + +pub enum Matcher<'a> { + Wildcard(wildcard::Wildcard<'a>), + Regex(regex::Regex), +} + +impl<'a> Matcher<'a> { + pub fn new(pattern: &'a str) -> anyhow::Result { + if pattern.starts_with('\\') && pattern.ends_with('\\') { + let inner = &pattern[1..(pattern.len() - 1)]; + let regex = regex::Regex::new(inner) + .with_context(|| format!("invalid regex. got: {inner}"))?; + Ok(Self::Regex(regex)) + } else { + let wildcard = wildcard::Wildcard::new(pattern.as_bytes()) + .with_context(|| { + format!("invalid wildcard. got: {pattern}") + })?; + Ok(Self::Wildcard(wildcard)) + } + } + + pub fn is_match(&self, source: &str) -> bool { + match self { + Matcher::Wildcard(wildcard) => { + wildcard.is_match(source.as_bytes()) + } + Matcher::Regex(regex) => regex.is_match(source), + } + } +} diff --git a/bin/core/src/helpers/mod.rs b/bin/core/src/helpers/mod.rs index c58f12616..47a72abce 100644 --- a/bin/core/src/helpers/mod.rs +++ b/bin/core/src/helpers/mod.rs @@ -1,9 +1,12 @@ use std::time::Duration; use anyhow::{Context, anyhow}; +use indexmap::IndexSet; use komodo_client::entities::{ ResourceTarget, - permission::{Permission, PermissionLevel, UserTarget}, + permission::{ + Permission, PermissionLevel, SpecificPermission, UserTarget, + }, server::Server, user::User, }; @@ -19,6 +22,7 @@ pub mod builder; pub mod cache; pub mod channel; pub mod interpolate; +pub mod matcher; pub mod procedure; pub mod prune; pub mod query; @@ -147,6 +151,7 @@ pub async fn create_permission( user: &User, target: T, level: PermissionLevel, + specific: IndexSet, ) where T: Into + std::fmt::Debug, { @@ -162,6 +167,7 @@ pub async fn create_permission( user_target: UserTarget::User(user.id.clone()), resource_target: target.clone(), level, + specific, }) .await { diff --git a/bin/core/src/helpers/procedure.rs b/bin/core/src/helpers/procedure.rs index ad346946d..01d785604 100644 --- a/bin/core/src/helpers/procedure.rs +++ b/bin/core/src/helpers/procedure.rs @@ -9,6 +9,7 @@ use komodo_client::{ action::Action, build::Build, deployment::Deployment, + permission::PermissionLevel, procedure::Procedure, repo::Repo, stack::Stack, @@ -1189,6 +1190,7 @@ async fn extend_batch_exection( pattern, Default::default(), procedure_user(), + PermissionLevel::Read.into(), &[], ) .await? diff --git a/bin/core/src/helpers/query.rs b/bin/core/src/helpers/query.rs index 96148d021..0c7a387b7 100644 --- a/bin/core/src/helpers/query.rs +++ b/bin/core/src/helpers/query.rs @@ -14,7 +14,7 @@ use komodo_client::entities::{ builder::Builder, deployment::{Deployment, DeploymentState}, docker::container::{ContainerListItem, ContainerStateStatusEnum}, - permission::PermissionLevel, + permission::{PermissionLevel, PermissionLevelAndSpecifics}, procedure::Procedure, repo::Repo, server::{Server, ServerState}, @@ -39,7 +39,8 @@ use tokio::sync::Mutex; use crate::{ config::core_config, - resource::{self, get_user_permission_on_resource}, + permission::get_user_permission_on_resource, + resource, stack::compose_container_match_regex, state::{db_client, deployment_status_cache, stack_status_cache}, }; @@ -238,7 +239,10 @@ pub async fn get_user_user_groups( find_collect( &db_client().user_groups, doc! { - "users": user_id + "$or": [ + { "everyone": true }, + { "users": user_id }, + ] }, None, ) @@ -277,9 +281,9 @@ pub fn user_target_query( pub async fn get_user_permission_on_target( user: &User, target: &ResourceTarget, -) -> anyhow::Result { +) -> anyhow::Result { match target { - ResourceTarget::System(_) => Ok(PermissionLevel::None), + ResourceTarget::System(_) => Ok(PermissionLevel::None.into()), ResourceTarget::Build(id) => { get_user_permission_on_resource::(user, id).await } diff --git a/bin/core/src/main.rs b/bin/core/src/main.rs index a0b728201..b3c752fb7 100644 --- a/bin/core/src/main.rs +++ b/bin/core/src/main.rs @@ -22,6 +22,7 @@ mod db; mod helpers; mod listener; mod monitor; +mod permission; mod resource; mod schedule; mod stack; @@ -43,7 +44,12 @@ async fn app() -> anyhow::Result<()> { }; info!("Komodo Core version: v{}", env!("CARGO_PKG_VERSION")); - info!("{:?}", config.sanitized()); + + if core_config().pretty_startup_config { + info!("{:#?}", config.sanitized()); + } else { + info!("{:?}", config.sanitized()); + } // Init jwt client to crash on failure state::jwt_client(); @@ -55,7 +61,7 @@ async fn app() -> anyhow::Result<()> { ); // Run after db connection. startup::on_startup().await; - + // Spawn background tasks monitor::spawn_monitor_loop(); resource::spawn_resource_refresh_loop(); diff --git a/bin/core/src/monitor/alert/mod.rs b/bin/core/src/monitor/alert/mod.rs index 8a851d6e2..1981d2aa7 100644 --- a/bin/core/src/monitor/alert/mod.rs +++ b/bin/core/src/monitor/alert/mod.rs @@ -2,7 +2,8 @@ use std::collections::HashMap; use anyhow::Context; use komodo_client::entities::{ - resource::ResourceQuery, server::Server, user::User, + permission::PermissionLevel, resource::ResourceQuery, + server::Server, user::User, }; use crate::resource; @@ -39,6 +40,7 @@ async fn get_all_servers_map() admin: true, ..Default::default() }, + PermissionLevel::Read.into(), &[], ) .await diff --git a/bin/core/src/permission.rs b/bin/core/src/permission.rs new file mode 100644 index 000000000..5d37954cf --- /dev/null +++ b/bin/core/src/permission.rs @@ -0,0 +1,229 @@ +use std::collections::HashSet; + +use anyhow::{Context, anyhow}; +use futures::{FutureExt, future::BoxFuture}; +use indexmap::IndexSet; +use komodo_client::{ + api::read::GetPermission, + entities::{ + permission::{PermissionLevel, PermissionLevelAndSpecifics}, + resource::Resource, + user::User, + }, +}; +use mongo_indexed::doc; +use mungos::find::find_collect; +use resolver_api::Resolve; + +use crate::{ + api::read::ReadArgs, + config::core_config, + helpers::query::{get_user_user_groups, user_target_query}, + resource::{KomodoResource, get}, + state::db_client, +}; + +pub async fn get_check_permissions( + id_or_name: &str, + user: &User, + required_permissions: PermissionLevelAndSpecifics, +) -> anyhow::Result> { + let resource = get::(id_or_name).await?; + + // Allow all if admin + if user.admin { + return Ok(resource); + } + + let user_permissions = + get_user_permission_on_resource::(user, &resource.id).await?; + + if ( + // Allow if its just read or below, and transparent mode enabled + (required_permissions.level <= PermissionLevel::Read && core_config().transparent_mode) + // Allow if resource has base permission level greater than or equal to required permission level + || resource.base_permission.level >= required_permissions.level + ) && user_permissions + .fulfills_specific(&required_permissions.specific) + { + return Ok(resource); + } + + if user_permissions.fulfills(&required_permissions) { + Ok(resource) + } else { + Err(anyhow!( + "User does not have required permissions on this {}. Must have at least {} permissions{}", + T::resource_type(), + required_permissions.level, + if required_permissions.specific.is_empty() { + String::new() + } else { + format!( + ", as well as these specific permissions: [{}]", + required_permissions.specifics_for_log() + ) + } + )) + } +} + +#[instrument(level = "debug")] +pub fn get_user_permission_on_resource<'a, T: KomodoResource>( + user: &'a User, + resource_id: &'a str, +) -> BoxFuture<'a, anyhow::Result> { + Box::pin(async { + // Admin returns early with max permissions + if user.admin { + return Ok(PermissionLevel::Write.all()); + } + + let resource_type = T::resource_type(); + let resource = get::(resource_id).await?; + let initial_specific = if let Some(additional_target) = + T::inherit_specific_permissions_from(&resource) + { + GetPermission { + target: additional_target, + } + .resolve(&ReadArgs { user: user.clone() }) + .await + .map_err(|e| e.error) + .context("failed to get user permission on additional target")? + .specific + } else { + IndexSet::new() + }; + + let mut permission = PermissionLevelAndSpecifics { + level: if core_config().transparent_mode { + PermissionLevel::Read + } else { + PermissionLevel::None + }, + specific: initial_specific, + }; + + // Add in the resource level global base permissions + if resource.base_permission.level > permission.level { + permission.level = resource.base_permission.level; + } + permission + .specific + .extend(resource.base_permission.specific); + + // Overlay users base on resource variant + if let Some(user_permission) = + user.all.get(&resource_type).cloned() + { + if user_permission.level > permission.level { + permission.level = user_permission.level; + } + permission.specific.extend(user_permission.specific); + } + + // Overlay any user groups base on resource variant + let groups = get_user_user_groups(&user.id).await?; + for group in &groups { + if let Some(group_permission) = + group.all.get(&resource_type).cloned() + { + if group_permission.level > permission.level { + permission.level = group_permission.level; + } + permission.specific.extend(group_permission.specific); + } + } + + // Overlay any specific permissions + let permission = find_collect( + &db_client().permissions, + doc! { + "$or": user_target_query(&user.id, &groups)?, + "resource_target.type": resource_type.as_ref(), + "resource_target.id": resource_id + }, + None, + ) + .await + .context("failed to query db for permissions")? + .into_iter() + // get the max resource permission user has between personal / any user groups + .fold(permission, |mut permission, resource_permission| { + if resource_permission.level > permission.level { + permission.level = resource_permission.level + } + permission.specific.extend(resource_permission.specific); + permission + }); + Ok(permission) + }) +} + +/// Returns None if still no need to filter by resource id (eg transparent mode, group membership with all access). +#[instrument(level = "debug")] +pub async fn get_resource_ids_for_user( + user: &User, +) -> anyhow::Result>> { + // Check admin or transparent mode + if user.admin || core_config().transparent_mode { + return Ok(None); + } + + let resource_type = T::resource_type(); + + // Check user 'all' on variant + if let Some(permission) = user.all.get(&resource_type).cloned() { + if permission.level > PermissionLevel::None { + return Ok(None); + } + } + + // Check user groups 'all' on variant + let groups = get_user_user_groups(&user.id).await?; + for group in &groups { + if let Some(permission) = group.all.get(&resource_type).cloned() { + if permission.level > PermissionLevel::None { + return Ok(None); + } + } + } + + let (base, perms) = tokio::try_join!( + // Get any resources with non-none base permission, + find_collect( + T::coll(), + doc! { "$or": [ + { "base_permission": { "$in": ["Read", "Execute", "Write"] } }, + { "base_permission.level": { "$in": ["Read", "Execute", "Write"] } } + ] }, + None, + ) + .map(|res| res.with_context(|| format!( + "failed to query {resource_type} on db" + ))), + // And any ids using the permissions table + find_collect( + &db_client().permissions, + doc! { + "$or": user_target_query(&user.id, &groups)?, + "resource_target.type": resource_type.as_ref(), + "level": { "$in": ["Read", "Execute", "Write"] } + }, + None, + ) + .map(|res| res.context("failed to query permissions on db")) + )?; + + // Add specific ids + let ids = perms + .into_iter() + .map(|p| p.resource_target.extract_variant_id().1.to_string()) + // Chain in the ones with non-None base permissions + .chain(base.into_iter().map(|res| res.id)) + // collect into hashset first to remove any duplicates + .collect::>(); + + Ok(Some(ids.into_iter().collect())) +} diff --git a/bin/core/src/resource/build.rs b/bin/core/src/resource/build.rs index af6862b19..53357f112 100644 --- a/bin/core/src/resource/build.rs +++ b/bin/core/src/resource/build.rs @@ -15,6 +15,7 @@ use komodo_client::{ environment_vars_from_str, optional_string, permission::PermissionLevel, resource::Resource, + to_docker_compatible_name, update::Update, user::{User, build_user}, }, @@ -48,6 +49,10 @@ impl super::KomodoResource for Build { ResourceTarget::Build(id.into()) } + fn validated_name(name: &str) -> String { + to_docker_compatible_name(name) + } + fn coll() -> &'static Collection> { &db_client().builds @@ -214,7 +219,7 @@ async fn validate_config( let builder = super::get_check_permissions::( builder_id, user, - PermissionLevel::Read, + PermissionLevel::Read.attach(), ) .await .context("Cannot attach Build to this Builder")?; diff --git a/bin/core/src/resource/builder.rs b/bin/core/src/resource/builder.rs index 15fba7ffa..fd59cd7a9 100644 --- a/bin/core/src/resource/builder.rs +++ b/bin/core/src/resource/builder.rs @@ -1,4 +1,5 @@ use anyhow::Context; +use indexmap::IndexSet; use komodo_client::entities::{ MergePartial, Operation, ResourceTarget, ResourceTargetVariant, builder::{ @@ -6,7 +7,7 @@ use komodo_client::entities::{ BuilderListItem, BuilderListItemInfo, BuilderQuerySpecifics, PartialBuilderConfig, PartialServerBuilderConfig, }, - permission::PermissionLevel, + permission::{PermissionLevel, SpecificPermission}, resource::Resource, server::Server, update::Update, @@ -35,6 +36,10 @@ impl super::KomodoResource for Builder { ResourceTarget::Builder(id.into()) } + fn creator_specific_permissions() -> IndexSet { + [SpecificPermission::Attach].into_iter().collect() + } + fn coll() -> &'static Collection> { &db_client().builders @@ -180,7 +185,7 @@ async fn validate_config( let server = super::get_check_permissions::( server_id, user, - PermissionLevel::Write, + PermissionLevel::Read.attach(), ) .await?; *server_id = server.id; diff --git a/bin/core/src/resource/deployment.rs b/bin/core/src/resource/deployment.rs index 0085009e2..12e69e03b 100644 --- a/bin/core/src/resource/deployment.rs +++ b/bin/core/src/resource/deployment.rs @@ -1,5 +1,6 @@ use anyhow::Context; use formatting::format_serror; +use indexmap::IndexSet; use komodo_client::entities::{ Operation, ResourceTarget, ResourceTargetVariant, build::Build, @@ -10,9 +11,10 @@ use komodo_client::entities::{ PartialDeploymentConfig, conversions_from_str, }, environment_vars_from_str, - permission::PermissionLevel, + permission::{PermissionLevel, SpecificPermission}, resource::Resource, server::Server, + to_docker_compatible_name, update::Update, user::User, }; @@ -47,6 +49,26 @@ impl super::KomodoResource for Deployment { ResourceTarget::Deployment(id.into()) } + fn validated_name(name: &str) -> String { + to_docker_compatible_name(name) + } + + fn creator_specific_permissions() -> IndexSet { + [ + SpecificPermission::Inspect, + SpecificPermission::Logs, + SpecificPermission::Terminal, + ] + .into_iter() + .collect() + } + + fn inherit_specific_permissions_from( + _self: &Resource, + ) -> Option { + ResourceTarget::Server(_self.config.server_id.clone()).into() + } + fn coll() -> &'static Collection> { &db_client().deployments @@ -284,7 +306,7 @@ async fn validate_config( let server = get_check_permissions::( server_id, user, - PermissionLevel::Write, + PermissionLevel::Read.attach(), ) .await .context("Cannot attach Deployment to this Server")?; @@ -298,7 +320,7 @@ async fn validate_config( let build = get_check_permissions::( build_id, user, - PermissionLevel::Read, + PermissionLevel::Read.attach(), ) .await .context( diff --git a/bin/core/src/resource/mod.rs b/bin/core/src/resource/mod.rs index b45b97070..450774605 100644 --- a/bin/core/src/resource/mod.rs +++ b/bin/core/src/resource/mod.rs @@ -5,16 +5,20 @@ use std::{ use anyhow::{Context, anyhow}; use formatting::format_serror; -use futures::{FutureExt, future::join_all}; +use futures::future::join_all; +use indexmap::IndexSet; use komodo_client::{ api::{read::ExportResourcesToToml, write::CreateTag}, entities::{ Operation, ResourceTarget, ResourceTargetVariant, komodo_timestamp, - permission::PermissionLevel, + permission::{ + PermissionLevel, PermissionLevelAndSpecifics, + SpecificPermission, + }, resource::{AddFilters, Resource, ResourceQuery}, tag::Tag, - to_komodo_name, + to_general_name, update::Update, user::{User, system_user}, }, @@ -35,15 +39,12 @@ use serde::{Serialize, de::DeserializeOwned}; use crate::{ api::{read::ReadArgs, write::WriteArgs}, - config::core_config, helpers::{ create_permission, flatten_document, - query::{ - get_tag, get_user_user_groups, id_or_name_filter, - user_target_query, - }, + query::{get_tag, id_or_name_filter}, update::{add_update, make_update}, }, + permission::{get_check_permissions, get_resource_ids_for_user}, state::db_client, }; @@ -117,6 +118,28 @@ pub trait KomodoResource { #[allow(clippy::ptr_arg)] async fn busy(id: &String) -> anyhow::Result; + /// Some resource types have restrictions on the allowed formatting for names. + /// Stacks, Builds, and Deployments all require names to be "docker compatible", + /// which means all lowercase, and no spaces or dots. + fn validated_name(name: &str) -> String { + to_general_name(name) + } + + /// These permissions go to the creator of the resource, + /// and include full access to the resource. + fn creator_specific_permissions() -> IndexSet { + IndexSet::new() + } + + /// For Stacks / Deployments, they should inherit specific + /// permissions like `Logs`, `Inspect`, and `Terminal` + /// from their attached Server. + fn inherit_specific_permissions_from( + _self: &Resource, + ) -> Option { + None + } + // ======= // CREATE // ======= @@ -213,106 +236,6 @@ pub async fn get( }) } -pub async fn get_check_permissions( - id_or_name: &str, - user: &User, - permission_level: PermissionLevel, -) -> anyhow::Result> { - let resource = get::(id_or_name).await?; - if user.admin - // Allow if its just read or below, and transparent mode enabled - || (permission_level <= PermissionLevel::Read - && core_config().transparent_mode) - // Allow if resource has base permission level greater than or equal to required permission level - || resource.base_permission >= permission_level - { - return Ok(resource); - } - let permissions = - get_user_permission_on_resource::(user, &resource.id).await?; - if permissions >= permission_level { - Ok(resource) - } else { - Err(anyhow!( - "User does not have required permissions on this {}. Must have at least {permission_level} permissions", - T::resource_type() - )) - } -} - -#[instrument(level = "debug")] -pub async fn get_user_permission_on_resource( - user: &User, - resource_id: &str, -) -> anyhow::Result { - if user.admin { - return Ok(PermissionLevel::Write); - } - - let resource_type = T::resource_type(); - - // Start with base of Read or None - let mut base = if core_config().transparent_mode { - PermissionLevel::Read - } else { - PermissionLevel::None - }; - - // Add in the resource level global base permission - let resource_base = get::(resource_id).await?.base_permission; - if resource_base > base { - base = resource_base; - } - - // Overlay users base on resource variant - if let Some(level) = user.all.get(&resource_type).cloned() { - if level > base { - base = level; - } - } - if base == PermissionLevel::Write { - // No reason to keep going if already Write at this point. - return Ok(PermissionLevel::Write); - } - - // Overlay any user groups base on resource variant - let groups = get_user_user_groups(&user.id).await?; - for group in &groups { - if let Some(level) = group.all.get(&resource_type).cloned() { - if level > base { - base = level; - } - } - } - if base == PermissionLevel::Write { - // No reason to keep going if already Write at this point. - return Ok(PermissionLevel::Write); - } - - // Overlay any specific permissions - let permission = find_collect( - &db_client().permissions, - doc! { - "$or": user_target_query(&user.id, &groups)?, - "resource_target.type": resource_type.as_ref(), - "resource_target.id": resource_id - }, - None, - ) - .await - .context("failed to query db for permissions")? - .into_iter() - // get the max permission user has between personal / any user groups - .fold(base, |level, permission| { - if permission.level > level { - permission.level - } else { - level - } - }); - Ok(permission) -} - // ====== // LIST // ====== @@ -332,80 +255,17 @@ pub async fn get_resource_object_ids_for_user( }) } -/// Returns None if still no need to filter by resource id (eg transparent mode, group membership with all access). -#[instrument(level = "debug")] -pub async fn get_resource_ids_for_user( - user: &User, -) -> anyhow::Result>> { - // Check admin or transparent mode - if user.admin || core_config().transparent_mode { - return Ok(None); - } - - let resource_type = T::resource_type(); - - // Check user 'all' on variant - if let Some(level) = user.all.get(&resource_type).cloned() { - if level > PermissionLevel::None { - return Ok(None); - } - } - - // Check user groups 'all' on variant - let groups = get_user_user_groups(&user.id).await?; - for group in &groups { - if let Some(level) = group.all.get(&resource_type).cloned() { - if level > PermissionLevel::None { - return Ok(None); - } - } - } - - let (base, perms) = tokio::try_join!( - // Get any resources with non-none base permission, - find_collect( - T::coll(), - doc! { "base_permission": { "$exists": true, "$ne": "None" } }, - None, - ) - .map(|res| res.with_context(|| format!( - "failed to query {resource_type} on db" - ))), - // And any ids using the permissions table - find_collect( - &db_client().permissions, - doc! { - "$or": user_target_query(&user.id, &groups)?, - "resource_target.type": resource_type.as_ref(), - "level": { "$exists": true, "$ne": "None" } - }, - None, - ) - .map(|res| res.context("failed to query permissions on db")) - )?; - - // Add specific ids - let ids = perms - .into_iter() - .map(|p| p.resource_target.extract_variant_id().1.to_string()) - // Chain in the ones with non-None base permissions - .chain(base.into_iter().map(|res| res.id)) - // collect into hashset first to remove any duplicates - .collect::>(); - - Ok(Some(ids.into_iter().collect())) -} - #[instrument(level = "debug")] pub async fn list_for_user( mut query: ResourceQuery, user: &User, + permissions: PermissionLevelAndSpecifics, all_tags: &[Tag], ) -> anyhow::Result> { validate_resource_query_tags(&mut query, all_tags)?; let mut filters = Document::new(); query.add_filters(&mut filters); - list_for_user_using_document::(filters, user).await + list_for_user_using_document::(filters, user, permissions).await } #[instrument(level = "debug")] @@ -413,10 +273,15 @@ pub async fn list_for_user_using_pattern( pattern: &str, query: ResourceQuery, user: &User, + permissions: PermissionLevelAndSpecifics, all_tags: &[Tag], ) -> anyhow::Result> { let list = list_full_for_user_using_pattern::( - pattern, query, user, all_tags, + pattern, + query, + user, + permissions, + all_tags, ) .await? .into_iter() @@ -428,6 +293,7 @@ pub async fn list_for_user_using_pattern( pub async fn list_for_user_using_document( filters: Document, user: &User, + permissions: PermissionLevelAndSpecifics, ) -> anyhow::Result> { let list = list_full_for_user_using_document::(filters, user) .await? @@ -449,10 +315,12 @@ pub async fn list_full_for_user_using_pattern( pattern: &str, query: ResourceQuery, user: &User, + permissions: PermissionLevelAndSpecifics, all_tags: &[Tag], ) -> anyhow::Result>> { let resources = - list_full_for_user::(query, user, all_tags).await?; + list_full_for_user::(query, user, permissions, all_tags) + .await?; let patterns = parse_string_list(pattern); let mut names = HashSet::::new(); @@ -489,6 +357,7 @@ pub async fn list_full_for_user_using_pattern( pub async fn list_full_for_user( mut query: ResourceQuery, user: &User, + permissions: PermissionLevelAndSpecifics, all_tags: &[Tag], ) -> anyhow::Result>> { validate_resource_query_tags(&mut query, all_tags)?; @@ -590,7 +459,7 @@ pub async fn create( return Err(anyhow!("Must provide non-empty name for resource.")); } - let name = to_komodo_name(name); + let name = T::validated_name(name); if ObjectId::from_str(&name).is_ok() { return Err(anyhow!("valid ObjectIds cannot be used as names.")); @@ -598,11 +467,16 @@ pub async fn create( // Ensure an existing resource with same name doesn't already exist // The database indexing also ensures this but doesn't give a good error message. - if list_full_for_user::(Default::default(), system_user(), &[]) - .await - .context("Failed to list all resources for duplicate name check")? - .into_iter() - .any(|r| r.name == name) + if list_full_for_user::( + Default::default(), + system_user(), + PermissionLevel::Read.into(), + &[], + ) + .await + .context("Failed to list all resources for duplicate name check")? + .into_iter() + .any(|r| r.name == name) { return Err(anyhow!("Must provide unique name for resource.")); } @@ -619,7 +493,7 @@ pub async fn create( tags: Default::default(), config: config.into(), info: T::default_info().await?, - base_permission: PermissionLevel::None, + base_permission: PermissionLevel::None.into(), }; let resource_id = T::coll() @@ -636,8 +510,13 @@ pub async fn create( let resource = get::(&resource_id).await?; let target = resource_target::(resource_id); - create_permission(user, target.clone(), PermissionLevel::Write) - .await; + create_permission( + user, + target.clone(), + PermissionLevel::Write, + T::creator_specific_permissions(), + ) + .await; let mut update = make_update(target, T::create_operation(), user); update.start_ts = start_ts; @@ -676,7 +555,7 @@ pub async fn update( let resource = get_check_permissions::( id_or_name, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; @@ -788,7 +667,7 @@ pub async fn update_description( get_check_permissions::( id_or_name, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; T::coll() @@ -852,7 +731,7 @@ pub async fn rename( let resource = get_check_permissions::( id_or_name, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; @@ -862,7 +741,7 @@ pub async fn rename( user, ); - let name = to_komodo_name(name); + let name = T::validated_name(name); update_one_by_id( T::coll(), @@ -906,7 +785,7 @@ pub async fn delete( let resource = get_check_permissions::( id_or_name, &args.user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; diff --git a/bin/core/src/resource/procedure.rs b/bin/core/src/resource/procedure.rs index 900f4620e..ed605fd1c 100644 --- a/bin/core/src/resource/procedure.rs +++ b/bin/core/src/resource/procedure.rs @@ -180,7 +180,7 @@ async fn validate_config( let procedure = super::get_check_permissions::( ¶ms.procedure, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; match id { @@ -204,7 +204,7 @@ async fn validate_config( let action = super::get_check_permissions::( ¶ms.action, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.action = action.id; @@ -220,7 +220,7 @@ async fn validate_config( let build = super::get_check_permissions::( ¶ms.build, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.build = build.id; @@ -236,7 +236,7 @@ async fn validate_config( let build = super::get_check_permissions::( ¶ms.build, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.build = build.id; @@ -246,7 +246,7 @@ async fn validate_config( super::get_check_permissions::( ¶ms.deployment, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.deployment = deployment.id; @@ -263,7 +263,7 @@ async fn validate_config( super::get_check_permissions::( ¶ms.deployment, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.deployment = deployment.id; @@ -273,7 +273,7 @@ async fn validate_config( super::get_check_permissions::( ¶ms.deployment, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.deployment = deployment.id; @@ -283,7 +283,7 @@ async fn validate_config( super::get_check_permissions::( ¶ms.deployment, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.deployment = deployment.id; @@ -293,7 +293,7 @@ async fn validate_config( super::get_check_permissions::( ¶ms.deployment, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.deployment = deployment.id; @@ -303,7 +303,7 @@ async fn validate_config( super::get_check_permissions::( ¶ms.deployment, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.deployment = deployment.id; @@ -313,7 +313,7 @@ async fn validate_config( super::get_check_permissions::( ¶ms.deployment, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.deployment = deployment.id; @@ -323,7 +323,7 @@ async fn validate_config( super::get_check_permissions::( ¶ms.deployment, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.deployment = deployment.id; @@ -339,7 +339,7 @@ async fn validate_config( let repo = super::get_check_permissions::( ¶ms.repo, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.repo = repo.id; @@ -355,7 +355,7 @@ async fn validate_config( let repo = super::get_check_permissions::( ¶ms.repo, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.repo = repo.id; @@ -371,7 +371,7 @@ async fn validate_config( let repo = super::get_check_permissions::( ¶ms.repo, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.repo = repo.id; @@ -387,7 +387,7 @@ async fn validate_config( let repo = super::get_check_permissions::( ¶ms.repo, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.repo = repo.id; @@ -396,7 +396,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -405,7 +405,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -414,7 +414,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -423,7 +423,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -432,7 +432,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -441,7 +441,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -450,7 +450,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -459,7 +459,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -468,7 +468,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -477,7 +477,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -486,7 +486,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -495,7 +495,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -504,7 +504,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -513,7 +513,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -522,7 +522,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -531,7 +531,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -540,7 +540,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -549,7 +549,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -558,7 +558,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -567,7 +567,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -576,7 +576,7 @@ async fn validate_config( let server = super::get_check_permissions::( ¶ms.server, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.server = server.id; @@ -585,7 +585,7 @@ async fn validate_config( let sync = super::get_check_permissions::( ¶ms.sync, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.sync = sync.id; @@ -595,7 +595,7 @@ async fn validate_config( let sync = super::get_check_permissions::( ¶ms.sync, user, - PermissionLevel::Write, + PermissionLevel::Write.into(), ) .await?; params.sync = sync.id; @@ -604,7 +604,7 @@ async fn validate_config( let stack = super::get_check_permissions::( ¶ms.stack, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.stack = stack.id; @@ -620,7 +620,7 @@ async fn validate_config( let stack = super::get_check_permissions::( ¶ms.stack, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.stack = stack.id; @@ -636,7 +636,7 @@ async fn validate_config( let stack = super::get_check_permissions::( ¶ms.stack, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.stack = stack.id; @@ -652,7 +652,7 @@ async fn validate_config( let stack = super::get_check_permissions::( ¶ms.stack, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.stack = stack.id; @@ -661,7 +661,7 @@ async fn validate_config( let stack = super::get_check_permissions::( ¶ms.stack, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.stack = stack.id; @@ -670,7 +670,7 @@ async fn validate_config( let stack = super::get_check_permissions::( ¶ms.stack, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.stack = stack.id; @@ -679,7 +679,7 @@ async fn validate_config( let stack = super::get_check_permissions::( ¶ms.stack, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.stack = stack.id; @@ -688,7 +688,7 @@ async fn validate_config( let stack = super::get_check_permissions::( ¶ms.stack, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.stack = stack.id; @@ -697,7 +697,7 @@ async fn validate_config( let stack = super::get_check_permissions::( ¶ms.stack, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.stack = stack.id; @@ -713,7 +713,7 @@ async fn validate_config( let alerter = super::get_check_permissions::( ¶ms.alerter, user, - PermissionLevel::Execute, + PermissionLevel::Execute.into(), ) .await?; params.alerter = alerter.id; diff --git a/bin/core/src/resource/repo.rs b/bin/core/src/resource/repo.rs index 3a7ddce7d..44dfbdb8e 100644 --- a/bin/core/src/resource/repo.rs +++ b/bin/core/src/resource/repo.rs @@ -12,7 +12,7 @@ use komodo_client::entities::{ }, resource::Resource, server::Server, - to_komodo_name, + to_path_compatible_name, update::Update, user::User, }; @@ -48,6 +48,10 @@ impl super::KomodoResource for Repo { ResourceTarget::Repo(id.into()) } + fn validated_name(name: &str) -> String { + to_path_compatible_name(name) + } + fn coll() -> &'static Collection> { &db_client().repos @@ -170,7 +174,7 @@ impl super::KomodoResource for Repo { match periphery .request(DeleteRepo { name: if repo.config.path.is_empty() { - to_komodo_name(&repo.name) + to_path_compatible_name(&repo.name) } else { repo.config.path.clone() }, @@ -226,7 +230,7 @@ async fn validate_config( let server = get_check_permissions::( server_id, user, - PermissionLevel::Write, + PermissionLevel::Read.attach(), ) .await .context("Cannot attach Repo to this Server")?; @@ -238,7 +242,7 @@ async fn validate_config( let builder = super::get_check_permissions::( builder_id, user, - PermissionLevel::Read, + PermissionLevel::Read.attach(), ) .await .context("Cannot attach Repo to this Builder")?; diff --git a/bin/core/src/resource/server.rs b/bin/core/src/resource/server.rs index 49f05efff..08219cdef 100644 --- a/bin/core/src/resource/server.rs +++ b/bin/core/src/resource/server.rs @@ -1,6 +1,8 @@ use anyhow::Context; +use indexmap::IndexSet; use komodo_client::entities::{ Operation, ResourceTarget, ResourceTargetVariant, komodo_timestamp, + permission::SpecificPermission, resource::Resource, server::{ PartialServerConfig, Server, ServerConfig, ServerConfigDiff, @@ -34,6 +36,18 @@ impl super::KomodoResource for Server { ResourceTarget::Server(id.into()) } + fn creator_specific_permissions() -> IndexSet { + [ + SpecificPermission::Terminal, + SpecificPermission::Inspect, + SpecificPermission::Attach, + SpecificPermission::Logs, + SpecificPermission::Processes, + ] + .into_iter() + .collect() + } + fn coll() -> &'static Collection> { &db_client().servers diff --git a/bin/core/src/resource/stack.rs b/bin/core/src/resource/stack.rs index 8e436be1f..8b964f364 100644 --- a/bin/core/src/resource/stack.rs +++ b/bin/core/src/resource/stack.rs @@ -1,10 +1,11 @@ use anyhow::Context; use formatting::format_serror; +use indexmap::IndexSet; use komodo_client::{ api::write::RefreshStackCache, entities::{ Operation, ResourceTarget, ResourceTargetVariant, - permission::PermissionLevel, + permission::{PermissionLevel, SpecificPermission}, resource::Resource, server::Server, stack::{ @@ -12,6 +13,7 @@ use komodo_client::{ StackInfo, StackListItem, StackListItemInfo, StackQuerySpecifics, StackServiceWithUpdate, StackState, }, + to_docker_compatible_name, update::Update, user::{User, stack_user}, }, @@ -48,6 +50,26 @@ impl super::KomodoResource for Stack { ResourceTarget::Stack(id.into()) } + fn validated_name(name: &str) -> String { + to_docker_compatible_name(name) + } + + fn creator_specific_permissions() -> IndexSet { + [ + SpecificPermission::Inspect, + SpecificPermission::Logs, + SpecificPermission::Terminal, + ] + .into_iter() + .collect() + } + + fn inherit_specific_permissions_from( + _self: &Resource, + ) -> Option { + ResourceTarget::Server(_self.config.server_id.clone()).into() + } + fn coll() -> &'static Collection> { &db_client().stacks @@ -314,7 +336,7 @@ async fn validate_config( let server = get_check_permissions::( server_id, user, - PermissionLevel::Write, + PermissionLevel::Read.attach(), ) .await .context("Cannot attach stack to this Server")?; diff --git a/bin/core/src/stack/execute.rs b/bin/core/src/stack/execute.rs index 2b16e3c09..9e59c40e8 100644 --- a/bin/core/src/stack/execute.rs +++ b/bin/core/src/stack/execute.rs @@ -36,9 +36,13 @@ pub async fn execute_compose( mut update: Update, extras: T::Extras, ) -> anyhow::Result { - let (stack, server) = - get_stack_and_server(stack, user, PermissionLevel::Execute, true) - .await?; + let (stack, server) = get_stack_and_server( + stack, + user, + PermissionLevel::Execute.into(), + true, + ) + .await?; // get the action state for the stack (or insert default). let action_state = diff --git a/bin/core/src/stack/mod.rs b/bin/core/src/stack/mod.rs index c7c73bf0d..04d264063 100644 --- a/bin/core/src/stack/mod.rs +++ b/bin/core/src/stack/mod.rs @@ -1,13 +1,16 @@ use anyhow::{Context, anyhow}; use komodo_client::entities::{ - permission::PermissionLevel, + permission::PermissionLevelAndSpecifics, server::{Server, ServerState}, stack::Stack, user::User, }; use regex::Regex; -use crate::{helpers::query::get_server_with_state, resource}; +use crate::{ + helpers::query::get_server_with_state, + permission::get_check_permissions, +}; pub mod execute; pub mod remote; @@ -16,15 +19,11 @@ pub mod services; pub async fn get_stack_and_server( stack: &str, user: &User, - permission_level: PermissionLevel, + permissions: PermissionLevelAndSpecifics, block_if_server_unreachable: bool, ) -> anyhow::Result<(Stack, Server)> { - let stack = resource::get_check_permissions::( - stack, - user, - permission_level, - ) - .await?; + let stack = + get_check_permissions::(stack, user, permissions).await?; if stack.config.server_id.is_empty() { return Err(anyhow!("Stack has no server configured")); diff --git a/bin/core/src/sync/remote.rs b/bin/core/src/sync/remote.rs index b581ff904..099a2ef4d 100644 --- a/bin/core/src/sync/remote.rs +++ b/bin/core/src/sync/remote.rs @@ -3,7 +3,7 @@ use git::GitRes; use komodo_client::entities::{ CloneArgs, sync::{ResourceSync, SyncFileContents}, - to_komodo_name, + to_path_compatible_name, toml::ResourcesToml, update::Log, }; @@ -31,7 +31,7 @@ pub async fn get_remote_resources( // ============= let root_path = core_config() .sync_directory - .join(to_komodo_name(&sync.name)); + .join(to_path_compatible_name(&sync.name)); let (mut logs, mut files, mut file_errors) = (Vec::new(), Vec::new(), Vec::new()); let resources = super::file::read_resources( diff --git a/bin/core/src/sync/toml.rs b/bin/core/src/sync/toml.rs index 5acd08445..446e6fd83 100644 --- a/bin/core/src/sync/toml.rs +++ b/bin/core/src/sync/toml.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use anyhow::Context; +use indexmap::IndexMap; use komodo_client::{ api::execute::Execution, entities::{ @@ -19,7 +20,6 @@ use komodo_client::{ toml::ResourceToml, }, }; -use ordered_hash_map::OrderedHashMap; use partial_derive2::{MaybeNone, PartialDiff}; use crate::resource::KomodoResource; @@ -44,8 +44,8 @@ pub trait ToToml: KomodoResource { fn edit_config_object( _resource: &ResourceToml, - config: OrderedHashMap, - ) -> anyhow::Result> { + config: IndexMap, + ) -> anyhow::Result> { Ok(config) } @@ -62,9 +62,9 @@ pub trait ToToml: KomodoResource { resource.config = Self::Config::default().minimize_partial(resource.config); - let mut resource_map: OrderedHashMap = + let mut resource_map: IndexMap = serde_json::from_str(&serde_json::to_string(&resource)?)?; - resource_map.remove("config"); + resource_map.shift_remove("config"); let config = serde_json::from_str(&serde_json::to_string( &resource.config, @@ -182,8 +182,8 @@ impl ToToml for Stack { fn edit_config_object( _resource: &ResourceToml, - config: OrderedHashMap, - ) -> anyhow::Result> { + config: IndexMap, + ) -> anyhow::Result> { config .into_iter() .map(|(key, value)| { @@ -225,8 +225,8 @@ impl ToToml for Deployment { fn edit_config_object( resource: &ResourceToml, - config: OrderedHashMap, - ) -> anyhow::Result> { + config: IndexMap, + ) -> anyhow::Result> { config .into_iter() .map(|(key, mut value)| { @@ -278,8 +278,8 @@ impl ToToml for Build { fn edit_config_object( resource: &ResourceToml, - config: OrderedHashMap, - ) -> anyhow::Result> { + config: IndexMap, + ) -> anyhow::Result> { config .into_iter() .map(|(key, value)| match key.as_str() { @@ -330,8 +330,8 @@ impl ToToml for Repo { fn edit_config_object( _resource: &ResourceToml, - config: OrderedHashMap, - ) -> anyhow::Result> { + config: IndexMap, + ) -> anyhow::Result> { config .into_iter() .map(|(key, value)| { @@ -791,7 +791,7 @@ impl ToToml for Procedure { resource.config = Self::Config::default().minimize_partial(resource.config); - let mut parsed: OrderedHashMap = + let mut parsed: IndexMap = serde_json::from_str(&serde_json::to_string(&resource)?)?; let config = parsed diff --git a/bin/core/src/sync/user_groups.rs b/bin/core/src/sync/user_groups.rs index 887064a74..2d5bffef2 100644 --- a/bin/core/src/sync/user_groups.rs +++ b/bin/core/src/sync/user_groups.rs @@ -1,18 +1,25 @@ -use std::{cmp::Ordering, collections::HashMap}; +use std::{ + cmp::Ordering, collections::HashMap, fmt::Write, sync::OnceLock, +}; use anyhow::Context; use formatting::{Color, bold, colored, muted}; +use indexmap::{IndexMap, IndexSet}; use komodo_client::{ api::{ read::ListUserTargetPermissions, write::{ - CreateUserGroup, DeleteUserGroup, SetUsersInUserGroup, - UpdatePermissionOnResourceType, UpdatePermissionOnTarget, + CreateUserGroup, DeleteUserGroup, SetEveryoneUserGroup, + SetUsersInUserGroup, UpdatePermissionOnResourceType, + UpdatePermissionOnTarget, }, }, entities::{ ResourceTarget, ResourceTargetVariant, - permission::{PermissionLevel, UserTarget}, + permission::{ + PermissionLevel, PermissionLevelAndSpecifics, + SpecificPermission, UserTarget, + }, sync::DiffData, toml::{PermissionToml, UserGroupToml}, update::Log, @@ -21,20 +28,109 @@ use komodo_client::{ }, }; use mungos::find::find_collect; -use regex::Regex; use resolver_api::Resolve; +use serde::Serialize; use crate::{ api::{read::ReadArgs, write::WriteArgs}, + helpers::matcher::Matcher, state::db_client, }; use super::{AllResourcesById, toml::TOML_PRETTY_OPTIONS}; +/// Used to serialize user group +#[derive(Serialize)] +struct BasicUserGroupToml { + name: String, + #[serde(skip_serializing_if = "is_false")] + everyone: bool, + #[serde(skip_serializing_if = "Vec::is_empty")] + users: Vec, +} + +fn is_false(b: &bool) -> bool { + !b +} + +/// Used to serialize user group +#[derive(Serialize)] +struct Permissions { + permissions: Vec, +} + +pub fn user_group_to_toml( + user_group: UserGroupToml, +) -> anyhow::Result { + // Start with the basic body + let basic = BasicUserGroupToml { + name: user_group.name, + everyone: user_group.everyone, + users: if user_group.everyone { + Vec::new() + } else { + user_group.users + }, + }; + let basic = toml_pretty::to_string(&basic, TOML_PRETTY_OPTIONS) + .context("failed to serialize user group to toml")?; + let mut res = format!("[[user_group]]\n{basic}"); + + // Add "all" permissions + for (variant, PermissionLevelAndSpecifics { level, specific }) in + user_group.all + { + // skip 'zero' all permissions + if level == PermissionLevel::None && specific.is_empty() { + continue; + } + write!(&mut res, "\nall.{variant} = ") + .context("failed to serialize user group 'all' to toml")?; + if specific.is_empty() { + res.push('"'); + res.push_str(level.as_ref()); + res.push('"'); + } else { + let specific = serde_json::to_string(&specific) + .context( + "failed to serialize user group specifics to... json?", + )? + .replace(",", ", "); + write!( + &mut res, + "{{ level = \"{level}\", specific = {specific} }}" + ) + .context( + "failed to serialize user group 'all' with specifics to toml", + )?; + } + } + + // End with resource permissions array + if !user_group.permissions.is_empty() { + res.push('\n'); + res.push_str( + &toml_pretty::to_string( + &Permissions { + permissions: user_group.permissions, + }, + TOML_PRETTY_OPTIONS, + ) + .context( + "failed to serialize user group permissions to toml", + )?, + ); + } + + Ok(res) +} + pub struct UpdateItem { user_group: UserGroupToml, update_users: bool, - all_diff: HashMap, + update_everyone: bool, + all_diff: + IndexMap, } pub struct DeleteItem { @@ -64,17 +160,17 @@ pub async fn get_updates_for_view( for (_id, user_group) in map.values() { if !user_groups.iter().any(|ug| ug.name == user_group.name) { diffs.push(DiffData::Delete { - current: format!( - "[[user_group]]\n{}", - toml_pretty::to_string(user_group, TOML_PRETTY_OPTIONS) - .context("failed to serialize user group to toml")? - ), + current: user_group_to_toml(user_group.clone())?, }); } } } for mut user_group in user_groups { + if user_group.everyone { + user_group.users.clear(); + } + user_group .permissions .retain(|p| p.level > PermissionLevel::None); @@ -91,23 +187,17 @@ pub async fn get_updates_for_view( ) })?; - let (_original_id, original) = match map - .get(&user_group.name) - .cloned() - { - Some(original) => original, - None => { - diffs.push(DiffData::Create { - name: user_group.name.clone(), - proposed: format!( - "[[user_group]]\n{}", - toml_pretty::to_string(&user_group, TOML_PRETTY_OPTIONS) - .context("failed to serialize user group to toml")? - ), - }); - continue; - } - }; + let (_original_id, original) = + match map.get(&user_group.name).cloned() { + Some(original) => original, + None => { + diffs.push(DiffData::Create { + name: user_group.name.clone(), + proposed: user_group_to_toml(user_group.clone())?, + }); + continue; + } + }; user_group.users.sort(); let all_diff = diff_group_all(&original.all, &user_group.all); @@ -115,23 +205,20 @@ pub async fn get_updates_for_view( user_group.permissions.sort_by(sort_permissions); let update_users = user_group.users != original.users; + let update_everyone = user_group.everyone != original.everyone; let update_all = !all_diff.is_empty(); let update_permissions = user_group.permissions != original.permissions; // only add log after diff detected - if update_users || update_all || update_permissions { + if update_users + || update_everyone + || update_all + || update_permissions + { diffs.push(DiffData::Update { - proposed: format!( - "[[user_group]]\n{}", - toml_pretty::to_string(&user_group, TOML_PRETTY_OPTIONS) - .context("failed to serialize user group to toml")? - ), - current: format!( - "[[user_group]]\n{}", - toml_pretty::to_string(&original, TOML_PRETTY_OPTIONS) - .context("failed to serialize user group to toml")? - ), + proposed: user_group_to_toml(user_group.clone())?, + current: user_group_to_toml(original.clone())?, }); } } @@ -152,7 +239,15 @@ pub async fn get_updates_for_execution( .await .context("failed to query db for UserGroups")? .into_iter() - .map(|ug| (ug.name.clone(), ug)) + .map(|mut ug| { + if ug.everyone { + ug.users.clear(); + } + ug.all.retain(|_, p| { + p.level > PermissionLevel::None || !p.specific.is_empty() + }); + (ug.name.clone(), ug) + }) .collect::>(); let mut to_create = Vec::::new(); @@ -182,6 +277,10 @@ pub async fn get_updates_for_execution( .collect::>(); for mut user_group in user_groups { + if user_group.everyone { + user_group.users.clear(); + } + user_group .permissions .retain(|p| p.level > PermissionLevel::None); @@ -193,7 +292,7 @@ pub async fn get_updates_for_execution( .await .with_context(|| { format!( - "failed to expand user group {} permissions", + "Failed to expand user group {} permissions", user_group.name ) })?; @@ -303,6 +402,7 @@ pub async fn get_updates_for_execution( PermissionToml { target: p.resource_target, level: p.level, + specific: p.specific, } }) .collect::>(); @@ -316,8 +416,10 @@ pub async fn get_updates_for_execution( original_permissions.sort_by(sort_permissions); let update_users = user_group.users != original_users; + let update_everyone = user_group.everyone != original.everyone; // Extend permissions with any existing that have no target in incoming + // This makes sure to set those permissions back to None. let to_remove = original_permissions .iter() .filter(|permission| { @@ -329,32 +431,37 @@ pub async fn get_updates_for_execution( .map(|permission| PermissionToml { target: permission.target.clone(), level: PermissionLevel::None, + specific: IndexSet::new(), }) .collect::>(); user_group.permissions.extend(to_remove); // remove any permissions that already exist on original user_group.permissions.retain(|permission| { - let Some(level) = original_permissions + let Some(original_permission) = original_permissions .iter() .find(|p| p.target == permission.target) - .map(|p| p.level) else { // not in original, keep it return true; }; - // keep it if level doesn't match - level != permission.level + original_permission.level != permission.level + || !specific_equal( + &original_permission.specific, + &permission.specific, + ) }); // only push update after diff detected if update_users + || update_everyone || !all_diff.is_empty() || !user_group.permissions.is_empty() { to_update.push(UpdateItem { user_group, update_users, + update_everyone, all_diff: all_diff .into_iter() .map(|(k, (_, v))| (k, v)) @@ -432,6 +539,13 @@ pub async fn run_updates( &mut has_error, ) .await; + set_everyone( + user_group.name.clone(), + user_group.everyone, + &mut log, + &mut has_error, + ) + .await; run_update_all( user_group.name.clone(), user_group.all, @@ -452,6 +566,7 @@ pub async fn run_updates( for UpdateItem { user_group, update_users, + update_everyone, all_diff, } in to_update { @@ -464,6 +579,15 @@ pub async fn run_updates( ) .await; } + if update_everyone { + set_everyone( + user_group.name.clone(), + user_group.everyone, + &mut log, + &mut has_error, + ) + .await; + } if !all_diff.is_empty() { run_update_all( user_group.name.clone(), @@ -548,9 +672,44 @@ async fn set_users( } } +async fn set_everyone( + user_group: String, + everyone: bool, + log: &mut String, + has_error: &mut bool, +) { + if let Err(e) = (SetEveryoneUserGroup { + user_group: user_group.clone(), + everyone, + }) + .resolve(&WriteArgs { + user: sync_user().to_owned(), + }) + .await + { + *has_error = true; + log.push_str(&format!( + "\n{}: failed to set everyone for group {} | {:#}", + colored("ERROR", Color::Red), + bold(&user_group), + e.error + )) + } else { + log.push_str(&format!( + "\n{}: {} user group '{}' everyone", + muted("INFO"), + colored("updated", Color::Blue), + bold(&user_group) + )) + } +} + async fn run_update_all( user_group: String, - all_diff: HashMap, + all_diff: IndexMap< + ResourceTargetVariant, + PermissionLevelAndSpecifics, + >, log: &mut String, has_error: &mut bool, ) { @@ -589,11 +748,16 @@ async fn run_update_permissions( log: &mut String, has_error: &mut bool, ) { - for PermissionToml { target, level } in permissions { + for PermissionToml { + target, + level, + specific, + } in permissions + { if let Err(e) = (UpdatePermissionOnTarget { user_target: UserTarget::UserGroup(user_group.clone()), resource_target: target.clone(), - permission: level, + permission: level.specifics(specific.clone()), }) .resolve(&WriteArgs { user: sync_user().to_owned(), @@ -609,12 +773,14 @@ async fn run_update_permissions( )) } else { log.push_str(&format!( - "\n{}: {} user group '{}' permissions | {}: {target:?} | {}: {level}", + "\n{}: {} user group '{}' permissions | {}: {target:?} | {}: {level} | {}: {}", muted("INFO"), colored("updated", Color::Blue), bold(&user_group), muted("target"), - muted("level") + muted("level"), + muted("specific"), + specific.into_iter().map(|s| s.into()).collect::>().join(", ") )) } } @@ -633,171 +799,217 @@ async fn expand_user_group_permissions( if id.is_empty() { continue; } - if id.starts_with('\\') && id.ends_with('\\') { - let inner = &id[1..(id.len() - 1)]; - let regex = Regex::new(inner) - .with_context(|| format!("invalid regex. got: {inner}"))?; - match variant { - ResourceTargetVariant::Build => { - let permissions = all_resources - .builds - .values() - .filter(|resource| regex.is_match(&resource.name)) - .map(|resource| PermissionToml { - target: ResourceTarget::Build(resource.name.clone()), - level: permission.level, - }); - expanded.extend(permissions); - } - ResourceTargetVariant::Builder => { - let permissions = all_resources - .builders - .values() - .filter(|resource| regex.is_match(&resource.name)) - .map(|resource| PermissionToml { - target: ResourceTarget::Builder(resource.name.clone()), - level: permission.level, - }); - expanded.extend(permissions); - } - ResourceTargetVariant::Deployment => { - let permissions = all_resources - .deployments - .values() - .filter(|resource| regex.is_match(&resource.name)) - .map(|resource| PermissionToml { - target: ResourceTarget::Deployment( - resource.name.clone(), - ), - level: permission.level, - }); - expanded.extend(permissions); - } - ResourceTargetVariant::Server => { - let permissions = all_resources - .servers - .values() - .filter(|resource| regex.is_match(&resource.name)) - .map(|resource| PermissionToml { - target: ResourceTarget::Server(resource.name.clone()), - level: permission.level, - }); - expanded.extend(permissions); - } - ResourceTargetVariant::Repo => { - let permissions = all_resources - .repos - .values() - .filter(|resource| regex.is_match(&resource.name)) - .map(|resource| PermissionToml { - target: ResourceTarget::Repo(resource.name.clone()), - level: permission.level, - }); - expanded.extend(permissions); - } - ResourceTargetVariant::Alerter => { - let permissions = all_resources - .alerters - .values() - .filter(|resource| regex.is_match(&resource.name)) - .map(|resource| PermissionToml { - target: ResourceTarget::Alerter(resource.name.clone()), - level: permission.level, - }); - expanded.extend(permissions); - } - ResourceTargetVariant::Procedure => { - let permissions = all_resources - .procedures - .values() - .filter(|resource| regex.is_match(&resource.name)) - .map(|resource| PermissionToml { - target: ResourceTarget::Procedure( - resource.name.clone(), - ), - level: permission.level, - }); - expanded.extend(permissions); - } - ResourceTargetVariant::Action => { - let permissions = all_resources - .actions - .values() - .filter(|resource| regex.is_match(&resource.name)) - .map(|resource| PermissionToml { - target: ResourceTarget::Action(resource.name.clone()), - level: permission.level, - }); - expanded.extend(permissions); - } - ResourceTargetVariant::ResourceSync => { - let permissions = all_resources - .syncs - .values() - .filter(|resource| regex.is_match(&resource.name)) - .map(|resource| PermissionToml { - target: ResourceTarget::ResourceSync( - resource.name.clone(), - ), - level: permission.level, - }); - expanded.extend(permissions); - } - ResourceTargetVariant::Stack => { - let permissions = all_resources - .stacks - .values() - .filter(|resource| regex.is_match(&resource.name)) - .map(|resource| PermissionToml { - target: ResourceTarget::Stack(resource.name.clone()), - level: permission.level, - }); - expanded.extend(permissions); - } - ResourceTargetVariant::System => {} + let matcher = Matcher::new(&id)?; + match variant { + ResourceTargetVariant::Build => { + let permissions = all_resources + .builds + .values() + .filter(|resource| matcher.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::Build(resource.name.clone()), + level: permission.level, + specific: permission.specific.clone(), + }); + expanded.extend(permissions); } - } else { - // No regex - expanded.push(permission); + ResourceTargetVariant::Builder => { + let permissions = all_resources + .builders + .values() + .filter(|resource| matcher.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::Builder(resource.name.clone()), + level: permission.level, + specific: permission.specific.clone(), + }); + expanded.extend(permissions); + } + ResourceTargetVariant::Deployment => { + let permissions = all_resources + .deployments + .values() + .filter(|resource| matcher.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::Deployment(resource.name.clone()), + level: permission.level, + specific: permission.specific.clone(), + }); + expanded.extend(permissions); + } + ResourceTargetVariant::Server => { + let permissions = all_resources + .servers + .values() + .filter(|resource| matcher.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::Server(resource.name.clone()), + level: permission.level, + specific: permission.specific.clone(), + }); + expanded.extend(permissions); + } + ResourceTargetVariant::Repo => { + let permissions = all_resources + .repos + .values() + .filter(|resource| matcher.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::Repo(resource.name.clone()), + level: permission.level, + specific: permission.specific.clone(), + }); + expanded.extend(permissions); + } + ResourceTargetVariant::Alerter => { + let permissions = all_resources + .alerters + .values() + .filter(|resource| matcher.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::Alerter(resource.name.clone()), + level: permission.level, + specific: permission.specific.clone(), + }); + expanded.extend(permissions); + } + ResourceTargetVariant::Procedure => { + let permissions = all_resources + .procedures + .values() + .filter(|resource| matcher.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::Procedure(resource.name.clone()), + level: permission.level, + specific: permission.specific.clone(), + }); + expanded.extend(permissions); + } + ResourceTargetVariant::Action => { + let permissions = all_resources + .actions + .values() + .filter(|resource| matcher.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::Action(resource.name.clone()), + level: permission.level, + specific: permission.specific.clone(), + }); + expanded.extend(permissions); + } + ResourceTargetVariant::ResourceSync => { + let permissions = all_resources + .syncs + .values() + .filter(|resource| matcher.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::ResourceSync( + resource.name.clone(), + ), + level: permission.level, + specific: permission.specific.clone(), + }); + expanded.extend(permissions); + } + ResourceTargetVariant::Stack => { + let permissions = all_resources + .stacks + .values() + .filter(|resource| matcher.is_match(&resource.name)) + .map(|resource| PermissionToml { + target: ResourceTarget::Stack(resource.name.clone()), + level: permission.level, + specific: permission.specific.clone(), + }); + expanded.extend(permissions); + } + ResourceTargetVariant::System => {} } } Ok(expanded) } -type AllDiff = - HashMap; +type AllDiff = IndexMap< + ResourceTargetVariant, + (PermissionLevelAndSpecifics, PermissionLevelAndSpecifics), +>; + +fn default_permission() -> &'static PermissionLevelAndSpecifics { + static DEFAULT_PERMISSION: OnceLock = + OnceLock::new(); + DEFAULT_PERMISSION.get_or_init(Default::default) +} /// diffs user_group.all fn diff_group_all( - original: &HashMap, - incoming: &HashMap, + original: &IndexMap< + ResourceTargetVariant, + PermissionLevelAndSpecifics, + >, + incoming: &IndexMap< + ResourceTargetVariant, + PermissionLevelAndSpecifics, + >, ) -> AllDiff { - let mut to_update = HashMap::new(); + let mut to_update = IndexMap::new(); // need to compare both forward and backward because either hashmap could be sparse. // forward direction - for (variant, level) in incoming { - let original_level = original.get(variant).unwrap_or_default(); - if level == original_level { - continue; + for (variant, permission) in incoming { + let original_permission = + original.get(variant).unwrap_or(default_permission()); + if permission.level != original_permission.level + || !specific_equal( + &original_permission.specific, + &permission.specific, + ) + { + to_update.insert( + *variant, + (original_permission.clone(), permission.clone()), + ); } - to_update.insert(*variant, (*original_level, *level)); } // backward direction - for (variant, level) in original { - let incoming_level = incoming.get(variant).unwrap_or_default(); - if level == incoming_level { - continue; + for (variant, permission) in original { + let incoming_permission = + incoming.get(variant).unwrap_or(default_permission()); + if permission.level != incoming_permission.level + || !specific_equal( + &incoming_permission.specific, + &permission.specific, + ) + { + to_update.insert( + *variant, + (permission.clone(), incoming_permission.clone()), + ); } - to_update.insert(*variant, (*level, *incoming_level)); } to_update } +fn specific_equal( + a: &IndexSet, + b: &IndexSet, +) -> bool { + for item in a { + if !b.contains(item) { + return false; + } + } + for item in b { + if !a.contains(item) { + return false; + } + } + true +} + pub async fn convert_user_groups( user_groups: impl Iterator, all: &AllResourcesById, @@ -811,7 +1023,11 @@ pub async fn convert_user_groups( .map(|user| (user.id, user.username)) .collect::>(); - for user_group in user_groups { + for mut user_group in user_groups { + user_group.all.retain(|_, p| { + p.level > PermissionLevel::None || !p.specific.is_empty() + }); + // this method is admin only, but we already know user can see user group if above does not return Err let mut permissions = (ListUserTargetPermissions { user_target: UserTarget::UserGroup(user_group.id.clone()), @@ -825,6 +1041,7 @@ pub async fn convert_user_groups( .await .map_err(|e| e.error)? .into_iter() + .filter(|permission| permission.level > PermissionLevel::None) .map(|mut permission| { match &mut permission.resource_target { ResourceTarget::Build(id) => { @@ -902,14 +1119,20 @@ pub async fn convert_user_groups( PermissionToml { target: permission.resource_target, level: permission.level, + specific: permission.specific, } }) .collect::>(); - let mut users = user_group - .users - .into_iter() - .filter_map(|user_id| usernames.get(&user_id).cloned()) - .collect::>(); + + let mut users = if user_group.everyone { + Vec::new() + } else { + user_group + .users + .into_iter() + .filter_map(|user_id| usernames.get(&user_id).cloned()) + .collect::>() + }; permissions.sort_by(sort_permissions); users.sort(); @@ -918,8 +1141,9 @@ pub async fn convert_user_groups( user_group.id, UserGroupToml { name: user_group.name, - users, + everyone: user_group.everyone, all: user_group.all, + users, permissions, }, )); diff --git a/bin/core/src/sync/variables.rs b/bin/core/src/sync/variables.rs index b45922b73..556c5774f 100644 --- a/bin/core/src/sync/variables.rs +++ b/bin/core/src/sync/variables.rs @@ -15,6 +15,14 @@ use crate::{api::write::WriteArgs, state::db_client}; use super::toml::TOML_PRETTY_OPTIONS; +pub fn variable_to_toml( + variable: &Variable, +) -> anyhow::Result { + let inner = toml_pretty::to_string(variable, TOML_PRETTY_OPTIONS) + .context("failed to serialize variable to toml")?; + Ok(format!("[[variable]]\n{inner}")) +} + pub struct ToUpdateItem { pub variable: Variable, pub update_value: bool, @@ -39,11 +47,7 @@ pub async fn get_updates_for_view( for variable in map.values() { if !variables.iter().any(|v| v.name == variable.name) { diffs.push(DiffData::Delete { - current: format!( - "[[variable]]\n{}", - toml_pretty::to_string(&variable, TOML_PRETTY_OPTIONS) - .context("failed to serialize variable to toml")? - ), + current: variable_to_toml(variable)?, }); } } @@ -58,26 +62,14 @@ pub async fn get_updates_for_view( continue; } diffs.push(DiffData::Update { - proposed: format!( - "[[variable]]\n{}", - toml_pretty::to_string(variable, TOML_PRETTY_OPTIONS) - .context("failed to serialize variable to toml")? - ), - current: format!( - "[[variable]]\n{}", - toml_pretty::to_string(original, TOML_PRETTY_OPTIONS) - .context("failed to serialize variable to toml")? - ), + proposed: variable_to_toml(variable)?, + current: variable_to_toml(original)?, }); } None => { diffs.push(DiffData::Create { name: variable.name.clone(), - proposed: format!( - "[[variable]]\n{}", - toml_pretty::to_string(variable, TOML_PRETTY_OPTIONS) - .context("failed to serialize variable to toml")? - ), + proposed: variable_to_toml(variable)?, }); } } diff --git a/bin/core/src/ws/container.rs b/bin/core/src/ws/container.rs index 43713cb4e..38ab5524a 100644 --- a/bin/core/src/ws/container.rs +++ b/bin/core/src/ws/container.rs @@ -8,12 +8,10 @@ use komodo_client::{ entities::{permission::PermissionLevel, server::Server}, }; -use crate::{ - helpers::periphery_client, resource, ws::core_periphery_forward_ws, -}; +use crate::permission::get_check_permissions; #[instrument(name = "ConnectContainerExec", skip(ws))] -pub async fn handler( +pub async fn terminal( Query(ConnectContainerExecQuery { server, container, @@ -22,60 +20,36 @@ pub async fn handler( ws: WebSocketUpgrade, ) -> impl IntoResponse { ws.on_upgrade(|socket| async move { - let Some((mut client_socket, user)) = super::ws_login(socket).await + let Some((mut client_socket, user)) = + super::ws_login(socket).await else { return; }; - let server = match resource::get_check_permissions::( + let server = match get_check_permissions::( &server, &user, - PermissionLevel::Write, + PermissionLevel::Read.terminal(), ) .await { Ok(server) => server, Err(e) => { debug!("could not get server | {e:#}"); - let _ = - client_socket.send(Message::text(format!("ERROR: {e:#}"))).await; + let _ = client_socket + .send(Message::text(format!("ERROR: {e:#}"))) + .await; let _ = client_socket.close().await; return; } }; - let periphery = match periphery_client(&server) { - Ok(periphery) => periphery, - Err(e) => { - debug!("couldn't get periphery | {e:#}"); - let _ = - client_socket.send(Message::text(format!("ERROR: {e:#}"))).await; - let _ = client_socket.close().await; - return; - } - }; - - trace!("connecting to periphery container exec websocket"); - - let periphery_socket = match periphery - .connect_container_exec( - container, - shell - ) - .await - { - Ok(ws) => ws, - Err(e) => { - debug!("Failed connect to periphery container exec websocket | {e:#}"); - let _ = - client_socket.send(Message::text(format!("ERROR: {e:#}"))).await; - let _ = client_socket.close().await; - return; - } - }; - - trace!("connected to periphery container exec websocket"); - - core_periphery_forward_ws(client_socket, periphery_socket).await + super::handle_container_terminal( + client_socket, + &server, + container, + shell, + ) + .await }) } diff --git a/bin/core/src/ws/deployment.rs b/bin/core/src/ws/deployment.rs new file mode 100644 index 000000000..11462593e --- /dev/null +++ b/bin/core/src/ws/deployment.rs @@ -0,0 +1,69 @@ +use axum::{ + extract::{Query, WebSocketUpgrade, ws::Message}, + response::IntoResponse, +}; +use futures::SinkExt; +use komodo_client::{ + api::terminal::ConnectDeploymentExecQuery, + entities::{ + deployment::Deployment, permission::PermissionLevel, + server::Server, + }, +}; + +use crate::{permission::get_check_permissions, resource::get}; + +#[instrument(name = "ConnectDeploymentExec", skip(ws))] +pub async fn terminal( + Query(ConnectDeploymentExecQuery { deployment, shell }): Query< + ConnectDeploymentExecQuery, + >, + ws: WebSocketUpgrade, +) -> impl IntoResponse { + ws.on_upgrade(|socket| async move { + let Some((mut client_socket, user)) = + super::ws_login(socket).await + else { + return; + }; + + let deployment = match get_check_permissions::( + &deployment, + &user, + PermissionLevel::Read.terminal(), + ) + .await + { + Ok(deployment) => deployment, + Err(e) => { + debug!("could not get deployment | {e:#}"); + let _ = client_socket + .send(Message::text(format!("ERROR: {e:#}"))) + .await; + let _ = client_socket.close().await; + return; + } + }; + + let server = + match get::(&deployment.config.server_id).await { + Ok(server) => server, + Err(e) => { + debug!("could not get server | {e:#}"); + let _ = client_socket + .send(Message::text(format!("ERROR: {e:#}"))) + .await; + let _ = client_socket.close().await; + return; + } + }; + + super::handle_container_terminal( + client_socket, + &server, + deployment.name, + shell, + ) + .await + }) +} diff --git a/bin/core/src/ws/mod.rs b/bin/core/src/ws/mod.rs index 176a42056..3458b8612 100644 --- a/bin/core/src/ws/mod.rs +++ b/bin/core/src/ws/mod.rs @@ -9,7 +9,10 @@ use axum::{ routing::get, }; use futures::{SinkExt, StreamExt}; -use komodo_client::{entities::user::User, ws::WsLoginMessage}; +use komodo_client::{ + entities::{server::Server, user::User}, + ws::WsLoginMessage, +}; use tokio::net::TcpStream; use tokio_tungstenite::{ MaybeTlsStream, WebSocketStream, tungstenite, @@ -17,6 +20,8 @@ use tokio_tungstenite::{ use tokio_util::sync::CancellationToken; mod container; +mod deployment; +mod stack; mod terminal; mod update; @@ -24,7 +29,9 @@ pub fn router() -> Router { Router::new() .route("/update", get(update::handler)) .route("/terminal", get(terminal::handler)) - .route("/container", get(container::handler)) + .route("/container/terminal", get(container::terminal)) + .route("/deployment/terminal", get(deployment::terminal)) + .route("/stack/terminal", get(stack::terminal)) } #[instrument(level = "debug")] @@ -118,6 +125,48 @@ async fn check_user_valid(user_id: &str) -> anyhow::Result { Ok(user) } +async fn handle_container_terminal( + mut client_socket: WebSocket, + server: &Server, + container: String, + shell: String, +) { + let periphery = match crate::helpers::periphery_client(server) { + Ok(periphery) => periphery, + Err(e) => { + debug!("couldn't get periphery | {e:#}"); + let _ = client_socket + .send(Message::text(format!("ERROR: {e:#}"))) + .await; + let _ = client_socket.close().await; + return; + } + }; + + trace!("connecting to periphery container exec websocket"); + + let periphery_socket = match periphery + .connect_container_exec(container, shell) + .await + { + Ok(ws) => ws, + Err(e) => { + debug!( + "Failed connect to periphery container exec websocket | {e:#}" + ); + let _ = client_socket + .send(Message::text(format!("ERROR: {e:#}"))) + .await; + let _ = client_socket.close().await; + return; + } + }; + + trace!("connected to periphery container exec websocket"); + + core_periphery_forward_ws(client_socket, periphery_socket).await +} + async fn core_periphery_forward_ws( client_socket: axum::extract::ws::WebSocket, periphery_socket: WebSocketStream>, @@ -143,9 +192,7 @@ async fn core_periphery_forward_ws( if let Err(e) = periphery_send.send(axum_to_tungstenite(msg)).await { - debug!( - "Failed to send terminal message | {e:?}", - ); + debug!("Failed to send terminal message | {e:?}",); cancel.cancel(); break; }; diff --git a/bin/core/src/ws/stack.rs b/bin/core/src/ws/stack.rs new file mode 100644 index 000000000..e8a298e84 --- /dev/null +++ b/bin/core/src/ws/stack.rs @@ -0,0 +1,90 @@ +use axum::{ + extract::{Query, WebSocketUpgrade, ws::Message}, + response::IntoResponse, +}; +use futures::SinkExt; +use komodo_client::{ + api::terminal::ConnectStackExecQuery, + entities::{ + permission::PermissionLevel, server::Server, stack::Stack, + }, +}; + +use crate::{permission::get_check_permissions, resource::get}; + +#[instrument(name = "ConnectStackExec", skip(ws))] +pub async fn terminal( + Query(ConnectStackExecQuery { + stack, + service, + shell, + }): Query, + ws: WebSocketUpgrade, +) -> impl IntoResponse { + ws.on_upgrade(|socket| async move { + let Some((mut client_socket, user)) = + super::ws_login(socket).await + else { + return; + }; + + let stack = match get_check_permissions::( + &stack, + &user, + PermissionLevel::Read.terminal(), + ) + .await + { + Ok(stack) => stack, + Err(e) => { + debug!("could not get stack | {e:#}"); + let _ = client_socket + .send(Message::text(format!("ERROR: {e:#}"))) + .await; + let _ = client_socket.close().await; + return; + } + }; + + let server = match get::(&stack.config.server_id).await { + Ok(server) => server, + Err(e) => { + debug!("could not get server | {e:#}"); + let _ = client_socket + .send(Message::text(format!("ERROR: {e:#}"))) + .await; + let _ = client_socket.close().await; + return; + } + }; + + let services = stack + .info + .deployed_services + .unwrap_or(stack.info.latest_services); + + let container = match services + .into_iter() + .find(|s| s.service_name == service) + { + Some(service) => service.container_name, + None => { + let _ = client_socket + .send(Message::text(format!( + "ERROR: Service {service} could not be found" + ))) + .await; + let _ = client_socket.close().await; + return; + } + }; + + super::handle_container_terminal( + client_socket, + &server, + container, + shell, + ) + .await + }) +} diff --git a/bin/core/src/ws/terminal.rs b/bin/core/src/ws/terminal.rs index b8b310834..3ec6b6aa1 100644 --- a/bin/core/src/ws/terminal.rs +++ b/bin/core/src/ws/terminal.rs @@ -9,7 +9,8 @@ use komodo_client::{ }; use crate::{ - helpers::periphery_client, resource, ws::core_periphery_forward_ws, + helpers::periphery_client, permission::get_check_permissions, + ws::core_periphery_forward_ws, }; #[instrument(name = "ConnectTerminal", skip(ws))] @@ -26,10 +27,10 @@ pub async fn handler( return; }; - let server = match resource::get_check_permissions::( + let server = match get_check_permissions::( &server, &user, - PermissionLevel::Write, + PermissionLevel::Read.terminal(), ) .await { diff --git a/bin/core/src/ws/update.rs b/bin/core/src/ws/update.rs index d0cd1681f..705172044 100644 --- a/bin/core/src/ws/update.rs +++ b/bin/core/src/ws/update.rs @@ -90,9 +90,9 @@ async fn user_can_see_update( if user.admin { return Ok(()); } - let permissions = + let permission = get_user_permission_on_target(user, update_target).await?; - if permissions > PermissionLevel::None { + if permission.level > PermissionLevel::None { Ok(()) } else { Err(anyhow!( diff --git a/bin/periphery/aio.Dockerfile b/bin/periphery/aio.Dockerfile index ce6419deb..5b5b43341 100644 --- a/bin/periphery/aio.Dockerfile +++ b/bin/periphery/aio.Dockerfile @@ -1,6 +1,6 @@ ## All in one, multi stage compile + runtime Docker build for your architecture. -FROM rust:1.86.0-bullseye AS builder +FROM rust:1.87.0-bullseye AS builder WORKDIR /builder COPY Cargo.toml Cargo.lock ./ diff --git a/bin/periphery/src/api/build.rs b/bin/periphery/src/api/build.rs index 95db16888..42f11db1d 100644 --- a/bin/periphery/src/api/build.rs +++ b/bin/periphery/src/api/build.rs @@ -14,7 +14,7 @@ use komodo_client::{ EnvironmentVar, Version, build::{Build, BuildConfig}, environment_vars_from_str, get_image_name, optional_string, - to_komodo_name, + to_docker_compatible_name, to_path_compatible_name, update::Log, }, parsers::QUOTE_PATTERN, @@ -45,8 +45,9 @@ impl Resolve for GetDockerfileContentsOnHost { dockerfile_path, } = self; - let root = - periphery_config().build_dir().join(to_komodo_name(&name)); + let root = periphery_config() + .build_dir() + .join(to_path_compatible_name(&name)); let build_dir = root.join(&build_path).components().collect::(); @@ -92,7 +93,7 @@ impl Resolve for WriteDockerfileContentsToHost { } = self; let full_path = periphery_config() .build_dir() - .join(to_komodo_name(&name)) + .join(to_path_compatible_name(&name)) .join(&build_path) .join(dockerfile_path) .components() @@ -177,7 +178,7 @@ impl Resolve for build::Build { } }; - let name = to_komodo_name(name); + let name = to_docker_compatible_name(name); let build_path = periphery_config().build_dir().join(&name).join(build_path); diff --git a/bin/periphery/src/api/compose.rs b/bin/periphery/src/api/compose.rs index 324bd44c7..baf6b9389 100644 --- a/bin/periphery/src/api/compose.rs +++ b/bin/periphery/src/api/compose.rs @@ -5,7 +5,8 @@ use command::run_komodo_command; use formatting::format_serror; use git::{GitRes, write_commit_file}; use komodo_client::entities::{ - FileContents, stack::ComposeProject, to_komodo_name, update::Log, + FileContents, stack::ComposeProject, to_path_compatible_name, + update::Log, }; use periphery_client::api::{compose::*, git::RepoActionResponse}; use resolver_api::Resolve; @@ -137,8 +138,9 @@ impl Resolve for GetComposeContentsOnHost { run_directory, file_paths, } = self; - let root = - periphery_config().stack_dir().join(to_komodo_name(&name)); + let root = periphery_config() + .stack_dir() + .join(to_path_compatible_name(&name)); let run_directory = root.join(&run_directory).components().collect::(); @@ -197,7 +199,7 @@ impl Resolve for WriteComposeContentsToHost { } = self; let file_path = periphery_config() .stack_dir() - .join(to_komodo_name(&name)) + .join(to_path_compatible_name(&name)) .join(&run_directory) .join(file_path) .components() diff --git a/bin/periphery/src/api/container.rs b/bin/periphery/src/api/container.rs index c6440ac56..0f0a0d7fc 100644 --- a/bin/periphery/src/api/container.rs +++ b/bin/periphery/src/api/container.rs @@ -3,7 +3,7 @@ use command::run_komodo_command; use futures::future::join_all; use komodo_client::entities::{ docker::container::{Container, ContainerListItem, ContainerStats}, - to_komodo_name, + to_docker_compatible_name, update::Log, }; use periphery_client::api::container::*; @@ -234,7 +234,7 @@ impl Resolve for RenameContainer { curr_name, new_name, } = self; - let new = to_komodo_name(&new_name); + let new = to_docker_compatible_name(&new_name); let command = format!("docker rename {curr_name} {new}"); Ok(run_komodo_command("Docker Rename", None, command).await) } diff --git a/bin/periphery/src/api/deploy.rs b/bin/periphery/src/api/deploy.rs index f123f4c85..b35c5aa5a 100644 --- a/bin/periphery/src/api/deploy.rs +++ b/bin/periphery/src/api/deploy.rs @@ -10,7 +10,7 @@ use komodo_client::{ Conversion, Deployment, DeploymentConfig, DeploymentImage, RestartMode, conversions_from_str, extract_registry_domain, }, - environment_vars_from_str, to_komodo_name, + environment_vars_from_str, to_docker_compatible_name, update::Log, }, parsers::QUOTE_PATTERN, @@ -129,7 +129,7 @@ fn docker_run_command( }: &Deployment, image: &str, ) -> anyhow::Result { - let name = to_komodo_name(name); + let name = to_docker_compatible_name(name); let ports = parse_conversions( &conversions_from_str(ports).context("Invalid ports")?, "-p", diff --git a/bin/periphery/src/compose.rs b/bin/periphery/src/compose.rs index d7c917d8a..833aba139 100644 --- a/bin/periphery/src/compose.rs +++ b/bin/periphery/src/compose.rs @@ -14,7 +14,7 @@ use komodo_client::entities::{ ComposeFile, ComposeService, ComposeServiceDeploy, Stack, StackServiceNames, }, - to_komodo_name, + to_path_compatible_name, update::Log, }; use periphery_client::api::{ @@ -431,7 +431,7 @@ pub async fn write_stack( )> { let root = periphery_config() .stack_dir() - .join(to_komodo_name(&stack.name)); + .join(to_path_compatible_name(&stack.name)); let run_directory = root.join(&stack.config.run_directory); // This will remove any intermediate '/./' in the path, which is a problem for some OS. // Cannot use 'canonicalize' yet as directory may not exist. @@ -694,7 +694,7 @@ async fn compose_down( format!(" {}", services.join(" ")) }; let log = run_komodo_command( - "compose down", + "Compose Down", None, format!("{docker_compose} -p {project} down{service_args}"), ) diff --git a/bin/periphery/src/config.rs b/bin/periphery/src/config.rs index 719cddbdb..1a3d1a49f 100644 --- a/bin/periphery/src/config.rs +++ b/bin/periphery/src/config.rs @@ -73,6 +73,9 @@ pub fn periphery_config() -> &'static PeripheryConfig { .periphery_logging_opentelemetry_service_name .unwrap_or(config.logging.opentelemetry_service_name), }, + pretty_startup_config: env + .periphery_pretty_startup_config + .unwrap_or(config.pretty_startup_config), allowed_ips: env .periphery_allowed_ips .unwrap_or(config.allowed_ips), diff --git a/bin/periphery/src/docker.rs b/bin/periphery/src/docker.rs index 111270c27..f80f5d315 100644 --- a/bin/periphery/src/docker.rs +++ b/bin/periphery/src/docker.rs @@ -3,8 +3,11 @@ use std::{collections::HashMap, sync::OnceLock}; use anyhow::{Context, anyhow}; use bollard::{ Docker, - container::{InspectContainerOptions, ListContainersOptions}, - network::InspectNetworkOptions, + query_parameters::{ + InspectContainerOptions, InspectNetworkOptions, + ListContainersOptions, ListImagesOptions, ListNetworksOptions, + ListVolumesOptions, + }, }; use command::run_komodo_command; use komodo_client::entities::{ @@ -13,7 +16,7 @@ use komodo_client::entities::{ ContainerConfig, GraphDriverData, HealthConfig, PortBinding, container::*, image::*, network::*, volume::*, }, - to_komodo_name, + to_docker_compatible_name, update::Log, }; use run_command::async_run_command; @@ -42,7 +45,7 @@ impl DockerClient { ) -> anyhow::Result> { let mut containers = self .docker - .list_containers(Some(ListContainersOptions:: { + .list_containers(Some(ListContainersOptions { all: true, ..Default::default() })) @@ -63,11 +66,16 @@ impl DockerClient { created: container.created, size_rw: container.size_rw, size_root_fs: container.size_root_fs, - state: container - .state - .context("no container state")? - .parse() - .context("failed to parse container state")?, + state: match container.state.context("no container state")? { + bollard::secret::ContainerSummaryStateEnum::EMPTY => ContainerStateStatusEnum::Empty, + bollard::secret::ContainerSummaryStateEnum::CREATED => ContainerStateStatusEnum::Created, + bollard::secret::ContainerSummaryStateEnum::RUNNING => ContainerStateStatusEnum::Running, + bollard::secret::ContainerSummaryStateEnum::PAUSED => ContainerStateStatusEnum::Paused, + bollard::secret::ContainerSummaryStateEnum::RESTARTING => ContainerStateStatusEnum::Restarting, + bollard::secret::ContainerSummaryStateEnum::EXITED => ContainerStateStatusEnum::Exited, + bollard::secret::ContainerSummaryStateEnum::REMOVING => ContainerStateStatusEnum::Removing, + bollard::secret::ContainerSummaryStateEnum::DEAD => ContainerStateStatusEnum::Dead, + }, status: container.status, network_mode: container .host_config @@ -371,6 +379,7 @@ impl DockerClient { bollard::secret::MountTypeEnum::EMPTY => MountTypeEnum::Empty, bollard::secret::MountTypeEnum::BIND => MountTypeEnum::Bind, bollard::secret::MountTypeEnum::VOLUME => MountTypeEnum::Volume, + bollard::secret::MountTypeEnum::IMAGE => MountTypeEnum::Image, bollard::secret::MountTypeEnum::TMPFS => MountTypeEnum::Tmpfs, bollard::secret::MountTypeEnum::NPIPE => MountTypeEnum::Npipe, bollard::secret::MountTypeEnum::CLUSTER => MountTypeEnum::Cluster, @@ -456,6 +465,7 @@ impl DockerClient { bollard::secret::MountPointTypeEnum::EMPTY => MountTypeEnum::Empty, bollard::secret::MountPointTypeEnum::BIND => MountTypeEnum::Bind, bollard::secret::MountPointTypeEnum::VOLUME => MountTypeEnum::Volume, + bollard::secret::MountPointTypeEnum::IMAGE => MountTypeEnum::Image, bollard::secret::MountPointTypeEnum::TMPFS => MountTypeEnum::Tmpfs, bollard::secret::MountPointTypeEnum::NPIPE => MountTypeEnum::Npipe, bollard::secret::MountPointTypeEnum::CLUSTER => MountTypeEnum::Cluster, @@ -543,7 +553,7 @@ impl DockerClient { ) -> anyhow::Result> { let networks = self .docker - .list_networks::(None) + .list_networks(Option::::None) .await? .into_iter() .map(|network| { @@ -593,7 +603,7 @@ impl DockerClient { ) -> anyhow::Result { let network = self .docker - .inspect_network::( + .inspect_network( network_name, InspectNetworkOptions { verbose: true, @@ -653,7 +663,7 @@ impl DockerClient { ) -> anyhow::Result> { let images = self .docker - .list_images::(None) + .list_images(Option::::None) .await? .into_iter() .map(|image| { @@ -787,7 +797,7 @@ impl DockerClient { ) -> anyhow::Result> { let volumes = self .docker - .list_volumes::(None) + .list_volumes(Option::::None) .await? .volumes .unwrap_or_default() @@ -977,7 +987,7 @@ pub fn stop_container_command( signal: Option, time: Option, ) -> String { - let container_name = to_komodo_name(container_name); + let container_name = to_docker_compatible_name(container_name); let signal = signal .map(|signal| format!(" --signal {signal}")) .unwrap_or_default(); diff --git a/bin/periphery/src/helpers.rs b/bin/periphery/src/helpers.rs index b8098dc67..945dfb6b7 100644 --- a/bin/periphery/src/helpers.rs +++ b/bin/periphery/src/helpers.rs @@ -4,7 +4,7 @@ use anyhow::{Context, anyhow}; use komodo_client::{ entities::{ CloneArgs, EnvironmentVar, SearchCombinator, stack::Stack, - to_komodo_name, + to_path_compatible_name, }, parsers::QUOTE_PATTERN, }; @@ -102,7 +102,7 @@ pub async fn pull_or_clone_stack( let root = periphery_config() .stack_dir() - .join(to_komodo_name(&stack.name)); + .join(to_path_compatible_name(&stack.name)); let mut args: CloneArgs = stack.into(); // Set the clone destination to the one created for this run diff --git a/bin/periphery/src/main.rs b/bin/periphery/src/main.rs index b5026a0b9..b603a1b58 100644 --- a/bin/periphery/src/main.rs +++ b/bin/periphery/src/main.rs @@ -6,6 +6,7 @@ use std::{net::SocketAddr, str::FromStr}; use anyhow::Context; use axum_server::tls_rustls::RustlsConfig; +use config::periphery_config; mod api; mod compose; @@ -22,7 +23,12 @@ async fn app() -> anyhow::Result<()> { logger::init(&config.logging)?; info!("Komodo Periphery version: v{}", env!("CARGO_PKG_VERSION")); - info!("{:?}", config.sanitized()); + + if periphery_config().pretty_startup_config { + info!("{:#?}", config.sanitized()); + } else { + info!("{:?}", config.sanitized()); + } stats::spawn_system_stats_polling_thread(); diff --git a/bin/util/Cargo.toml b/bin/util/Cargo.toml new file mode 100644 index 000000000..25e798f3c --- /dev/null +++ b/bin/util/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "komodo_util" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[[bin]] +name = "util" +path = "src/main.rs" + +[dependencies] +tracing-subscriber.workspace = true +futures-util.workspace = true +dotenvy.workspace = true +tracing.workspace = true +anyhow.workspace = true +mungos.workspace = true +tokio.workspace = true +serde.workspace = true +envy.workspace = true \ No newline at end of file diff --git a/bin/util/aio.Dockerfile b/bin/util/aio.Dockerfile new file mode 100644 index 000000000..542d34e52 --- /dev/null +++ b/bin/util/aio.Dockerfile @@ -0,0 +1,22 @@ +FROM rust:1.87.0-bullseye AS builder + +WORKDIR /builder +COPY Cargo.toml Cargo.lock ./ +COPY ./lib ./lib +COPY ./client/core/rs ./client/core/rs +COPY ./client/periphery ./client/periphery +COPY ./bin/util ./bin/util + +# Compile bin +RUN cargo build -p komodo_util --release + +# Copy binaries to distroless base +FROM gcr.io/distroless/cc + +COPY --from=builder /builder/target/release/util /usr/local/bin/util + +CMD [ "util" ] + +LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo +LABEL org.opencontainers.image.description="Komodo Util" +LABEL org.opencontainers.image.licenses=GPL-3.0 \ No newline at end of file diff --git a/bin/util/docs/copy-database.md b/bin/util/docs/copy-database.md new file mode 100644 index 000000000..b308d5e26 --- /dev/null +++ b/bin/util/docs/copy-database.md @@ -0,0 +1,139 @@ +# Copy Database Utility + +Copy the Komodo database contents between running, mongo-compatible databases. +Can be used to move between MongoDB / FerretDB, or upgrade from FerretDB v1 to v2. + +```yaml +services: + + copy_database: + image: ghcr.io/moghtech/komodo-util + environment: + MODE: CopyDatabase + SOURCE_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@source:27017 + SOURCE_DB_NAME: ${KOMODO_DATABASE_DB_NAME:-komodo} + TARGET_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@target:27017 + TARGET_DB_NAME: ${KOMODO_DATABASE_DB_NAME:-komodo} + +``` + +## FerretDB v2 Update Guide + +Up to Komodo 1.17.5, users who wanted to use Postgres / Sqlite were instructed to deploy FerretDB v1. +Now that v2 is out however, v1 will go largely unsupported. Users are recommended to migrate to v2 for +the best performance and ongoing support / updates, however the internal data structures +have changed and this cannot be done in-place. + +Also note that FerretDB v2 no longer supports Sqlite, and only supports +a [customized Postgres distribution](https://docs.ferretdb.io/installation/documentdb/docker/). +Nonetheless, it remains a solid option for hosts which [do not support mongo](https://github.com/moghtech/komodo/issues/59). + +Also note, the same basic process outlined below can also be used to move between MongoDB and FerretDB, just replace FerretDB v2 +with the database you wish to move to. + +### **Step 1**: *Add* the new database to the top of your existing Komodo compose file. + +**Don't forget to also add the new volumes.** + +```yaml +## In Komodo compose.yaml +services: + postgres2: + # Recommended: Pin to a specific version + # https://github.com/FerretDB/documentdb/pkgs/container/postgres-documentdb + image: ghcr.io/ferretdb/postgres-documentdb + labels: + komodo.skip: # Prevent Komodo from stopping with StopAllContainers + restart: unless-stopped + logging: + driver: ${COMPOSE_LOGGING_DRIVER:-local} + # ports: + # - 5432:5432 + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + POSTGRES_USER: ${KOMODO_DB_USERNAME} + POSTGRES_PASSWORD: ${KOMODO_DB_PASSWORD} + POSTGRES_DB: postgres + + ferretdb2: + # Recommended: Pin to a specific version + # https://github.com/FerretDB/FerretDB/pkgs/container/ferretdb + image: ghcr.io/ferretdb/ferretdb + labels: + komodo.skip: # Prevent Komodo from stopping with StopAllContainers + restart: unless-stopped + depends_on: + - postgres2 + logging: + driver: ${COMPOSE_LOGGING_DRIVER:-local} + # ports: + # - 27017:27017 + volumes: + - ferretdb-state:/state + environment: + FERRETDB_POSTGRESQL_URL: postgres://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@postgres2:5432/postgres + + ...(unchanged) + +volumes: + ...(unchanged) + postgres-data: + ferretdb-state: +``` + +### **Step 2**: *Add* the database copy utility to Komodo compose file. + +The SOURCE_URI points to the existing database, ie the old FerretDB v1, and it depends +on whether it was deployed using Postgres or Sqlite. The example below uses the Postgres one, +but if you use Sqlite it should just be something like `mongodb://ferretdb:27017`. + +```yaml +## In Komodo compose.yaml +services: + ...(new database) + + copy_database: + image: ghcr.io/moghtech/komodo-util + environment: + MODE: CopyDatabase + SOURCE_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@ferretdb:27017/${KOMODO_DATABASE_DB_NAME:-komodo}?authMechanism=PLAIN + SOURCE_DB_NAME: ${KOMODO_DATABASE_DB_NAME:-komodo} + TARGET_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@ferretdb2:27017 + TARGET_DB_NAME: ${KOMODO_DATABASE_DB_NAME:-komodo} + + ...(unchanged) +``` + +### **Step 3**: *Compose Up* the new additions + +Run `docker compose -p komodo --env-file compose.env -f xxxxx.compose.yaml up -d`, filling in the name of your compose.yaml. +This will start up both the old and new database, and copy the data to the new one. + +Wait a few moments for the `copy_database` service to finish. When it exits, +confirm the logs show the data was moved successfully, and move on to the next step. + +### **Step 4**: Point Komodo Core to the new database + +In your Komodo compose.yaml, first *comment out* the `copy_database` service and old ferretdb v1 service/s. +Then update the `core` service environment to point to `ferretdb2`. + +```yaml +services: + ... + + core: + ...(unchanged) + environment: + KOMODO_DATABASE_ADDRESS: ferretdb2:27017 + KOMODO_DATABASE_USERNAME: ${KOMODO_DB_USERNAME} + KOMODO_DATABASE_PASSWORD: ${KOMODO_DB_PASSWORD} +``` + +### **Step 5**: Final *Compose Up* + +Repeat the same `docker compose` command as before to apply the changes, and then try navigating to your Komodo web page. +If it works, congrats, **you are done**. You can clean up the compose file if you would like, removing the old volumes etc. + +If it does not work, check the logs for any obvious issues, and if necessary you can undo the previous steps +to go back to using the previous database. diff --git a/bin/util/multi-arch.Dockerfile b/bin/util/multi-arch.Dockerfile new file mode 100644 index 000000000..129f4b7ba --- /dev/null +++ b/bin/util/multi-arch.Dockerfile @@ -0,0 +1,27 @@ +## Assumes the latest binaries for x86_64 and aarch64 are already built (by binaries.Dockerfile). +## Since theres no heavy build here, QEMU multi-arch builds are fine for this image. + +ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest +ARG X86_64_BINARIES=${BINARIES_IMAGE}-x86_64 +ARG AARCH64_BINARIES=${BINARIES_IMAGE}-aarch64 + +# This is required to work with COPY --from +FROM ${X86_64_BINARIES} AS x86_64 +FROM ${AARCH64_BINARIES} AS aarch64 + +FROM debian:bullseye-slim + +WORKDIR /app + +## Copy both binaries initially, but only keep appropriate one for the TARGETPLATFORM. +COPY --from=x86_64 /util /app/arch/linux/amd64 +COPY --from=aarch64 /util /app/arch/linux/arm64 + +ARG TARGETPLATFORM +RUN mv /app/arch/${TARGETPLATFORM} /usr/local/bin/util && rm -r /app/arch + +LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo +LABEL org.opencontainers.image.description="Komodo Util" +LABEL org.opencontainers.image.licenses=GPL-3.0 + +CMD [ "util" ] \ No newline at end of file diff --git a/bin/util/single-arch.Dockerfile b/bin/util/single-arch.Dockerfile new file mode 100644 index 000000000..80a1cdf3d --- /dev/null +++ b/bin/util/single-arch.Dockerfile @@ -0,0 +1,16 @@ +## Assumes the latest binaries for the required arch are already built (by binaries.Dockerfile). + +ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest + +# This is required to work with COPY --from +FROM ${BINARIES_IMAGE} AS binaries + +FROM gcr.io/distroless/cc + +COPY --from=binaries /util /usr/local/bin/util + +LABEL org.opencontainers.image.source=https://github.com/moghtech/komodo +LABEL org.opencontainers.image.description="Komodo Util" +LABEL org.opencontainers.image.licenses=GPL-3.0 + +CMD [ "util" ] \ No newline at end of file diff --git a/bin/util/src/copy_database.rs b/bin/util/src/copy_database.rs new file mode 100644 index 000000000..200f8fcbc --- /dev/null +++ b/bin/util/src/copy_database.rs @@ -0,0 +1,131 @@ +use std::time::Duration; + +use anyhow::Context; +use futures_util::{TryStreamExt, future::join_all}; +use mungos::{ + init::MongoBuilder, + mongodb::{ + bson::{Document, RawDocumentBuf}, + options::InsertManyOptions, + }, +}; +use serde::Deserialize; + +#[derive(Deserialize)] +struct Env { + /// Provide the source mongo uri to copy from + source_uri: String, + /// Provide the source db name to copy from. + /// Default: komodo + #[serde(default = "default_db_name")] + source_db_name: String, + /// Provide the source mongo uri to copy to + target_uri: String, + /// Provide the target db name to copy to. + /// Default: komodo + #[serde(default = "default_db_name")] + target_db_name: String, + /// Give the target database some time to initialize. + #[serde(default = "default_startup_sleep_seconds")] + startup_sleep_seconds: u64, +} + +fn default_db_name() -> String { + String::from("komodo") +} + +fn default_startup_sleep_seconds() -> u64 { + 5 +} + +pub async fn main() -> anyhow::Result<()> { + let env = envy::from_env::()?; + + info!("Sleeping for {} seconds...", env.startup_sleep_seconds); + tokio::time::sleep(Duration::from_secs(env.startup_sleep_seconds)) + .await; + + info!("Copying database..."); + + let source_db = MongoBuilder::default() + .uri(env.source_uri) + .build() + .await + .context("Invalid SOURCE_URI")? + .database(&env.source_db_name); + let target_db = MongoBuilder::default() + .uri(env.target_uri) + .build() + .await + .context("Invalid SOURCE_URI")? + .database(&env.target_db_name); + + let mut handles = Vec::new(); + + for collection in source_db + .list_collection_names() + .await + .context("Failed to list collections on source db")? + { + let source = source_db.collection::(&collection); + let target = target_db.collection::(&collection); + + handles.push(tokio::spawn(async move { + let res = async { + let mut buffer = Vec::::new(); + let mut count = 0; + let mut cursor = source + .find(Document::new()) + .await + .context("Failed to query source collection")?; + while let Some(doc) = cursor + .try_next() + .await + .context("Failed to get next document")? + { + count += 1; + buffer.push(doc); + if buffer.len() >= 20_000 { + if let Err(e) = target + .insert_many(&buffer) + .with_options( + InsertManyOptions::builder().ordered(false).build(), + ) + .await + { + error!("Failed to flush document batch in {collection} collection | {e:#}"); + }; + buffer.clear(); + } + } + if !buffer.is_empty() { + target + .insert_many(&buffer) + .with_options( + InsertManyOptions::builder().ordered(false).build(), + ) + .await + .context("Failed to flush documents")?; + } + anyhow::Ok(count) + } + .await; + match res { + Ok(count) => { + if count > 0 { + info!("Finished copying {collection} collection | Copied {count}"); + } + } + Err(e) => { + error!("Failed to copy {collection} collection | {e:#}") + } + } + })); + } + + join_all(handles).await; + + info!("Finished copying database ✅"); + + Ok(()) +} diff --git a/bin/util/src/main.rs b/bin/util/src/main.rs new file mode 100644 index 000000000..af9c2c478 --- /dev/null +++ b/bin/util/src/main.rs @@ -0,0 +1,42 @@ +#[macro_use] +extern crate tracing; + +use serde::Deserialize; + +mod copy_database; + +#[derive(Deserialize, Debug, Default)] +enum Mode { + #[default] + CopyDatabase, +} + +#[derive(Deserialize)] +struct Env { + mode: Mode, +} + +async fn app() -> anyhow::Result<()> { + dotenvy::dotenv().ok(); + tracing_subscriber::fmt::init(); + + let env = envy::from_env::()?; + + info!("Komodo Util version: v{}", env!("CARGO_PKG_VERSION")); + info!("Mode: {:?}", env.mode); + + match env.mode { + Mode::CopyDatabase => copy_database::main().await, + } +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let mut term_signal = tokio::signal::unix::signal( + tokio::signal::unix::SignalKind::terminate(), + )?; + tokio::select! { + res = tokio::spawn(app()) => res?, + _ = term_signal.recv() => Ok(()), + } +} diff --git a/client/core/rs/Cargo.toml b/client/core/rs/Cargo.toml index f6de964e4..1b5411786 100644 --- a/client/core/rs/Cargo.toml +++ b/client/core/rs/Cargo.toml @@ -32,6 +32,7 @@ serde_json.workspace = true tokio-util.workspace = true thiserror.workspace = true typeshare.workspace = true +indexmap.workspace = true futures.workspace = true reqwest.workspace = true tracing.workspace = true diff --git a/client/core/rs/src/api/read/deployment.rs b/client/core/rs/src/api/read/deployment.rs index aabaf58b5..e2be49d21 100644 --- a/client/core/rs/src/api/read/deployment.rs +++ b/client/core/rs/src/api/read/deployment.rs @@ -9,7 +9,7 @@ use crate::entities::{ Deployment, DeploymentActionState, DeploymentListItem, DeploymentQuery, DeploymentState, }, - docker::container::{ContainerListItem, ContainerStats}, + docker::container::{Container, ContainerListItem, ContainerStats}, update::Log, }; @@ -105,6 +105,26 @@ pub struct GetDeploymentContainerResponse { // +/// Inspect the docker container associated with the Deployment. +/// Response: [Container]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits, +)] +#[empty_traits(KomodoReadRequest)] +#[response(InspectDeploymentContainerResponse)] +#[error(serror::Error)] +pub struct InspectDeploymentContainer { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub deployment: String, +} + +#[typeshare] +pub type InspectDeploymentContainerResponse = Container; + +// + /// Get the deployment log's tail, split by stdout/stderr. /// Response: [Log]. /// diff --git a/client/core/rs/src/api/read/permission.rs b/client/core/rs/src/api/read/permission.rs index 6adeed507..313411a96 100644 --- a/client/core/rs/src/api/read/permission.rs +++ b/client/core/rs/src/api/read/permission.rs @@ -5,7 +5,7 @@ use typeshare::typeshare; use crate::entities::{ ResourceTarget, - permission::{Permission, PermissionLevel, UserTarget}, + permission::{Permission, PermissionLevelAndSpecifics, UserTarget}, }; use super::KomodoReadRequest; @@ -35,15 +35,15 @@ pub type ListPermissionsResponse = Vec; Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits, )] #[empty_traits(KomodoReadRequest)] -#[response(GetPermissionLevelResponse)] +#[response(GetPermissionResponse)] #[error(serror::Error)] -pub struct GetPermissionLevel { +pub struct GetPermission { /// The target to get user permission on. pub target: ResourceTarget, } #[typeshare] -pub type GetPermissionLevelResponse = PermissionLevel; +pub type GetPermissionResponse = PermissionLevelAndSpecifics; // diff --git a/client/core/rs/src/api/read/stack.rs b/client/core/rs/src/api/read/stack.rs index 39557d408..0d634b3cb 100644 --- a/client/core/rs/src/api/read/stack.rs +++ b/client/core/rs/src/api/read/stack.rs @@ -5,6 +5,7 @@ use typeshare::typeshare; use crate::entities::{ SearchCombinator, U64, + docker::container::Container, stack::{ Stack, StackActionState, StackListItem, StackQuery, StackService, }, @@ -53,6 +54,28 @@ pub type ListStackServicesResponse = Vec; // +/// Inspect the docker container associated with the Stack. +/// Response: [Container]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits, +)] +#[empty_traits(KomodoReadRequest)] +#[response(InspectStackContainerResponse)] +#[error(serror::Error)] +pub struct InspectStackContainer { + /// Id or name + #[serde(alias = "id", alias = "name")] + pub stack: String, + /// The service name to inspect + pub service: String, +} + +#[typeshare] +pub type InspectStackContainerResponse = Container; + +// + /// Get a stack's logs. Filter down included services. Response: [GetStackLogResponse]. /// /// Note. This call will hit the underlying server directly for most up to date log. diff --git a/client/core/rs/src/api/terminal.rs b/client/core/rs/src/api/terminal.rs index d7be9213f..d56058d1a 100644 --- a/client/core/rs/src/api/terminal.rs +++ b/client/core/rs/src/api/terminal.rs @@ -28,6 +28,32 @@ pub struct ConnectContainerExecQuery { pub shell: String, } +/// Query to connect to a container exec session (interactive shell over websocket) on the given Deployment. +/// This call will use access to the Deployment Terminal to permission the call. +/// TODO: Document calling. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ConnectDeploymentExecQuery { + /// Deployment Id or name + pub deployment: String, + /// The shell to connect to + pub shell: String, +} + +/// Query to connect to a container exec session (interactive shell over websocket) on the given Stack / service. +/// This call will use access to the Stack Terminal to permission the call. +/// TODO: Document calling. +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ConnectStackExecQuery { + /// Stack Id or name + pub stack: String, + /// The service name to connect to + pub service: String, + /// The shell to connect to + pub shell: String, +} + /// Execute a terminal command on the given server. /// TODO: Document calling. #[typeshare] diff --git a/client/core/rs/src/api/write/permissions.rs b/client/core/rs/src/api/write/permissions.rs index 2b1738d4b..e26699f93 100644 --- a/client/core/rs/src/api/write/permissions.rs +++ b/client/core/rs/src/api/write/permissions.rs @@ -5,7 +5,7 @@ use typeshare::typeshare; use crate::entities::{ NoData, ResourceTarget, ResourceTargetVariant, - permission::{PermissionLevel, UserTarget}, + permission::{PermissionLevelAndSpecifics, UserTarget}, }; use super::KomodoWriteRequest; @@ -25,7 +25,7 @@ pub struct UpdatePermissionOnTarget { /// Specify the target resource. pub resource_target: ResourceTarget, /// Specify the permission level. - pub permission: PermissionLevel, + pub permission: PermissionLevelAndSpecifics, } #[typeshare] @@ -48,7 +48,7 @@ pub struct UpdatePermissionOnResourceType { /// The resource type: eg. Server, Build, Deployment, etc. pub resource_type: ResourceTargetVariant, /// The base permission level. - pub permission: PermissionLevel, + pub permission: PermissionLevelAndSpecifics, } #[typeshare] diff --git a/client/core/rs/src/api/write/user_group.rs b/client/core/rs/src/api/write/user_group.rs index b7e8b37ca..f79c8448c 100644 --- a/client/core/rs/src/api/write/user_group.rs +++ b/client/core/rs/src/api/write/user_group.rs @@ -88,7 +88,7 @@ pub struct RemoveUserFromUserGroup { // -/// **Admin only.** Completely override the user in the group. +/// **Admin only.** Completely override the users in the group. /// Response: [UserGroup] #[typeshare] #[derive( @@ -103,3 +103,21 @@ pub struct SetUsersInUserGroup { /// The user ids or usernames to hard set as the group's users. pub users: Vec, } + +// + +/// **Admin only.** Set `everyone` property of User Group. +/// Response: [UserGroup] +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits, +)] +#[empty_traits(KomodoWriteRequest)] +#[response(UserGroup)] +#[error(serror::Error)] +pub struct SetEveryoneUserGroup { + /// Id or name. + pub user_group: String, + /// Whether this user group applies to everyone. + pub everyone: bool, +} diff --git a/client/core/rs/src/deserializers/mod.rs b/client/core/rs/src/deserializers/mod.rs index feb3d853a..962997e3e 100644 --- a/client/core/rs/src/deserializers/mod.rs +++ b/client/core/rs/src/deserializers/mod.rs @@ -5,6 +5,7 @@ mod environment; mod file_contents; mod labels; mod maybe_string_i64; +mod permission; mod string_list; mod term_signal_labels; diff --git a/client/core/rs/src/deserializers/permission.rs b/client/core/rs/src/deserializers/permission.rs new file mode 100644 index 000000000..c2fabad7f --- /dev/null +++ b/client/core/rs/src/deserializers/permission.rs @@ -0,0 +1,111 @@ +//! This is a module to deserialize [PermissionLevelAndSpecifics]. +//! +//! ## As just [PermissionLevel] +//! permission = "Write" +//! +//! ## As expanded with [SpecificPermission] +//! permission = { level = "Write", specific = ["Terminal"] } + +use std::str::FromStr; + +use indexmap::IndexSet; +use serde::{ + Deserialize, Serialize, + de::{Visitor, value::MapAccessDeserializer}, +}; + +use crate::entities::permission::{ + PermissionLevel, PermissionLevelAndSpecifics, SpecificPermission, +}; + +#[derive(Serialize, Deserialize)] +struct _PermissionLevelAndSpecifics { + #[serde(default)] + level: PermissionLevel, + #[serde(default)] + specific: IndexSet, +} + +impl Serialize for PermissionLevelAndSpecifics { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if self.specific.is_empty() { + // Serialize to simple string + self.level.serialize(serializer) + } else { + _PermissionLevelAndSpecifics { + level: self.level, + specific: self.specific.clone(), + } + .serialize(serializer) + } + } +} + +impl<'de> Deserialize<'de> for PermissionLevelAndSpecifics { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(PermissionLevelAndSpecificsVisitor) + } +} + +struct PermissionLevelAndSpecificsVisitor; + +impl<'de> Visitor<'de> for PermissionLevelAndSpecificsVisitor { + type Value = PermissionLevelAndSpecifics; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + write!( + formatter, + "PermissionLevel or PermissionLevelAndSpecifics" + ) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(PermissionLevelAndSpecifics { + level: PermissionLevel::from_str(v) + .map_err(|e| serde::de::Error::custom(e))?, + specific: IndexSet::new(), + }) + } + + fn visit_map(self, map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + _PermissionLevelAndSpecifics::deserialize( + MapAccessDeserializer::new(map), + ) + .map(|p| PermissionLevelAndSpecifics { + level: p.level, + specific: p.specific, + }) + } + + fn visit_unit(self) -> Result + where + E: serde::de::Error, + { + Ok(PermissionLevelAndSpecifics { + level: PermissionLevel::None, + specific: IndexSet::new(), + }) + } + + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + self.visit_unit() + } +} diff --git a/client/core/rs/src/entities/alerter.rs b/client/core/rs/src/entities/alerter.rs index 16e9742b8..551a8bae6 100644 --- a/client/core/rs/src/entities/alerter.rs +++ b/client/core/rs/src/entities/alerter.rs @@ -211,12 +211,17 @@ pub struct NtfyAlerterEndpoint { #[serde(default = "default_ntfy_url")] #[builder(default = "default_ntfy_url()")] pub url: String, + + /// Optional E-Mail Address to enable ntfy email notifications. + /// SMTP must be configured on the ntfy server. + pub email: Option, } impl Default for NtfyAlerterEndpoint { fn default() -> Self { Self { url: default_ntfy_url(), + email: None, } } } diff --git a/client/core/rs/src/entities/config/core.rs b/client/core/rs/src/entities/config/core.rs index ca34bbf61..ecf65c482 100644 --- a/client/core/rs/src/entities/config/core.rs +++ b/client/core/rs/src/entities/config/core.rs @@ -91,6 +91,8 @@ pub struct Env { pub komodo_logging_otlp_endpoint: Option, /// Override `logging.opentelemetry_service_name` pub komodo_logging_opentelemetry_service_name: Option, + /// Override `pretty_startup_config` + pub komodo_pretty_startup_config: Option, /// Override `transparent_mode` pub komodo_transparent_mode: Option, @@ -421,6 +423,11 @@ pub struct CoreConfig { #[serde(default)] pub logging: LogConfig, + /// Pretty-log (multi-line) the startup config + /// for easier human readability. + #[serde(default)] + pub pretty_startup_config: bool, + // =========== // = Pruning = // =========== @@ -595,6 +602,7 @@ impl CoreConfig { keep_stats_for_days: config.keep_stats_for_days, keep_alerts_for_days: config.keep_alerts_for_days, logging: config.logging, + pretty_startup_config: config.pretty_startup_config, transparent_mode: config.transparent_mode, ui_write_disabled: config.ui_write_disabled, disable_confirm_dialog: config.disable_confirm_dialog, diff --git a/client/core/rs/src/entities/config/periphery.rs b/client/core/rs/src/entities/config/periphery.rs index 31f9dd6e0..954c9cce7 100644 --- a/client/core/rs/src/entities/config/periphery.rs +++ b/client/core/rs/src/entities/config/periphery.rs @@ -144,6 +144,8 @@ pub struct Env { pub periphery_logging_otlp_endpoint: Option, /// Override `logging.opentelemetry_service_name` pub periphery_logging_opentelemetry_service_name: Option, + /// Override `pretty_startup_config` + pub periphery_pretty_startup_config: Option, /// Override `allowed_ips` pub periphery_allowed_ips: Option>, @@ -234,6 +236,11 @@ pub struct PeripheryConfig { #[serde(default)] pub logging: LogConfig, + /// Pretty-log (multi-line) the startup config + /// for easier human readability. + #[serde(default)] + pub pretty_startup_config: bool, + /// Limits which IPv4 addresses are allowed to call the api. /// Default: none /// @@ -319,6 +326,7 @@ impl Default for PeripheryConfig { stats_polling_rate: default_stats_polling_rate(), legacy_compose_cli: Default::default(), logging: Default::default(), + pretty_startup_config: Default::default(), allowed_ips: Default::default(), passkeys: Default::default(), include_disk_mounts: Default::default(), @@ -347,6 +355,7 @@ impl PeripheryConfig { stats_polling_rate: self.stats_polling_rate, legacy_compose_cli: self.legacy_compose_cli, logging: self.logging.clone(), + pretty_startup_config: self.pretty_startup_config, allowed_ips: self.allowed_ips.clone(), passkeys: self .passkeys diff --git a/client/core/rs/src/entities/docker/container.rs b/client/core/rs/src/entities/docker/container.rs index c948ba16e..281750841 100644 --- a/client/core/rs/src/entities/docker/container.rs +++ b/client/core/rs/src/entities/docker/container.rs @@ -268,10 +268,10 @@ pub enum ContainerStateStatusEnum { Paused, #[serde(rename = "restarting")] Restarting, - #[serde(rename = "removing")] - Removing, #[serde(rename = "exited")] Exited, + #[serde(rename = "removing")] + Removing, #[serde(rename = "dead")] Dead, } @@ -866,6 +866,8 @@ pub enum MountTypeEnum { Bind, #[serde(rename = "volume")] Volume, + #[serde(rename = "image")] + Image, #[serde(rename = "tmpfs")] Tmpfs, #[serde(rename = "npipe")] diff --git a/client/core/rs/src/entities/mod.rs b/client/core/rs/src/entities/mod.rs index 6a2ce58c1..2080e33e4 100644 --- a/client/core/rs/src/entities/mod.rs +++ b/client/core/rs/src/entities/mod.rs @@ -143,9 +143,9 @@ pub fn get_image_name( }: &build::Build, ) -> anyhow::Result { let name = if image_name.is_empty() { - to_komodo_name(name) + to_docker_compatible_name(name) } else { - to_komodo_name(image_name) + to_docker_compatible_name(image_name) }; let name = match ( !domain.is_empty(), @@ -164,10 +164,18 @@ pub fn get_image_name( Ok(name) } -pub fn to_komodo_name(name: &str) -> String { +pub fn to_general_name(name: &str) -> String { + name.replace('\n', "_").trim().to_string() +} + +pub fn to_path_compatible_name(name: &str) -> String { + name.replace([' ', '\n'], "_").trim().to_string() +} + +pub fn to_docker_compatible_name(name: &str) -> String { name .to_lowercase() - .replace([' ', '.', ',', '\n'], "_") + .replace([' ', '.', ',', '\n', '&'], "_") .trim() .to_string() } @@ -409,7 +417,7 @@ impl CloneArgs { pub fn path(&self, repo_dir: &Path) -> PathBuf { let path = match &self.destination { Some(destination) => PathBuf::from(&destination), - None => repo_dir.join(to_komodo_name(&self.name)), + None => repo_dir.join(to_path_compatible_name(&self.name)), }; path.components().collect::() } diff --git a/client/core/rs/src/entities/permission.rs b/client/core/rs/src/entities/permission.rs index 4a44879a1..1b5251b1d 100644 --- a/client/core/rs/src/entities/permission.rs +++ b/client/core/rs/src/entities/permission.rs @@ -1,6 +1,9 @@ +use std::fmt::Write; + use derive_variants::EnumVariants; +use indexmap::IndexSet; use serde::{Deserialize, Serialize}; -use strum::{AsRefStr, Display, EnumString}; +use strum::{AsRefStr, Display, EnumString, IntoStaticStr, VariantArray}; use typeshare::typeshare; use super::{MongoId, ResourceTarget}; @@ -36,9 +39,12 @@ pub struct Permission { pub user_target: UserTarget, /// The target resource pub resource_target: ResourceTarget, - /// The permission level + /// The permission level for the [user_target] on the [resource_target]. #[serde(default)] pub level: PermissionLevel, + /// Any specific permissions for the [user_target] on the [resource_target]. + #[serde(default)] + pub specific: IndexSet, } #[typeshare] @@ -90,7 +96,7 @@ pub enum PermissionLevel { /// No permissions. #[default] None, - /// Can see the rousource + /// Can read resource information and config Read, /// Can execute actions on the resource Execute, @@ -103,3 +109,220 @@ impl Default for &PermissionLevel { &PermissionLevel::None } } + +/// The specific types of permission that a User or UserGroup can have on a resource. +#[typeshare] +#[derive( + Serialize, + Deserialize, + Debug, + Display, + EnumString, + AsRefStr, + IntoStaticStr, + VariantArray, + Hash, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, +)] +pub enum SpecificPermission { + /// On **Server** + /// - Access the terminal apis + /// On **Stack / Deployment** + /// - Access the container exec Apis + Terminal, + /// On **Server** + /// - Allowed to attach Stacks, Deployments, Repos, Builders to the Server + /// On **Builder** + /// - Allowed to attach Builds to the Builder + /// On **Build** + /// - Allowed to attach Deployments to the Build + Attach, + /// On **Server** + /// - Access the `container inspect` apis + /// On **Stack / Deployment** + /// - Access `container inspect` apis for associated containers + Inspect, + /// On **Server** + /// - Read all container logs on the server + /// On **Stack / Deployment** + /// - Read the container logs + Logs, + /// On **Server** + /// - Read all the processes on the host + Processes, +} + +impl SpecificPermission { + fn all() -> IndexSet { + SpecificPermission::VARIANTS.into_iter().cloned().collect() + } +} + +#[typeshare] +#[derive(Debug, Clone, Default)] +pub struct PermissionLevelAndSpecifics { + pub level: PermissionLevel, + pub specific: IndexSet, +} + +impl From for PermissionLevelAndSpecifics { + fn from(level: PermissionLevel) -> Self { + Self { + level, + specific: IndexSet::new(), + } + } +} + +impl From<&Permission> for PermissionLevelAndSpecifics { + fn from(value: &Permission) -> Self { + Self { + level: value.level, + specific: value.specific.clone(), + } + } +} + +impl PermissionLevel { + /// Add all possible permissions (for use in admin case) + pub fn all(self) -> PermissionLevelAndSpecifics { + PermissionLevelAndSpecifics { + level: self, + specific: SpecificPermission::all(), + } + } + + pub fn specifics( + self, + specific: IndexSet, + ) -> PermissionLevelAndSpecifics { + PermissionLevelAndSpecifics { + level: self, + specific, + } + } + + fn specific( + self, + specific: SpecificPermission, + ) -> PermissionLevelAndSpecifics { + PermissionLevelAndSpecifics { + level: self, + specific: [specific].into_iter().collect(), + } + } + + /// Operation requires Terminal permission + pub fn terminal(self) -> PermissionLevelAndSpecifics { + self.specific(SpecificPermission::Terminal) + } + + /// Operation requires Attach permission + pub fn attach(self) -> PermissionLevelAndSpecifics { + self.specific(SpecificPermission::Attach) + } + + /// Operation requires Inspect permission + pub fn inspect(self) -> PermissionLevelAndSpecifics { + self.specific(SpecificPermission::Inspect) + } + + /// Operation requires Logs permission + pub fn logs(self) -> PermissionLevelAndSpecifics { + self.specific(SpecificPermission::Logs) + } + + /// Operation requires Processes permission + pub fn processes(self) -> PermissionLevelAndSpecifics { + self.specific(SpecificPermission::Processes) + } +} + +impl PermissionLevelAndSpecifics { + /// Returns true when self.level >= other.level, + /// and has all required specific permissions. + pub fn fulfills( + &self, + other: &PermissionLevelAndSpecifics, + ) -> bool { + if self.level < other.level { + return false; + } + for specific in other.specific.iter() { + if !self.specific.contains(specific) { + return false; + } + } + true + } + + /// Returns true when self has all required specific permissions. + pub fn fulfills_specific( + &self, + specifics: &IndexSet, + ) -> bool { + for specific in specifics.iter() { + if !self.specific.contains(specific) { + return false; + } + } + true + } + + pub fn specifics_for_log(&self) -> String { + let mut res = String::new(); + for specific in self.specific.iter() { + write!(&mut res, ", {specific}").unwrap(); + } + res + } + + pub fn specifics( + mut self, + specific: IndexSet, + ) -> PermissionLevelAndSpecifics { + self.specific = specific; + self + } + + fn specific( + mut self, + specific: SpecificPermission, + ) -> PermissionLevelAndSpecifics { + self.specific.insert(specific); + PermissionLevelAndSpecifics { + level: self.level, + specific: self.specific, + } + } + + /// Operation requires Terminal permission + pub fn terminal(self) -> PermissionLevelAndSpecifics { + self.specific(SpecificPermission::Terminal) + } + + /// Operation requires Attach permission + pub fn attach(self) -> PermissionLevelAndSpecifics { + self.specific(SpecificPermission::Attach) + } + + /// Operation requires Inspect permission + pub fn inspect(self) -> PermissionLevelAndSpecifics { + self.specific(SpecificPermission::Inspect) + } + + /// Operation requires Logs permission + pub fn logs(self) -> PermissionLevelAndSpecifics { + self.specific(SpecificPermission::Logs) + } + + /// Operation requires Processes permission + pub fn processes(self) -> PermissionLevelAndSpecifics { + self.specific(SpecificPermission::Processes) + } +} diff --git a/client/core/rs/src/entities/resource.rs b/client/core/rs/src/entities/resource.rs index 9daba02f5..73fd4e1d9 100644 --- a/client/core/rs/src/entities/resource.rs +++ b/client/core/rs/src/entities/resource.rs @@ -9,7 +9,9 @@ use crate::{ entities::{I64, MongoId}, }; -use super::{ResourceTargetVariant, permission::PermissionLevel}; +use super::{ + ResourceTargetVariant, permission::PermissionLevelAndSpecifics, +}; #[typeshare] #[derive(Debug, Clone, Serialize, Deserialize, Builder)] @@ -59,7 +61,7 @@ pub struct Resource { /// resource. #[serde(default)] #[builder(default)] - pub base_permission: PermissionLevel, + pub base_permission: PermissionLevelAndSpecifics, } impl Default for Resource { @@ -72,7 +74,7 @@ impl Default for Resource { tags: Vec::new(), info: I::default(), config: C::default(), - base_permission: PermissionLevel::None, + base_permission: Default::default(), } } } diff --git a/client/core/rs/src/entities/stack.rs b/client/core/rs/src/entities/stack.rs index 882f7d030..852f9b9f6 100644 --- a/client/core/rs/src/entities/stack.rs +++ b/client/core/rs/src/entities/stack.rs @@ -19,7 +19,7 @@ use super::{ FileContents, SystemCommand, docker::container::ContainerListItem, resource::{Resource, ResourceListItem, ResourceQuery}, - to_komodo_name, + to_docker_compatible_name, }; #[typeshare] @@ -38,8 +38,10 @@ impl Stack { .config .project_name .is_empty() - .then(|| to_komodo_name(&self.name)) - .unwrap_or_else(|| to_komodo_name(&self.config.project_name)) + .then(|| to_docker_compatible_name(&self.name)) + .unwrap_or_else(|| { + to_docker_compatible_name(&self.config.project_name) + }) } pub fn file_paths(&self) -> &[String] { diff --git a/client/core/rs/src/entities/toml.rs b/client/core/rs/src/entities/toml.rs index f5e6a8eae..a796241ea 100644 --- a/client/core/rs/src/entities/toml.rs +++ b/client/core/rs/src/entities/toml.rs @@ -1,16 +1,22 @@ -use std::collections::HashMap; - +use indexmap::{IndexMap, IndexSet}; use serde::{Deserialize, Serialize}; use typeshare::typeshare; use super::{ ResourceTarget, ResourceTargetVariant, - action::_PartialActionConfig, alerter::_PartialAlerterConfig, - build::_PartialBuildConfig, builder::_PartialBuilderConfig, - deployment::_PartialDeploymentConfig, permission::PermissionLevel, - procedure::_PartialProcedureConfig, repo::_PartialRepoConfig, + action::_PartialActionConfig, + alerter::_PartialAlerterConfig, + build::_PartialBuildConfig, + builder::_PartialBuilderConfig, + deployment::_PartialDeploymentConfig, + permission::{ + PermissionLevel, PermissionLevelAndSpecifics, SpecificPermission, + }, + procedure::_PartialProcedureConfig, + repo::_PartialRepoConfig, server::_PartialServerConfig, - stack::_PartialStackConfig, sync::_PartialResourceSyncConfig, + stack::_PartialStackConfig, + sync::_PartialResourceSyncConfig, variable::Variable, }; @@ -147,13 +153,18 @@ pub struct UserGroupToml { /// User group name pub name: String, + /// Whether all users will implicitly have the permissions in this group. + #[serde(default)] + pub everyone: bool, + /// Users in the group #[serde(default)] pub users: Vec, /// Give the user group elevated permissions on all resources of a certain type #[serde(default)] - pub all: HashMap, + pub all: + IndexMap, /// Permissions given to the group #[serde(default, alias = "permission")] @@ -173,5 +184,10 @@ pub struct PermissionToml { /// - Read /// - Execute /// - Write + #[serde(default)] pub level: PermissionLevel, + + /// Any [SpecificPermissions](SpecificPermission) on the resource + #[serde(default, skip_serializing_if = "IndexSet::is_empty")] + pub specific: IndexSet, } diff --git a/client/core/rs/src/entities/user.rs b/client/core/rs/src/entities/user.rs index ed207500c..609302017 100644 --- a/client/core/rs/src/entities/user.rs +++ b/client/core/rs/src/entities/user.rs @@ -1,11 +1,14 @@ use std::{collections::HashMap, sync::OnceLock}; +use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use typeshare::typeshare; use crate::entities::{I64, MongoId}; -use super::{ResourceTargetVariant, permission::PermissionLevel}; +use super::{ + ResourceTargetVariant, permission::PermissionLevelAndSpecifics, +}; #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Default)] @@ -66,7 +69,8 @@ pub struct User { /// Give the user elevated permissions on all resources of a certain type #[serde(default)] - pub all: HashMap, + pub all: + IndexMap, #[serde(default)] pub updated_at: I64, diff --git a/client/core/rs/src/entities/user_group.rs b/client/core/rs/src/entities/user_group.rs index 3668643d2..86e6c1ac8 100644 --- a/client/core/rs/src/entities/user_group.rs +++ b/client/core/rs/src/entities/user_group.rs @@ -1,12 +1,12 @@ -use std::collections::HashMap; - +use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use typeshare::typeshare; use crate::deserializers::string_list_deserializer; use super::{ - I64, MongoId, ResourceTargetVariant, permission::PermissionLevel, + I64, MongoId, ResourceTargetVariant, + permission::PermissionLevelAndSpecifics, }; /// Permission users at the group level. @@ -37,6 +37,11 @@ pub struct UserGroup { #[cfg_attr(feature = "mongo", unique_index)] pub name: String, + /// Whether all users will implicitly have the permissions in this group. + #[cfg_attr(feature = "mongo", index)] + #[serde(default)] + pub everyone: bool, + /// User ids of group members #[cfg_attr(feature = "mongo", index)] #[serde(default, deserialize_with = "string_list_deserializer")] @@ -44,7 +49,8 @@ pub struct UserGroup { /// Give the user group elevated permissions on all resources of a certain type #[serde(default)] - pub all: HashMap, + pub all: + IndexMap, /// Unix time (ms) when user group last updated #[serde(default)] diff --git a/client/core/ts/generate_types.mjs b/client/core/ts/generate_types.mjs index c526dc0ec..0e6599672 100644 --- a/client/core/ts/generate_types.mjs +++ b/client/core/ts/generate_types.mjs @@ -31,6 +31,16 @@ function fix_types() { .replaceAll("AlertDataVariant", 'AlertData["type"]') .replaceAll("ServerTemplateConfigVariant", 'ServerTemplateConfig["type"]') // Add '| string' to env vars - .replaceAll("EnvironmentVar[]", "EnvironmentVar[] | string"); + .replaceAll("EnvironmentVar[]", "EnvironmentVar[] | string") + .replaceAll("IndexSet", "Array") + .replaceAll( + ": PermissionLevelAndSpecifics", + ": PermissionLevelAndSpecifics | PermissionLevel" + ) + .replaceAll( + ", PermissionLevelAndSpecifics", + ", PermissionLevelAndSpecifics | PermissionLevel" + ) + .replaceAll("IndexMap", "Record"); writeFileSync(types_path, fixed); } diff --git a/client/core/ts/package.json b/client/core/ts/package.json index 8501f8d7d..6a3ec44c0 100644 --- a/client/core/ts/package.json +++ b/client/core/ts/package.json @@ -1,6 +1,6 @@ { "name": "komodo_client", - "version": "1.17.5", + "version": "1.18.0", "description": "Komodo client package", "homepage": "https://komo.do", "main": "dist/lib.js", diff --git a/client/core/ts/src/lib.ts b/client/core/ts/src/lib.ts index 76f7e4a68..c1e1d22f4 100644 --- a/client/core/ts/src/lib.ts +++ b/client/core/ts/src/lib.ts @@ -9,6 +9,8 @@ import { AuthRequest, BatchExecutionResponse, ConnectContainerExecQuery, + ConnectDeploymentExecQuery, + ConnectStackExecQuery, ConnectTerminalQuery, ExecuteRequest, ExecuteTerminalBody, @@ -23,7 +25,7 @@ import { export * as Types from "./types.js"; -type InitOptions = +export type InitOptions = | { type: "jwt"; params: { jwt: string } } | { type: "api-key"; params: { key: string; secret: string } }; @@ -37,6 +39,27 @@ export class CancelToken { } } +export type ContainerExecQuery = + | { + type: "container"; + query: ConnectContainerExecQuery; + } + | { + type: "deployment"; + query: ConnectDeploymentExecQuery; + } + | { + type: "stack"; + query: ConnectStackExecQuery; + }; + +export type TerminalCallbacks = { + on_message?: (e: MessageEvent) => void; + on_login?: () => void; + on_open?: () => void; + on_close?: () => void; +}; + /** Initialize a new client for Komodo */ export function KomodoClient(url: string, options: InitOptions) { const state = { @@ -317,11 +340,7 @@ export function KomodoClient(url: string, options: InitOptions) { on_close, }: { query: ConnectTerminalQuery; - on_message?: (e: MessageEvent) => void; - on_login?: () => void; - on_open?: () => void; - on_close?: () => void; - }) => { + } & TerminalCallbacks) => { const url_query = new URLSearchParams( query as any as Record ).toString(); @@ -366,23 +385,19 @@ export function KomodoClient(url: string, options: InitOptions) { }; const connect_container_exec = ({ - query, + query: { type, query }, on_message, on_login, on_open, on_close, }: { - query: ConnectContainerExecQuery; - on_message?: (e: MessageEvent) => void; - on_login?: () => void; - on_open?: () => void; - on_close?: () => void; - }) => { + query: ContainerExecQuery; + } & TerminalCallbacks) => { const url_query = new URLSearchParams( query as any as Record ).toString(); const ws = new WebSocket( - url.replace("http", "ws") + "/ws/container?" + url_query + url.replace("http", "ws") + `/ws/${type}/terminal?` + url_query ); // Handle login on websocket open ws.onopen = () => { @@ -617,7 +632,9 @@ export function KomodoClient(url: string, options: InitOptions) { connect_terminal, /** * Subscribes to container exec io over websocket message, - * for use with xtermjs. + * for use with xtermjs. Can connect to Deployment, Stack, + * or any container on a Server. The permission used to allow the connection + * depends on `query.type`. */ connect_container_exec, /** diff --git a/client/core/ts/src/responses.ts b/client/core/ts/src/responses.ts index 3b2b98084..c18d6194d 100644 --- a/client/core/ts/src/responses.ts +++ b/client/core/ts/src/responses.ts @@ -24,7 +24,7 @@ export type ReadResponses = { // ==== USER ==== GetUsername: Types.GetUsernameResponse; - GetPermissionLevel: Types.GetPermissionLevelResponse; + GetPermission: Types.GetPermissionResponse; FindUser: Types.FindUserResponse; ListUsers: Types.ListUsersResponse; ListApiKeys: Types.ListApiKeysResponse; @@ -76,6 +76,20 @@ export type ReadResponses = { ListFullServers: Types.ListFullServersResponse; ListTerminals: Types.ListTerminalsResponse; + // ==== STACK ==== + GetStacksSummary: Types.GetStacksSummaryResponse; + GetStack: Types.GetStackResponse; + GetStackActionState: Types.GetStackActionStateResponse; + GetStackWebhooksEnabled: Types.GetStackWebhooksEnabledResponse; + GetStackLog: Types.GetStackLogResponse; + SearchStackLog: Types.SearchStackLogResponse; + InspectStackContainer: Types.InspectStackContainerResponse; + ListStacks: Types.ListStacksResponse; + ListFullStacks: Types.ListFullStacksResponse; + ListStackServices: Types.ListStackServicesResponse; + ListCommonStackExtraArgs: Types.ListCommonStackExtraArgsResponse; + ListCommonStackBuildExtraArgs: Types.ListCommonStackBuildExtraArgsResponse; + // ==== DEPLOYMENT ==== GetDeploymentsSummary: Types.GetDeploymentsSummaryResponse; GetDeployment: Types.GetDeploymentResponse; @@ -84,6 +98,7 @@ export type ReadResponses = { GetDeploymentStats: Types.GetDeploymentStatsResponse; GetDeploymentLog: Types.GetDeploymentLogResponse; SearchDeploymentLog: Types.SearchDeploymentLogResponse; + InspectDeploymentContainer: Types.InspectDeploymentContainerResponse; ListDeployments: Types.ListDeploymentsResponse; ListFullDeployments: Types.ListFullDeploymentsResponse; ListCommonDeploymentExtraArgs: Types.ListCommonDeploymentExtraArgsResponse; @@ -115,19 +130,6 @@ export type ReadResponses = { ListResourceSyncs: Types.ListResourceSyncsResponse; ListFullResourceSyncs: Types.ListFullResourceSyncsResponse; - // ==== STACK ==== - GetStacksSummary: Types.GetStacksSummaryResponse; - GetStack: Types.GetStackResponse; - GetStackActionState: Types.GetStackActionStateResponse; - GetStackWebhooksEnabled: Types.GetStackWebhooksEnabledResponse; - GetStackLog: Types.GetStackLogResponse; - SearchStackLog: Types.SearchStackLogResponse; - ListStacks: Types.ListStacksResponse; - ListFullStacks: Types.ListFullStacksResponse; - ListStackServices: Types.ListStackServicesResponse; - ListCommonStackExtraArgs: Types.ListCommonStackExtraArgsResponse; - ListCommonStackBuildExtraArgs: Types.ListCommonStackBuildExtraArgsResponse; - // ==== BUILDER ==== GetBuildersSummary: Types.GetBuildersSummaryResponse; GetBuilder: Types.GetBuilderResponse; @@ -191,6 +193,7 @@ export type WriteResponses = { AddUserToUserGroup: Types.UserGroup; RemoveUserFromUserGroup: Types.UserGroup; SetUsersInUserGroup: Types.UserGroup; + SetEveryoneUserGroup: Types.UserGroup; // ==== PERMISSIONS ==== UpdateUserAdmin: Types.UpdateUserAdminResponse; @@ -211,6 +214,17 @@ export type WriteResponses = { DeleteTerminal: Types.NoData; DeleteAllTerminals: Types.NoData; + // ==== STACK ==== + CreateStack: Types.Stack; + CopyStack: Types.Stack; + DeleteStack: Types.Stack; + UpdateStack: Types.Stack; + RenameStack: Types.Update; + WriteStackFileContents: Types.Update; + RefreshStackCache: Types.NoData; + CreateStackWebhook: Types.CreateStackWebhookResponse; + DeleteStackWebhook: Types.DeleteStackWebhookResponse; + // ==== DEPLOYMENT ==== CreateDeployment: Types.Deployment; CopyDeployment: Types.Deployment; @@ -280,17 +294,6 @@ export type WriteResponses = { CreateSyncWebhook: Types.CreateSyncWebhookResponse; DeleteSyncWebhook: Types.DeleteSyncWebhookResponse; - // ==== STACK ==== - CreateStack: Types.Stack; - CopyStack: Types.Stack; - DeleteStack: Types.Stack; - UpdateStack: Types.Stack; - RenameStack: Types.Update; - WriteStackFileContents: Types.Update; - RefreshStackCache: Types.NoData; - CreateStackWebhook: Types.CreateStackWebhookResponse; - DeleteStackWebhook: Types.DeleteStackWebhookResponse; - // ==== TAG ==== CreateTag: Types.Tag; DeleteTag: Types.Tag; @@ -338,6 +341,21 @@ export type ExecuteResponses = { PruneBuildx: Types.Update; PruneSystem: Types.Update; + // ==== STACK ==== + DeployStack: Types.Update; + BatchDeployStack: Types.BatchExecutionResponse; + DeployStackIfChanged: Types.Update; + BatchDeployStackIfChanged: Types.BatchExecutionResponse; + PullStack: Types.Update; + BatchPullStack: Types.BatchExecutionResponse; + StartStack: Types.Update; + RestartStack: Types.Update; + StopStack: Types.Update; + PauseStack: Types.Update; + UnpauseStack: Types.Update; + DestroyStack: Types.Update; + BatchDestroyStack: Types.BatchExecutionResponse; + // ==== DEPLOYMENT ==== Deploy: Types.Update; BatchDeploy: Types.BatchExecutionResponse; @@ -375,21 +393,6 @@ export type ExecuteResponses = { // ==== SYNC ==== RunSync: Types.Update; - // ==== STACK ==== - DeployStack: Types.Update; - BatchDeployStack: Types.BatchExecutionResponse; - DeployStackIfChanged: Types.Update; - BatchDeployStackIfChanged: Types.BatchExecutionResponse; - PullStack: Types.Update; - BatchPullStack: Types.BatchExecutionResponse; - StartStack: Types.Update; - RestartStack: Types.Update; - StopStack: Types.Update; - PauseStack: Types.Update; - UnpauseStack: Types.Update; - DestroyStack: Types.Update; - BatchDestroyStack: Types.BatchExecutionResponse; - // ==== STACK Service ==== DeployStackService: Types.Update; StartStackService: Types.Update; diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index a1ee106b1..f1b4f43e4 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -14,7 +14,7 @@ export type I64 = number; export enum PermissionLevel { /** No permissions. */ None = "None", - /** Can see the rousource */ + /** Can read resource information and config */ Read = "Read", /** Can execute actions on the resource */ Execute = "Execute", @@ -22,6 +22,11 @@ export enum PermissionLevel { Write = "Write", } +export interface PermissionLevelAndSpecifics { + level: PermissionLevel; + specific: Array; +} + export interface Resource { /** * The Mongo ID of the resource. @@ -48,7 +53,7 @@ export interface Resource { * Set a base permission level that all users will have on the * resource. */ - base_permission?: PermissionLevel; + base_permission?: PermissionLevelAndSpecifics | PermissionLevel; } export enum ScheduleFormat { @@ -798,7 +803,7 @@ export interface User { /** Recently viewed ids */ recents?: Record; /** Give the user elevated permissions on all resources of a certain type */ - all?: Record; + all?: Record; updated_at?: I64; } @@ -1353,7 +1358,7 @@ export type GetDockerRegistryAccountResponse = DockerRegistryAccount; export type GetGitProviderAccountResponse = GitProviderAccount; -export type GetPermissionLevelResponse = PermissionLevel; +export type GetPermissionResponse = PermissionLevelAndSpecifics; export interface ProcedureActionState { running: boolean; @@ -2332,10 +2337,12 @@ export interface UserGroup { _id?: MongoId; /** A name for the user group */ name: string; + /** Whether all users will implicitly have the permissions in this group. */ + everyone?: boolean; /** User ids of group members */ users?: string[]; /** Give the user group elevated permissions on all resources of a certain type */ - all?: Record; + all?: Record; /** Unix time (ms) when user group last updated */ updated_at?: I64; } @@ -2352,8 +2359,8 @@ export enum ContainerStateStatusEnum { Running = "running", Paused = "paused", Restarting = "restarting", - Removing = "removing", Exited = "exited", + Removing = "removing", Dead = "dead", } @@ -2487,6 +2494,7 @@ export enum MountTypeEnum { Empty = "", Bind = "bind", Volume = "volume", + Image = "image", Tmpfs = "tmpfs", Npipe = "npipe", Cluster = "cluster", @@ -2895,6 +2903,8 @@ export interface Container { NetworkSettings?: NetworkSettings; } +export type InspectDeploymentContainerResponse = Container; + export type InspectDockerContainerResponse = Container; /** Information about the image's RootFS, including the layer IDs. */ @@ -3145,6 +3155,8 @@ export interface Volume { export type InspectDockerVolumeResponse = Volume; +export type InspectStackContainerResponse = Container; + export type JsonValue = any; export type ListActionsResponse = ActionListItem[]; @@ -3375,8 +3387,10 @@ export interface Permission { user_target: UserTarget; /** The target resource */ resource_target: ResourceTarget; - /** The permission level */ + /** The permission level for the [user_target] on the [resource_target]. */ level?: PermissionLevel; + /** Any specific permissions for the [user_target] on the [resource_target]. */ + specific?: Array; } export type ListPermissionsResponse = Permission[]; @@ -4176,6 +4190,32 @@ export interface ConnectContainerExecQuery { shell: string; } +/** + * Query to connect to a container exec session (interactive shell over websocket) on the given Deployment. + * This call will use access to the Deployment Terminal to permission the call. + * TODO: Document calling. + */ +export interface ConnectDeploymentExecQuery { + /** Deployment Id or name */ + deployment: string; + /** The shell to connect to */ + shell: string; +} + +/** + * Query to connect to a container exec session (interactive shell over websocket) on the given Stack / service. + * This call will use access to the Stack Terminal to permission the call. + * TODO: Document calling. + */ +export interface ConnectStackExecQuery { + /** Stack Id or name */ + stack: string; + /** The service name to connect to */ + service: string; + /** The shell to connect to */ + shell: string; +} + /** * Query to connect to a terminal (interactive shell over websocket) on the given server. * TODO: Document calling. @@ -5491,7 +5531,7 @@ export interface GetPeripheryVersionResponse { * Factors in any UserGroup's permissions they may be a part of. * Response: [PermissionLevel] */ -export interface GetPermissionLevel { +export interface GetPermission { /** The target to get user permission on. */ target: ResourceTarget; } @@ -5868,6 +5908,15 @@ export interface GetVersionResponse { version: string; } +/** + * Inspect the docker container associated with the Deployment. + * Response: [Container]. + */ +export interface InspectDeploymentContainer { + /** Id or name */ + deployment: string; +} + /** Inspect a docker container on the server. Response: [Container]. */ export interface InspectDockerContainer { /** Id or name */ @@ -5900,6 +5949,17 @@ export interface InspectDockerVolume { volume: string; } +/** + * Inspect the docker container associated with the Stack. + * Response: [Container]. + */ +export interface InspectStackContainer { + /** Id or name */ + stack: string; + /** The service name to inspect */ + service: string; +} + export interface LatestCommit { hash: string; message: string; @@ -6442,6 +6502,11 @@ export interface NameAndId { export interface NtfyAlerterEndpoint { /** The ntfy topic URL */ url: string; + /** + * Optional E-Mail Address to enable ntfy email notifications. + * SMTP must be configured on the ntfy server. + */ + email?: string; } /** Pauses all containers on the target server. Response: [Update] */ @@ -6498,6 +6563,8 @@ export interface PermissionToml { * - Write */ level: PermissionLevel; + /** Any [SpecificPermissions](SpecificPermission) on the resource */ + specific: Array; } export enum PortTypeEnum { @@ -6824,10 +6891,12 @@ export interface ResourceToml { export interface UserGroupToml { /** User group name */ name: string; + /** Whether all users will implicitly have the permissions in this group. */ + everyone?: boolean; /** Users in the group */ users?: string[]; /** Give the user group elevated permissions on all resources of a certain type */ - all?: Record; + all?: Record; /** Permissions given to the group */ permissions?: PermissionToml[]; } @@ -7033,6 +7102,17 @@ export interface ServerHealth { disks: Record; } +/** + * **Admin only.** Set `everyone` property of User Group. + * Response: [UserGroup] + */ +export interface SetEveryoneUserGroup { + /** Id or name. */ + user_group: string; + /** Whether this user group applies to everyone. */ + everyone: boolean; +} + /** * Set the time the user last opened the UI updates. * Used for unseen notification dot. @@ -7042,7 +7122,7 @@ export interface SetLastSeenUpdate { } /** - * **Admin only.** Completely override the user in the group. + * **Admin only.** Completely override the users in the group. * Response: [UserGroup] */ export interface SetUsersInUserGroup { @@ -7355,7 +7435,7 @@ export interface UpdatePermissionOnResourceType { /** The resource type: eg. Server, Build, Deployment, etc. */ resource_type: ResourceTarget["type"]; /** The base permission level. */ - permission: PermissionLevel; + permission: PermissionLevelAndSpecifics | PermissionLevel; } /** @@ -7368,7 +7448,7 @@ export interface UpdatePermissionOnTarget { /** Specify the target resource. */ resource_target: ResourceTarget; /** Specify the permission level. */ - permission: PermissionLevel; + permission: PermissionLevelAndSpecifics | PermissionLevel; } /** @@ -7630,16 +7710,6 @@ export type ExecuteRequest = | { type: "PruneDockerBuilders", params: PruneDockerBuilders } | { type: "PruneBuildx", params: PruneBuildx } | { type: "PruneSystem", params: PruneSystem } - | { type: "Deploy", params: Deploy } - | { type: "BatchDeploy", params: BatchDeploy } - | { type: "PullDeployment", params: PullDeployment } - | { type: "StartDeployment", params: StartDeployment } - | { type: "RestartDeployment", params: RestartDeployment } - | { type: "PauseDeployment", params: PauseDeployment } - | { type: "UnpauseDeployment", params: UnpauseDeployment } - | { type: "StopDeployment", params: StopDeployment } - | { type: "DestroyDeployment", params: DestroyDeployment } - | { type: "BatchDestroyDeployment", params: BatchDestroyDeployment } | { type: "DeployStack", params: DeployStack } | { type: "BatchDeployStack", params: BatchDeployStack } | { type: "DeployStackIfChanged", params: DeployStackIfChanged } @@ -7653,6 +7723,16 @@ export type ExecuteRequest = | { type: "UnpauseStack", params: UnpauseStack } | { type: "DestroyStack", params: DestroyStack } | { type: "BatchDestroyStack", params: BatchDestroyStack } + | { type: "Deploy", params: Deploy } + | { type: "BatchDeploy", params: BatchDeploy } + | { type: "PullDeployment", params: PullDeployment } + | { type: "StartDeployment", params: StartDeployment } + | { type: "RestartDeployment", params: RestartDeployment } + | { type: "PauseDeployment", params: PauseDeployment } + | { type: "UnpauseDeployment", params: UnpauseDeployment } + | { type: "StopDeployment", params: StopDeployment } + | { type: "DestroyDeployment", params: DestroyDeployment } + | { type: "BatchDestroyDeployment", params: BatchDestroyDeployment } | { type: "RunBuild", params: RunBuild } | { type: "BatchRunBuild", params: BatchRunBuild } | { type: "CancelBuild", params: CancelBuild } @@ -7684,7 +7764,7 @@ export type ReadRequest = | { type: "ListGitProvidersFromConfig", params: ListGitProvidersFromConfig } | { type: "ListDockerRegistriesFromConfig", params: ListDockerRegistriesFromConfig } | { type: "GetUsername", params: GetUsername } - | { type: "GetPermissionLevel", params: GetPermissionLevel } + | { type: "GetPermission", params: GetPermission } | { type: "FindUser", params: FindUser } | { type: "ListUsers", params: ListUsers } | { type: "ListApiKeys", params: ListApiKeys } @@ -7727,6 +7807,21 @@ export type ReadRequest = | { type: "ListDockerVolumes", params: ListDockerVolumes } | { type: "ListComposeProjects", params: ListComposeProjects } | { type: "ListTerminals", params: ListTerminals } + | { type: "GetSystemInformation", params: GetSystemInformation } + | { type: "GetSystemStats", params: GetSystemStats } + | { type: "ListSystemProcesses", params: ListSystemProcesses } + | { type: "GetStacksSummary", params: GetStacksSummary } + | { type: "GetStack", params: GetStack } + | { type: "GetStackActionState", params: GetStackActionState } + | { type: "GetStackWebhooksEnabled", params: GetStackWebhooksEnabled } + | { type: "GetStackLog", params: GetStackLog } + | { type: "SearchStackLog", params: SearchStackLog } + | { type: "InspectStackContainer", params: InspectStackContainer } + | { type: "ListStacks", params: ListStacks } + | { type: "ListFullStacks", params: ListFullStacks } + | { type: "ListStackServices", params: ListStackServices } + | { type: "ListCommonStackExtraArgs", params: ListCommonStackExtraArgs } + | { type: "ListCommonStackBuildExtraArgs", params: ListCommonStackBuildExtraArgs } | { type: "GetDeploymentsSummary", params: GetDeploymentsSummary } | { type: "GetDeployment", params: GetDeployment } | { type: "GetDeploymentContainer", params: GetDeploymentContainer } @@ -7734,6 +7829,7 @@ export type ReadRequest = | { type: "GetDeploymentStats", params: GetDeploymentStats } | { type: "GetDeploymentLog", params: GetDeploymentLog } | { type: "SearchDeploymentLog", params: SearchDeploymentLog } + | { type: "InspectDeploymentContainer", params: InspectDeploymentContainer } | { type: "ListDeployments", params: ListDeployments } | { type: "ListFullDeployments", params: ListFullDeployments } | { type: "ListCommonDeploymentExtraArgs", params: ListCommonDeploymentExtraArgs } @@ -7758,17 +7854,6 @@ export type ReadRequest = | { type: "GetSyncWebhooksEnabled", params: GetSyncWebhooksEnabled } | { type: "ListResourceSyncs", params: ListResourceSyncs } | { type: "ListFullResourceSyncs", params: ListFullResourceSyncs } - | { type: "GetStacksSummary", params: GetStacksSummary } - | { type: "GetStack", params: GetStack } - | { type: "GetStackActionState", params: GetStackActionState } - | { type: "GetStackWebhooksEnabled", params: GetStackWebhooksEnabled } - | { type: "GetStackLog", params: GetStackLog } - | { type: "SearchStackLog", params: SearchStackLog } - | { type: "ListStacks", params: ListStacks } - | { type: "ListFullStacks", params: ListFullStacks } - | { type: "ListStackServices", params: ListStackServices } - | { type: "ListCommonStackExtraArgs", params: ListCommonStackExtraArgs } - | { type: "ListCommonStackBuildExtraArgs", params: ListCommonStackBuildExtraArgs } | { type: "GetBuildersSummary", params: GetBuildersSummary } | { type: "GetBuilder", params: GetBuilder } | { type: "ListBuilders", params: ListBuilders } @@ -7785,9 +7870,6 @@ export type ReadRequest = | { type: "ListUpdates", params: ListUpdates } | { type: "ListAlerts", params: ListAlerts } | { type: "GetAlert", params: GetAlert } - | { type: "GetSystemInformation", params: GetSystemInformation } - | { type: "GetSystemStats", params: GetSystemStats } - | { type: "ListSystemProcesses", params: ListSystemProcesses } | { type: "GetVariable", params: GetVariable } | { type: "ListVariables", params: ListVariables } | { type: "GetGitProviderAccount", params: GetGitProviderAccount } @@ -7795,6 +7877,45 @@ export type ReadRequest = | { type: "GetDockerRegistryAccount", params: GetDockerRegistryAccount } | { type: "ListDockerRegistryAccounts", params: ListDockerRegistryAccounts }; +/** The specific types of permission that a User or UserGroup can have on a resource. */ +export enum SpecificPermission { + /** + * On **Server** + * - Access the terminal apis + * On **Stack / Deployment** + * - Access the container exec Apis + */ + Terminal = "Terminal", + /** + * On **Server** + * - Allowed to attach Stacks, Deployments, Repos, Builders to the Server + * On **Builder** + * - Allowed to attach Builds to the Builder + * On **Build** + * - Allowed to attach Deployments to the Build + */ + Attach = "Attach", + /** + * On **Server** + * - Access the `docker inspect` apis + * On **Stack / Deployment** + * - Access `docker inspect $container` for associated containers + */ + Inspect = "Inspect", + /** + * On **Server** + * - Read all container logs on the server + * On **Stack / Deployment** + * - Read the container logs + */ + Logs = "Logs", + /** + * On **Server** + * - Read all the processes on the host + */ + Processes = "Processes", +} + export type UserRequest = | { type: "PushRecentlyViewed", params: PushRecentlyViewed } | { type: "SetLastSeenUpdate", params: SetLastSeenUpdate } @@ -7815,6 +7936,7 @@ export type WriteRequest = | { type: "AddUserToUserGroup", params: AddUserToUserGroup } | { type: "RemoveUserFromUserGroup", params: RemoveUserFromUserGroup } | { type: "SetUsersInUserGroup", params: SetUsersInUserGroup } + | { type: "SetEveryoneUserGroup", params: SetEveryoneUserGroup } | { type: "UpdateUserAdmin", params: UpdateUserAdmin } | { type: "UpdateUserBasePermissions", params: UpdateUserBasePermissions } | { type: "UpdatePermissionOnResourceType", params: UpdatePermissionOnResourceType } @@ -7828,6 +7950,15 @@ export type WriteRequest = | { type: "CreateTerminal", params: CreateTerminal } | { type: "DeleteTerminal", params: DeleteTerminal } | { type: "DeleteAllTerminals", params: DeleteAllTerminals } + | { type: "CreateStack", params: CreateStack } + | { type: "CopyStack", params: CopyStack } + | { type: "DeleteStack", params: DeleteStack } + | { type: "UpdateStack", params: UpdateStack } + | { type: "RenameStack", params: RenameStack } + | { type: "WriteStackFileContents", params: WriteStackFileContents } + | { type: "RefreshStackCache", params: RefreshStackCache } + | { type: "CreateStackWebhook", params: CreateStackWebhook } + | { type: "DeleteStackWebhook", params: DeleteStackWebhook } | { type: "CreateDeployment", params: CreateDeployment } | { type: "CopyDeployment", params: CopyDeployment } | { type: "CreateDeploymentFromContainer", params: CreateDeploymentFromContainer } @@ -7881,15 +8012,6 @@ export type WriteRequest = | { type: "RefreshResourceSyncPending", params: RefreshResourceSyncPending } | { type: "CreateSyncWebhook", params: CreateSyncWebhook } | { type: "DeleteSyncWebhook", params: DeleteSyncWebhook } - | { type: "CreateStack", params: CreateStack } - | { type: "CopyStack", params: CopyStack } - | { type: "DeleteStack", params: DeleteStack } - | { type: "UpdateStack", params: UpdateStack } - | { type: "RenameStack", params: RenameStack } - | { type: "WriteStackFileContents", params: WriteStackFileContents } - | { type: "RefreshStackCache", params: RefreshStackCache } - | { type: "CreateStackWebhook", params: CreateStackWebhook } - | { type: "DeleteStackWebhook", params: DeleteStackWebhook } | { type: "CreateTag", params: CreateTag } | { type: "DeleteTag", params: DeleteTag } | { type: "RenameTag", params: RenameTag } diff --git a/compose/compose.env b/compose/compose.env index 1365bec20..995050611 100644 --- a/compose/compose.env +++ b/compose/compose.env @@ -10,11 +10,7 @@ ## Stick to a specific version, or use `latest` COMPOSE_KOMODO_IMAGE_TAG=latest -## Note: 🚨 Podman does NOT support local logging driver 🚨. See Podman options here: -## `https://docs.podman.io/en/v4.6.1/markdown/podman-run.1.html#log-driver-driver` -COMPOSE_LOGGING_DRIVER=local # Enable log rotation with the local driver. - -## DB credentials - Ignored for Sqlite +## DB credentials KOMODO_DB_USERNAME=admin KOMODO_DB_PASSWORD=admin @@ -60,6 +56,9 @@ KOMODO_RESOURCE_POLL_INTERVAL="5-min" KOMODO_WEBHOOK_SECRET=a_random_secret ## Used to generate jwt. Alt: KOMODO_JWT_SECRET_FILE KOMODO_JWT_SECRET=a_random_jwt_secret +## Time to live for jwt tokens. +## Options: 1-hr, 12-hr, 1-day, 3-day, 1-wk, 2-wk +KOMODO_JWT_TTL="1-day" ## Enable login with username + password. KOMODO_LOCAL_AUTH=true @@ -72,9 +71,10 @@ KOMODO_DISABLE_NON_ADMIN_CREATE=false ## Allows all users to have Read level access to all resources. KOMODO_TRANSPARENT_MODE=false -## Time to live for jwt tokens. -## Options: 1-hr, 12-hr, 1-day, 3-day, 1-wk, 2-wk -KOMODO_JWT_TTL="1-day" +## Prettier logging with empty lines between logs +KOMODO_LOGGING_PRETTY=false +## More human readable logging of startup config (multi-line) +KOMODO_PRETTY_STARTUP_CONFIG=false ## OIDC Login KOMODO_OIDC_ENABLED=false @@ -136,3 +136,8 @@ PERIPHERY_SSL_ENABLED=true ## Usually whitelisting just /etc/hostname gives correct size. PERIPHERY_INCLUDE_DISK_MOUNTS=/etc/hostname # PERIPHERY_EXCLUDE_DISK_MOUNTS=/snap,/etc/repos + +## Prettier logging with empty lines between logs +PERIPHERY_LOGGING_PRETTY=false +## More human readable logging of startup config (multi-line) +PERIPHERY_PRETTY_STARTUP_CONFIG=false \ No newline at end of file diff --git a/compose/postgres.compose.yaml b/compose/ferretdb.compose.yaml similarity index 72% rename from compose/postgres.compose.yaml rename to compose/ferretdb.compose.yaml index 4971c747b..973ef1721 100644 --- a/compose/postgres.compose.yaml +++ b/compose/ferretdb.compose.yaml @@ -1,5 +1,5 @@ ################################### -# 🦎 KOMODO COMPOSE - POSTGRES 🦎 # +# 🦎 KOMODO COMPOSE - FERRETDB 🦎 # ################################### ## This compose file will deploy: @@ -9,34 +9,36 @@ services: postgres: - image: postgres:17 + # Recommended: Pin to a specific version + # https://github.com/FerretDB/documentdb/pkgs/container/postgres-documentdb + image: ghcr.io/ferretdb/postgres-documentdb labels: komodo.skip: # Prevent Komodo from stopping with StopAllContainers restart: unless-stopped - logging: - driver: ${COMPOSE_LOGGING_DRIVER:-local} # ports: # - 5432:5432 volumes: - - pg-data:/var/lib/postgresql/data + - postgres-data:/var/lib/postgresql/data environment: - - POSTGRES_USER=${KOMODO_DB_USERNAME} - - POSTGRES_PASSWORD=${KOMODO_DB_PASSWORD} - - POSTGRES_DB=${KOMODO_DATABASE_DB_NAME:-komodo} + POSTGRES_USER: ${KOMODO_DB_USERNAME} + POSTGRES_PASSWORD: ${KOMODO_DB_PASSWORD} + POSTGRES_DB: postgres ferretdb: - image: ghcr.io/ferretdb/ferretdb:1 + # Recommended: Pin to a specific version + # https://github.com/FerretDB/FerretDB/pkgs/container/ferretdb + image: ghcr.io/ferretdb/ferretdb labels: komodo.skip: # Prevent Komodo from stopping with StopAllContainers restart: unless-stopped depends_on: - postgres - logging: - driver: ${COMPOSE_LOGGING_DRIVER:-local} # ports: # - 27017:27017 + volumes: + - ferretdb-state:/state environment: - - FERRETDB_POSTGRESQL_URL=postgres://postgres:5432/${KOMODO_DATABASE_DB_NAME:-komodo} + FERRETDB_POSTGRESQL_URL: postgres://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@postgres:5432/postgres core: image: ghcr.io/moghtech/komodo-core:${COMPOSE_KOMODO_IMAGE_TAG:-latest} @@ -45,13 +47,13 @@ services: restart: unless-stopped depends_on: - ferretdb - logging: - driver: ${COMPOSE_LOGGING_DRIVER:-local} ports: - 9120:9120 env_file: ./compose.env environment: - KOMODO_DATABASE_URI: mongodb://${KOMODO_DB_USERNAME}:${KOMODO_DB_PASSWORD}@ferretdb:27017/${KOMODO_DATABASE_DB_NAME:-komodo}?authMechanism=PLAIN + KOMODO_DATABASE_ADDRESS: ferretdb:27017 + KOMODO_DATABASE_USERNAME: ${KOMODO_DB_USERNAME} + KOMODO_DATABASE_PASSWORD: ${KOMODO_DB_PASSWORD} volumes: ## Core cache for repos for latest commit hash / contents - repo-cache:/repo-cache @@ -72,8 +74,6 @@ services: labels: komodo.skip: # Prevent Komodo from stopping with StopAllContainers restart: unless-stopped - logging: - driver: ${COMPOSE_LOGGING_DRIVER:-local} env_file: ./compose.env volumes: ## Mount external docker socket @@ -88,6 +88,8 @@ services: volumes: # Postgres - pg-data: + postgres-data: + # FerretDB + ferretdb-state: # Core repo-cache: \ No newline at end of file diff --git a/compose/mongo.compose.yaml b/compose/mongo.compose.yaml index 37a7480f5..bb85f1760 100644 --- a/compose/mongo.compose.yaml +++ b/compose/mongo.compose.yaml @@ -14,8 +14,6 @@ services: komodo.skip: # Prevent Komodo from stopping with StopAllContainers command: --quiet --wiredTigerCacheSizeGB 0.25 restart: unless-stopped - logging: - driver: ${COMPOSE_LOGGING_DRIVER:-local} # ports: # - 27017:27017 volumes: @@ -32,8 +30,6 @@ services: restart: unless-stopped depends_on: - mongo - logging: - driver: ${COMPOSE_LOGGING_DRIVER:-local} ports: - 9120:9120 env_file: ./compose.env @@ -61,8 +57,6 @@ services: labels: komodo.skip: # Prevent Komodo from stopping with StopAllContainers restart: unless-stopped - logging: - driver: ${COMPOSE_LOGGING_DRIVER:-local} env_file: ./compose.env volumes: ## Mount external docker socket diff --git a/compose/periphery.compose.yaml b/compose/periphery.compose.yaml index 6727e0c30..53014e5f6 100644 --- a/compose/periphery.compose.yaml +++ b/compose/periphery.compose.yaml @@ -11,8 +11,6 @@ services: labels: komodo.skip: # Prevent Komodo from stopping with StopAllContainers restart: unless-stopped - logging: - driver: ${COMPOSE_LOGGING_DRIVER:-local} ## https://komo.do/docs/connect-servers#configuration environment: PERIPHERY_ROOT_DIRECTORY: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo} @@ -39,8 +37,7 @@ services: ## or docker will get confused. See https://github.com/moghtech/komodo/discussions/180. ## Default: /etc/komodo. - ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo} - ## If periphery is being run remote from the core server, ports need - ## to be exposed on the host. + ## If periphery is being run remote from the core server, ports need to be exposed # ports: # - 8120:8120 ## If you want to use a custom periphery config file, use command to pass it to periphery. diff --git a/compose/sqlite.compose.yaml b/compose/sqlite.compose.yaml deleted file mode 100644 index cfdd7360a..000000000 --- a/compose/sqlite.compose.yaml +++ /dev/null @@ -1,77 +0,0 @@ -################################# -# 🦎 KOMODO COMPOSE - SQLITE 🦎 # -################################# - -## This compose file will deploy: -## 1. Sqlite + FerretDB Mongo adapter (https://www.ferretdb.com) -## 2. Komodo Core -## 3. Komodo Periphery - -services: - ferretdb: - image: ghcr.io/ferretdb/ferretdb:1 - labels: - komodo.skip: # Prevent Komodo from stopping with StopAllContainers - restart: unless-stopped - logging: - driver: ${COMPOSE_LOGGING_DRIVER:-local} - # ports: - # - 27017:27017 - volumes: - - sqlite-data:/state - environment: - - FERRETDB_HANDLER=sqlite - - core: - image: ghcr.io/moghtech/komodo-core:${COMPOSE_KOMODO_IMAGE_TAG:-latest} - labels: - komodo.skip: # Prevent Komodo from stopping with StopAllContainers - restart: unless-stopped - depends_on: - - ferretdb - logging: - driver: ${COMPOSE_LOGGING_DRIVER:-local} - ports: - - 9120:9120 - env_file: ./compose.env - environment: - KOMODO_DATABASE_ADDRESS: ferretdb - volumes: - ## Core cache for repos for latest commit hash / contents - - repo-cache:/repo-cache - ## Store sync files on server - # - /path/to/syncs:/syncs - ## Optionally mount a custom core.config.toml - # - /path/to/core.config.toml:/config/config.toml - ## Allows for systemd Periphery connection at - ## "http://host.docker.internal:8120" - # extra_hosts: - # - host.docker.internal:host-gateway - - ## Deploy Periphery container using this block, - ## or deploy the Periphery binary with systemd using - ## https://github.com/moghtech/komodo/tree/main/scripts - periphery: - image: ghcr.io/moghtech/komodo-periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest} - labels: - komodo.skip: # Prevent Komodo from stopping with StopAllContainers - restart: unless-stopped - logging: - driver: ${COMPOSE_LOGGING_DRIVER:-local} - env_file: ./compose.env - volumes: - ## Mount external docker socket - - /var/run/docker.sock:/var/run/docker.sock - ## Allow Periphery to see processes outside of container - - /proc:/proc - ## Specify the Periphery agent root directory. - ## Must be the same inside and outside the container, - ## or docker will get confused. See https://github.com/moghtech/komodo/discussions/180. - ## Default: /etc/komodo. - - ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo} - -volumes: - # Sqlite - sqlite-data: - # Core - repo-cache: \ No newline at end of file diff --git a/config/core.config.toml b/config/core.config.toml index 54cd34d0b..66e07777c 100644 --- a/config/core.config.toml +++ b/config/core.config.toml @@ -353,12 +353,6 @@ logging.level = "info" ## Default: standard logging.stdio = "standard" -## Specify whether logging is more human readable. -## Note. Single logs will span multiple lines. -## Env: KOMODO_LOGGING_PRETTY -## Default: false -logging.pretty = false - ## Optionally specify a opentelemetry otlp endpoint to send traces to. ## Example: http://localhost:4317 ## Env: KOMODO_LOGGING_OTLP_ENDPOINT @@ -370,6 +364,18 @@ logging.otlp_endpoint = "" ## Default: "Komodo" logging.opentelemetry_service_name = "Komodo" +## Specify whether logging is more human readable. +## Note. Single logs will span multiple lines. +## Env: KOMODO_LOGGING_PRETTY +## Default: false +logging.pretty = false + +## Specify whether startup config log +## is more human readable (multi-line) +## Env: KOMODO_PRETTY_STARTUP_CONFIG +## Default: false +pretty_startup_config = false + ########### # PRUNING # ########### diff --git a/config/periphery.config.toml b/config/periphery.config.toml index 026f708ca..aa65af3eb 100644 --- a/config/periphery.config.toml +++ b/config/periphery.config.toml @@ -26,6 +26,7 @@ bind_ip = "[::]" ## The directory periphery will use as the default base for the directories it uses. ## The periphery user must have write access to this directory. +## Each specific directory (like stack_dir) can be overridden below. ## Env: PERIPHERY_ROOT_DIRECTORY ## Default: /etc/komodo root_directory = "/etc/komodo" @@ -114,13 +115,13 @@ ssl_enabled = true ## Path to the ssl key. ## Env: PERIPHERY_SSL_KEY_FILE -## Default: /etc/komodo/ssl/key.pem -ssl_key_file = "/etc/komodo/ssl/key.pem" +## Default: ${root_directory}/ssl/key.pem +# ssl_key_file = "/etc/komodo/ssl/key.pem" ## Path to the ssl cert. ## Env: PERIPHERY_SSL_CERT_FILE -## Default: /etc/komodo/ssl/cert.pem -ssl_cert_file = "/etc/komodo/ssl/cert.pem" +## Default: ${root_directory}/ssl/cert.pem +# ssl_cert_file = "/etc/komodo/ssl/cert.pem" ########### # LOGGING # @@ -138,12 +139,6 @@ logging.level = "info" ## Default: standard logging.stdio = "standard" -## Specify whether logging is more human readable. -## Note. Single logs will span multiple lines. -## Env: PERIPHERY_LOGGING_PRETTY -## Default: false -logging.pretty = false - ## Specify a opentelemetry otlp endpoint to send traces to. ## Example: http://localhost:4317. ## Env: PERIPHERY_LOGGING_OTLP_ENDPOINT @@ -155,6 +150,18 @@ logging.otlp_endpoint = "" ## Default: "Komodo" logging.opentelemetry_service_name = "Periphery" +## Specify whether logging is more human readable. +## Note. Single logs will span multiple lines. +## Env: PERIPHERY_LOGGING_PRETTY +## Default: false +logging.pretty = false + +## Specify whether startup config log +## is more human readable (multi-line) +## Env: PERIPHERY_PRETTY_STARTUP_CONFIG +## Default: false +pretty_startup_config = false + ################# # GIT PROVIDERS # ################# diff --git a/docsite/docs/permissioning.md b/docsite/docs/permissioning.md index decb98096..c5dfbc842 100644 --- a/docsite/docs/permissioning.md +++ b/docsite/docs/permissioning.md @@ -7,6 +7,7 @@ Komodo has a granular, layer-based permissioning system to provide non-admin use While Komodo can assign permissions to specific users directly, it is recommended to instead **create User Groups and assign permissions to them**, as if they were a user. Users can then be **added to multiple User Groups** and they **inherit the group's permissions**, similar to linux permissions. +There is also an `Everyone` mode for User Groups, if this is enabled then **all users implicitly gain the groups permissions**. For permissioning at scale, users can define [**User Groups in Resource Syncs**](/docs/sync-resources#user-group). @@ -16,13 +17,31 @@ There are 4 permission levels a user / group can be given on a Resource: 1. **None**. The user will not have any access to the resource. The user **will not see it in the GUI, and it will not show up if the user queries the Komodo API directly**. All attempts to view or update the resource will be blocked. This is the default for non-admins, unless using `KOMODO_TRANSPARENT_MODE=true`. - 2. **Read**. This is the first permission level that grants any access. It will enable the user to **see the resource in the GUI, read the configuration, and see any logs**. Any attempts to update configuration or trigger any action **will be blocked**. Using `KOMODO_TRANSPARENT_MODE=true` will make this level the base level on all resources, for all users. + 2. **Read**. This is the first permission level that grants any access. It will enable the user to **see the resource in the GUI and read the configuration**. Any attempts to update configuration or trigger any action **will be blocked**. Using `KOMODO_TRANSPARENT_MODE=true` will make this level the base level on all resources, for all users. 3. **Execute**. This level will allow the user to execute actions on the resource, **like send a build command** or **trigger a redeploy**. The user will still be blocked from updating configuration on the resource. - 4. **Write**. The user has full access to the resource, **they can execute any actions, update the configuration, and delete the resource**. + 4. **Write**. The user has full config write access to the resource, **they can execute any actions, update the configuration, and delete the resource**. -## Global permissions +## Specific Permissions + +Permission levels alone are not quite enough to provide granular access control. +Some features are additionally gated behind a specific permission for that feature. + +- `Terminal`: User can access the associated resource's terminal. + - If given on a `Server`, this allows server level terminal access. + - If given on a `Stack` or `Deployment`, this allows container exec terminal (even without `Terminal` on `Server`) +- `Attach`: User can "attach" *other resources* to the resource. + - If given on a `Server`, allows users to attach `Stacks` and `Deployments` + - If given on a `Builder`, allows users to attach `Builds` +- `DockerInspect`: User can "inspect" docker resources (like containers) on the `Server` + - Access to this api will expose all container environments on the given server, and can easily lead to secrets being leaked to unintended users if not protected. +- `DockerLogs`: User can retrieve docker / docker compose logs on the associated resource. + - Valid on `Server`, `Stack`, `Deployment` + - For admins wanting this permission by default for all users with read permissions, see below on default user groups. +- `ProcessList`: User can retrieve the full running process list on the `Server` + +## Permissioning by Resource Type Users or User Groups can be given a base permission level on all Resources of a particular type, such as Stack. In TOML form, this looks like: @@ -32,14 +51,15 @@ In TOML form, this looks like: name = "groupo" users = ["mbecker20", "karamvirsingh98"] all.Build = "Execute" # <- Group members can run all builds (but not update config), -all.Stack = "Read" # <- And see all Stacks / logs (not deploy / update). +all.Stack = { level = "Read", specific = ["Logs"] } # <- And see all Stacks / logs (no deploy / update, inspect, or terminal access). ``` A user / group can still be given a greater permission level on select resources: ```toml permissions = [ - { target.type = "Stack", target.id = "my-stack", level = "Execute" }, + # Grant addition specific permission (Logs are already granted above) + { target.type = "Stack", target.id = "my-stack", level = "Execute", specific = ["Inspect", "Terminal"] }, # Use regex to match multiple resources, for example give john execute on all of their Stacks { target.type = "Stack", target.id = "\\^john-(.+)$\\", level = "Execute" }, ] diff --git a/docsite/docs/setup/postgres.mdx b/docsite/docs/setup/ferretdb.mdx similarity index 61% rename from docsite/docs/setup/postgres.mdx rename to docsite/docs/setup/ferretdb.mdx index 60fbb46cd..b03db3414 100644 --- a/docsite/docs/setup/postgres.mdx +++ b/docsite/docs/setup/ferretdb.mdx @@ -1,19 +1,19 @@ -# Postgres +# FerretDB -1. Copy `komodo/postgres.compose.yaml` and `komodo/compose.env` to your host: +1. Copy `komodo/ferretdb.compose.yaml` and `komodo/compose.env` to your host: ```bash -wget -P komodo https://raw.githubusercontent.com/moghtech/komodo/main/compose/postgres.compose.yaml && \ +wget -P komodo https://raw.githubusercontent.com/moghtech/komodo/main/compose/ferretdb.compose.yaml && \ wget -P komodo https://raw.githubusercontent.com/moghtech/komodo/main/compose/compose.env ``` 2. Edit the variables in `komodo/compose.env`. 3. Deploy: ```bash -docker compose -p komodo -f komodo/postgres.compose.yaml --env-file komodo/compose.env up -d +docker compose -p komodo -f komodo/ferretdb.compose.yaml --env-file komodo/compose.env up -d ``` ```mdx-code-block import ComposeAndEnv from "@site/src/components/ComposeAndEnv"; - + ``` \ No newline at end of file diff --git a/docsite/docs/setup/index.mdx b/docsite/docs/setup/index.mdx index a64289d26..59e2268b0 100644 --- a/docsite/docs/setup/index.mdx +++ b/docsite/docs/setup/index.mdx @@ -5,16 +5,16 @@ To run Komodo, you will need Docker. See [the docker install docs](https://docs. ### Deploy with Docker Compose - [Using MongoDB](./mongo.mdx) -- [Using Postgres](./postgres.mdx) -- [Using Sqlite](./sqlite.mdx) +- [Using FerretDB (Postgres)](./ferretdb.mdx) :::info -Komodo is able to support Postgres and Sqlite by utilizing the [FerretDB Mongo Adapter](https://www.ferretdb.com/). +**FerretDB v1** users: +There is an [**upgrade guide for FerretDB v2** available here](https://github.com/moghtech/komodo/blob/main/bin/util/docs/copy-database.md#ferretdb-v2-update-guide). ::: ### First login -Core should now be accessible on the specified port, so navigating to `http://
:` will display the login page. +Core should now be accessible on the specified port and navigating to `http://
:` will display the login page. Enter your preferred admin username and password, and click **"Sign Up"**, _not_ "Log In", to create your admin user for Komodo. Any additional users to create accounts will be disabled by default, and must be enabled by an admin. diff --git a/docsite/docs/setup/sqlite.mdx b/docsite/docs/setup/sqlite.mdx deleted file mode 100644 index 7c7fc81a3..000000000 --- a/docsite/docs/setup/sqlite.mdx +++ /dev/null @@ -1,25 +0,0 @@ -# Sqlite - -:::info -FerretDB + Sqlite authentication support is [still experimental](https://docs.ferretdb.io/security/authentication/#experimental-authentication-mode), -and is not enabled in this database configuration. Because of this, the values for `DB_USERNAME` and `DB_PASSWORD` -are ignored when using Sqlite, and the database port is not exposed publicly by default. -::: - -1. Copy `komodo/sqlite.compose.yaml` and `komodo/compose.env` to your host: -```bash -wget -P komodo https://raw.githubusercontent.com/moghtech/komodo/main/compose/sqlite.compose.yaml && \ - wget -P komodo https://raw.githubusercontent.com/moghtech/komodo/main/compose/compose.env -``` -2. Edit the variables in `komodo/compose.env`. -3. Deploy: - -```bash -docker compose -p komodo -f komodo/sqlite.compose.yaml --env-file komodo/compose.env up -d -``` - -```mdx-code-block -import ComposeAndEnv from "@site/src/components/ComposeAndEnv"; - - -``` diff --git a/docsite/docs/sync-resources.md b/docsite/docs/sync-resources.md index fd67de8cd..2aa952ba6 100644 --- a/docsite/docs/sync-resources.md +++ b/docsite/docs/sync-resources.md @@ -1,6 +1,6 @@ # Sync Resources -Komodo is able to create, update, delete, and deploy resources declared in TOML files by diffing them against the existing resources, +Komodo is able to create, update, delete, and deploy resources declared in TOML files by diffing them against the existing resources, and apply updates based on the diffs. Similar to Stacks, the files can be configured in UI, in a local file, or in files pushed to a remote git repo. The Komodo Core backend will poll the files for for any updates, and alert about pending changes when diffs are detected. @@ -97,7 +97,7 @@ value = "http://localhost:4317" name = "test-logger-01" description = "test logger deployment 1" tags = ["test"] -# sync will deploy the container: +# sync will deploy the container: # - if it is not running. # - has relevant config updates. # - the attached build has new version. @@ -247,10 +247,14 @@ resource_path = ["stacks.toml", "repos.toml"] ```toml [[user_group]] name = "groupo" +everyone = false # Set to true to give these permission to all users. users = ["mbecker20", "karamvirsingh98"] +# Configure write access with all specific permissions +all.Server = { level = "Write", specific = ["Attach", "Logs", "Inspect", "Terminal", "Processes"] } # Attach base level of Execute on all builds all.Build = "Execute" -all.Alerter = "Write" +# Allow users to see all Builders, and attach builds to them. +all.Builder = { level = "Read", specific = ["Attach"] } permissions = [ # Attach permissions to specific resources by name { target.type = "Repo", target.id = "komodo-periphery", level = "Execute" }, @@ -258,4 +262,4 @@ permissions = [ { target.type = "Server", target.id = "\\^(.+)-(.+)$\\", level = "Read" }, { target.type = "Deployment", target.id = "\\^immich\\", level = "Execute" }, ] -``` \ No newline at end of file +``` diff --git a/docsite/sidebars.ts b/docsite/sidebars.ts index 00c3bfb39..1d32730dd 100644 --- a/docsite/sidebars.ts +++ b/docsite/sidebars.ts @@ -23,8 +23,7 @@ const sidebars: SidebarsConfig = { }, items: [ "setup/mongo", - "setup/postgres", - "setup/sqlite", + "setup/ferretdb", "setup/advanced", ], }, diff --git a/example/alerter/Dockerfile b/example/alerter/Dockerfile index f920938c9..d8c88e32d 100644 --- a/example/alerter/Dockerfile +++ b/example/alerter/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.80.1 as builder +FROM rust:1.87.0 as builder WORKDIR /builder COPY . . diff --git a/example/update_logger/Dockerfile b/example/update_logger/Dockerfile index 713360383..af13660d0 100644 --- a/example/update_logger/Dockerfile +++ b/example/update_logger/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.80.1 as builder +FROM rust:1.87.0 as builder WORKDIR /builder COPY . . diff --git a/frontend/package.json b/frontend/package.json index 02a139e85..7fdebb044 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,60 +11,60 @@ "build-client": "cd ../client/core/ts && yarn && yarn build && yarn link" }, "dependencies": { - "@floating-ui/react": "0.27.5", + "@floating-ui/react": "0.27.9", "@monaco-editor/react": "4.7.0", - "@radix-ui/react-checkbox": "1.1.4", - "@radix-ui/react-dialog": "1.1.6", - "@radix-ui/react-dropdown-menu": "2.1.6", - "@radix-ui/react-hover-card": "1.1.6", + "@radix-ui/react-checkbox": "1.3.2", + "@radix-ui/react-dialog": "1.1.14", + "@radix-ui/react-dropdown-menu": "2.1.15", + "@radix-ui/react-hover-card": "1.1.14", "@radix-ui/react-icons": "1.3.2", - "@radix-ui/react-label": "2.1.2", - "@radix-ui/react-popover": "1.1.6", - "@radix-ui/react-progress": "1.1.2", - "@radix-ui/react-select": "2.1.6", - "@radix-ui/react-separator": "1.1.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-switch": "1.1.3", - "@radix-ui/react-tabs": "1.1.3", - "@radix-ui/react-toast": "1.2.6", - "@radix-ui/react-toggle": "1.1.2", - "@radix-ui/react-toggle-group": "1.1.2", - "@tanstack/react-query": "5.67.3", - "@tanstack/react-table": "8.21.2", - "@xterm/addon-fit": "^0.10.0", - "@xterm/xterm": "^5.5.0", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-popover": "1.1.14", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-select": "2.2.5", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.5", + "@radix-ui/react-tabs": "1.1.12", + "@radix-ui/react-toast": "1.2.14", + "@radix-ui/react-toggle": "1.1.9", + "@radix-ui/react-toggle-group": "1.1.10", + "@tanstack/react-query": "5.77.2", + "@tanstack/react-table": "8.21.3", + "@xterm/addon-fit": "0.10.0", + "@xterm/xterm": "5.5.0", "ansi-to-html": "0.7.2", "class-variance-authority": "0.7.1", "clsx": "2.1.1", - "cmdk": "1.0.4", - "jotai": "2.12.2", - "lucide-react": "0.479.0", + "cmdk": "1.1.1", + "jotai": "2.12.5", + "lucide-react": "0.511.0", "monaco-editor": "0.52.2", "prettier": "3.5.3", - "react": "19.0.0", + "react": "19.1.0", "react-charts": "3.0.0-beta.57", - "react-dom": "19.0.0", + "react-dom": "19.1.0", "react-minimal-pie-chart": "9.1.0", - "react-router-dom": "7.3.0", - "react-xtermjs": "^1.0.10", - "sanitize-html": "2.14.0", + "react-router-dom": "7.6.1", + "react-xtermjs": "1.0.10", + "sanitize-html": "2.17.0", "tailwind-merge": "2.6.0", "tailwindcss-animate": "1.0.7" }, "devDependencies": { - "@types/react": "19.0.10", - "@types/react-dom": "19.0.4", - "@types/sanitize-html": "2.13.0", - "@typescript-eslint/eslint-plugin": "8.26.1", - "@typescript-eslint/parser": "8.26.1", - "@vitejs/plugin-react": "4.3.4", - "autoprefixer": "10.4.20", - "eslint": "9.22.0", + "@types/react": "19.1.6", + "@types/react-dom": "19.1.5", + "@types/sanitize-html": "2.16.0", + "@typescript-eslint/eslint-plugin": "8.33.0", + "@typescript-eslint/parser": "8.33.0", + "@vitejs/plugin-react": "4.5.0", + "autoprefixer": "10.4.21", + "eslint": "9.27.0", "eslint-plugin-react-hooks": "5.2.0", - "eslint-plugin-react-refresh": "0.4.19", + "eslint-plugin-react-refresh": "0.4.20", "postcss": "8.5.3", "tailwindcss": "3.4.17", - "typescript": "5.8.2", + "typescript": "5.8.3", "vite": "6.0.7", "vite-tsconfig-paths": "5.1.4" }, diff --git a/frontend/public/client/lib.d.ts b/frontend/public/client/lib.d.ts index 62c4addd8..275cbeaf7 100644 --- a/frontend/public/client/lib.d.ts +++ b/frontend/public/client/lib.d.ts @@ -1,7 +1,7 @@ import { AuthResponses, ExecuteResponses, ReadResponses, UserResponses, WriteResponses } from "./responses.js"; -import { AuthRequest, ConnectContainerExecQuery, ConnectTerminalQuery, ExecuteRequest, ExecuteTerminalBody, ReadRequest, Update, UpdateListItem, UserRequest, WriteRequest } from "./types.js"; +import { AuthRequest, ConnectContainerExecQuery, ConnectDeploymentExecQuery, ConnectStackExecQuery, ConnectTerminalQuery, ExecuteRequest, ExecuteTerminalBody, ReadRequest, Update, UpdateListItem, UserRequest, WriteRequest } from "./types.js"; export * as Types from "./types.js"; -type InitOptions = { +export type InitOptions = { type: "jwt"; params: { jwt: string; @@ -18,6 +18,22 @@ export declare class CancelToken { constructor(); cancel(): void; } +export type ContainerExecQuery = { + type: "container"; + query: ConnectContainerExecQuery; +} | { + type: "deployment"; + query: ConnectDeploymentExecQuery; +} | { + type: "stack"; + query: ConnectStackExecQuery; +}; +export type TerminalCallbacks = { + on_message?: (e: MessageEvent) => void; + on_login?: () => void; + on_open?: () => void; + on_close?: () => void; +}; /** Initialize a new client for Komodo */ export declare function KomodoClient(url: string, options: InitOptions): { /** @@ -149,22 +165,16 @@ export declare function KomodoClient(url: string, options: InitOptions): { */ connect_terminal: ({ query, on_message, on_login, on_open, on_close, }: { query: ConnectTerminalQuery; - on_message?: (e: MessageEvent) => void; - on_login?: () => void; - on_open?: () => void; - on_close?: () => void; - }) => WebSocket; + } & TerminalCallbacks) => WebSocket; /** * Subscribes to container exec io over websocket message, - * for use with xtermjs. + * for use with xtermjs. Can connect to Deployment, Stack, + * or any container on a Server. The permission used to allow the connection + * depends on `query.type`. */ - connect_container_exec: ({ query, on_message, on_login, on_open, on_close, }: { - query: ConnectContainerExecQuery; - on_message?: (e: MessageEvent) => void; - on_login?: () => void; - on_open?: () => void; - on_close?: () => void; - }) => WebSocket; + connect_container_exec: ({ query: { type, query }, on_message, on_login, on_open, on_close, }: { + query: ContainerExecQuery; + } & TerminalCallbacks) => WebSocket; /** * Executes a command on a given Server / terminal, * and returns a stream to process the output as it comes in. diff --git a/frontend/public/client/lib.js b/frontend/public/client/lib.js index bd5b223ce..147220449 100644 --- a/frontend/public/client/lib.js +++ b/frontend/public/client/lib.js @@ -208,9 +208,9 @@ export function KomodoClient(url, options) { ws.onclose = () => on_close?.(); return ws; }; - const connect_container_exec = ({ query, on_message, on_login, on_open, on_close, }) => { + const connect_container_exec = ({ query: { type, query }, on_message, on_login, on_open, on_close, }) => { const url_query = new URLSearchParams(query).toString(); - const ws = new WebSocket(url.replace("http", "ws") + "/ws/container?" + url_query); + const ws = new WebSocket(url.replace("http", "ws") + `/ws/${type}/terminal?` + url_query); // Handle login on websocket open ws.onopen = () => { const login_msg = options.type === "jwt" @@ -436,7 +436,9 @@ export function KomodoClient(url, options) { connect_terminal, /** * Subscribes to container exec io over websocket message, - * for use with xtermjs. + * for use with xtermjs. Can connect to Deployment, Stack, + * or any container on a Server. The permission used to allow the connection + * depends on `query.type`. */ connect_container_exec, /** diff --git a/frontend/public/client/responses.d.ts b/frontend/public/client/responses.d.ts index f649d8a09..e11cb751d 100644 --- a/frontend/public/client/responses.d.ts +++ b/frontend/public/client/responses.d.ts @@ -19,7 +19,7 @@ export type ReadResponses = { ListGitProvidersFromConfig: Types.ListGitProvidersFromConfigResponse; ListDockerRegistriesFromConfig: Types.ListDockerRegistriesFromConfigResponse; GetUsername: Types.GetUsernameResponse; - GetPermissionLevel: Types.GetPermissionLevelResponse; + GetPermission: Types.GetPermissionResponse; FindUser: Types.FindUserResponse; ListUsers: Types.ListUsersResponse; ListApiKeys: Types.ListApiKeysResponse; @@ -62,6 +62,18 @@ export type ReadResponses = { ListServers: Types.ListServersResponse; ListFullServers: Types.ListFullServersResponse; ListTerminals: Types.ListTerminalsResponse; + GetStacksSummary: Types.GetStacksSummaryResponse; + GetStack: Types.GetStackResponse; + GetStackActionState: Types.GetStackActionStateResponse; + GetStackWebhooksEnabled: Types.GetStackWebhooksEnabledResponse; + GetStackLog: Types.GetStackLogResponse; + SearchStackLog: Types.SearchStackLogResponse; + InspectStackContainer: Types.InspectStackContainerResponse; + ListStacks: Types.ListStacksResponse; + ListFullStacks: Types.ListFullStacksResponse; + ListStackServices: Types.ListStackServicesResponse; + ListCommonStackExtraArgs: Types.ListCommonStackExtraArgsResponse; + ListCommonStackBuildExtraArgs: Types.ListCommonStackBuildExtraArgsResponse; GetDeploymentsSummary: Types.GetDeploymentsSummaryResponse; GetDeployment: Types.GetDeploymentResponse; GetDeploymentContainer: Types.GetDeploymentContainerResponse; @@ -69,6 +81,7 @@ export type ReadResponses = { GetDeploymentStats: Types.GetDeploymentStatsResponse; GetDeploymentLog: Types.GetDeploymentLogResponse; SearchDeploymentLog: Types.SearchDeploymentLogResponse; + InspectDeploymentContainer: Types.InspectDeploymentContainerResponse; ListDeployments: Types.ListDeploymentsResponse; ListFullDeployments: Types.ListFullDeploymentsResponse; ListCommonDeploymentExtraArgs: Types.ListCommonDeploymentExtraArgsResponse; @@ -93,17 +106,6 @@ export type ReadResponses = { GetSyncWebhooksEnabled: Types.GetSyncWebhooksEnabledResponse; ListResourceSyncs: Types.ListResourceSyncsResponse; ListFullResourceSyncs: Types.ListFullResourceSyncsResponse; - GetStacksSummary: Types.GetStacksSummaryResponse; - GetStack: Types.GetStackResponse; - GetStackActionState: Types.GetStackActionStateResponse; - GetStackWebhooksEnabled: Types.GetStackWebhooksEnabledResponse; - GetStackLog: Types.GetStackLogResponse; - SearchStackLog: Types.SearchStackLogResponse; - ListStacks: Types.ListStacksResponse; - ListFullStacks: Types.ListFullStacksResponse; - ListStackServices: Types.ListStackServicesResponse; - ListCommonStackExtraArgs: Types.ListCommonStackExtraArgsResponse; - ListCommonStackBuildExtraArgs: Types.ListCommonStackBuildExtraArgsResponse; GetBuildersSummary: Types.GetBuildersSummaryResponse; GetBuilder: Types.GetBuilderResponse; ListBuilders: Types.ListBuildersResponse; @@ -144,6 +146,7 @@ export type WriteResponses = { AddUserToUserGroup: Types.UserGroup; RemoveUserFromUserGroup: Types.UserGroup; SetUsersInUserGroup: Types.UserGroup; + SetEveryoneUserGroup: Types.UserGroup; UpdateUserAdmin: Types.UpdateUserAdminResponse; UpdateUserBasePermissions: Types.UpdateUserBasePermissionsResponse; UpdatePermissionOnResourceType: Types.UpdatePermissionOnResourceTypeResponse; @@ -157,6 +160,15 @@ export type WriteResponses = { CreateTerminal: Types.NoData; DeleteTerminal: Types.NoData; DeleteAllTerminals: Types.NoData; + CreateStack: Types.Stack; + CopyStack: Types.Stack; + DeleteStack: Types.Stack; + UpdateStack: Types.Stack; + RenameStack: Types.Update; + WriteStackFileContents: Types.Update; + RefreshStackCache: Types.NoData; + CreateStackWebhook: Types.CreateStackWebhookResponse; + DeleteStackWebhook: Types.DeleteStackWebhookResponse; CreateDeployment: Types.Deployment; CopyDeployment: Types.Deployment; CreateDeploymentFromContainer: Types.Deployment; @@ -210,15 +222,6 @@ export type WriteResponses = { RefreshResourceSyncPending: Types.ResourceSync; CreateSyncWebhook: Types.CreateSyncWebhookResponse; DeleteSyncWebhook: Types.DeleteSyncWebhookResponse; - CreateStack: Types.Stack; - CopyStack: Types.Stack; - DeleteStack: Types.Stack; - UpdateStack: Types.Stack; - RenameStack: Types.Update; - WriteStackFileContents: Types.Update; - RefreshStackCache: Types.NoData; - CreateStackWebhook: Types.CreateStackWebhookResponse; - DeleteStackWebhook: Types.DeleteStackWebhookResponse; CreateTag: Types.Tag; DeleteTag: Types.Tag; RenameTag: Types.Tag; @@ -258,6 +261,19 @@ export type ExecuteResponses = { PruneDockerBuilders: Types.Update; PruneBuildx: Types.Update; PruneSystem: Types.Update; + DeployStack: Types.Update; + BatchDeployStack: Types.BatchExecutionResponse; + DeployStackIfChanged: Types.Update; + BatchDeployStackIfChanged: Types.BatchExecutionResponse; + PullStack: Types.Update; + BatchPullStack: Types.BatchExecutionResponse; + StartStack: Types.Update; + RestartStack: Types.Update; + StopStack: Types.Update; + PauseStack: Types.Update; + UnpauseStack: Types.Update; + DestroyStack: Types.Update; + BatchDestroyStack: Types.BatchExecutionResponse; Deploy: Types.Update; BatchDeploy: Types.BatchExecutionResponse; PullDeployment: Types.Update; @@ -283,19 +299,6 @@ export type ExecuteResponses = { RunAction: Types.Update; BatchRunAction: Types.BatchExecutionResponse; RunSync: Types.Update; - DeployStack: Types.Update; - BatchDeployStack: Types.BatchExecutionResponse; - DeployStackIfChanged: Types.Update; - BatchDeployStackIfChanged: Types.BatchExecutionResponse; - PullStack: Types.Update; - BatchPullStack: Types.BatchExecutionResponse; - StartStack: Types.Update; - RestartStack: Types.Update; - StopStack: Types.Update; - PauseStack: Types.Update; - UnpauseStack: Types.Update; - DestroyStack: Types.Update; - BatchDestroyStack: Types.BatchExecutionResponse; DeployStackService: Types.Update; StartStackService: Types.Update; RestartStackService: Types.Update; diff --git a/frontend/public/client/types.d.ts b/frontend/public/client/types.d.ts index 144466c15..4b07671e8 100644 --- a/frontend/public/client/types.d.ts +++ b/frontend/public/client/types.d.ts @@ -7,13 +7,17 @@ export type I64 = number; export declare enum PermissionLevel { /** No permissions. */ None = "None", - /** Can see the rousource */ + /** Can read resource information and config */ Read = "Read", /** Can execute actions on the resource */ Execute = "Execute", /** Can update the resource configuration */ Write = "Write" } +export interface PermissionLevelAndSpecifics { + level: PermissionLevel; + specific: Array; +} export interface Resource { /** * The Mongo ID of the resource. @@ -40,7 +44,7 @@ export interface Resource { * Set a base permission level that all users will have on the * resource. */ - base_permission?: PermissionLevel; + base_permission?: PermissionLevelAndSpecifics | PermissionLevel; } export declare enum ScheduleFormat { English = "English", @@ -922,7 +926,7 @@ export interface User { /** Recently viewed ids */ recents?: Record; /** Give the user elevated permissions on all resources of a certain type */ - all?: Record; + all?: Record; updated_at?: I64; } export type CreateServiceUserResponse = User; @@ -1484,7 +1488,7 @@ export interface ContainerStats { export type GetDeploymentStatsResponse = ContainerStats; export type GetDockerRegistryAccountResponse = DockerRegistryAccount; export type GetGitProviderAccountResponse = GitProviderAccount; -export type GetPermissionLevelResponse = PermissionLevel; +export type GetPermissionResponse = PermissionLevelAndSpecifics; export interface ProcedureActionState { running: boolean; } @@ -2423,10 +2427,12 @@ export interface UserGroup { _id?: MongoId; /** A name for the user group */ name: string; + /** Whether all users will implicitly have the permissions in this group. */ + everyone?: boolean; /** User ids of group members */ users?: string[]; /** Give the user group elevated permissions on all resources of a certain type */ - all?: Record; + all?: Record; /** Unix time (ms) when user group last updated */ updated_at?: I64; } @@ -2439,8 +2445,8 @@ export declare enum ContainerStateStatusEnum { Running = "running", Paused = "paused", Restarting = "restarting", - Removing = "removing", Exited = "exited", + Removing = "removing", Dead = "dead" } export declare enum HealthStatusEnum { @@ -2559,6 +2565,7 @@ export declare enum MountTypeEnum { Empty = "", Bind = "bind", Volume = "volume", + Image = "image", Tmpfs = "tmpfs", Npipe = "npipe", Cluster = "cluster" @@ -2949,6 +2956,7 @@ export interface Container { Config?: ContainerConfig; NetworkSettings?: NetworkSettings; } +export type InspectDeploymentContainerResponse = Container; export type InspectDockerContainerResponse = Container; /** Information about the image's RootFS, including the layer IDs. */ export interface ImageInspectRootFs { @@ -3170,6 +3178,7 @@ export interface Volume { UsageData?: VolumeUsageData; } export type InspectDockerVolumeResponse = Volume; +export type InspectStackContainerResponse = Container; export type JsonValue = any; export type ListActionsResponse = ActionListItem[]; export type ListAlertersResponse = AlerterListItem[]; @@ -3360,8 +3369,10 @@ export interface Permission { user_target: UserTarget; /** The target resource */ resource_target: ResourceTarget; - /** The permission level */ + /** The permission level for the [user_target] on the [resource_target]. */ level?: PermissionLevel; + /** Any specific permissions for the [user_target] on the [resource_target]. */ + specific?: Array; } export type ListPermissionsResponse = Permission[]; export declare enum ProcedureState { @@ -4053,6 +4064,30 @@ export interface ConnectContainerExecQuery { /** The shell to connect to */ shell: string; } +/** + * Query to connect to a container exec session (interactive shell over websocket) on the given Deployment. + * This call will use access to the Deployment Terminal to permission the call. + * TODO: Document calling. + */ +export interface ConnectDeploymentExecQuery { + /** Deployment Id or name */ + deployment: string; + /** The shell to connect to */ + shell: string; +} +/** + * Query to connect to a container exec session (interactive shell over websocket) on the given Stack / service. + * This call will use access to the Stack Terminal to permission the call. + * TODO: Document calling. + */ +export interface ConnectStackExecQuery { + /** Stack Id or name */ + stack: string; + /** The service name to connect to */ + service: string; + /** The shell to connect to */ + shell: string; +} /** * Query to connect to a terminal (interactive shell over websocket) on the given server. * TODO: Document calling. @@ -5248,7 +5283,7 @@ export interface GetPeripheryVersionResponse { * Factors in any UserGroup's permissions they may be a part of. * Response: [PermissionLevel] */ -export interface GetPermissionLevel { +export interface GetPermission { /** The target to get user permission on. */ target: ResourceTarget; } @@ -5582,6 +5617,14 @@ export interface GetVersionResponse { /** The version of the core api. */ version: string; } +/** + * Inspect the docker container associated with the Deployment. + * Response: [Container]. + */ +export interface InspectDeploymentContainer { + /** Id or name */ + deployment: string; +} /** Inspect a docker container on the server. Response: [Container]. */ export interface InspectDockerContainer { /** Id or name */ @@ -5610,6 +5653,16 @@ export interface InspectDockerVolume { /** The volume name */ volume: string; } +/** + * Inspect the docker container associated with the Stack. + * Response: [Container]. + */ +export interface InspectStackContainer { + /** Id or name */ + stack: string; + /** The service name to inspect */ + service: string; +} export interface LatestCommit { hash: string; message: string; @@ -6096,6 +6149,11 @@ export interface NameAndId { export interface NtfyAlerterEndpoint { /** The ntfy topic URL */ url: string; + /** + * Optional E-Mail Address to enable ntfy email notifications. + * SMTP must be configured on the ntfy server. + */ + email?: string; } /** Pauses all containers on the target server. Response: [Update] */ export interface PauseAllContainers { @@ -6147,6 +6205,8 @@ export interface PermissionToml { * - Write */ level: PermissionLevel; + /** Any [SpecificPermissions](SpecificPermission) on the resource */ + specific: Array; } export declare enum PortTypeEnum { EMPTY = "", @@ -6440,10 +6500,12 @@ export interface ResourceToml { export interface UserGroupToml { /** User group name */ name: string; + /** Whether all users will implicitly have the permissions in this group. */ + everyone?: boolean; /** Users in the group */ users?: string[]; /** Give the user group elevated permissions on all resources of a certain type */ - all?: Record; + all?: Record; /** Permissions given to the group */ permissions?: PermissionToml[]; } @@ -6632,6 +6694,16 @@ export interface ServerHealth { mem: ServerHealthState; disks: Record; } +/** + * **Admin only.** Set `everyone` property of User Group. + * Response: [UserGroup] + */ +export interface SetEveryoneUserGroup { + /** Id or name. */ + user_group: string; + /** Whether this user group applies to everyone. */ + everyone: boolean; +} /** * Set the time the user last opened the UI updates. * Used for unseen notification dot. @@ -6640,7 +6712,7 @@ export interface ServerHealth { export interface SetLastSeenUpdate { } /** - * **Admin only.** Completely override the user in the group. + * **Admin only.** Completely override the users in the group. * Response: [UserGroup] */ export interface SetUsersInUserGroup { @@ -6926,7 +6998,7 @@ export interface UpdatePermissionOnResourceType { /** The resource type: eg. Server, Build, Deployment, etc. */ resource_type: ResourceTarget["type"]; /** The base permission level. */ - permission: PermissionLevel; + permission: PermissionLevelAndSpecifics | PermissionLevel; } /** * **Admin only.** Update a user or user groups permission on a resource. @@ -6938,7 +7010,7 @@ export interface UpdatePermissionOnTarget { /** Specify the target resource. */ resource_target: ResourceTarget; /** Specify the permission level. */ - permission: PermissionLevel; + permission: PermissionLevelAndSpecifics | PermissionLevel; } /** * Update the procedure at the given id, and return the updated procedure. @@ -7230,36 +7302,6 @@ export type ExecuteRequest = { } | { type: "PruneSystem"; params: PruneSystem; -} | { - type: "Deploy"; - params: Deploy; -} | { - type: "BatchDeploy"; - params: BatchDeploy; -} | { - type: "PullDeployment"; - params: PullDeployment; -} | { - type: "StartDeployment"; - params: StartDeployment; -} | { - type: "RestartDeployment"; - params: RestartDeployment; -} | { - type: "PauseDeployment"; - params: PauseDeployment; -} | { - type: "UnpauseDeployment"; - params: UnpauseDeployment; -} | { - type: "StopDeployment"; - params: StopDeployment; -} | { - type: "DestroyDeployment"; - params: DestroyDeployment; -} | { - type: "BatchDestroyDeployment"; - params: BatchDestroyDeployment; } | { type: "DeployStack"; params: DeployStack; @@ -7299,6 +7341,36 @@ export type ExecuteRequest = { } | { type: "BatchDestroyStack"; params: BatchDestroyStack; +} | { + type: "Deploy"; + params: Deploy; +} | { + type: "BatchDeploy"; + params: BatchDeploy; +} | { + type: "PullDeployment"; + params: PullDeployment; +} | { + type: "StartDeployment"; + params: StartDeployment; +} | { + type: "RestartDeployment"; + params: RestartDeployment; +} | { + type: "PauseDeployment"; + params: PauseDeployment; +} | { + type: "UnpauseDeployment"; + params: UnpauseDeployment; +} | { + type: "StopDeployment"; + params: StopDeployment; +} | { + type: "DestroyDeployment"; + params: DestroyDeployment; +} | { + type: "BatchDestroyDeployment"; + params: BatchDestroyDeployment; } | { type: "RunBuild"; params: RunBuild; @@ -7379,8 +7451,8 @@ export type ReadRequest = { type: "GetUsername"; params: GetUsername; } | { - type: "GetPermissionLevel"; - params: GetPermissionLevel; + type: "GetPermission"; + params: GetPermission; } | { type: "FindUser"; params: FindUser; @@ -7507,6 +7579,51 @@ export type ReadRequest = { } | { type: "ListTerminals"; params: ListTerminals; +} | { + type: "GetSystemInformation"; + params: GetSystemInformation; +} | { + type: "GetSystemStats"; + params: GetSystemStats; +} | { + type: "ListSystemProcesses"; + params: ListSystemProcesses; +} | { + type: "GetStacksSummary"; + params: GetStacksSummary; +} | { + type: "GetStack"; + params: GetStack; +} | { + type: "GetStackActionState"; + params: GetStackActionState; +} | { + type: "GetStackWebhooksEnabled"; + params: GetStackWebhooksEnabled; +} | { + type: "GetStackLog"; + params: GetStackLog; +} | { + type: "SearchStackLog"; + params: SearchStackLog; +} | { + type: "InspectStackContainer"; + params: InspectStackContainer; +} | { + type: "ListStacks"; + params: ListStacks; +} | { + type: "ListFullStacks"; + params: ListFullStacks; +} | { + type: "ListStackServices"; + params: ListStackServices; +} | { + type: "ListCommonStackExtraArgs"; + params: ListCommonStackExtraArgs; +} | { + type: "ListCommonStackBuildExtraArgs"; + params: ListCommonStackBuildExtraArgs; } | { type: "GetDeploymentsSummary"; params: GetDeploymentsSummary; @@ -7528,6 +7645,9 @@ export type ReadRequest = { } | { type: "SearchDeploymentLog"; params: SearchDeploymentLog; +} | { + type: "InspectDeploymentContainer"; + params: InspectDeploymentContainer; } | { type: "ListDeployments"; params: ListDeployments; @@ -7600,39 +7720,6 @@ export type ReadRequest = { } | { type: "ListFullResourceSyncs"; params: ListFullResourceSyncs; -} | { - type: "GetStacksSummary"; - params: GetStacksSummary; -} | { - type: "GetStack"; - params: GetStack; -} | { - type: "GetStackActionState"; - params: GetStackActionState; -} | { - type: "GetStackWebhooksEnabled"; - params: GetStackWebhooksEnabled; -} | { - type: "GetStackLog"; - params: GetStackLog; -} | { - type: "SearchStackLog"; - params: SearchStackLog; -} | { - type: "ListStacks"; - params: ListStacks; -} | { - type: "ListFullStacks"; - params: ListFullStacks; -} | { - type: "ListStackServices"; - params: ListStackServices; -} | { - type: "ListCommonStackExtraArgs"; - params: ListCommonStackExtraArgs; -} | { - type: "ListCommonStackBuildExtraArgs"; - params: ListCommonStackBuildExtraArgs; } | { type: "GetBuildersSummary"; params: GetBuildersSummary; @@ -7681,15 +7768,6 @@ export type ReadRequest = { } | { type: "GetAlert"; params: GetAlert; -} | { - type: "GetSystemInformation"; - params: GetSystemInformation; -} | { - type: "GetSystemStats"; - params: GetSystemStats; -} | { - type: "ListSystemProcesses"; - params: ListSystemProcesses; } | { type: "GetVariable"; params: GetVariable; @@ -7709,6 +7787,44 @@ export type ReadRequest = { type: "ListDockerRegistryAccounts"; params: ListDockerRegistryAccounts; }; +/** The specific types of permission that a User or UserGroup can have on a resource. */ +export declare enum SpecificPermission { + /** + * On **Server** + * - Access the terminal apis + * On **Stack / Deployment** + * - Access the container exec Apis + */ + Terminal = "Terminal", + /** + * On **Server** + * - Allowed to attach Stacks, Deployments, Repos, Builders to the Server + * On **Builder** + * - Allowed to attach Builds to the Builder + * On **Build** + * - Allowed to attach Deployments to the Build + */ + Attach = "Attach", + /** + * On **Server** + * - Access the `docker inspect` apis + * On **Stack / Deployment** + * - Access `docker inspect $container` for associated containers + */ + Inspect = "Inspect", + /** + * On **Server** + * - Read all container logs on the server + * On **Stack / Deployment** + * - Read the container logs + */ + Logs = "Logs", + /** + * On **Server** + * - Read all the processes on the host + */ + Processes = "Processes" +} export type UserRequest = { type: "PushRecentlyViewed"; params: PushRecentlyViewed; @@ -7761,6 +7877,9 @@ export type WriteRequest = { } | { type: "SetUsersInUserGroup"; params: SetUsersInUserGroup; +} | { + type: "SetEveryoneUserGroup"; + params: SetEveryoneUserGroup; } | { type: "UpdateUserAdmin"; params: UpdateUserAdmin; @@ -7800,6 +7919,33 @@ export type WriteRequest = { } | { type: "DeleteAllTerminals"; params: DeleteAllTerminals; +} | { + type: "CreateStack"; + params: CreateStack; +} | { + type: "CopyStack"; + params: CopyStack; +} | { + type: "DeleteStack"; + params: DeleteStack; +} | { + type: "UpdateStack"; + params: UpdateStack; +} | { + type: "RenameStack"; + params: RenameStack; +} | { + type: "WriteStackFileContents"; + params: WriteStackFileContents; +} | { + type: "RefreshStackCache"; + params: RefreshStackCache; +} | { + type: "CreateStackWebhook"; + params: CreateStackWebhook; +} | { + type: "DeleteStackWebhook"; + params: DeleteStackWebhook; } | { type: "CreateDeployment"; params: CreateDeployment; @@ -7959,33 +8105,6 @@ export type WriteRequest = { } | { type: "DeleteSyncWebhook"; params: DeleteSyncWebhook; -} | { - type: "CreateStack"; - params: CreateStack; -} | { - type: "CopyStack"; - params: CopyStack; -} | { - type: "DeleteStack"; - params: DeleteStack; -} | { - type: "UpdateStack"; - params: UpdateStack; -} | { - type: "RenameStack"; - params: RenameStack; -} | { - type: "WriteStackFileContents"; - params: WriteStackFileContents; -} | { - type: "RefreshStackCache"; - params: RefreshStackCache; -} | { - type: "CreateStackWebhook"; - params: CreateStackWebhook; -} | { - type: "DeleteStackWebhook"; - params: DeleteStackWebhook; } | { type: "CreateTag"; params: CreateTag; diff --git a/frontend/public/client/types.js b/frontend/public/client/types.js index 3733dd9cc..505f60902 100644 --- a/frontend/public/client/types.js +++ b/frontend/public/client/types.js @@ -6,7 +6,7 @@ export var PermissionLevel; (function (PermissionLevel) { /** No permissions. */ PermissionLevel["None"] = "None"; - /** Can see the rousource */ + /** Can read resource information and config */ PermissionLevel["Read"] = "Read"; /** Can execute actions on the resource */ PermissionLevel["Execute"] = "Execute"; @@ -303,8 +303,8 @@ export var ContainerStateStatusEnum; ContainerStateStatusEnum["Running"] = "running"; ContainerStateStatusEnum["Paused"] = "paused"; ContainerStateStatusEnum["Restarting"] = "restarting"; - ContainerStateStatusEnum["Removing"] = "removing"; ContainerStateStatusEnum["Exited"] = "exited"; + ContainerStateStatusEnum["Removing"] = "removing"; ContainerStateStatusEnum["Dead"] = "dead"; })(ContainerStateStatusEnum || (ContainerStateStatusEnum = {})); export var HealthStatusEnum; @@ -328,6 +328,7 @@ export var MountTypeEnum; MountTypeEnum["Empty"] = ""; MountTypeEnum["Bind"] = "bind"; MountTypeEnum["Volume"] = "volume"; + MountTypeEnum["Image"] = "image"; MountTypeEnum["Tmpfs"] = "tmpfs"; MountTypeEnum["Npipe"] = "npipe"; MountTypeEnum["Cluster"] = "cluster"; @@ -505,3 +506,42 @@ export var SearchCombinator; SearchCombinator["Or"] = "Or"; SearchCombinator["And"] = "And"; })(SearchCombinator || (SearchCombinator = {})); +/** The specific types of permission that a User or UserGroup can have on a resource. */ +export var SpecificPermission; +(function (SpecificPermission) { + /** + * On **Server** + * - Access the terminal apis + * On **Stack / Deployment** + * - Access the container exec Apis + */ + SpecificPermission["Terminal"] = "Terminal"; + /** + * On **Server** + * - Allowed to attach Stacks, Deployments, Repos, Builders to the Server + * On **Builder** + * - Allowed to attach Builds to the Builder + * On **Build** + * - Allowed to attach Deployments to the Build + */ + SpecificPermission["Attach"] = "Attach"; + /** + * On **Server** + * - Access the `docker inspect` apis + * On **Stack / Deployment** + * - Access `docker inspect $container` for associated containers + */ + SpecificPermission["Inspect"] = "Inspect"; + /** + * On **Server** + * - Read all container logs on the server + * On **Stack / Deployment** + * - Read the container logs + */ + SpecificPermission["Logs"] = "Logs"; + /** + * On **Server** + * - Read all the processes on the host + */ + SpecificPermission["Processes"] = "Processes"; +})(SpecificPermission || (SpecificPermission = {})); diff --git a/frontend/src/components/config/util.tsx b/frontend/src/components/config/util.tsx index 25f7b6474..1624cc29d 100644 --- a/frontend/src/components/config/util.tsx +++ b/frontend/src/components/config/util.tsx @@ -1035,36 +1035,6 @@ export const SecretSelector = ({ ); }; -export const PermissionLevelSelector = ({ - level, - onSelect, -}: { - level: Types.PermissionLevel; - onSelect: (level: Types.PermissionLevel) => void; -}) => { - return ( - - ); -}; - export const WebhookBuilder = ({ git_provider, children, diff --git a/frontend/src/components/inspect.tsx b/frontend/src/components/inspect.tsx new file mode 100644 index 000000000..6f65525cb --- /dev/null +++ b/frontend/src/components/inspect.tsx @@ -0,0 +1,46 @@ +import { Types } from "komodo_client"; +import { Loader2 } from "lucide-react"; +import { MonacoEditor } from "./monaco"; + +export const InspectContainerView = ({ + container, + error, + isPending, + isError, +}: { + container: Types.Container | undefined; + error: unknown; + isPending: boolean; + isError: boolean; +}) => { + if (isPending) { + return ( +
+ +
+ ); + } + if (isError) { + return ( +
+

Failed to inspect container.

+ {(error ?? undefined) && ( + + )} +
+ ); + } + return ( +
+ +
+ ); +}; diff --git a/frontend/src/components/resources/action/config.tsx b/frontend/src/components/resources/action/config.tsx index a69d8f175..1b55a031d 100644 --- a/frontend/src/components/resources/action/config.tsx +++ b/frontend/src/components/resources/action/config.tsx @@ -1,5 +1,6 @@ import { useLocalStorage, + usePermissions, useRead, useWebhookIdOrName, useWebhookIntegrations, @@ -32,9 +33,7 @@ const ACTION_GIT_PROVIDER = "Action"; export const ActionConfig = ({ id }: { id: string }) => { const [branch, setBranch] = useState("main"); - const perms = useRead("GetPermissionLevel", { - target: { type: "Action", id }, - }).data; + const { canWrite } = usePermissions({ type: "Action", id }); const action = useRead("GetAction", { action: id }).data; const config = action?.config; const name = action?.name; @@ -50,7 +49,7 @@ export const ActionConfig = ({ id }: { id: string }) => { if (!config) return null; - const disabled = global_disabled || perms !== Types.PermissionLevel.Write; + const disabled = global_disabled || !canWrite; const webhook_integration = integrations[ACTION_GIT_PROVIDER] ?? "Github"; return ( @@ -251,7 +250,7 @@ export const ActionConfig = ({ id }: { id: string }) => { ), diff --git a/frontend/src/components/resources/alerter/config/endpoint.tsx b/frontend/src/components/resources/alerter/config/endpoint.tsx index 500fb8ab6..3cf5ee01c 100644 --- a/frontend/src/components/resources/alerter/config/endpoint.tsx +++ b/frontend/src/components/resources/alerter/config/endpoint.tsx @@ -8,6 +8,7 @@ import { SelectTrigger, SelectValue, } from "@ui/select"; +import { Input } from "@ui/input"; const ENDPOINT_TYPES: Types.AlerterEndpoint["type"][] = [ "Custom", @@ -58,6 +59,27 @@ export const EndpointConfig = ({ } readOnly={disabled} /> + {endpoint.type == "Ntfy" ? ( + + + set({ + ...endpoint, + params: { ...endpoint.params, email: input.target.value }, + }) + } + > + + ) : ( + "" + )} ); }; @@ -66,12 +88,12 @@ const default_url = (type: Types.AlerterEndpoint["type"]) => { return type === "Custom" ? "http://localhost:7000" : type === "Slack" - ? "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" - : type === "Discord" - ? "https://discord.com/api/webhooks/XXXXXXXXXXXX/XXXX-XXXXXXXXXX" - : type === "Ntfy" - ? "https://ntfy.sh/komodo" - : type === "Pushover" - ? "https://api.pushover.net/1/messages.json?token=XXXXXXXXXXXXX&user=XXXXXXXXXXXXX" - : ""; + ? "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX" + : type === "Discord" + ? "https://discord.com/api/webhooks/XXXXXXXXXXXX/XXXX-XXXXXXXXXX" + : type === "Ntfy" + ? "https://ntfy.sh/komodo" + : type === "Pushover" + ? "https://api.pushover.net/1/messages.json?token=XXXXXXXXXXXXX&user=XXXXXXXXXXXXX" + : ""; }; diff --git a/frontend/src/components/resources/alerter/config/index.tsx b/frontend/src/components/resources/alerter/config/index.tsx index 098c9be63..5578dc27c 100644 --- a/frontend/src/components/resources/alerter/config/index.tsx +++ b/frontend/src/components/resources/alerter/config/index.tsx @@ -1,14 +1,12 @@ import { Config } from "@components/config"; -import { useLocalStorage, useRead, useWrite } from "@lib/hooks"; +import { useLocalStorage, usePermissions, useRead, useWrite } from "@lib/hooks"; import { Types } from "komodo_client"; import { EndpointConfig } from "./endpoint"; import { AlertTypeConfig } from "./alert_types"; import { ResourcesConfig } from "./resources"; export const AlerterConfig = ({ id }: { id: string }) => { - const perms = useRead("GetPermissionLevel", { - target: { type: "Alerter", id }, - }).data; + const { canWrite } = usePermissions({ type: "Alerter", id }); const config = useRead("GetAlerter", { alerter: id }).data?.config; const global_disabled = useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false; @@ -19,7 +17,7 @@ export const AlerterConfig = ({ id }: { id: string }) => { ); if (!config) return null; - const disabled = global_disabled || perms !== Types.PermissionLevel.Write; + const disabled = global_disabled || !canWrite; return ( { - const perms = useRead("GetPermissionLevel", { - target: { type: "Build", id }, - }).data; + const { canExecute } = usePermissions({ type: "Build", id }); const building = useRead( "GetBuildActionState", { build: id }, @@ -22,19 +20,13 @@ export const RunBuild = ({ id }: { id: string }) => { const { mutate: run_mutate, isPending: runPending } = useExecute("RunBuild"); const { mutate: cancel_mutate, isPending: cancelPending } = useExecute("CancelBuild"); - const build = useRead("ListBuilds", {}).data?.find( - (d) => d.id === id - ); + const build = useRead("ListBuilds", {}).data?.find((d) => d.id === id); const builder = useBuilder(build?.info.builder_id); const canCancel = builder?.info.builder_type !== "Server"; // make sure hidden without perms. // not usually necessary, but this button also used in deployment actions. - if ( - perms !== Types.PermissionLevel.Execute && - perms !== Types.PermissionLevel.Write - ) - return null; + if (!canExecute) return null; // updates come in in descending order, so 'find' will find latest update matching operation const latestBuild = updates?.updates.find( diff --git a/frontend/src/components/resources/build/config.tsx b/frontend/src/components/resources/build/config.tsx index a5fef636c..574bd2dac 100644 --- a/frontend/src/components/resources/build/config.tsx +++ b/frontend/src/components/resources/build/config.tsx @@ -15,6 +15,7 @@ import { getWebhookIntegration, useInvalidate, useLocalStorage, + usePermissions, useRead, useWebhookIdOrName, useWebhookIntegrations, @@ -63,9 +64,7 @@ export const BuildConfig = ({ git: true, webhooks: true, }); - const perms = useRead("GetPermissionLevel", { - target: { type: "Build", id }, - }).data; + const { canWrite } = usePermissions({ type: "Build", id }); const build = useRead("GetBuild", { build: id }).data; const config = build?.config; const name = build?.name; @@ -82,7 +81,7 @@ export const BuildConfig = ({ if (!config) return null; - const disabled = global_disabled || perms !== Types.PermissionLevel.Write; + const disabled = global_disabled || !canWrite; const git_provider = update.git_provider ?? config.git_provider; const webhook_integration = getWebhookIntegration(integrations, git_provider); @@ -520,7 +519,7 @@ export const BuildConfig = ({ ), diff --git a/frontend/src/components/resources/build/info.tsx b/frontend/src/components/resources/build/info.tsx index 779095fa1..b8b7d8a64 100644 --- a/frontend/src/components/resources/build/info.tsx +++ b/frontend/src/components/resources/build/info.tsx @@ -10,7 +10,7 @@ import { import { useFullBuild } from "."; import { cn, updateLogToHtml } from "@lib/utils"; import { MonacoEditor } from "@components/monaco"; -import { useEditPermissions } from "@pages/resource"; +import { usePermissions } from "@lib/hooks"; import { ConfirmUpdate } from "@components/config/util"; import { useLocalStorage, useRead, useWrite } from "@lib/hooks"; import { Button } from "@ui/button"; @@ -32,7 +32,7 @@ export const BuildInfo = ({ { contents: undefined } ); const [showContents, setShowContents] = useState(true); - const { canWrite } = useEditPermissions({ type: "Build", id }); + const { canWrite } = usePermissions({ type: "Build", id }); const { toast } = useToast(); const { mutateAsync, isPending } = useWrite("WriteBuildFileContents", { onSuccess: (res) => { diff --git a/frontend/src/components/resources/builder/config.tsx b/frontend/src/components/resources/builder/config.tsx index d7f9b91a3..80a5cdf2d 100644 --- a/frontend/src/components/resources/builder/config.tsx +++ b/frontend/src/components/resources/builder/config.tsx @@ -1,6 +1,6 @@ import { Config } from "@components/config"; import { ConfigItem, ConfigList } from "@components/config/util"; -import { useLocalStorage, useRead, useWrite } from "@lib/hooks"; +import { useLocalStorage, usePermissions, useRead, useWrite } from "@lib/hooks"; import { Types } from "komodo_client"; import { useState } from "react"; import { ResourceLink, ResourceSelector } from "../common"; @@ -26,9 +26,7 @@ export const BuilderConfig = ({ id }: { id: string }) => { }; const AwsBuilderConfig = ({ id }: { id: string }) => { - const perms = useRead("GetPermissionLevel", { - target: { type: "Builder", id }, - }).data; + const { canWrite } = usePermissions({ type: "Builder", id }); const config = useRead("GetBuilder", { builder: id }).data?.config ?.params as Types.AwsBuilderConfig; const global_disabled = @@ -40,7 +38,7 @@ const AwsBuilderConfig = ({ id }: { id: string }) => { const { mutateAsync } = useWrite("UpdateBuilder"); if (!config) return null; - const disabled = global_disabled || perms !== Types.PermissionLevel.Write; + const disabled = global_disabled || !canWrite; return ( { }; const ServerBuilderConfig = ({ id }: { id: string }) => { - const perms = useRead("GetPermissionLevel", { - target: { type: "Builder", id }, - }).data; + const { canWrite } = usePermissions({ type: "Builder", id }); const config = useRead("GetBuilder", { builder: id }).data?.config; const [update, set] = useLocalStorage>( `server-builder-${id}-update-v1`, @@ -252,7 +248,7 @@ const ServerBuilderConfig = ({ id }: { id: string }) => { const { mutateAsync } = useWrite("UpdateBuilder"); if (!config) return null; - const disabled = perms !== Types.PermissionLevel.Write; + const disabled = !canWrite; return ( { }; const UrlBuilderConfig = ({ id }: { id: string }) => { - const perms = useRead("GetPermissionLevel", { - target: { type: "Builder", id }, - }).data; + const { canWrite } = usePermissions({ type: "Builder", id }); const config = useRead("GetBuilder", { builder: id }).data?.config; const [update, set] = useLocalStorage>( `url-builder-${id}-update-v1`, @@ -314,7 +308,7 @@ const UrlBuilderConfig = ({ id }: { id: string }) => { const { mutateAsync } = useWrite("UpdateBuilder"); if (!config) return null; - const disabled = perms !== Types.PermissionLevel.Write; + const disabled = !canWrite; return ( { placeholder: "https://periphery:8120", }, passkey: { - description: "Use a custom passkey to authenticate with Periphery", + description: + "Use a custom passkey to authenticate with Periphery", placeholder: "Custom passkey", }, }, diff --git a/frontend/src/components/resources/deployment/config/index.tsx b/frontend/src/components/resources/deployment/config/index.tsx index e574f7ee9..801941d2a 100644 --- a/frontend/src/components/resources/deployment/config/index.tsx +++ b/frontend/src/components/resources/deployment/config/index.tsx @@ -1,4 +1,4 @@ -import { useLocalStorage, useRead, useWrite } from "@lib/hooks"; +import { useLocalStorage, usePermissions, useRead, useWrite } from "@lib/hooks"; import { Types } from "komodo_client"; import { ReactNode } from "react"; import { @@ -30,9 +30,7 @@ export const DeploymentConfig = ({ id: string; titleOther: ReactNode; }) => { - const perms = useRead("GetPermissionLevel", { - target: { type: "Deployment", id }, - }).data; + const { canWrite } = usePermissions({ type: "Deployment", id }); const config = useRead("GetDeployment", { deployment: id }).data?.config; const builds = useRead("ListBuilds", {}).data; const global_disabled = @@ -49,7 +47,7 @@ export const DeploymentConfig = ({ const hide_ports = network === "host" || network === "none"; const auto_update = update.auto_update ?? config.auto_update ?? false; - const disabled = global_disabled || perms !== Types.PermissionLevel.Write; + const disabled = global_disabled || !canWrite; return ( { // const [view, setView] = useAtom(configOrLog); - const [_view, setView] = useLocalStorage<"Config" | "Log" | "Terminal">( - "deployment-tabs-v1", - "Config" - ); - const { canWrite: canWriteServer } = useEditPermissions({ - type: "Server", - id: deployment.info.server_id, + const [_view, setView] = useLocalStorage< + "Config" | "Log" | "Inspect" | "Terminal" + >("deployment-tabs-v1", "Config"); + const { specific } = usePermissions({ + type: "Deployment", + id: deployment.id, }); const container_exec_disabled = useServer(deployment.info.server_id)?.info.container_exec_disabled ?? true; const state = deployment.info.state; const logsDisabled = + !specific.includes(Types.SpecificPermission.Logs) || + state === undefined || + state === Types.DeploymentState.Unknown || + state === Types.DeploymentState.NotDeployed; + const inspectDisabled = + !specific.includes(Types.SpecificPermission.Inspect) || state === undefined || state === Types.DeploymentState.Unknown || state === Types.DeploymentState.NotDeployed; const terminalDisabled = - !canWriteServer || + !specific.includes(Types.SpecificPermission.Terminal) || container_exec_disabled || state !== Types.DeploymentState.Running; const view = (logsDisabled && _view === "Log") || + (inspectDisabled && _view === "Inspect") || (terminalDisabled && _view === "Terminal") ? "Config" : _view; @@ -86,11 +93,26 @@ const ConfigTabsInner = ({ Config - - Log - - {!terminalDisabled && ( - + {specific.includes(Types.SpecificPermission.Logs) && ( + + Log + + )} + {specific.includes(Types.SpecificPermission.Inspect) && ( + + Inspect + + )} + {specific.includes(Types.SpecificPermission.Terminal) && ( + Terminal )} @@ -104,8 +126,21 @@ const ConfigTabsInner = ({ + + + - + ); diff --git a/frontend/src/components/resources/deployment/inspect.tsx b/frontend/src/components/resources/deployment/inspect.tsx new file mode 100644 index 000000000..affc810b1 --- /dev/null +++ b/frontend/src/components/resources/deployment/inspect.tsx @@ -0,0 +1,48 @@ +import { usePermissions, useRead } from "@lib/hooks"; +import { ReactNode } from "react"; +import { Types } from "komodo_client"; +import { Section } from "@components/layouts"; +import { InspectContainerView } from "@components/inspect"; + +export const DeploymentInspect = ({ + id, + titleOther, +}: { + id: string; + titleOther: ReactNode; +}) => { + const { specific } = usePermissions({ type: "Deployment", id }); + if (!specific.includes(Types.SpecificPermission.Inspect)) { + return ( +
+
+

User does not have permission to inspect this Deployment.

+
+
+ ); + } + return ( +
+ +
+ ); +}; + +const DeploymentInspectInner = ({ id }: { id: string }) => { + const { + data: container, + error, + isPending, + isError, + } = useRead("InspectDeploymentContainer", { + deployment: id, + }); + return ( + + ); +}; diff --git a/frontend/src/components/resources/deployment/terminal.tsx b/frontend/src/components/resources/deployment/terminal.tsx deleted file mode 100644 index 4bbedb0c8..000000000 --- a/frontend/src/components/resources/deployment/terminal.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { ReactNode } from "react"; -import { ContainerTerminal } from "@components/terminal"; -import { Types } from "komodo_client"; - -export const DeploymentTerminal = ({ - deployment, - titleOther, -}: { - deployment: Types.DeploymentListItem; - titleOther?: ReactNode; -}) => { - return ( - deployment.info.server_id && ( - - ) - ); -}; diff --git a/frontend/src/components/resources/procedure/config.tsx b/frontend/src/components/resources/procedure/config.tsx index b852001a9..9db1fb56a 100644 --- a/frontend/src/components/resources/procedure/config.tsx +++ b/frontend/src/components/resources/procedure/config.tsx @@ -1,5 +1,6 @@ import { useLocalStorage, + usePermissions, useRead, useWebhookIdOrName, useWebhookIntegrations, @@ -113,9 +114,7 @@ const default_enabled_execution: () => Types.EnabledExecution = () => ({ export const ProcedureConfig = ({ id }: { id: string }) => { const [branch, setBranch] = useState("main"); - const perms = useRead("GetPermissionLevel", { - target: { type: "Procedure", id }, - }).data; + const { canWrite } = usePermissions({ type: "Procedure", id }); const procedure = useRead("GetProcedure", { procedure: id }).data; const config = procedure?.config; const name = procedure?.name; @@ -131,7 +130,7 @@ export const ProcedureConfig = ({ id }: { id: string }) => { if (!config) return null; - const disabled = global_disabled || perms !== Types.PermissionLevel.Write; + const disabled = global_disabled || !canWrite; const webhook_integration = integrations[PROCEDURE_GIT_PROVIDER] ?? "Github"; const stages = update.stages || procedure.config?.stages || []; @@ -381,7 +380,7 @@ export const ProcedureConfig = ({ id }: { id: string }) => { ), diff --git a/frontend/src/components/resources/repo/actions.tsx b/frontend/src/components/resources/repo/actions.tsx index ee4297176..1a4cf26ac 100644 --- a/frontend/src/components/resources/repo/actions.tsx +++ b/frontend/src/components/resources/repo/actions.tsx @@ -1,5 +1,5 @@ import { ConfirmButton } from "@components/util"; -import { useExecute, useRead } from "@lib/hooks"; +import { useExecute, usePermissions, useRead } from "@lib/hooks"; import { ArrowDownToDot, ArrowDownToLine, @@ -71,9 +71,7 @@ export const PullRepo = ({ id }: { id: string }) => { }; export const BuildRepo = ({ id }: { id: string }) => { - const perms = useRead("GetPermissionLevel", { - target: { type: "Repo", id }, - }).data; + const { canExecute } = usePermissions({ type: "Repo", id }); const building = useRead( "GetRepoActionState", { repo: id }, @@ -98,11 +96,7 @@ export const BuildRepo = ({ id }: { id: string }) => { // make sure hidden without perms. // not usually necessary, but this button also used in deployment actions. - if ( - perms !== Types.PermissionLevel.Execute && - perms !== Types.PermissionLevel.Write - ) - return null; + if (!canExecute) return null; // updates come in in descending order, so 'find' will find latest update matching operation const latestBuild = updates?.updates.find( diff --git a/frontend/src/components/resources/repo/config.tsx b/frontend/src/components/resources/repo/config.tsx index 158571f95..ae9387437 100644 --- a/frontend/src/components/resources/repo/config.tsx +++ b/frontend/src/components/resources/repo/config.tsx @@ -11,6 +11,7 @@ import { getWebhookIntegration, useInvalidate, useLocalStorage, + usePermissions, useRead, useWebhookIdOrName, useWebhookIntegrations, @@ -27,9 +28,7 @@ import { SecretsSearch } from "@components/config/env_vars"; import { MonacoEditor } from "@components/monaco"; export const RepoConfig = ({ id }: { id: string }) => { - const perms = useRead("GetPermissionLevel", { - target: { type: "Repo", id }, - }).data; + const { canWrite } = usePermissions({ type: "Repo", id }); const repo = useRead("GetRepo", { repo: id }).data; const config = repo?.config; const name = repo?.name; @@ -46,7 +45,7 @@ export const RepoConfig = ({ id }: { id: string }) => { if (!config) return null; - const disabled = global_disabled || perms !== Types.PermissionLevel.Write; + const disabled = global_disabled || !canWrite; const git_provider = update.git_provider ?? config.git_provider; const webhook_integration = getWebhookIntegration(integrations, git_provider); @@ -257,7 +256,7 @@ export const RepoConfig = ({ id }: { id: string }) => { ), @@ -265,7 +264,7 @@ export const RepoConfig = ({ id }: { id: string }) => { ), @@ -273,7 +272,7 @@ export const RepoConfig = ({ id }: { id: string }) => { ), diff --git a/frontend/src/components/resources/resource-sync/actions.tsx b/frontend/src/components/resources/resource-sync/actions.tsx index 4a3bfc594..3b0293d37 100644 --- a/frontend/src/components/resources/resource-sync/actions.tsx +++ b/frontend/src/components/resources/resource-sync/actions.tsx @@ -1,7 +1,7 @@ import { ActionButton, ActionWithDialog } from "@components/util"; import { useExecute, useInvalidate, useRead, useWrite } from "@lib/hooks"; import { file_contents_empty, sync_no_changes } from "@lib/utils"; -import { useEditPermissions } from "@pages/resource"; +import { usePermissions } from "@lib/hooks"; import { NotebookPen, RefreshCcw, SquarePlay } from "lucide-react"; import { useFullResourceSync, usePendingView } from "."; @@ -69,7 +69,7 @@ export const ExecuteSync = ({ id }: { id: string }) => { export const CommitSync = ({ id }: { id: string }) => { const { mutate, isPending } = useWrite("CommitSync"); const sync = useFullResourceSync(id); - const { canWrite } = useEditPermissions({ type: "ResourceSync", id }); + const { canWrite } = usePermissions({ type: "ResourceSync", id }); const [_pendingView] = usePendingView(); const pendingView = sync?.config?.managed ? _pendingView : "Execute"; diff --git a/frontend/src/components/resources/resource-sync/config.tsx b/frontend/src/components/resources/resource-sync/config.tsx index 53d43ac0d..1ed7bdad6 100644 --- a/frontend/src/components/resources/resource-sync/config.tsx +++ b/frontend/src/components/resources/resource-sync/config.tsx @@ -11,6 +11,7 @@ import { getWebhookIntegration, useInvalidate, useLocalStorage, + usePermissions, useRead, useWebhookIdOrName, useWebhookIntegrations, @@ -68,9 +69,7 @@ export const ResourceSyncConfig = ({ git: true, webhooks: true, }); - const perms = useRead("GetPermissionLevel", { - target: { type: "ResourceSync", id }, - }).data; + const { canWrite } = usePermissions({ type: "ResourceSync", id }); const sync = useRead("GetResourceSync", { sync: id }).data; const config = sync?.config; const name = sync?.name; @@ -87,7 +86,7 @@ export const ResourceSyncConfig = ({ if (!config) return null; - const disabled = global_disabled || perms !== Types.PermissionLevel.Write; + const disabled = global_disabled || !canWrite; const git_provider = update.git_provider ?? config.git_provider; const webhook_integration = getWebhookIntegration(integrations, git_provider); @@ -366,7 +365,7 @@ export const ResourceSyncConfig = ({ > ), @@ -377,7 +376,7 @@ export const ResourceSyncConfig = ({ > ), diff --git a/frontend/src/components/resources/resource-sync/info.tsx b/frontend/src/components/resources/resource-sync/info.tsx index 3c5cadf36..649158b1f 100644 --- a/frontend/src/components/resources/resource-sync/info.tsx +++ b/frontend/src/components/resources/resource-sync/info.tsx @@ -4,7 +4,7 @@ import { Card, CardContent, CardHeader } from "@ui/card"; import { useFullResourceSync } from "."; import { cn, updateLogToHtml } from "@lib/utils"; import { MonacoEditor } from "@components/monaco"; -import { useEditPermissions } from "@pages/resource"; +import { usePermissions } from "@lib/hooks"; import { useLocalStorage, useWrite } from "@lib/hooks"; import { useToast } from "@ui/use-toast"; import { Button } from "@ui/button"; @@ -24,7 +24,7 @@ export const ResourceSyncInfo = ({ {} ); const [show, setShow] = useState>({}); - const { canWrite } = useEditPermissions({ type: "ResourceSync", id }); + const { canWrite } = usePermissions({ type: "ResourceSync", id }); const { toast } = useToast(); const { mutateAsync, isPending } = useWrite("WriteSyncFileContents", { onSuccess: (res) => { diff --git a/frontend/src/components/resources/resource-sync/pending.tsx b/frontend/src/components/resources/resource-sync/pending.tsx index a9cf6dfde..b23280f5c 100644 --- a/frontend/src/components/resources/resource-sync/pending.tsx +++ b/frontend/src/components/resources/resource-sync/pending.tsx @@ -9,7 +9,7 @@ import { diff_type_intention, text_color_class_by_intention } from "@lib/color"; import { cn, sanitizeOnlySpan } from "@lib/utils"; import { ConfirmButton } from "@components/util"; import { SquarePlay } from "lucide-react"; -import { useEditPermissions } from "@pages/resource"; +import { usePermissions } from "@lib/hooks"; import { useFullResourceSync, usePendingView } from "."; import { Tabs, TabsList, TabsTrigger } from "@ui/tabs"; import { ResourceDiff } from "komodo_client/dist/types"; @@ -24,7 +24,7 @@ export const ResourceSyncPending = ({ const syncing = useRead("GetResourceSyncActionState", { sync: id }).data ?.syncing; const sync = useFullResourceSync(id); - const { canExecute } = useEditPermissions({ type: "ResourceSync", id }); + const { canExecute } = usePermissions({ type: "ResourceSync", id }); const [_pendingView, setPendingView] = usePendingView(); const pendingView = sync?.config?.managed ? _pendingView : "Execute"; const { mutate, isPending } = useExecute("RunSync"); diff --git a/frontend/src/components/resources/server/actions.tsx b/frontend/src/components/resources/server/actions.tsx index 2b998e281..53afe87d7 100644 --- a/frontend/src/components/resources/server/actions.tsx +++ b/frontend/src/components/resources/server/actions.tsx @@ -1,9 +1,7 @@ import { ActionWithDialog, ConfirmButton } from "@components/util"; -import { useExecute, useRead } from "@lib/hooks"; +import { useExecute, usePermissions, useRead } from "@lib/hooks"; import { Scissors } from "lucide-react"; import { useServer } from "."; -import { has_minimum_permissions } from "@lib/utils"; -import { Types } from "komodo_client"; export const Prune = ({ server_id, @@ -19,17 +17,10 @@ export const Prune = ({ { server: server_id }, { refetchInterval: 5000 } ).data; - const perms = useRead("GetPermissionLevel", { - target: { type: "Server", id: server_id }, - }).data; + const { canExecute } = usePermissions({ type: "Server", id: server_id }); if (!server) return; - const canExecute = has_minimum_permissions( - perms, - Types.PermissionLevel.Execute - ); - const pruningKey = type === "Containers" ? "pruning_containers" diff --git a/frontend/src/components/resources/server/config.tsx b/frontend/src/components/resources/server/config.tsx index c18b6f966..1f2cb86db 100644 --- a/frontend/src/components/resources/server/config.tsx +++ b/frontend/src/components/resources/server/config.tsx @@ -1,6 +1,12 @@ import { Config } from "@components/config"; import { ConfigList } from "@components/config/util"; -import { useInvalidate, useLocalStorage, useRead, useWrite } from "@lib/hooks"; +import { + useInvalidate, + useLocalStorage, + usePermissions, + useRead, + useWrite, +} from "@lib/hooks"; import { Types } from "komodo_client"; import { ReactNode } from "react"; @@ -11,9 +17,7 @@ export const ServerConfig = ({ id: string; titleOther: ReactNode; }) => { - const perms = useRead("GetPermissionLevel", { - target: { type: "Server", id }, - }).data; + const { canWrite } = usePermissions({ type: "Server", id }); const invalidate = useInvalidate(); const config = useRead("GetServer", { server: id }).data?.config; const global_disabled = @@ -30,7 +34,7 @@ export const ServerConfig = ({ }); if (!config) return null; - const disabled = global_disabled || perms !== Types.PermissionLevel.Write; + const disabled = global_disabled || !canWrite; return ( @@ -67,7 +67,7 @@ const ConfigTabs = ({ id }: { id: string }) => { >(`server-${id}-tab`, "Config"); const is_admin = useUser().data?.admin ?? false; - const { canWrite } = useEditPermissions({ type: "Server", id }); + const { canWrite } = usePermissions({ type: "Server", id }); const server_info = useServer(id)?.info; const terminals_disabled = server_info?.terminals_disabled ?? true; const container_exec_disabled = server_info?.container_exec_disabled ?? true; diff --git a/frontend/src/components/resources/server/stats.tsx b/frontend/src/components/resources/server/stats.tsx index 2ba5e3ecd..56fe4925d 100644 --- a/frontend/src/components/resources/server/stats.tsx +++ b/frontend/src/components/resources/server/stats.tsx @@ -2,7 +2,7 @@ import { Section } from "@components/layouts"; import { Card, CardContent, CardHeader, CardTitle } from "@ui/card"; import { Progress } from "@ui/progress"; import { Cpu, Database, Loader2, MemoryStick } from "lucide-react"; -import { useRead } from "@lib/hooks"; +import { usePermissions, useRead } from "@lib/hooks"; import { Types } from "komodo_client"; import { DataTable, SortableHeader } from "@ui/data-table"; import { ReactNode, useMemo, useState } from "react"; @@ -27,6 +27,8 @@ export const ServerStats = ({ }) => { const [interval, setInterval] = useStatsGranularity(); + const { specific } = usePermissions({ type: "Server", id }); + const stats = useRead( "GetSystemStats", { server: id }, @@ -138,7 +140,11 @@ export const ServerStats = ({ >
- + - + {specific.includes(Types.SpecificPermission.Processes) && ( + + )}
); diff --git a/frontend/src/components/resources/stack/config.tsx b/frontend/src/components/resources/stack/config.tsx index f5f9e565f..41d832581 100644 --- a/frontend/src/components/resources/stack/config.tsx +++ b/frontend/src/components/resources/stack/config.tsx @@ -15,6 +15,7 @@ import { getWebhookIntegration, useInvalidate, useLocalStorage, + usePermissions, useRead, useWebhookIdOrName, useWebhookIntegrations, @@ -62,9 +63,7 @@ export const StackConfig = ({ git: true, webhooks: true, }); - const perms = useRead("GetPermissionLevel", { - target: { type: "Stack", id }, - }).data; + const { canWrite } = usePermissions({ type: "Stack", id }); const stack = useRead("GetStack", { stack: id }).data; const config = stack?.config; const name = stack?.name; @@ -81,7 +80,7 @@ export const StackConfig = ({ if (!config) return null; - const disabled = global_disabled || perms !== Types.PermissionLevel.Write; + const disabled = global_disabled || !canWrite; const run_build = update.run_build ?? config.run_build; const mode = getStackMode(update, config); @@ -626,21 +625,12 @@ export const StackConfig = ({ ["Builder" as any]: () => ( ), - // ["Refresh" as any]: () => - // (update.branch ?? config.branch) && ( - // - // - // - // ), ["Deploy" as any]: () => (update.branch ?? config.branch) && ( ), diff --git a/frontend/src/components/resources/stack/index.tsx b/frontend/src/components/resources/stack/index.tsx index 8f1559815..1f531f5e0 100644 --- a/frontend/src/components/resources/stack/index.tsx +++ b/frontend/src/components/resources/stack/index.tsx @@ -1,4 +1,10 @@ -import { useInvalidate, useLocalStorage, useRead, useWrite } from "@lib/hooks"; +import { + useInvalidate, + useLocalStorage, + usePermissions, + useRead, + useWrite, +} from "@lib/hooks"; import { RequiredResourceComponents } from "@types"; import { Card } from "@ui/card"; import { @@ -60,19 +66,21 @@ const ConfigInfoServicesLog = ({ id }: { id: string }) => { "Config" | "Info" | "Services" | "Log" >("stack-tabs-v1", "Config"); const info = useStack(id)?.info; + const { specific } = usePermissions({ type: "Stack", id }); const state = info?.state; const hideInfo = !info?.files_on_host && !info?.repo; - // Hides both services and logs const hideServices = state === undefined || state === Types.StackState.Unknown || state === Types.StackState.Down; + const hideLogs = + hideServices || !specific.includes(Types.SpecificPermission.Logs); const view = (_view === "Info" && hideInfo) || (_view === "Services" && hideServices) || - (_view === "Log" && hideServices) + (_view === "Log" && hideLogs) ? "Config" : _view; @@ -95,9 +103,11 @@ const ConfigInfoServicesLog = ({ id }: { id: string }) => { > Services
- - Log - + {specific.includes(Types.SpecificPermission.Logs) && ( + + Log + + )} ); return ( diff --git a/frontend/src/components/resources/stack/info.tsx b/frontend/src/components/resources/stack/info.tsx index 0c8e9a07c..a67e3dc6c 100644 --- a/frontend/src/components/resources/stack/info.tsx +++ b/frontend/src/components/resources/stack/info.tsx @@ -10,7 +10,7 @@ import { import { useFullStack, useStack } from "."; import { cn, updateLogToHtml } from "@lib/utils"; import { MonacoEditor } from "@components/monaco"; -import { useEditPermissions } from "@pages/resource"; +import { usePermissions } from "@lib/hooks"; import { ConfirmUpdate } from "@components/config/util"; import { useLocalStorage, useWrite } from "@lib/hooks"; import { Button } from "@ui/button"; @@ -32,7 +32,7 @@ export const StackInfo = ({ {} ); const [show, setShow] = useState>({}); - const { canWrite } = useEditPermissions({ type: "Stack", id }); + const { canWrite } = usePermissions({ type: "Stack", id }); const { toast } = useToast(); const { mutateAsync, isPending } = useWrite("WriteStackFileContents", { onSuccess: (res) => { diff --git a/frontend/src/components/terminal/container.tsx b/frontend/src/components/terminal/container.tsx new file mode 100644 index 000000000..34c22202d --- /dev/null +++ b/frontend/src/components/terminal/container.tsx @@ -0,0 +1,110 @@ +import { Section } from "@components/layouts"; +import { komodo_client, useLocalStorage } from "@lib/hooks"; +import { Button } from "@ui/button"; +import { CardTitle } from "@ui/card"; +import { Input } from "@ui/input"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@ui/select"; +import { RefreshCcw } from "lucide-react"; +import { ReactNode, useCallback, useState } from "react"; +import { Terminal } from "."; +import { ContainerExecQuery, TerminalCallbacks } from "komodo_client"; + +const BASE_SHELLS = ["sh", "bash"]; + +export const ContainerTerminal = ({ + query: { type, query }, + titleOther, +}: { + query: ContainerExecQuery; + titleOther?: ReactNode; +}) => { + const [_reconnect, _setReconnect] = useState(false); + const triggerReconnect = () => _setReconnect((r) => !r); + const [_clear, _setClear] = useState(false); + + const storageKey = + type === "container" + ? `server-${query.server}-${query.container}-shell-v1` + : type === "deployment" + ? `deployment-${query.deployment}-shell-v1` + : `stack-${query.stack}-${query.service}-shell-v1`; + + const [shell, setShell] = useLocalStorage(storageKey, "sh"); + const [otherShell, setOtherShell] = useState(""); + + const make_ws = useCallback( + (callbacks: TerminalCallbacks) => + komodo_client().connect_container_exec({ + query: { type, query: { ...query, shell } } as any, + ...callbacks, + }), + [query, shell] + ); + + return ( +
+ + docker exec -it container + setOtherShell(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + setShell(otherShell); + setOtherShell(""); + } else { + e.stopPropagation(); + } + }} + /> + + + + + + + } + > +
+ +
+
+ ); +}; diff --git a/frontend/src/components/terminal.tsx b/frontend/src/components/terminal/index.tsx similarity index 53% rename from frontend/src/components/terminal.tsx rename to frontend/src/components/terminal/index.tsx index b5761d833..1ce3647b9 100644 --- a/frontend/src/components/terminal.tsx +++ b/frontend/src/components/terminal/index.tsx @@ -1,118 +1,35 @@ -import { komodo_client, useLocalStorage } from "@lib/hooks"; import { cn } from "@lib/utils"; import { useTheme } from "@ui/theme"; import { FitAddon } from "@xterm/addon-fit"; import { ITheme } from "@xterm/xterm"; -import { ReactNode, useEffect, useMemo, useRef, useState } from "react"; +import { TerminalCallbacks } from "komodo_client"; +import { useEffect, useMemo, useRef } from "react"; import { useXTerm, UseXTermProps } from "react-xtermjs"; -import { Section } from "./layouts"; -import { CardTitle } from "@ui/card"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "@ui/select"; -import { Input } from "@ui/input"; -import { Button } from "@ui/button"; -import { RefreshCcw } from "lucide-react"; -const BASE_SHELLS = ["sh", "bash"]; +const LIGHT_THEME: ITheme = { + background: "#f7f8f9", + foreground: "#24292e", + cursor: "#24292e", + selectionBackground: "#c8d9fa", +}; -export const ContainerTerminal = ({ - server, - container, - titleOther, -}: { - server: string; - container: string; - titleOther?: ReactNode; -}) => { - const [_reconnect, _setReconnect] = useState(false); - const triggerReconnect = () => _setReconnect((r) => !r); - const [_clear, _setClear] = useState(false); - const [shell, setShell] = useLocalStorage( - `server-${server}-${container}-shell-v1`, - "sh" - ); - const [otherShell, setOtherShell] = useState(""); - - return ( -
- - docker exec -it {container} - setOtherShell(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - setShell(otherShell); - setOtherShell(""); - } else { - e.stopPropagation(); - } - }} - /> - - - - - - - } - > -
- -
-
- ); +const DARK_THEME: ITheme = { + background: "#151b25", + foreground: "#f6f8fa", + cursor: "#ffffff", + selectionBackground: "#6e778a", }; export const Terminal = ({ - query: { server, ...query }, + make_ws, selected, - _clear, _reconnect, + _clear, }: { - query: { server: string } & ( - | { terminal: string; container?: undefined; shell?: undefined } - | { container: string; shell: string; terminal?: undefined } - ); + make_ws: (callbacks: TerminalCallbacks) => WebSocket; selected: boolean; - _clear?: boolean; _reconnect: boolean; + _clear?: boolean; }) => { const { theme: __theme } = useTheme(); const _theme = @@ -206,7 +123,7 @@ export const Terminal = ({ let debounce = -1; - const options = { + const callbacks: TerminalCallbacks = { on_login: () => { // console.log("logged in terminal"); }, @@ -228,22 +145,7 @@ export const Terminal = ({ }, }; - const ws = query.container - ? komodo_client().connect_container_exec({ - query: { - server, - container: query.container, - shell: query.shell, - }, - ...options, - }) - : komodo_client().connect_terminal({ - query: { - server, - terminal: query.terminal!, - }, - ...options, - }); + const ws = make_ws(callbacks); wsRef.current = ws; @@ -251,7 +153,7 @@ export const Terminal = ({ ws.close(); wsRef.current = null; }; - }, [term, viewport, selected, query.shell, _reconnect]); + }, [term, viewport, make_ws, selected, _reconnect]); useEffect(() => term?.clear(), [_clear]); @@ -262,17 +164,3 @@ export const Terminal = ({ /> ); }; - -const LIGHT_THEME: ITheme = { - background: "#f7f8f9", - foreground: "#24292e", - cursor: "#24292e", - selectionBackground: "#c8d9fa", -}; - -const DARK_THEME: ITheme = { - background: "#151b25", - foreground: "#f6f8fa", - cursor: "#ffffff", - selectionBackground: "#6e778a", -}; diff --git a/frontend/src/components/resources/server/terminal.tsx b/frontend/src/components/terminal/server.tsx similarity index 89% rename from frontend/src/components/resources/server/terminal.tsx rename to frontend/src/components/terminal/server.tsx index 05177fb54..d40f32b96 100644 --- a/frontend/src/components/resources/server/terminal.tsx +++ b/frontend/src/components/terminal/server.tsx @@ -1,11 +1,10 @@ import { Section } from "@components/layouts"; -import { ReactNode, useState } from "react"; -import { useLocalStorage, useRead, useWrite } from "@lib/hooks"; +import { ReactNode, useCallback, useState } from "react"; +import { komodo_client, useLocalStorage, useRead, useWrite } from "@lib/hooks"; import { Card, CardContent, CardHeader } from "@ui/card"; import { Badge } from "@ui/badge"; import { Button } from "@ui/button"; import { Loader2, Plus, RefreshCcw, X } from "lucide-react"; -import { Terminal } from "@components/terminal"; import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover"; import { Command, @@ -15,7 +14,9 @@ import { CommandList, } from "@ui/command"; import { filterBySplit } from "@lib/utils"; -import { useServer } from "."; +import { useServer } from "@components/resources/server"; +import { Terminal } from "."; +import { TerminalCallbacks } from "komodo_client"; export const ServerTerminals = ({ id, @@ -118,9 +119,10 @@ export const ServerTerminals = ({ {terminals?.map(({ name: terminal }) => ( - @@ -131,6 +133,30 @@ export const ServerTerminals = ({ ); }; +const ServerTerminal = ({ + server, + terminal, + selected, + _reconnect, +}: { + server: string; + terminal: string; + selected: boolean; + _reconnect: boolean; +}) => { + const make_ws = useCallback( + (callbacks: TerminalCallbacks) => + komodo_client().connect_terminal({ + query: { server, terminal }, + ...callbacks, + }), + [server, terminal] + ); + return ( + + ); +}; + const BASE_SHELLS = ["bash", "sh"]; const NewTerminal = ({ diff --git a/frontend/src/components/users/permissions-selector.tsx b/frontend/src/components/users/permissions-selector.tsx new file mode 100644 index 000000000..a6ff19b26 --- /dev/null +++ b/frontend/src/components/users/permissions-selector.tsx @@ -0,0 +1,156 @@ +import { useState } from "react"; +import { UsableResource } from "@types"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@ui/select"; +import { Types } from "komodo_client"; +import { Button } from "@ui/button"; +import { filterBySplit } from "@lib/utils"; +import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover"; +import { fmt_upper_camelcase } from "@lib/formatting"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@ui/command"; +import { SearchX } from "lucide-react"; +import { Checkbox } from "@ui/checkbox"; + +export const PermissionLevelSelector = ({ + level, + onSelect, + disabled, +}: { + level: Types.PermissionLevel; + onSelect: (level: Types.PermissionLevel) => void; + disabled?: boolean; +}) => { + return ( + + ); +}; + +const ALL_PERMISSIONS_BY_TYPE: { + [type: string]: Types.SpecificPermission[] | undefined; +} = { + Server: [ + Types.SpecificPermission.Attach, + Types.SpecificPermission.Inspect, + Types.SpecificPermission.Logs, + Types.SpecificPermission.Processes, + Types.SpecificPermission.Terminal, + ], + Stack: [ + Types.SpecificPermission.Inspect, + Types.SpecificPermission.Logs, + Types.SpecificPermission.Terminal, + ], + Deployment: [ + Types.SpecificPermission.Inspect, + Types.SpecificPermission.Logs, + Types.SpecificPermission.Terminal, + ], + Builder: [Types.SpecificPermission.Attach], +}; + +export const SpecificPermissionSelector = ({ + open, + onOpenChange, + type, + specific, + onSelect, + disabled, +}: { + open?: boolean; + onOpenChange?: (open: boolean) => void; + type: UsableResource; + specific: Types.SpecificPermission[]; + onSelect: (permission: Types.SpecificPermission) => void; + disabled?: boolean; +}) => { + const [search, setSearch] = useState(""); + const all_permissions = ALL_PERMISSIONS_BY_TYPE[type]; + // These resources don't have any specific permissions to add + if (!all_permissions) { + return ( + + ); + } + const filtered = filterBySplit(all_permissions, search, (item) => item); + return ( + + + + + + + + + + No Permissions Found + + + + {filtered.map((permission) => ( + onSelect(permission)} + className="flex items-center justify-between cursor-pointer" + > +
{fmt_upper_camelcase(permission)}
+ +
+ ))} +
+
+
+
+
+ ); +}; diff --git a/frontend/src/components/users/permissions-table.tsx b/frontend/src/components/users/permissions-table.tsx index 26b127904..5b2f9229b 100644 --- a/frontend/src/components/users/permissions-table.tsx +++ b/frontend/src/components/users/permissions-table.tsx @@ -1,4 +1,4 @@ -import { useInvalidate, useWrite } from "@lib/hooks"; +import { useInvalidate, useRead, useWrite } from "@lib/hooks"; import { Types } from "komodo_client"; import { UsableResource } from "@types"; import { useToast } from "@ui/use-toast"; @@ -10,7 +10,12 @@ import { ResourceComponents } from "@components/resources"; import { Label } from "@ui/label"; import { Switch } from "@ui/switch"; import { DataTable, SortableHeader } from "@ui/data-table"; -import { level_to_number, resource_name } from "@lib/utils"; +import { + filterBySplit, + level_to_number, + resource_name, + RESOURCE_TARGETS, +} from "@lib/utils"; import { ResourceLink } from "@components/resources/common"; import { Select, @@ -19,15 +24,31 @@ import { SelectTrigger, SelectValue, } from "@ui/select"; -import { PermissionLevelSelector } from "@components/config/util"; +import { + PermissionLevelSelector, + SpecificPermissionSelector, +} from "./permissions-selector"; -export const PermissionsTable = ({ +export const PermissionsTableTabs = ({ + user_target, +}: { + user_target: Types.UserTarget; +}) => { + return ( + <> + + + + ); +}; + +const SpecificPermissionsTable = ({ user_target, }: { user_target: Types.UserTarget; }) => { const { toast } = useToast(); - const [showNone, setShowNone] = useState(false); + const [showAll, setShowAll] = useState(false); const [resourceType, setResourceType] = useState( "All" ); @@ -41,9 +62,22 @@ export const PermissionsTable = ({ inv(["ListUserTargetPermissions"]); }, }); + const tableData = + permissions?.filter( + (permission) => + (resourceType === "All" + ? true + : permission.resource_target.type === resourceType) && + (showAll ? true : permission.level !== Types.PermissionLevel.None) && + searchSplit.every( + (search) => + permission.name.toLowerCase().includes(search) || + permission.resource_target.type.toLowerCase().includes(search) + ) + ) ?? []; return (
setShowNone(!showNone)} + onClick={() => setShowAll((showAll) => !showAll)} > - - + +
} > - (resourceType === "All" - ? true - : permission.resource_target.type === resourceType) && - (showNone - ? true - : permission.level !== Types.PermissionLevel.None) && - searchSplit.every( - (search) => - permission.name.toLowerCase().includes(search) || - permission.resource_target.type.toLowerCase().includes(search) - ) - ) ?? [] - } + tableKey="specific-permissions-v1" + data={tableData} columns={[ { accessorKey: "resource_target.type", + size: 150, header: ({ column }) => ( ), @@ -118,6 +138,7 @@ export const PermissionsTable = ({ }, { accessorKey: "resource_target", + size: 250, sortingFn: (a, b) => { const ra = resource_name( a.original.resource_target.type as UsableResource, @@ -154,6 +175,7 @@ export const PermissionsTable = ({ }, { accessorKey: "level", + size: 150, sortingFn: (a, b) => { const al = level_to_number(a.original.level); const bl = level_to_number(b.original.level); @@ -170,14 +192,227 @@ export const PermissionsTable = ({ mutate({ ...permission, user_target, - permission: value, + permission: { + level: value, + specific: permission.specific ?? [], + }, }) } /> ), }, + { + header: "Specific", + size: 300, + cell: ({ row: { original: permission } }) => { + return ( + { + const _specific = permission.specific ?? []; + const specific = ( + _specific.includes(specific_permission) + ? _specific.filter((p) => p !== specific_permission) + : [..._specific, specific_permission] + ).sort(); + mutate({ + ...permission, + user_target, + permission: { + level: permission.level ?? Types.PermissionLevel.None, + specific, + }, + }); + }} + /> + ); + }, + }, ]} />
); }; + +type UpdateFn = ( + resource_type: Types.ResourceTarget["type"], + permission: Types.PermissionLevelAndSpecifics +) => void; + +const BasePermissionsTableInner = ({ + all, + update, +}: { + all: Types.User["all"]; + update: UpdateFn; +}) => { + const [showAll, setShowAll] = useState(false); + const [search, setSearch] = useState(""); + const permissions = RESOURCE_TARGETS.map((type) => { + const permission = all?.[type] ?? Types.PermissionLevel.None; + return { + type, + level: typeof permission === "string" ? permission : permission.level, + specific: typeof permission === "string" ? [] : permission.specific, + }; + }).filter( + (item) => + showAll || + item.level !== Types.PermissionLevel.None || + item.specific.length !== 0 + ); + const filtered = filterBySplit(permissions, search, (p) => p.type); + return ( +
+ setSearch(e.target.value)} + className="w-[300px]" + /> +
setShowAll((s) => !s)} + > + + +
+ + } + > + ( + + ), + cell: ({ row }) => { + const Components = + ResourceComponents[row.original.type as UsableResource]; + return ( +
+ + {row.original.type} +
+ ); + }, + }, + { + accessorKey: "level", + size: 150, + sortingFn: (a, b) => { + const al = level_to_number(a.original.level); + const bl = level_to_number(b.original.level); + const dif = al - bl; + return dif === 0 ? 0 : dif / Math.abs(dif); + }, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + { + update(row.original.type, { + level, + specific: row.original.specific, + }); + }} + /> + ), + }, + { + header: "Specific", + size: 300, + cell: ({ row }) => { + return ( + { + const _specific = row.original.specific ?? []; + const specific = ( + _specific.includes(specific_permission) + ? _specific.filter((p) => p !== specific_permission) + : [..._specific, specific_permission] + ).sort(); + update(row.original.type, { + level: row.original.level, + specific, + }); + }} + /> + ); + }, + }, + ]} + /> +
+ ); +}; + +const BasePermissionsTable = ({ + user_target, +}: { + user_target: Types.UserTarget; +}) => { + const { toast } = useToast(); + const inv = useInvalidate(); + + const { mutate } = useWrite("UpdatePermissionOnResourceType", { + onSuccess: () => { + toast({ title: "Updated permissions on target" }); + if (user_target.type === "User") { + inv(["FindUser", { user: user_target.id }]); + } else if (user_target.type === "UserGroup") { + inv(["GetUserGroup", { user_group: user_target.id }]); + } + }, + }); + + const update: UpdateFn = (resource_type, permission) => + mutate({ user_target, resource_type, permission }); + + if (user_target.type === "User") { + return ( + + ); + } else if (user_target.type === "UserGroup") { + return ( + + ); + } +}; + +const UserBasePermissionsTable = ({ + user_id, + update, +}: { + user_id: string; + update: UpdateFn; +}) => { + const user = useRead("FindUser", { user: user_id }).data; + return ; +}; + +const UserGroupBasePermissionsTable = ({ + group_id, + update, +}: { + group_id: string; + update: UpdateFn; +}) => { + const group = useRead("GetUserGroup", { user_group: group_id }).data; + return ; +}; diff --git a/frontend/src/components/users/resource-type-permissions.tsx b/frontend/src/components/users/resource-type-permissions.tsx deleted file mode 100644 index 9424b653c..000000000 --- a/frontend/src/components/users/resource-type-permissions.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { PermissionLevelSelector } from "@components/config/util"; -import { useInvalidate, useRead, useWrite } from "@lib/hooks"; -import { RESOURCE_TARGETS } from "@lib/utils"; -import { Types } from "komodo_client"; -import { useToast } from "@ui/use-toast"; -import { Card, CardContent, CardHeader } from "@ui/card"; - -export const UserTargetPermissionsOnResourceTypes = ({ - user_target, -}: { - user_target: Types.UserTarget; -}) => { - const { toast } = useToast(); - const inv = useInvalidate(); - - const { mutate } = useWrite("UpdatePermissionOnResourceType", { - onSuccess: () => { - toast({ title: "Updated permissions on target" }); - if (user_target.type === "User") { - inv(["FindUser", { user: user_target.id }]); - } else if (user_target.type === "UserGroup") { - inv(["GetUserGroup", { user_group: user_target.id }]); - } - }, - }); - - const update = (resource_type, permission) => - mutate({ user_target, resource_type, permission }); - - if (user_target.type === "User") { - return ( - - ); - } else if (user_target.type === "UserGroup") { - return ( - - ); - } -}; - -const UserPermissionsOnResourceType = ({ - user_id, - update, -}: { - user_id: string; - update: ( - resource_type: Types.ResourceTarget["type"], - permission: Types.PermissionLevel - ) => void; -}) => { - const user = useRead("FindUser", { user: user_id }).data; - return ; -}; - -const UserGroupPermissionsOnResourceType = ({ - group_id, - update, -}: { - group_id: string; - update: ( - resource_type: Types.ResourceTarget["type"], - permission: Types.PermissionLevel - ) => void; -}) => { - const group = useRead("GetUserGroup", { user_group: group_id }).data; - return ; -}; - -const PermissionsOnResourceType = ({ - all, - update, -}: { - all: Types.User["all"]; - update: ( - resource_type: Types.ResourceTarget["type"], - permission: Types.PermissionLevel - ) => void; -}) => { - return ( - - Base Permissions - - {RESOURCE_TARGETS.map((type) => { - const level = all?.[type] ?? Types.PermissionLevel.None; - return ( -
- {type}: - update(type, level)} - /> -
- ); - })} -
-
- ); -}; diff --git a/frontend/src/components/util.tsx b/frontend/src/components/util.tsx index 69a355a89..993f5c704 100644 --- a/frontend/src/components/util.tsx +++ b/frontend/src/components/util.tsx @@ -60,7 +60,7 @@ import { Prune } from "./resources/server/actions"; import { MonacoEditor, MonacoLanguage } from "./monaco"; import { UsableResource } from "@types"; import { ResourceComponents } from "./resources"; -import { useEditPermissions } from "@pages/resource"; +import { usePermissions } from "@lib/hooks"; export const WithLoading = ({ children, @@ -879,8 +879,8 @@ const ResourceName = ({ }) => { const invalidate = useInvalidate(); const { toast } = useToast(); - const { canWrite } = useEditPermissions({ type, id }); - const [newName, setName] = useState(name); + const { canWrite } = usePermissions({ type, id }); + const [newName, setName] = useState(""); const [editing, setEditing] = useState(false); const { mutate, isPending } = useWrite(`Rename${type}`, { onSuccess: () => { @@ -889,10 +889,12 @@ const ResourceName = ({ setEditing(false); }, onError: () => { - // If fails, set name back to no name + // If fails, set name back to original setName(name); }, }); + // Ensure the newName is updated if the outer name changes + useEffect(() => setName(name), [name]); if (editing) { return ( diff --git a/frontend/src/lib/hooks.ts b/frontend/src/lib/hooks.ts index 3b492fefc..b5c2f44c8 100644 --- a/frontend/src/lib/hooks.ts +++ b/frontend/src/lib/hooks.ts @@ -20,7 +20,7 @@ import { atom, useAtom } from "jotai"; import { atomFamily } from "jotai/utils"; import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; -import { RESOURCE_TARGETS } from "./utils"; +import { has_minimum_permissions, RESOURCE_TARGETS } from "./utils"; // ============== RESOLVER ============== @@ -537,30 +537,61 @@ export const useFilterByUpdateAvailable: () => [boolean, () => void] = () => { return [filter, () => set(!filter)]; }; -// export function useReadableLines(stream: ReadableStream): string[] { -// const [out, setOut] = useState([]); -// const cancelRef = useRef(null); +export const usePermissions = ({ type, id }: Types.ResourceTarget) => { + const user = useUser().data; + const perms = useRead("GetPermission", { target: { type, id } }).data as + | Types.PermissionLevelAndSpecifics + | Types.PermissionLevel + | undefined; + const info = useRead("GetCoreInfo", {}).data; + const ui_write_disabled = info?.ui_write_disabled ?? false; + const disable_non_admin_create = info?.disable_non_admin_create ?? false; -// useEffect(() => { -// if (!stream) return; + const level = + (perms && typeof perms === "string" ? perms : perms?.level) ?? + Types.PermissionLevel.None; + const specific = + (perms && typeof perms === "string" ? [] : perms?.specific) ?? []; -// const aborter = new AbortController(); -// cancelRef.current = aborter; -// setOut([]); // reset on new stream + const canWrite = !ui_write_disabled && level === Types.PermissionLevel.Write; + const canExecute = has_minimum_permissions( + { level, specific }, + Types.PermissionLevel.Execute + ); -// (async () => { -// try { -// for await (const line of lines(stream)) { -// if (aborter.signal.aborted) break; -// setOut((prev) => [...prev, line]); // append as we go -// } -// } catch (err) { -// if (err.name !== "AbortError") console.error(err); -// } -// })(); + if (type === "Server") { + return { + canWrite, + canExecute, + canCreate: + user?.admin || + (!disable_non_admin_create && user?.create_server_permissions), + specific, + }; + } + if (type === "Build") { + return { + canWrite, + canExecute, + canCreate: + user?.admin || + (!disable_non_admin_create && user?.create_build_permissions), + specific, + }; + } + if (type === "Alerter" || type === "Builder") { + return { + canWrite, + canExecute, + canCreate: user?.admin, + specific, + }; + } -// return () => aborter.abort(); // stop when unmounted -// }, [stream]); - -// return out; -// } + return { + canWrite, + canExecute, + canCreate: user?.admin || !disable_non_admin_create, + specific, + }; +}; diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 361cb28b7..169875ea9 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -120,11 +120,20 @@ export const level_to_number = (level: Types.PermissionLevel | undefined) => { }; export const has_minimum_permissions = ( - level: Types.PermissionLevel | undefined, - greater_than: Types.PermissionLevel + permission: Types.PermissionLevelAndSpecifics | undefined, + greater_than: Types.PermissionLevel, + specific?: Types.SpecificPermission[] ) => { - if (!level) return false; - return level_to_number(level) >= level_to_number(greater_than); + if (!permission) return false; + if (level_to_number(permission.level) < level_to_number(greater_than)) + return false; + if (!specific) return true; + for (const s of specific) { + if (!permission.specific.includes(s)) { + return false; + } + } + return true; }; const tzOffsetMs = new Date().getTimezoneOffset() * 60 * 1000; diff --git a/frontend/src/pages/resource.tsx b/frontend/src/pages/resource.tsx index 1ceafe9ce..30c33a0fa 100644 --- a/frontend/src/pages/resource.tsx +++ b/frontend/src/pages/resource.tsx @@ -7,72 +7,21 @@ import { } from "@components/resources/common"; import { AddTags, ResourceTags } from "@components/tags"; import { + usePermissions, usePushRecentlyViewed, useRead, useResourceParamType, useSetTitle, - useUser, } from "@lib/hooks"; -import { has_minimum_permissions, usableResourcePath } from "@lib/utils"; +import { usableResourcePath } from "@lib/utils"; import { Types } from "komodo_client"; import { UsableResource } from "@types"; import { Button } from "@ui/button"; -import { - AlertTriangle, - ChevronLeft, - LinkIcon, - Zap, -} from "lucide-react"; +import { AlertTriangle, ChevronLeft, LinkIcon, Zap } from "lucide-react"; import { Link, useParams } from "react-router-dom"; import { ResourceNotifications } from "./resource-notifications"; import { NotFound } from "@components/util"; -export const useEditPermissions = ({ type, id }: Types.ResourceTarget) => { - const user = useUser().data; - const perms = useRead("GetPermissionLevel", { target: { type, id } }).data; - const info = useRead("GetCoreInfo", {}).data; - const ui_write_disabled = info?.ui_write_disabled ?? false; - const disable_non_admin_create = info?.disable_non_admin_create ?? false; - - const canWrite = !ui_write_disabled && perms === Types.PermissionLevel.Write; - const canExecute = has_minimum_permissions( - perms, - Types.PermissionLevel.Execute - ); - - if (type === "Server") { - return { - canWrite, - canExecute, - canCreate: - user?.admin || - (!disable_non_admin_create && user?.create_server_permissions), - }; - } - if (type === "Build") { - return { - canWrite, - canExecute, - canCreate: - user?.admin || - (!disable_non_admin_create && user?.create_build_permissions), - }; - } - if (type === "Alerter" || type === "Builder") { - return { - canWrite, - canExecute, - canCreate: user?.admin, - }; - } - - return { - canWrite, - canExecute, - canCreate: user?.admin || !disable_non_admin_create, - }; -}; - export const Resource = () => { const type = useResourceParamType()!; const id = useParams().id as string; @@ -89,7 +38,7 @@ const ResourceInner = ({ type, id }: { type: UsableResource; id: string }) => { usePushRecentlyViewed({ type, id }); - const { canCreate, canExecute, canWrite } = useEditPermissions({ type, id }); + const { canCreate, canExecute, canWrite } = usePermissions({ type, id }); if (!type || !id) return null; @@ -174,7 +123,7 @@ export const ResourceHeader = ({ const infoEntries = Object.entries(Components.Info); const statusEntries = Object.entries(Components.Status); - const { canWrite } = useEditPermissions({ type, id }); + const { canWrite } = usePermissions({ type, id }); return (
diff --git a/frontend/src/pages/server-info/container/index.tsx b/frontend/src/pages/server-info/container/index.tsx index 83196253d..be6e8a4d4 100644 --- a/frontend/src/pages/server-info/container/index.tsx +++ b/frontend/src/pages/server-info/container/index.tsx @@ -7,7 +7,6 @@ import { DockerLabelsSection, DockerResourceLink, ResourcePageHeader, - ShowHideButton, } from "@components/util"; import { useLocalStorage, useRead, useSetTitle, useWrite } from "@lib/hooks"; import { Button } from "@ui/button"; @@ -18,7 +17,6 @@ import { Info, Loader2, PlusCircle, - SearchCode, } from "lucide-react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { ContainerLogs } from "./log"; @@ -27,12 +25,11 @@ import { Types } from "komodo_client"; import { container_state_intention } from "@lib/color"; import { UsableResource } from "@types"; import { Fragment } from "react/jsx-runtime"; -import { useEditPermissions } from "@pages/resource"; +import { usePermissions } from "@lib/hooks"; import { ResourceNotifications } from "@pages/resource-notifications"; -import { MonacoEditor } from "@components/monaco"; -import { useState } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs"; -import { ContainerTerminal } from "@components/terminal"; +import { ContainerTerminal } from "@components/terminal/container"; +import { ContainerInspect } from "./inspect"; export const ContainerPage = () => { const { type, id, container } = useParams() as { @@ -55,18 +52,9 @@ const ContainerPageInner = ({ id: string; container: string; }) => { - const [showInspect, setShowInspect] = useState(false); const server = useServer(id); useSetTitle(`${server?.name} | container | ${container_name}`); - const { canExecute } = useEditPermissions({ type: "Server", id }); - const { - data: container, - isPending, - isError, - } = useRead("InspectDockerContainer", { - server: id, - container: container_name, - }); + const { canExecute } = usePermissions({ type: "Server", id }); const list_container = useRead( "ListDockerContainers", { @@ -75,24 +63,6 @@ const ContainerPageInner = ({ { refetchInterval: 10_000 } ).data?.find((container) => container.name === container_name); - if (isPending) { - return ( -
- -
- ); - } - if (isError) { - return
Failed to inspect container.
; - } - if (!container) { - return ( -
- No container found with given name: {container_name} -
- ); - } - const state = list_container?.state ?? Types.ContainerStateStatusEnum.Empty; const intention = container_state_intention(state); @@ -186,59 +156,43 @@ const ContainerPageInner = ({ )} - + {/* TOP LEVEL CONTAINER INFO */} -
}> - -
- - - -
} - titleRight={ -
- -
- } - > - {showInspect && ( - }> + - )} -
+ + )} + +
); }; -const LogOrTerminal = ({ +const ContainerTabs = ({ server, container, state, @@ -247,27 +201,42 @@ const LogOrTerminal = ({ container: string; state: Types.ContainerStateStatusEnum; }) => { - const [_view, setView] = useLocalStorage<"Log" | "Terminal">( + const [_view, setView] = useLocalStorage<"Log" | "Inspect" | "Terminal">( `server-${server}-${container}-tabs-v1`, "Log" ); - const { canWrite } = useEditPermissions({ + const { specific } = usePermissions({ type: "Server", id: server, }); const container_exec_disabled = useServer(server)?.info.container_exec_disabled ?? true; + const logDisabled = + !specific.includes(Types.SpecificPermission.Logs) || + state === Types.ContainerStateStatusEnum.Empty; + const inspectDisabled = + !specific.includes(Types.SpecificPermission.Inspect) || + state === Types.ContainerStateStatusEnum.Empty; const terminalDisabled = - !canWrite || + !specific.includes(Types.SpecificPermission.Terminal) || container_exec_disabled || state !== Types.ContainerStateStatusEnum.Running; - const view = terminalDisabled && _view === "Terminal" ? "Log" : _view; + const view = + (inspectDisabled && _view === "Inspect") || + (terminalDisabled && _view === "Terminal") + ? "Log" + : _view; const tabs = ( - + Log - {!terminalDisabled && ( + {specific.includes(Types.SpecificPermission.Inspect) && ( + + Inspect + + )} + {specific.includes(Types.SpecificPermission.Terminal) && ( Terminal @@ -281,12 +250,23 @@ const LogOrTerminal = ({ id={server} container_name={container} titleOther={tabs} + disabled={logDisabled} /> + + + diff --git a/frontend/src/pages/server-info/container/inspect.tsx b/frontend/src/pages/server-info/container/inspect.tsx new file mode 100644 index 000000000..fc9fc9400 --- /dev/null +++ b/frontend/src/pages/server-info/container/inspect.tsx @@ -0,0 +1,57 @@ +import { usePermissions, useRead } from "@lib/hooks"; +import { ReactNode } from "react"; +import { Types } from "komodo_client"; +import { Section } from "@components/layouts"; +import { InspectContainerView } from "@components/inspect"; + +export const ContainerInspect = ({ + id, + container, + titleOther, +}: { + id: string; + container: string; + titleOther: ReactNode; +}) => { + const { specific } = usePermissions({ type: "Server", id }); + if (!specific.includes(Types.SpecificPermission.Inspect)) { + return ( +
+
+

User does not have permission to inspect this Server.

+
+
+ ); + } + return ( +
+ +
+ ); +}; + +const ContainerInspectInner = ({ + id, + container, +}: { + id: string; + container: string; +}) => { + const { + data: inspect_container, + error, + isPending, + isError, + } = useRead("InspectDockerContainer", { + server: id, + container, + }); + return ( + + ); +}; diff --git a/frontend/src/pages/server-info/container/log.tsx b/frontend/src/pages/server-info/container/log.tsx index 62d21449c..004366205 100644 --- a/frontend/src/pages/server-info/container/log.tsx +++ b/frontend/src/pages/server-info/container/log.tsx @@ -1,3 +1,4 @@ +import { Section } from "@components/layouts"; import { Log, LogSection } from "@components/log"; import { useRead } from "@lib/hooks"; import { Types } from "komodo_client"; @@ -7,11 +8,20 @@ export const ContainerLogs = ({ id, container_name, titleOther, + disabled, }: { id: string; container_name: string; titleOther?: ReactNode; + disabled: boolean; }) => { + if (disabled) { + return ( +
+

Logs are disabled.

+
+ ); + } return ( -
} - titleRight={ -
- -
- } - > - {showInspect && ( - - )} -
+ {specific.includes(Types.SpecificPermission.Inspect) && ( +
} + titleRight={ +
+ +
+ } + > + {showInspect && ( + + )} +
+ )} ); }; diff --git a/frontend/src/pages/server-info/network.tsx b/frontend/src/pages/server-info/network.tsx index 6af4b1be8..f4595835a 100644 --- a/frontend/src/pages/server-info/network.tsx +++ b/frontend/src/pages/server-info/network.tsx @@ -10,8 +10,7 @@ import { DockerResourcePageName, ShowHideButton, } from "@components/util"; -import { useExecute, useRead, useSetTitle } from "@lib/hooks"; -import { has_minimum_permissions } from "@lib/utils"; +import { useExecute, usePermissions, useRead, useSetTitle } from "@lib/hooks"; import { Types } from "komodo_client"; import { Badge } from "@ui/badge"; import { Button } from "@ui/button"; @@ -53,9 +52,7 @@ const NetworkPageInner = ({ useSetTitle(`${server?.name} | network | ${network_name}`); const nav = useNavigate(); - const perms = useRead("GetPermissionLevel", { - target: { type: "Server", id }, - }).data; + const { canExecute, specific } = usePermissions({ type: "Server", id }); const { data: network, @@ -93,11 +90,6 @@ const NetworkPageInner = ({ ); } - const canExecute = has_minimum_permissions( - perms, - Types.PermissionLevel.Execute - ); - const containers = Object.values(network.Containers ?? {}); const ipam_driver = network.IPAM?.Driver; @@ -283,23 +275,25 @@ const NetworkPageInner = ({ -
} - titleRight={ -
- -
- } - > - {showInspect && ( - - )} -
+ {specific.includes(Types.SpecificPermission.Inspect) && ( +
} + titleRight={ +
+ +
+ } + > + {showInspect && ( + + )} +
+ )} ); }; diff --git a/frontend/src/pages/server-info/volume.tsx b/frontend/src/pages/server-info/volume.tsx index b8aca165b..792a29172 100644 --- a/frontend/src/pages/server-info/volume.tsx +++ b/frontend/src/pages/server-info/volume.tsx @@ -10,8 +10,7 @@ import { DockerResourcePageName, ShowHideButton, } from "@components/util"; -import { useExecute, useRead, useSetTitle } from "@lib/hooks"; -import { has_minimum_permissions } from "@lib/utils"; +import { useExecute, usePermissions, useRead, useSetTitle } from "@lib/hooks"; import { Types } from "komodo_client"; import { Badge } from "@ui/badge"; import { Button } from "@ui/button"; @@ -45,9 +44,7 @@ const VolumePageInner = ({ useSetTitle(`${server?.name} | volume | ${volume_name}`); const nav = useNavigate(); - const perms = useRead("GetPermissionLevel", { - target: { type: "Server", id }, - }).data; + const { canExecute, specific } = usePermissions({ type: "Server", id }); const { data: volume, @@ -93,11 +90,6 @@ const VolumePageInner = ({ ); } - const canExecute = has_minimum_permissions( - perms, - Types.PermissionLevel.Execute - ); - const unused = containers && containers.length === 0 ? true : false; return ( @@ -180,23 +172,25 @@ const VolumePageInner = ({ -
} - titleRight={ -
- -
- } - > - {showInspect && ( - - )} -
+ {specific.includes(Types.SpecificPermission.Inspect) && ( +
} + titleRight={ +
+ +
+ } + > + {showInspect && ( + + )} +
+ )} ); }; diff --git a/frontend/src/pages/settings/users.tsx b/frontend/src/pages/settings/users.tsx index e68eba60f..ada663bfd 100644 --- a/frontend/src/pages/settings/users.tsx +++ b/frontend/src/pages/settings/users.tsx @@ -68,7 +68,8 @@ const UserGroupsSection = () => { { header: "Name", accessorKey: "name" }, { header: "Members", - accessorFn: (group) => (group.users ?? []).length, + accessorFn: (group) => + group.everyone ? "Everyone" : (group.users ?? []).length, }, { header: "Delete", diff --git a/frontend/src/pages/stack-service/index.tsx b/frontend/src/pages/stack-service/index.tsx index eb6df250a..ef25ae82c 100644 --- a/frontend/src/pages/stack-service/index.tsx +++ b/frontend/src/pages/stack-service/index.tsx @@ -16,7 +16,12 @@ import { container_state_intention, stroke_color_class_by_intention, } from "@lib/color"; -import { useLocalStorage, useRead, useSetTitle } from "@lib/hooks"; +import { + usePermissions, + useLocalStorage, + useRead, + useSetTitle, +} from "@lib/hooks"; import { cn } from "@lib/utils"; import { Types } from "komodo_client"; import { ChevronLeft, Clapperboard, Layers2 } from "lucide-react"; @@ -25,12 +30,12 @@ import { StackServiceLogs } from "./log"; import { Button } from "@ui/button"; import { ExportButton } from "@components/export"; import { DockerResourceLink, ResourcePageHeader } from "@components/util"; -import { useEditPermissions } from "@pages/resource"; import { ResourceNotifications } from "@pages/resource-notifications"; import { Fragment } from "react/jsx-runtime"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs"; -import { ContainerTerminal } from "@components/terminal"; +import { ContainerTerminal } from "@components/terminal/container"; import { useServer } from "@components/resources/server"; +import { StackServiceInspect } from "./inspect"; type IdServiceComponent = React.FC<{ id: string; service?: string }>; @@ -64,7 +69,7 @@ const StackServicePageInner = ({ }) => { const stack = useStack(stack_id); useSetTitle(`${stack?.name} | ${service}`); - const { canExecute, canWrite } = useEditPermissions({ + const { canExecute, canWrite } = usePermissions({ type: "Stack", id: stack_id, }); @@ -186,13 +191,12 @@ const StackServicePageInner = ({ )} - {/* Logs */} + {/* Tabs */}
{stack && ( - )} @@ -202,39 +206,61 @@ const StackServicePageInner = ({ ); }; -const LogOrTerminal = ({ +const StackServiceTabs = ({ stack, service, - container_name, container_state, }: { stack: Types.StackListItem; service: string; - container_name: string | undefined; container_state: Types.ContainerStateStatusEnum; }) => { - const [_view, setView] = useLocalStorage<"Log" | "Terminal">( + const [_view, setView] = useLocalStorage<"Log" | "Inspect" | "Terminal">( `stack-${stack.id}-${service}-tabs-v1`, "Log" ); - const { canWrite: canWriteServer } = useEditPermissions({ - type: "Server", - id: stack.info.server_id, + const { specific } = usePermissions({ + type: "Stack", + id: stack.id, }); + console.log(specific); const container_exec_disabled = useServer(stack.info.server_id)?.info.container_exec_disabled ?? true; + const logDisabled = + !specific.includes(Types.SpecificPermission.Logs) || + container_state === Types.ContainerStateStatusEnum.Empty; + const inspectDisabled = + !specific.includes(Types.SpecificPermission.Inspect) || + container_state === Types.ContainerStateStatusEnum.Empty; const terminalDisabled = - !canWriteServer || + !specific.includes(Types.SpecificPermission.Terminal) || container_exec_disabled || container_state !== Types.ContainerStateStatusEnum.Running; - const view = terminalDisabled && _view === "Terminal" ? "Log" : _view; + const view = + (inspectDisabled && _view === "Inspect") || + (terminalDisabled && _view === "Terminal") + ? "Log" + : _view; const tabs = ( - + Log - {!terminalDisabled && ( - + {specific.includes(Types.SpecificPermission.Inspect) && ( + + Inspect + + )} + {specific.includes(Types.SpecificPermission.Terminal) && ( + Terminal )} @@ -243,16 +269,33 @@ const LogOrTerminal = ({ return ( - + + + + - {stack.info.server_id && container_name && ( - - )} + ); diff --git a/frontend/src/pages/stack-service/inspect.tsx b/frontend/src/pages/stack-service/inspect.tsx new file mode 100644 index 000000000..300725a41 --- /dev/null +++ b/frontend/src/pages/stack-service/inspect.tsx @@ -0,0 +1,57 @@ +import { usePermissions, useRead } from "@lib/hooks"; +import { ReactNode } from "react"; +import { Types } from "komodo_client"; +import { Section } from "@components/layouts"; +import { InspectContainerView } from "@components/inspect"; + +export const StackServiceInspect = ({ + id, + service, + titleOther, +}: { + id: string; + service: string; + titleOther: ReactNode; +}) => { + const { specific } = usePermissions({ type: "Stack", id }); + if (!specific.includes(Types.SpecificPermission.Inspect)) { + return ( +
+
+

User does not have permission to inspect this Stack service.

+
+
+ ); + } + return ( +
+ +
+ ); +}; + +const StackServiceInspectInner = ({ + id, + service, +}: { + id: string; + service: string; +}) => { + const { + data: container, + error, + isPending, + isError, + } = useRead("InspectStackContainer", { + stack: id, + service, + }); + return ( + + ); +}; diff --git a/frontend/src/pages/stack-service/log.tsx b/frontend/src/pages/stack-service/log.tsx index f1c1a5487..162c48267 100644 --- a/frontend/src/pages/stack-service/log.tsx +++ b/frontend/src/pages/stack-service/log.tsx @@ -2,24 +2,35 @@ import { useRead } from "@lib/hooks"; import { Types } from "komodo_client"; import { Log, LogSection } from "@components/log"; import { ReactNode } from "react"; +import { Section } from "@components/layouts"; export const StackServiceLogs = ({ id, service, titleOther, + disabled, }: { /// Stack id id: string; service: string; titleOther?: ReactNode; + disabled: boolean; }) => { // const stack = useStack(id); const services = useRead("ListStackServices", { stack: id }).data; const container = services?.find((s) => s.service === service)?.container; const state = container?.state ?? Types.ContainerStateStatusEnum.Empty; - if (state === undefined || state === Types.ContainerStateStatusEnum.Empty) { - return null; + if ( + disabled || + state === undefined || + state === Types.ContainerStateStatusEnum.Empty + ) { + return ( +
+

Logs are disabled.

+
+ ); } return ; diff --git a/frontend/src/pages/user-group.tsx b/frontend/src/pages/user-group.tsx index 5d7b315c3..c9b9cc113 100644 --- a/frontend/src/pages/user-group.tsx +++ b/frontend/src/pages/user-group.tsx @@ -1,7 +1,6 @@ -import { UserTargetPermissionsOnResourceTypes } from "@components/users/resource-type-permissions"; import { ExportButton } from "@components/export"; import { Page, Section } from "@components/layouts"; -import { PermissionsTable } from "@components/users/permissions-table"; +import { PermissionsTableTabs } from "@components/users/permissions-table"; import { UserTable } from "@components/users/table"; import { ActionWithDialog } from "@components/util"; import { useInvalidate, useRead, useWrite } from "@lib/hooks"; @@ -22,6 +21,7 @@ import { useToast } from "@ui/use-toast"; import { PlusCircle, Save, SearchX, Trash, User, Users } from "lucide-react"; import { useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; +import { Switch } from "@ui/switch"; export const UserGroupPage = () => { const { toast } = useToast(); @@ -52,6 +52,12 @@ export const UserGroupPage = () => { toast({ title: "Removed User from User Group" }); }, }).mutate; + const everyoneMutate = useWrite("SetEveryoneUserGroup", { + onSuccess: () => { + inv(["ListUserGroups"]); + toast({ title: "Toggled User Group 'everyone'" }); + }, + }).mutate; if (!group) return null; return ( { icon={} actions={} subtitle={ -
+
User Group
| - {(group.users ?? []).length > 0 && ( -
- {(group.users ?? []).length} User - {(group.users ?? []).length > 1 ? "s" : ""} -
- )} - {(group.users ?? []).length === 0 &&
No Users
} +
+ {group.everyone && "Everyone"} + {!group.everyone && (group.users ?? []).length > 0 && ( + <> + {(group.users ?? []).length} User + {(group.users ?? []).length > 1 ? "s" : ""} + + )} + {!group.everyone && (group.users ?? []).length === 0 && "No Users"} +
} >
} - actions={} + titleRight={ +
+ {!group.everyone && } +
+ Everyone + + everyoneMutate({ user_group: group_id, everyone }) + } + /> +
+ {group.everyone && ( +
+ All users will inherit the permissions in this group. +
+ )} +
+ } > - - group ? (group.users ?? []).includes(user._id?.$oid!) : false - ) ?? [] - } - onUserRemove={(user_id) => - removeMutate({ user_group: group_id, user: user_id }) - } - /> + {!group.everyone && ( + + group ? (group.users ?? []).includes(user._id?.$oid!) : false + ) ?? [] + } + onUserRemove={(user_id) => + removeMutate({ user_group: group_id, user: user_id }) + } + /> + )}
- - + + +
@@ -159,7 +187,7 @@ const AddUserToGroup = ({ group_id }: { group_id: string }) => { { {user.enabled && !user.admin && ( <> - - )} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 34c03f501..37944487e 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -20,79 +20,79 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" - integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== +"@babel/code-frame@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== dependencies: - "@babel/helper-validator-identifier" "^7.25.9" + "@babel/helper-validator-identifier" "^7.27.1" js-tokens "^4.0.0" - picocolors "^1.0.0" + picocolors "^1.1.1" -"@babel/compat-data@^7.26.5": - version "7.26.5" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.5.tgz#df93ac37f4417854130e21d72c66ff3d4b897fc7" - integrity sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg== +"@babel/compat-data@^7.27.2": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.27.3.tgz#cc49c2ac222d69b889bf34c795f537c0c6311111" + integrity sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw== -"@babel/core@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40" - integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== +"@babel/core@^7.26.10": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.27.3.tgz#d7d05502bccede3cab36373ed142e6a1df554c2f" + integrity sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA== dependencies: "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.26.0" - "@babel/generator" "^7.26.0" - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-module-transforms" "^7.26.0" - "@babel/helpers" "^7.26.0" - "@babel/parser" "^7.26.0" - "@babel/template" "^7.25.9" - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.26.0" + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.27.3" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-module-transforms" "^7.27.3" + "@babel/helpers" "^7.27.3" + "@babel/parser" "^7.27.3" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.27.3" + "@babel/types" "^7.27.3" convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.26.0", "@babel/generator@^7.26.5": - version "7.26.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.5.tgz#e44d4ab3176bbcaf78a5725da5f1dc28802a9458" - integrity sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw== +"@babel/generator@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.27.3.tgz#ef1c0f7cfe3b5fc8cbb9f6cc69f93441a68edefc" + integrity sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q== dependencies: - "@babel/parser" "^7.26.5" - "@babel/types" "^7.26.5" + "@babel/parser" "^7.27.3" + "@babel/types" "^7.27.3" "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" jsesc "^3.0.2" -"@babel/helper-compilation-targets@^7.25.9": - version "7.26.5" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz#75d92bb8d8d51301c0d49e52a65c9a7fe94514d8" - integrity sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA== +"@babel/helper-compilation-targets@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d" + integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== dependencies: - "@babel/compat-data" "^7.26.5" - "@babel/helper-validator-option" "^7.25.9" + "@babel/compat-data" "^7.27.2" + "@babel/helper-validator-option" "^7.27.1" browserslist "^4.24.0" lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-module-imports@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" - integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== +"@babel/helper-module-imports@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" + integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" -"@babel/helper-module-transforms@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" - integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== +"@babel/helper-module-transforms@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz#db0bbcfba5802f9ef7870705a7ef8788508ede02" + integrity sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg== dependencies: - "@babel/helper-module-imports" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - "@babel/traverse" "^7.25.9" + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.27.3" "@babel/helper-plugin-utils@^7.25.9": version "7.26.5" @@ -104,45 +104,45 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== -"@babel/helper-string-parser@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" - integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== "@babel/helper-validator-identifier@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== -"@babel/helper-validator-identifier@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" - integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== +"@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== -"@babel/helper-validator-option@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" - integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== -"@babel/helpers@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.0.tgz#30e621f1eba5aa45fe6f4868d2e9154d884119a4" - integrity sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw== +"@babel/helpers@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.3.tgz#387d65d279290e22fe7a47a8ffcd2d0c0184edd0" + integrity sha512-h/eKy9agOya1IGuLaZ9tEUgz+uIRXcbtOhRtUyyMf8JFmn1iT13vnl/IGVWSkdOCG/pC57U4S1jnAabAavTMwg== dependencies: - "@babel/template" "^7.25.9" - "@babel/types" "^7.26.0" + "@babel/template" "^7.27.2" + "@babel/types" "^7.27.3" "@babel/parser@^7.1.0", "@babel/parser@^7.20.7": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.6.tgz#ba1c9e512bda72a47e285ae42aff9d2a635a9e3b" integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ== -"@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.5": - version "7.26.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.5.tgz#6fec9aebddef25ca57a935c86dbb915ae2da3e1f" - integrity sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw== +"@babel/parser@^7.27.2", "@babel/parser@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.3.tgz#1b7533f0d908ad2ac545c4d05cbe2fb6dc8cfaaf" + integrity sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw== dependencies: - "@babel/types" "^7.26.5" + "@babel/types" "^7.27.3" "@babel/plugin-transform-react-jsx-self@^7.25.9": version "7.25.9" @@ -165,25 +165,25 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" - integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== +"@babel/template@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" + integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== dependencies: - "@babel/code-frame" "^7.25.9" - "@babel/parser" "^7.25.9" - "@babel/types" "^7.25.9" + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.2" + "@babel/types" "^7.27.1" -"@babel/traverse@^7.25.9": - version "7.26.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.5.tgz#6d0be3e772ff786456c1a37538208286f6e79021" - integrity sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ== +"@babel/traverse@^7.27.1", "@babel/traverse@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.3.tgz#8b62a6c2d10f9d921ba7339c90074708509cffae" + integrity sha512-lId/IfN/Ye1CIu8xG7oKBHXd2iNb2aW1ilPszzGcJug6M8RCKfVNcYhpI5+bMvFYjK7lXIM0R+a+6r8xhHp2FQ== dependencies: - "@babel/code-frame" "^7.26.2" - "@babel/generator" "^7.26.5" - "@babel/parser" "^7.26.5" - "@babel/template" "^7.25.9" - "@babel/types" "^7.26.5" + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.27.3" + "@babel/parser" "^7.27.3" + "@babel/template" "^7.27.2" + "@babel/types" "^7.27.3" debug "^4.3.1" globals "^11.1.0" @@ -196,13 +196,13 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" -"@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.5": - version "7.26.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.5.tgz#7a1e1c01d28e26d1fe7f8ec9567b3b92b9d07747" - integrity sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg== +"@babel/types@^7.27.1", "@babel/types@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.3.tgz#c0257bedf33aad6aad1f406d35c44758321eb3ec" + integrity sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw== dependencies: - "@babel/helper-string-parser" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" "@esbuild/aix-ppc64@0.24.2": version "0.24.2" @@ -329,13 +329,20 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz#34aa0b52d0fbb1a654b596acfa595f0c7b77a77b" integrity sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg== -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": +"@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== dependencies: eslint-visitor-keys "^3.3.0" +"@eslint-community/eslint-utils@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a" + integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw== + dependencies: + eslint-visitor-keys "^3.4.3" + "@eslint-community/regexpp@^4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" @@ -346,31 +353,31 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== -"@eslint/config-array@^0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.19.2.tgz#3060b809e111abfc97adb0bb1172778b90cb46aa" - integrity sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w== +"@eslint/config-array@^0.20.0": + version "0.20.0" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.20.0.tgz#7a1232e82376712d3340012a2f561a2764d1988f" + integrity sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ== dependencies: "@eslint/object-schema" "^2.1.6" debug "^4.3.1" minimatch "^3.1.2" -"@eslint/config-helpers@^0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.1.0.tgz#62f1b7821e9d9ced1b3f512c7ea731825765d1cc" - integrity sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA== +"@eslint/config-helpers@^0.2.1": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.2.2.tgz#3779f76b894de3a8ec4763b79660e6d54d5b1010" + integrity sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg== -"@eslint/core@^0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.12.0.tgz#5f960c3d57728be9f6c65bd84aa6aa613078798e" - integrity sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg== +"@eslint/core@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.14.0.tgz#326289380968eaf7e96f364e1e4cf8f3adf2d003" + integrity sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg== dependencies: "@types/json-schema" "^7.0.15" -"@eslint/eslintrc@^3.3.0": - version "3.3.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.0.tgz#96a558f45842989cca7ea1ecd785ad5491193846" - integrity sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ== +"@eslint/eslintrc@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz#e55f7f1dd400600dd066dbba349c4c0bac916964" + integrity sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -382,22 +389,22 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.22.0": - version "9.22.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.22.0.tgz#4ff53649ded7cbce90b444b494c234137fa1aa3d" - integrity sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ== +"@eslint/js@9.27.0": + version "9.27.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.27.0.tgz#181a23460877c484f6dd03890f4e3fa2fdeb8ff0" + integrity sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA== "@eslint/object-schema@^2.1.6": version "2.1.6" resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f" integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA== -"@eslint/plugin-kit@^0.2.7": - version "0.2.7" - resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz#9901d52c136fb8f375906a73dcc382646c3b6a27" - integrity sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g== +"@eslint/plugin-kit@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz#b71b037b2d4d68396df04a8c35a49481e5593067" + integrity sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w== dependencies: - "@eslint/core" "^0.12.0" + "@eslint/core" "^0.14.0" levn "^0.4.1" "@floating-ui/core@^1.4.1": @@ -444,10 +451,10 @@ dependencies: "@floating-ui/dom" "^1.0.0" -"@floating-ui/react@0.27.5": - version "0.27.5" - resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.27.5.tgz#27a6e63a8ef35eb8712ef304a154ea706da26814" - integrity sha512-BX3jKxo39Ba05pflcQmqPPwc0qdNsdNi/eweAFtoIdrJWNen2sVEWMEac3i6jU55Qfx+lOcdMNKYn2CtWmlnOQ== +"@floating-ui/react@0.27.9": + version "0.27.9" + resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.27.9.tgz#b3c445a85ead27b73715c978a19b5506532ff097" + integrity sha512-Y0aCJBNtfVF6ikI1kVzA0WzSAhVBz79vFWOhvb5MLCRNODZ1ylGSLTuncchR7JsLyn9QzV6JD44DyZhhOtvpRw== dependencies: "@floating-ui/react-dom" "^2.1.2" "@floating-ui/utils" "^0.2.9" @@ -597,517 +604,448 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@radix-ui/number@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.0.tgz#1e95610461a09cdf8bb05c152e76ca1278d5da46" - integrity sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ== - -"@radix-ui/primitive@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2" - integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA== - -"@radix-ui/primitive@1.1.1": +"@radix-ui/number@1.1.1": version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.1.tgz#fc169732d755c7fbad33ba8d0cd7fd10c90dc8e3" - integrity sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA== + resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.1.tgz#7b2c9225fbf1b126539551f5985769d0048d9090" + integrity sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g== -"@radix-ui/react-arrow@1.1.2": +"@radix-ui/primitive@1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz#30c0d574d7bb10eed55cd7007b92d38b03c6b2ab" - integrity sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg== - dependencies: - "@radix-ui/react-primitive" "2.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.2.tgz#83f415c4425f21e3d27914c12b3272a32e3dae65" + integrity sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA== -"@radix-ui/react-checkbox@1.1.4": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.1.4.tgz#d7f5cb0a82ca6bb4eb717b74e9b2b0cc73ecf7a0" - integrity sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw== +"@radix-ui/react-arrow@1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz#e14a2657c81d961598c5e72b73dd6098acc04f09" + integrity sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w== dependencies: - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-presence" "1.1.2" - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-use-controllable-state" "1.1.0" - "@radix-ui/react-use-previous" "1.1.0" - "@radix-ui/react-use-size" "1.1.0" + "@radix-ui/react-primitive" "2.1.3" -"@radix-ui/react-collection@1.1.2": +"@radix-ui/react-checkbox@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz#28097244d968aa8f93249b0d3df02a172fd4bee5" + integrity sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-presence" "1.1.4" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-controllable-state" "1.2.2" + "@radix-ui/react-use-previous" "1.1.1" + "@radix-ui/react-use-size" "1.1.1" + +"@radix-ui/react-collection@1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz#d05c25ca9ac4695cc19ba91f42f686e3ea2d9aec" + integrity sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-slot" "1.2.3" + +"@radix-ui/react-compose-refs@1.1.2", "@radix-ui/react-compose-refs@^1.1.1": version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.2.tgz#b45eccca1cb902fd078b237316bd9fa81e621e15" - integrity sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw== + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30" + integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg== + +"@radix-ui/react-context@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36" + integrity sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA== + +"@radix-ui/react-dialog@1.1.14", "@radix-ui/react-dialog@^1.1.6": + version "1.1.14" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz#4c69c80c258bc6561398cfce055202ea11075107" + integrity sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw== dependencies: - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-slot" "1.1.2" - -"@radix-ui/react-compose-refs@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74" - integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw== - -"@radix-ui/react-compose-refs@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec" - integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw== - -"@radix-ui/react-context@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a" - integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q== - -"@radix-ui/react-dialog@1.1.6": - version "1.1.6" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz#65b4465e99ad900f28a98eed9a94bb21ec644bf7" - integrity sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw== - dependencies: - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-dismissable-layer" "1.1.5" - "@radix-ui/react-focus-guards" "1.1.1" - "@radix-ui/react-focus-scope" "1.1.2" - "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-portal" "1.1.4" - "@radix-ui/react-presence" "1.1.2" - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-slot" "1.1.2" - "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-dismissable-layer" "1.1.10" + "@radix-ui/react-focus-guards" "1.1.2" + "@radix-ui/react-focus-scope" "1.1.7" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-portal" "1.1.9" + "@radix-ui/react-presence" "1.1.4" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-slot" "1.2.3" + "@radix-ui/react-use-controllable-state" "1.2.2" aria-hidden "^1.2.4" react-remove-scroll "^2.6.3" -"@radix-ui/react-dialog@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz#d9345575211d6f2d13e209e84aec9a8584b54d6c" - integrity sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA== - dependencies: - "@radix-ui/primitive" "1.1.0" - "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-dismissable-layer" "1.1.1" - "@radix-ui/react-focus-guards" "1.1.1" - "@radix-ui/react-focus-scope" "1.1.0" - "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-portal" "1.1.2" - "@radix-ui/react-presence" "1.1.1" - "@radix-ui/react-primitive" "2.0.0" - "@radix-ui/react-slot" "1.1.0" - "@radix-ui/react-use-controllable-state" "1.1.0" - aria-hidden "^1.1.1" - react-remove-scroll "2.6.0" - -"@radix-ui/react-direction@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz#a7d39855f4d077adc2a1922f9c353c5977a09cdc" - integrity sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg== - -"@radix-ui/react-dismissable-layer@1.1.1": +"@radix-ui/react-direction@1.1.1": version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz#cbdcb739c5403382bdde5f9243042ba643883396" - integrity sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ== + resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz#39e5a5769e676c753204b792fbe6cf508e550a14" + integrity sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw== + +"@radix-ui/react-dismissable-layer@1.1.10": + version "1.1.10" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz#429b9bada3672c6895a5d6a642aca6ecaf4f18c3" + integrity sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ== dependencies: - "@radix-ui/primitive" "1.1.0" - "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-primitive" "2.0.0" - "@radix-ui/react-use-callback-ref" "1.1.0" - "@radix-ui/react-use-escape-keydown" "1.1.0" + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-escape-keydown" "1.1.1" -"@radix-ui/react-dismissable-layer@1.1.5": - version "1.1.5" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz#96dde2be078c694a621e55e047406c58cd5fe774" - integrity sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg== +"@radix-ui/react-dropdown-menu@2.1.15": + version "2.1.15" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz#f507320de8e11bc1e671a6ec0c27a7a89e725131" + integrity sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ== dependencies: - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-use-callback-ref" "1.1.0" - "@radix-ui/react-use-escape-keydown" "1.1.0" + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-menu" "2.1.15" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-controllable-state" "1.2.2" -"@radix-ui/react-dropdown-menu@2.1.6": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz#b66b62648b378370aa3c38e5727fd3bc5b8792a3" - integrity sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA== - dependencies: - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-menu" "2.1.6" - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-use-controllable-state" "1.1.0" - -"@radix-ui/react-focus-guards@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz#8635edd346304f8b42cae86b05912b61aef27afe" - integrity sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg== - -"@radix-ui/react-focus-scope@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz#ebe2891a298e0a33ad34daab2aad8dea31caf0b2" - integrity sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA== - dependencies: - "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-primitive" "2.0.0" - "@radix-ui/react-use-callback-ref" "1.1.0" - -"@radix-ui/react-focus-scope@1.1.2": +"@radix-ui/react-focus-guards@1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz#c0a4519cd95c772606a82fc5b96226cd7fdd2602" - integrity sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA== - dependencies: - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-use-callback-ref" "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz#4ec9a7e50925f7fb661394460045b46212a33bed" + integrity sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA== -"@radix-ui/react-hover-card@1.1.6": - version "1.1.6" - resolved "https://registry.yarnpkg.com/@radix-ui/react-hover-card/-/react-hover-card-1.1.6.tgz#94fb87c047e1bb3bfd70439cf7ee48165ea4efa5" - integrity sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ== +"@radix-ui/react-focus-scope@1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz#dfe76fc103537d80bf42723a183773fd07bfb58d" + integrity sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw== dependencies: - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-dismissable-layer" "1.1.5" - "@radix-ui/react-popper" "1.2.2" - "@radix-ui/react-portal" "1.1.4" - "@radix-ui/react-presence" "1.1.2" - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + +"@radix-ui/react-hover-card@1.1.14": + version "1.1.14" + resolved "https://registry.yarnpkg.com/@radix-ui/react-hover-card/-/react-hover-card-1.1.14.tgz#a557cda6470e214e744e46ede839496e8b291843" + integrity sha512-CPYZ24Mhirm+g6D8jArmLzjYu4Eyg3TTUHswR26QgzXBHBe64BO/RHOJKzmF/Dxb4y4f9PKyJdwm/O/AhNkb+Q== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-dismissable-layer" "1.1.10" + "@radix-ui/react-popper" "1.2.7" + "@radix-ui/react-portal" "1.1.9" + "@radix-ui/react-presence" "1.1.4" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-controllable-state" "1.2.2" "@radix-ui/react-icons@1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.2.tgz#09be63d178262181aeca5fb7f7bc944b10a7f441" integrity sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g== -"@radix-ui/react-id@1.1.0", "@radix-ui/react-id@^1.1.0": +"@radix-ui/react-id@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.1.tgz#1404002e79a03fe062b7e3864aa01e24bd1471f7" + integrity sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-id@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed" integrity sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA== dependencies: "@radix-ui/react-use-layout-effect" "1.1.0" -"@radix-ui/react-label@2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-2.1.2.tgz#994a5d815c2ff46e151410ae4e301f1b639f9971" - integrity sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw== +"@radix-ui/react-label@2.1.7": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-2.1.7.tgz#ad959ff9c6e4968d533329eb95696e1ba8ad72ab" + integrity sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ== dependencies: - "@radix-ui/react-primitive" "2.0.2" + "@radix-ui/react-primitive" "2.1.3" -"@radix-ui/react-menu@2.1.6": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.6.tgz#05fb1ef3fd7545c8abe61178372902970cdec3ce" - integrity sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg== +"@radix-ui/react-menu@2.1.15": + version "2.1.15" + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.15.tgz#a1a8f06cab3c309f9998cdbd2b3ad279e42ed483" + integrity sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew== dependencies: - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-collection" "1.1.2" - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-direction" "1.1.0" - "@radix-ui/react-dismissable-layer" "1.1.5" - "@radix-ui/react-focus-guards" "1.1.1" - "@radix-ui/react-focus-scope" "1.1.2" - "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-popper" "1.2.2" - "@radix-ui/react-portal" "1.1.4" - "@radix-ui/react-presence" "1.1.2" - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-roving-focus" "1.1.2" - "@radix-ui/react-slot" "1.1.2" - "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-collection" "1.1.7" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-direction" "1.1.1" + "@radix-ui/react-dismissable-layer" "1.1.10" + "@radix-ui/react-focus-guards" "1.1.2" + "@radix-ui/react-focus-scope" "1.1.7" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-popper" "1.2.7" + "@radix-ui/react-portal" "1.1.9" + "@radix-ui/react-presence" "1.1.4" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-roving-focus" "1.1.10" + "@radix-ui/react-slot" "1.2.3" + "@radix-ui/react-use-callback-ref" "1.1.1" aria-hidden "^1.2.4" react-remove-scroll "^2.6.3" -"@radix-ui/react-popover@1.1.6": - version "1.1.6" - resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.6.tgz#699634dbc7899429f657bb590d71fb3ca0904087" - integrity sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg== +"@radix-ui/react-popover@1.1.14": + version "1.1.14" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.14.tgz#5496d1986f0287cdfc77e73f70a887e4cb77ad08" + integrity sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw== dependencies: - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-dismissable-layer" "1.1.5" - "@radix-ui/react-focus-guards" "1.1.1" - "@radix-ui/react-focus-scope" "1.1.2" - "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-popper" "1.2.2" - "@radix-ui/react-portal" "1.1.4" - "@radix-ui/react-presence" "1.1.2" - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-slot" "1.1.2" - "@radix-ui/react-use-controllable-state" "1.1.0" + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-dismissable-layer" "1.1.10" + "@radix-ui/react-focus-guards" "1.1.2" + "@radix-ui/react-focus-scope" "1.1.7" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-popper" "1.2.7" + "@radix-ui/react-portal" "1.1.9" + "@radix-ui/react-presence" "1.1.4" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-slot" "1.2.3" + "@radix-ui/react-use-controllable-state" "1.2.2" aria-hidden "^1.2.4" react-remove-scroll "^2.6.3" -"@radix-ui/react-popper@1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.2.tgz#d2e1ee5a9b24419c5936a1b7f6f472b7b412b029" - integrity sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA== +"@radix-ui/react-popper@1.2.7": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.7.tgz#531cf2eebb3d3270d58f7d8136e4517646429978" + integrity sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ== dependencies: "@floating-ui/react-dom" "^2.0.0" - "@radix-ui/react-arrow" "1.1.2" - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-use-callback-ref" "1.1.0" - "@radix-ui/react-use-layout-effect" "1.1.0" - "@radix-ui/react-use-rect" "1.1.0" - "@radix-ui/react-use-size" "1.1.0" - "@radix-ui/rect" "1.1.0" + "@radix-ui/react-arrow" "1.1.7" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-use-rect" "1.1.1" + "@radix-ui/react-use-size" "1.1.1" + "@radix-ui/rect" "1.1.1" -"@radix-ui/react-portal@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.2.tgz#51eb46dae7505074b306ebcb985bf65cc547d74e" - integrity sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg== +"@radix-ui/react-portal@1.1.9": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz#14c3649fe48ec474ac51ed9f2b9f5da4d91c4472" + integrity sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ== dependencies: - "@radix-ui/react-primitive" "2.0.0" - "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-layout-effect" "1.1.1" -"@radix-ui/react-portal@1.1.4": +"@radix-ui/react-presence@1.1.4": version "1.1.4" - resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.4.tgz#ff5401ff63c8a825c46eea96d3aef66074b8c0c8" - integrity sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA== + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.4.tgz#253ac0ad4946c5b4a9c66878335f5cf07c967ced" + integrity sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA== dependencies: - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-use-layout-effect" "1.1.1" -"@radix-ui/react-presence@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.1.tgz#98aba423dba5e0c687a782c0669dcd99de17f9b1" - integrity sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A== +"@radix-ui/react-primitive@2.1.3", "@radix-ui/react-primitive@^2.0.2": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz#db9b8bcff49e01be510ad79893fb0e4cda50f1bc" + integrity sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ== dependencies: - "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-slot" "1.2.3" -"@radix-ui/react-presence@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc" - integrity sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg== +"@radix-ui/react-progress@1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-1.1.7.tgz#a2b76398b3f24b6bd5e37f112b1e30fbedd4f38e" + integrity sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg== dependencies: - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-primitive" "2.1.3" -"@radix-ui/react-primitive@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz#fe05715faa9203a223ccc0be15dc44b9f9822884" - integrity sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw== +"@radix-ui/react-roving-focus@1.1.10": + version "1.1.10" + resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz#46030496d2a490c4979d29a7e1252465e51e4b0b" + integrity sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q== dependencies: - "@radix-ui/react-slot" "1.1.0" + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-collection" "1.1.7" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-direction" "1.1.1" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-controllable-state" "1.2.2" -"@radix-ui/react-primitive@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz#ac8b7854d87b0d7af388d058268d9a7eb64ca8ef" - integrity sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w== +"@radix-ui/react-select@2.2.5": + version "2.2.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-2.2.5.tgz#9e2fa5b8f4cc99b86ef5bba3cb9b73828afb51f0" + integrity sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA== dependencies: - "@radix-ui/react-slot" "1.1.2" - -"@radix-ui/react-primitive@^2.0.0": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz#6d9efc550f7520135366f333d1e820cf225fad9e" - integrity sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg== - dependencies: - "@radix-ui/react-slot" "1.1.1" - -"@radix-ui/react-progress@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-1.1.2.tgz#3584c346d47f2a6f86076ce5af56ab00c66ded2b" - integrity sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA== - dependencies: - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-primitive" "2.0.2" - -"@radix-ui/react-roving-focus@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz#815d051a54299114a68db6eb8d34c41a3c0a646f" - integrity sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw== - dependencies: - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-collection" "1.1.2" - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-direction" "1.1.0" - "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-use-callback-ref" "1.1.0" - "@radix-ui/react-use-controllable-state" "1.1.0" - -"@radix-ui/react-select@2.1.6": - version "2.1.6" - resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-2.1.6.tgz#79c07cac4de0188e6f7afb2720a87a0405d88849" - integrity sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg== - dependencies: - "@radix-ui/number" "1.1.0" - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-collection" "1.1.2" - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-direction" "1.1.0" - "@radix-ui/react-dismissable-layer" "1.1.5" - "@radix-ui/react-focus-guards" "1.1.1" - "@radix-ui/react-focus-scope" "1.1.2" - "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-popper" "1.2.2" - "@radix-ui/react-portal" "1.1.4" - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-slot" "1.1.2" - "@radix-ui/react-use-callback-ref" "1.1.0" - "@radix-ui/react-use-controllable-state" "1.1.0" - "@radix-ui/react-use-layout-effect" "1.1.0" - "@radix-ui/react-use-previous" "1.1.0" - "@radix-ui/react-visually-hidden" "1.1.2" + "@radix-ui/number" "1.1.1" + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-collection" "1.1.7" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-direction" "1.1.1" + "@radix-ui/react-dismissable-layer" "1.1.10" + "@radix-ui/react-focus-guards" "1.1.2" + "@radix-ui/react-focus-scope" "1.1.7" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-popper" "1.2.7" + "@radix-ui/react-portal" "1.1.9" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-slot" "1.2.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-controllable-state" "1.2.2" + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-use-previous" "1.1.1" + "@radix-ui/react-visually-hidden" "1.2.3" aria-hidden "^1.2.4" react-remove-scroll "^2.6.3" -"@radix-ui/react-separator@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.1.2.tgz#24c5450db20f341f2b743ed4b07b907e18579216" - integrity sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ== +"@radix-ui/react-separator@1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.1.7.tgz#a18bd7fd07c10fda1bba14f2a3032e7b1a2b3470" + integrity sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA== dependencies: - "@radix-ui/react-primitive" "2.0.2" + "@radix-ui/react-primitive" "2.1.3" -"@radix-ui/react-slot@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84" - integrity sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw== +"@radix-ui/react-slot@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz#502d6e354fc847d4169c3bc5f189de777f68cfe1" + integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A== dependencies: - "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.2" -"@radix-ui/react-slot@1.1.1": +"@radix-ui/react-switch@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.2.5.tgz#56c15a4cd219e00b0745ec6b2ea1c0feeb0b21d0" + integrity sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-controllable-state" "1.2.2" + "@radix-ui/react-use-previous" "1.1.1" + "@radix-ui/react-use-size" "1.1.1" + +"@radix-ui/react-tabs@1.1.12": + version "1.1.12" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz#99b3522c73db9263f429a6d0f5a9acb88df3b129" + integrity sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-direction" "1.1.1" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-presence" "1.1.4" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-roving-focus" "1.1.10" + "@radix-ui/react-use-controllable-state" "1.2.2" + +"@radix-ui/react-toast@1.2.14": + version "1.2.14" + resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.2.14.tgz#ac021bdde74792fe8613c510eb6944f0fbcf57b0" + integrity sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-collection" "1.1.7" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-dismissable-layer" "1.1.10" + "@radix-ui/react-portal" "1.1.9" + "@radix-ui/react-presence" "1.1.4" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-controllable-state" "1.2.2" + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-visually-hidden" "1.2.3" + +"@radix-ui/react-toggle-group@1.1.10": + version "1.1.10" + resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.10.tgz#4406b3be3869cad497ca7ee4993c3598731f774e" + integrity sha512-kiU694Km3WFLTC75DdqgM/3Jauf3rD9wxeS9XtyWFKsBUeZA337lC+6uUazT7I1DhanZ5gyD5Stf8uf2dbQxOQ== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-direction" "1.1.1" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-roving-focus" "1.1.10" + "@radix-ui/react-toggle" "1.1.9" + "@radix-ui/react-use-controllable-state" "1.2.2" + +"@radix-ui/react-toggle@1.1.9": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle/-/react-toggle-1.1.9.tgz#9cb99a29bc7cd15186ba3ba797808a013a726fba" + integrity sha512-ZoFkBBz9zv9GWer7wIjvdRxmh2wyc2oKWw6C6CseWd6/yq1DK/l5lJ+wnsmFwJZbBYqr02mrf8A2q/CVCuM3ZA== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-controllable-state" "1.2.2" + +"@radix-ui/react-use-callback-ref@1.1.1": version "1.1.1" - resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.1.tgz#ab9a0ffae4027db7dc2af503c223c978706affc3" - integrity sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g== - dependencies: - "@radix-ui/react-compose-refs" "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz#62a4dba8b3255fdc5cc7787faeac1c6e4cc58d40" + integrity sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg== -"@radix-ui/react-slot@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.2.tgz#daffff7b2bfe99ade63b5168407680b93c00e1c6" - integrity sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ== +"@radix-ui/react-use-controllable-state@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz#905793405de57d61a439f4afebbb17d0645f3190" + integrity sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg== dependencies: - "@radix-ui/react-compose-refs" "1.1.1" + "@radix-ui/react-use-effect-event" "0.0.2" + "@radix-ui/react-use-layout-effect" "1.1.1" -"@radix-ui/react-switch@1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.3.tgz#cb6386909d1d3f65a2b81a3b15da8c91d18f49b0" - integrity sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ== +"@radix-ui/react-use-effect-event@0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz#090cf30d00a4c7632a15548512e9152217593907" + integrity sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA== dependencies: - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-use-controllable-state" "1.1.0" - "@radix-ui/react-use-previous" "1.1.0" - "@radix-ui/react-use-size" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.1" -"@radix-ui/react-tabs@1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz#c47c8202dc676dea47676215863d2ef9b141c17a" - integrity sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng== +"@radix-ui/react-use-escape-keydown@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz#b3fed9bbea366a118f40427ac40500aa1423cc29" + integrity sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g== dependencies: - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-direction" "1.1.0" - "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-presence" "1.1.2" - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-roving-focus" "1.1.2" - "@radix-ui/react-use-controllable-state" "1.1.0" - -"@radix-ui/react-toast@1.2.6": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.2.6.tgz#f8d4bb2217851d221d700ac48fbe866b35023361" - integrity sha512-gN4dpuIVKEgpLn1z5FhzT9mYRUitbfZq9XqN/7kkBMUgFTzTG8x/KszWJugJXHcwxckY8xcKDZPz7kG3o6DsUA== - dependencies: - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-collection" "1.1.2" - "@radix-ui/react-compose-refs" "1.1.1" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-dismissable-layer" "1.1.5" - "@radix-ui/react-portal" "1.1.4" - "@radix-ui/react-presence" "1.1.2" - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-use-callback-ref" "1.1.0" - "@radix-ui/react-use-controllable-state" "1.1.0" - "@radix-ui/react-use-layout-effect" "1.1.0" - "@radix-ui/react-visually-hidden" "1.1.2" - -"@radix-ui/react-toggle-group@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.2.tgz#a4063a06124a008a206f5678018612e3ddd5923c" - integrity sha512-JBm6s6aVG/nwuY5eadhU2zDi/IwYS0sDM5ZWb4nymv/hn3hZdkw+gENn0LP4iY1yCd7+bgJaCwueMYJIU3vk4A== - dependencies: - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-context" "1.1.1" - "@radix-ui/react-direction" "1.1.0" - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-roving-focus" "1.1.2" - "@radix-ui/react-toggle" "1.1.2" - "@radix-ui/react-use-controllable-state" "1.1.0" - -"@radix-ui/react-toggle@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle/-/react-toggle-1.1.2.tgz#c11cd1550046a2c78afbdffd35b541979df9ffc6" - integrity sha512-lntKchNWx3aCHuWKiDY+8WudiegQvBpDRAYL8dKLRvKEH8VOpl0XX6SSU/bUBqIRJbcTy4+MW06Wv8vgp10rzQ== - dependencies: - "@radix-ui/primitive" "1.1.1" - "@radix-ui/react-primitive" "2.0.2" - "@radix-ui/react-use-controllable-state" "1.1.0" - -"@radix-ui/react-use-callback-ref@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1" - integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw== - -"@radix-ui/react-use-controllable-state@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0" - integrity sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw== - dependencies: - "@radix-ui/react-use-callback-ref" "1.1.0" - -"@radix-ui/react-use-escape-keydown@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz#31a5b87c3b726504b74e05dac1edce7437b98754" - integrity sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw== - dependencies: - "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-callback-ref" "1.1.1" "@radix-ui/react-use-layout-effect@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27" integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w== -"@radix-ui/react-use-previous@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz#d4dd37b05520f1d996a384eb469320c2ada8377c" - integrity sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og== +"@radix-ui/react-use-layout-effect@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz#0c4230a9eed49d4589c967e2d9c0d9d60a23971e" + integrity sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ== -"@radix-ui/react-use-rect@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz#13b25b913bd3e3987cc9b073a1a164bb1cf47b88" - integrity sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ== +"@radix-ui/react-use-previous@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz#1a1ad5568973d24051ed0af687766f6c7cb9b5b5" + integrity sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ== + +"@radix-ui/react-use-rect@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz#01443ca8ed071d33023c1113e5173b5ed8769152" + integrity sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w== dependencies: - "@radix-ui/rect" "1.1.0" + "@radix-ui/rect" "1.1.1" -"@radix-ui/react-use-size@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz#b4dba7fbd3882ee09e8d2a44a3eed3a7e555246b" - integrity sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw== +"@radix-ui/react-use-size@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz#6de276ffbc389a537ffe4316f5b0f24129405b37" + integrity sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ== dependencies: - "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.1" -"@radix-ui/react-visually-hidden@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz#8f6025507eb5d8b4b3215ebfd2c71a6632323a62" - integrity sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q== +"@radix-ui/react-visually-hidden@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz#a8c38c8607735dc9f05c32f87ab0f9c2b109efbf" + integrity sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug== dependencies: - "@radix-ui/react-primitive" "2.0.2" + "@radix-ui/react-primitive" "2.1.3" -"@radix-ui/rect@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438" - integrity sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg== +"@radix-ui/rect@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.1.tgz#78244efe12930c56fd255d7923865857c41ac8cb" + integrity sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw== + +"@rolldown/pluginutils@1.0.0-beta.9": + version "1.0.0-beta.9" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz#68ef9fff5a9791a642cea0dc4380ce6cb487a84a" + integrity sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w== "@rollup/rollup-android-arm-eabi@4.30.1": version "4.30.1" @@ -1204,29 +1142,29 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz#73bf1885ff052b82fbb0f82f8671f73c36e9137c" integrity sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og== -"@tanstack/query-core@5.67.3": - version "5.67.3" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.67.3.tgz#222795762c584d572f6f41bd4eb97f86f46f2eea" - integrity sha512-pq76ObpjcaspAW4OmCbpXLF6BCZP2Zr/J5ztnyizXhSlNe7fIUp0QKZsd0JMkw9aDa+vxDX/OY7N+hjNY/dCGg== +"@tanstack/query-core@5.77.2": + version "5.77.2" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.77.2.tgz#a59f34068b96c5888075f04e6bc74b8076a3f97b" + integrity sha512-1lqJwPsR6GX6nZFw06erRt518O19tWU6Q+x0fJUygl4lxHCYF2nhzBPwLKk2NPjYOrpR0K567hxPc5K++xDe9Q== -"@tanstack/react-query@5.67.3": - version "5.67.3" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.67.3.tgz#4a1a1a54828465ab6bc421e66dc7ebd88191e851" - integrity sha512-u/n2HsQeH1vpZIOzB/w2lqKlXUDUKo6BxTdGXSMvNzIq5MHYFckRMVuFABp+QB7RN8LFXWV6X1/oSkuDq+MPIA== +"@tanstack/react-query@5.77.2": + version "5.77.2" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.77.2.tgz#cf3bc22b27630fdaf786527015fe2480512df55f" + integrity sha512-BRHxWdy1mHmgAcYA/qy2IPLylT81oebLgkm9K85viN2Qol/Vq48t1dzDFeDIVQjTWDV96AmqsLNPlH5HjyKCxA== dependencies: - "@tanstack/query-core" "5.67.3" + "@tanstack/query-core" "5.77.2" -"@tanstack/react-table@8.21.2": - version "8.21.2" - resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.21.2.tgz#6a7fce828b64547e33f4606ada8114db496007cc" - integrity sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg== +"@tanstack/react-table@8.21.3": + version "8.21.3" + resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.21.3.tgz#2c38c747a5731c1a07174fda764b9c2b1fb5e91b" + integrity sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww== dependencies: - "@tanstack/table-core" "8.21.2" + "@tanstack/table-core" "8.21.3" -"@tanstack/table-core@8.21.2": - version "8.21.2" - resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.2.tgz#dd57595a1773652bb6fb437e90a5f5386a49fd7e" - integrity sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA== +"@tanstack/table-core@8.21.3": + version "8.21.3" + resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c" + integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg== "@types/babel__core@^7.20.5": version "7.20.5" @@ -1261,11 +1199,6 @@ dependencies: "@babel/types" "^7.20.7" -"@types/cookie@^0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" - integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA== - "@types/d3-array@^3.0.1": version "3.2.1" resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" @@ -1315,10 +1248,10 @@ resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.3.tgz#85f1d1d17569b28b8db45e16e996407a56b0ab04" integrity sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw== -"@types/react-dom@19.0.4": - version "19.0.4" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.4.tgz#bedba97f9346bd4c0fe5d39e689713804ec9ac89" - integrity sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg== +"@types/react-dom@19.1.5": + version "19.1.5" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.5.tgz#cdfe2c663742887372f54804b16e8dbc26bd794a" + integrity sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg== "@types/react-dom@^17.0.9": version "17.0.25" @@ -1327,10 +1260,10 @@ dependencies: "@types/react" "^17" -"@types/react@19.0.10": - version "19.0.10" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.10.tgz#d0c66dafd862474190fe95ce11a68de69ed2b0eb" - integrity sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g== +"@types/react@19.1.6": + version "19.1.6" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.6.tgz#dee39f3e1e9a7d693f156a5840570b6d57f325ea" + integrity sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q== dependencies: csstype "^3.0.2" @@ -1343,10 +1276,10 @@ "@types/scheduler" "^0.16" csstype "^3.0.2" -"@types/sanitize-html@2.13.0": - version "2.13.0" - resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.13.0.tgz#ac3620e867b7c68deab79c72bd117e2049cdd98e" - integrity sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ== +"@types/sanitize-html@2.16.0": + version "2.16.0" + resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.16.0.tgz#860d72c1ba8a5d044946f37559cc359c0a13b24e" + integrity sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw== dependencies: htmlparser2 "^8.0.0" @@ -1355,104 +1288,121 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== -"@typescript-eslint/eslint-plugin@8.26.1": - version "8.26.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz#3e48eb847924161843b092c87a9b65176b53782f" - integrity sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA== +"@typescript-eslint/eslint-plugin@8.33.0": + version "8.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz#51ed03649575ba51bcee7efdbfd85283249b5447" + integrity sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.26.1" - "@typescript-eslint/type-utils" "8.26.1" - "@typescript-eslint/utils" "8.26.1" - "@typescript-eslint/visitor-keys" "8.26.1" + "@typescript-eslint/scope-manager" "8.33.0" + "@typescript-eslint/type-utils" "8.33.0" + "@typescript-eslint/utils" "8.33.0" + "@typescript-eslint/visitor-keys" "8.33.0" graphemer "^1.4.0" - ignore "^5.3.1" + ignore "^7.0.0" natural-compare "^1.4.0" - ts-api-utils "^2.0.1" + ts-api-utils "^2.1.0" -"@typescript-eslint/parser@8.26.1": - version "8.26.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.26.1.tgz#0e2f915a497519fc43f52cf2ecbfa607ff56f72e" - integrity sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ== +"@typescript-eslint/parser@8.33.0": + version "8.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.33.0.tgz#8e523c2b447ad7cd6ac91b719d8b37449481784d" + integrity sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ== dependencies: - "@typescript-eslint/scope-manager" "8.26.1" - "@typescript-eslint/types" "8.26.1" - "@typescript-eslint/typescript-estree" "8.26.1" - "@typescript-eslint/visitor-keys" "8.26.1" + "@typescript-eslint/scope-manager" "8.33.0" + "@typescript-eslint/types" "8.33.0" + "@typescript-eslint/typescript-estree" "8.33.0" + "@typescript-eslint/visitor-keys" "8.33.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@8.26.1": - version "8.26.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.26.1.tgz#5e6ad0ac258ccf79462e91c3f43a3f1f7f31a6cc" - integrity sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg== +"@typescript-eslint/project-service@8.33.0": + version "8.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.33.0.tgz#71f37ef9010de47bf20963914743c5cbef851e08" + integrity sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A== dependencies: - "@typescript-eslint/types" "8.26.1" - "@typescript-eslint/visitor-keys" "8.26.1" - -"@typescript-eslint/type-utils@8.26.1": - version "8.26.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.26.1.tgz#462f0bae09de72ac6e8e1af2ebe588c23224d7f8" - integrity sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg== - dependencies: - "@typescript-eslint/typescript-estree" "8.26.1" - "@typescript-eslint/utils" "8.26.1" + "@typescript-eslint/tsconfig-utils" "^8.33.0" + "@typescript-eslint/types" "^8.33.0" debug "^4.3.4" - ts-api-utils "^2.0.1" -"@typescript-eslint/types@8.26.1": - version "8.26.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.26.1.tgz#d5978721670cff263348d5062773389231a64132" - integrity sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ== - -"@typescript-eslint/typescript-estree@8.26.1": - version "8.26.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.1.tgz#eb0e4ce31753683d83be53441a409fd5f0b34afd" - integrity sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA== +"@typescript-eslint/scope-manager@8.33.0": + version "8.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz#459cf0c49d410800b1a023b973c62d699b09bf4c" + integrity sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw== dependencies: - "@typescript-eslint/types" "8.26.1" - "@typescript-eslint/visitor-keys" "8.26.1" + "@typescript-eslint/types" "8.33.0" + "@typescript-eslint/visitor-keys" "8.33.0" + +"@typescript-eslint/tsconfig-utils@8.33.0", "@typescript-eslint/tsconfig-utils@^8.33.0": + version "8.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz#316adab038bbdc43e448781d5a816c2973eab73e" + integrity sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug== + +"@typescript-eslint/type-utils@8.33.0": + version "8.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz#f06124b2d6db8a51b24990cb123c9543af93fef5" + integrity sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ== + dependencies: + "@typescript-eslint/typescript-estree" "8.33.0" + "@typescript-eslint/utils" "8.33.0" + debug "^4.3.4" + ts-api-utils "^2.1.0" + +"@typescript-eslint/types@8.33.0", "@typescript-eslint/types@^8.33.0": + version "8.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.33.0.tgz#02a7dbba611a8abf1ad2a9e00f72f7b94b5ab0ee" + integrity sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg== + +"@typescript-eslint/typescript-estree@8.33.0": + version "8.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz#abcc1d3db75a8e9fd2e274ee8c4099fa2399abfd" + integrity sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ== + dependencies: + "@typescript-eslint/project-service" "8.33.0" + "@typescript-eslint/tsconfig-utils" "8.33.0" + "@typescript-eslint/types" "8.33.0" + "@typescript-eslint/visitor-keys" "8.33.0" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" minimatch "^9.0.4" semver "^7.6.0" - ts-api-utils "^2.0.1" + ts-api-utils "^2.1.0" -"@typescript-eslint/utils@8.26.1": - version "8.26.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.26.1.tgz#54cc58469955f25577f659753b71a0e117a0539f" - integrity sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg== +"@typescript-eslint/utils@8.33.0": + version "8.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.33.0.tgz#574ad5edee371077b9e28ca6fb804f2440f447c1" + integrity sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw== dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.26.1" - "@typescript-eslint/types" "8.26.1" - "@typescript-eslint/typescript-estree" "8.26.1" + "@eslint-community/eslint-utils" "^4.7.0" + "@typescript-eslint/scope-manager" "8.33.0" + "@typescript-eslint/types" "8.33.0" + "@typescript-eslint/typescript-estree" "8.33.0" -"@typescript-eslint/visitor-keys@8.26.1": - version "8.26.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.1.tgz#c5267fcc82795cf10280363023837deacad2647c" - integrity sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg== +"@typescript-eslint/visitor-keys@8.33.0": + version "8.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz#fbae16fd3594531f8cad95d421125d634e9974fe" + integrity sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ== dependencies: - "@typescript-eslint/types" "8.26.1" + "@typescript-eslint/types" "8.33.0" eslint-visitor-keys "^4.2.0" -"@vitejs/plugin-react@4.3.4": - version "4.3.4" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz#c64be10b54c4640135a5b28a2432330e88ad7c20" - integrity sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug== +"@vitejs/plugin-react@4.5.0": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz#ef2bad6be3031af2b2105b7ab2754f710e890a32" + integrity sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg== dependencies: - "@babel/core" "^7.26.0" + "@babel/core" "^7.26.10" "@babel/plugin-transform-react-jsx-self" "^7.25.9" "@babel/plugin-transform-react-jsx-source" "^7.25.9" + "@rolldown/pluginutils" "1.0.0-beta.9" "@types/babel__core" "^7.20.5" - react-refresh "^0.14.2" + react-refresh "^0.17.0" -"@xterm/addon-fit@^0.10.0": +"@xterm/addon-fit@0.10.0": version "0.10.0" resolved "https://registry.yarnpkg.com/@xterm/addon-fit/-/addon-fit-0.10.0.tgz#bebf87fadd74e3af30fdcdeef47030e2592c6f55" integrity sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ== -"@xterm/xterm@^5.5.0": +"@xterm/xterm@5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0.tgz#275fb8f6e14afa6e8a0c05d4ebc94523ff775396" integrity sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A== @@ -1534,13 +1484,6 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-hidden@^1.1.1: - version "1.2.3" - resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954" - integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ== - dependencies: - tslib "^2.0.0" - aria-hidden@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522" @@ -1548,16 +1491,16 @@ aria-hidden@^1.2.4: dependencies: tslib "^2.0.0" -autoprefixer@10.4.20: - version "10.4.20" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b" - integrity sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g== +autoprefixer@10.4.21: + version "10.4.21" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.21.tgz#77189468e7a8ad1d9a37fbc08efc9f480cf0a95d" + integrity sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ== dependencies: - browserslist "^4.23.3" - caniuse-lite "^1.0.30001646" + browserslist "^4.24.4" + caniuse-lite "^1.0.30001702" fraction.js "^4.3.7" normalize-range "^0.1.2" - picocolors "^1.0.1" + picocolors "^1.1.1" postcss-value-parser "^4.2.0" balanced-match@^1.0.0: @@ -1599,16 +1542,6 @@ braces@^3.0.3: dependencies: fill-range "^7.1.1" -browserslist@^4.23.3: - version "4.23.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" - integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== - dependencies: - caniuse-lite "^1.0.30001646" - electron-to-chromium "^1.5.4" - node-releases "^2.0.18" - update-browserslist-db "^1.1.0" - browserslist@^4.24.0: version "4.24.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.0.tgz#a1325fe4bc80b64fda169629fc01b3d6cecd38d4" @@ -1619,6 +1552,16 @@ browserslist@^4.24.0: node-releases "^2.0.18" update-browserslist-db "^1.1.0" +browserslist@^4.24.4: + version "4.24.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.5.tgz#aa0f5b8560fe81fde84c6dcb38f759bafba0e11b" + integrity sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw== + dependencies: + caniuse-lite "^1.0.30001716" + electron-to-chromium "^1.5.149" + node-releases "^2.0.19" + update-browserslist-db "^1.1.3" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1629,16 +1572,16 @@ camelcase-css@^2.0.1: resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== -caniuse-lite@^1.0.30001646: - version "1.0.30001651" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz#52de59529e8b02b1aedcaaf5c05d9e23c0c28138" - integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg== - caniuse-lite@^1.0.30001663: version "1.0.30001669" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz#fda8f1d29a8bfdc42de0c170d7f34a9cf19ed7a3" integrity sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w== +caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001716: + version "1.0.30001718" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz#dae13a9c80d517c30c6197515a96131c194d8f82" + integrity sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw== + chalk@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -1674,15 +1617,15 @@ clsx@2.1.1, clsx@^2.1.1: resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== -cmdk@1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cmdk/-/cmdk-1.0.4.tgz#cbddef6f5ade2378f85c80a0b9ad9a8a712779b5" - integrity sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg== +cmdk@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/cmdk/-/cmdk-1.1.1.tgz#b8524272699ccaa37aaf07f36850b376bf3d58e5" + integrity sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg== dependencies: - "@radix-ui/react-dialog" "^1.1.2" + "@radix-ui/react-compose-refs" "^1.1.1" + "@radix-ui/react-dialog" "^1.1.6" "@radix-ui/react-id" "^1.1.0" - "@radix-ui/react-primitive" "^2.0.0" - use-sync-external-store "^1.2.2" + "@radix-ui/react-primitive" "^2.0.2" color-convert@^2.0.1: version "2.0.1" @@ -1896,16 +1839,16 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +electron-to-chromium@^1.5.149: + version "1.5.159" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.159.tgz#b909c4a5dbd00674f18419199f71c945a199effe" + integrity sha512-CEvHptWAMV5p6GJ0Lq8aheyvVbfzVrv5mmidu1D3pidoVNkB3tTBsTMVtPJ+rzRK5oV229mCLz9Zj/hNvU8GBA== + electron-to-chromium@^1.5.28: version "1.5.41" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.41.tgz#eae1ba6c49a1a61d84cf8263351d3513b2bcc534" integrity sha512-dfdv/2xNjX0P8Vzme4cfzHqnPm5xsZXwsolTYr0eyW18IUmNyG08vL+fttvinTfhKfIKdRoqkDIC9e9iWQCNYQ== -electron-to-chromium@^1.5.4: - version "1.5.6" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz#c81d9938b5a877314ad370feb73b4e5409b36abd" - integrity sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw== - emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -1962,6 +1905,11 @@ escalade@^3.1.2: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -1972,10 +1920,10 @@ eslint-plugin-react-hooks@5.2.0: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz#1be0080901e6ac31ce7971beed3d3ec0a423d9e3" integrity sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg== -eslint-plugin-react-refresh@0.4.19: - version "0.4.19" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz#f15020c0caa58e33fc4efda27d328281ca74e53d" - integrity sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ== +eslint-plugin-react-refresh@0.4.20: + version "0.4.20" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz#3bbfb5c8637e28d19ce3443686445e502ecd18ba" + integrity sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA== eslint-scope@^8.3.0: version "8.3.0" @@ -1985,7 +1933,7 @@ eslint-scope@^8.3.0: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.3.0: +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== @@ -2000,19 +1948,19 @@ eslint-visitor-keys@^4.2.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== -eslint@9.22.0: - version "9.22.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.22.0.tgz#0760043809fbf836f582140345233984d613c552" - integrity sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ== +eslint@9.27.0: + version "9.27.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.27.0.tgz#a587d3cd5b844b68df7898944323a702afe38979" + integrity sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.12.1" - "@eslint/config-array" "^0.19.2" - "@eslint/config-helpers" "^0.1.0" - "@eslint/core" "^0.12.0" - "@eslint/eslintrc" "^3.3.0" - "@eslint/js" "9.22.0" - "@eslint/plugin-kit" "^0.2.7" + "@eslint/config-array" "^0.20.0" + "@eslint/config-helpers" "^0.2.1" + "@eslint/core" "^0.14.0" + "@eslint/eslintrc" "^3.3.1" + "@eslint/js" "9.27.0" + "@eslint/plugin-kit" "^0.3.1" "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" "@humanwhocodes/retry" "^0.4.2" @@ -2276,10 +2224,10 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== -ignore@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" - integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== +ignore@^7.0.0: + version "7.0.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.4.tgz#a12c70d0f2607c5bf508fb65a40c75f037d7a078" + integrity sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A== import-fresh@^3.2.1: version "3.3.0" @@ -2304,13 +2252,6 @@ internmap@^1.0.0: resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== -invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -2378,12 +2319,12 @@ jiti@^1.21.6: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.7.tgz#9dd81043424a3d28458b193d965f0d18a2300ba9" integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== -jotai@2.12.2: - version "2.12.2" - resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.12.2.tgz#21626f5f019fc971b4da6bc528790a02d1448e3c" - integrity sha512-oN8715y7MkjXlSrpyjlR887TOuc/NLZMs9gvgtfWH/JP47ChwO0lR2ijSwBvPMYyXRAPT+liIAhuBavluKGgtA== +jotai@2.12.5: + version "2.12.5" + resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.12.5.tgz#2decdf41dfcad5e4afc7278da81c45646d4ae8ad" + integrity sha512-G8m32HW3lSmcz/4mbqx0hgJIQ0ekndKWiYP7kWVKi0p6saLXdSoye+FZiOFyonnd7Q482LCzm8sMDl7Ar1NWDw== -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: +js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -2457,13 +2398,6 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -loose-envify@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - lru-cache@^10.2.0: version "10.4.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" @@ -2483,10 +2417,10 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lucide-react@0.479.0: - version "0.479.0" - resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.479.0.tgz#7321f979a389ec5dd86747b2deb6444cf0922f8d" - integrity sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ== +lucide-react@0.511.0: + version "0.511.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.511.0.tgz#1dfef3065c725ac83f5fe0a6f02d1e0b0c36e641" + integrity sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w== merge2@^1.3.0: version "1.4.1" @@ -2567,6 +2501,11 @@ node-releases@^2.0.18: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -2794,12 +2733,12 @@ react-charts@3.0.0-beta.57: d3-time-format "^4.1.0" ts-toolbelt "^9.6.0" -react-dom@19.0.0: - version "19.0.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0.tgz#43446f1f01c65a4cd7f7588083e686a6726cfb57" - integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ== +react-dom@19.1.0: + version "19.1.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.0.tgz#133558deca37fa1d682708df8904b25186793623" + integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g== dependencies: - scheduler "^0.25.0" + scheduler "^0.26.0" react-minimal-pie-chart@9.1.0: version "9.1.0" @@ -2808,18 +2747,10 @@ react-minimal-pie-chart@9.1.0: dependencies: svg-partial-circle "^1.0.0" -react-refresh@^0.14.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" - integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== - -react-remove-scroll-bar@^2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" - integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== - dependencies: - react-style-singleton "^2.2.1" - tslib "^2.0.0" +react-refresh@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.17.0.tgz#b7e579c3657f23d04eccbe4ad2e58a8ed51e7e53" + integrity sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ== react-remove-scroll-bar@^2.3.7: version "2.3.8" @@ -2829,17 +2760,6 @@ react-remove-scroll-bar@^2.3.7: react-style-singleton "^2.2.2" tslib "^2.0.0" -react-remove-scroll@2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz#fb03a0845d7768a4f1519a99fdb84983b793dc07" - integrity sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ== - dependencies: - react-remove-scroll-bar "^2.3.6" - react-style-singleton "^2.2.1" - tslib "^2.1.0" - use-callback-ref "^1.3.0" - use-sidecar "^1.1.2" - react-remove-scroll@^2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz#df02cde56d5f2731e058531f8ffd7f9adec91ac2" @@ -2851,31 +2771,20 @@ react-remove-scroll@^2.6.3: use-callback-ref "^1.3.3" use-sidecar "^1.1.3" -react-router-dom@7.3.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.3.0.tgz#170e569b7536ffd71ff16016cb0f4458d1f23d83" - integrity sha512-z7Q5FTiHGgQfEurX/FBinkOXhWREJIAB2RiU24lvcBa82PxUpwqvs/PAXb9lJyPjTs2jrl6UkLvCZVGJPeNuuQ== +react-router-dom@7.6.1: + version "7.6.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.6.1.tgz#263c9102e96b58d336258a51d68080b40c28f526" + integrity sha512-vxU7ei//UfPYQ3iZvHuO1D/5fX3/JOqhNTbRR+WjSBWxf9bIvpWK+ftjmdfJHzPOuMQKe2fiEdG+dZX6E8uUpA== dependencies: - react-router "7.3.0" + react-router "7.6.1" -react-router@7.3.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.3.0.tgz#14fb630f088d919386e97f91199a2bc4abcdd85d" - integrity sha512-466f2W7HIWaNXTKM5nHTqNxLrHTyXybm7R0eBlVSt0k/u55tTCDO194OIx/NrYD4TS5SXKTNekXfT37kMKUjgw== +react-router@7.6.1: + version "7.6.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.6.1.tgz#a54f9b980b94594bcb4b7f26611612a9f6e17461" + integrity sha512-hPJXXxHJZEsPFNVbtATH7+MMX43UDeOauz+EAU4cgqTn7ojdI9qQORqS8Z0qmDlL1TclO/6jLRYUEtbWidtdHQ== dependencies: - "@types/cookie" "^0.6.0" cookie "^1.0.1" set-cookie-parser "^2.6.0" - turbo-stream "2.4.0" - -react-style-singleton@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" - integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== - dependencies: - get-nonce "^1.0.0" - invariant "^2.2.4" - tslib "^2.0.0" react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: version "2.2.3" @@ -2885,15 +2794,15 @@ react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: get-nonce "^1.0.0" tslib "^2.0.0" -react-xtermjs@^1.0.10: +react-xtermjs@1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/react-xtermjs/-/react-xtermjs-1.0.10.tgz#faac189f60ca599345b69b5c1a6b662f537df667" integrity sha512-+xpKEKbmsypWzRKE0FR1LNIGcI8gx+R6VMHe8IQW7iTbgeqp3Qg7SbiVNOzR+Ovb1QK4DPA3KqsIQV+XP0iRUA== -react@19.0.0: - version "19.0.0" - resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd" - integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ== +react@19.1.0: + version "19.1.0" + resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75" + integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg== read-cache@^1.0.0: version "1.0.0" @@ -2977,10 +2886,10 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -sanitize-html@2.14.0: - version "2.14.0" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.14.0.tgz#bd2a7b97ee1d86a7f0e0babf3a4468f639c3a429" - integrity sha512-CafX+IUPxZshXqqRaG9ZClSlfPVjSxI0td7n07hk8QO2oO+9JDnlcL8iM8TWeOXOIBFgIOx6zioTzM53AOMn3g== +sanitize-html@2.17.0: + version "2.17.0" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.17.0.tgz#a8f66420a6be981d8fe412e3397cc753782598e4" + integrity sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" @@ -2989,10 +2898,10 @@ sanitize-html@2.14.0: parse-srcset "^1.0.2" postcss "^8.3.11" -scheduler@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015" - integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA== +scheduler@^0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337" + integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA== semver@^6.3.1: version "6.3.1" @@ -3195,10 +3104,10 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -ts-api-utils@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.0.1.tgz#660729385b625b939aaa58054f45c058f33f10cd" - integrity sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w== +ts-api-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" + integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== ts-interface-checker@^0.1.9: version "0.1.13" @@ -3220,11 +3129,6 @@ tslib@^2.0.0, tslib@^2.1.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== -turbo-stream@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/turbo-stream/-/turbo-stream-2.4.0.tgz#1e4fca6725e90fa14ac4adb782f2d3759a5695f0" - integrity sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g== - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -3232,10 +3136,10 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -typescript@5.8.2: - version "5.8.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4" - integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ== +typescript@5.8.3: + version "5.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" + integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== update-browserslist-db@^1.1.0: version "1.1.0" @@ -3245,6 +3149,14 @@ update-browserslist-db@^1.1.0: escalade "^3.1.2" picocolors "^1.0.1" +update-browserslist-db@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -3252,13 +3164,6 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-callback-ref@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5" - integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w== - dependencies: - tslib "^2.0.0" - use-callback-ref@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf" @@ -3266,14 +3171,6 @@ use-callback-ref@^1.3.3: dependencies: tslib "^2.0.0" -use-sidecar@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" - integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== - dependencies: - detect-node-es "^1.1.0" - tslib "^2.0.0" - use-sidecar@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb" @@ -3282,11 +3179,6 @@ use-sidecar@^1.1.3: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@^1.2.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz#adbc795d8eeb47029963016cefdf89dc799fcebc" - integrity sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw== - util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" diff --git a/roadmap.md b/roadmap.md index 3d0983842..cadb5abf9 100644 --- a/roadmap.md +++ b/roadmap.md @@ -12,7 +12,8 @@ If you have an idea for Komodo, feel free to open an issue beginning with the `[ - **v1.15**: Support generic OIDC providers (including self-hosted) ✅ - **v1.16**: "Action" resource: Run requests on the Komodo API using snippets of typescript. ✅ - **v1.17**: Procedure Schedules: Run procedures / Actions at scheduled times, like CRON job. Connect to host terminals and exec into containers ✅ -- **v1.18**: Support "Swarm" resource - Manage docker swarms, attach Deployments / Stacks to "Swarm". -- **v1.19+**: Support "Cluster" resource - Manage Kubernetes cluster, can attach deployments to "Cluster" +- **v1.18**: Upgrade granular role based access control system ✅ +- **Undecided**: Support "Swarm" resource - Manage docker swarms, attach Deployments / Stacks to "Swarm". +- **Undecided**: Support "Cluster" resource - Manage Kubernetes cluster, can attach deployments to "Cluster" **Note. The specific versions associated with these features are not final.** \ No newline at end of file diff --git a/runfile.toml b/runfile.toml index 0fb5ab7db..936bfe85e 100644 --- a/runfile.toml +++ b/runfile.toml @@ -11,6 +11,11 @@ cmd = "KOMODO_CONFIG_PATH=.dev/core.config.toml cargo run -p komodo_core --relea description = "runs periphery --release pointing to .dev/periphery.config.toml" cmd = "PERIPHERY_CONFIG_PATH=.dev/periphery.config.toml cargo run -p komodo_periphery --release" +[dev-docsite] +description = "starts the documentation site (https://komo.do) in dev mode" +path = "docsite" +cmd = "yarn && yarn start" + [yarn-install] description = "downloads latest javacript dependencies for client and frontend" cmd = """ @@ -61,11 +66,6 @@ description = "builds and deploys dev.compose.yaml" cmd = """ docker compose -p komodo-dev -f dev.compose.yaml build""" -[dev-docsite] -description = "starts the documentation site (https://komo.do) in dev mode" -path = "docsite" -cmd = "yarn && yarn start" - [deploy-docsite] description = "deploys the documentation site (https://komo.do) to github pages" path = "docsite"