mirror of
https://github.com/moghtech/komodo.git
synced 2026-10-31 05:05:57 -05:00
Compare commits
77
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a5290e124 | ||
|
|
538e11ef3b | ||
|
|
9208e9bf86 | ||
|
|
31640e3df8 | ||
|
|
42751d1b90 | ||
|
|
673ab5c17d | ||
|
|
ad00f38758 | ||
|
|
b2459b5a64 | ||
|
|
5923dd0e22 | ||
|
|
51294edd73 | ||
|
|
baf77bf701 | ||
|
|
ddd99dc85d | ||
|
|
a4c902282d | ||
|
|
d7a2d4aadc | ||
|
|
c930468811 | ||
|
|
27efc81451 | ||
|
|
e44bd324f7 | ||
|
|
b64c7e7289 | ||
|
|
5c1a17fc91 | ||
|
|
50c3d77aed | ||
|
|
bba1e3ae0c | ||
|
|
fc26fede66 | ||
|
|
9cee723a1a | ||
|
|
b922e86d29 | ||
|
|
85dd3c0fe9 | ||
|
|
4e3ecf7e6c | ||
|
|
18ffe635c8 | ||
|
|
8758c9ff8b | ||
|
|
5da48ece82 | ||
|
|
9a48edc43f | ||
|
|
93430f6dc3 | ||
|
|
d893ae9241 | ||
|
|
24ed3cfcb1 | ||
|
|
36ee7ed72e | ||
|
|
5895fb843e | ||
|
|
98f53020fb | ||
|
|
1d10873e77 | ||
|
|
f5ee42029f | ||
|
|
525402f6f2 | ||
|
|
9f877cc188 | ||
|
|
69d6b8e6ec | ||
|
|
526bcc1907 | ||
|
|
f7badf5579 | ||
|
|
86fd7a155d | ||
|
|
628afc3baa | ||
|
|
04f944fcc2 | ||
|
|
f39447b513 | ||
|
|
612acd02ad | ||
|
|
90557ae514 | ||
|
|
0c292f7157 | ||
|
|
673ee05fe9 | ||
|
|
94b5bd6660 | ||
|
|
be40110004 | ||
|
|
2d4a34b820 | ||
|
|
2feb08725b | ||
|
|
161393805f | ||
|
|
f4b692080d | ||
|
|
e1c6c866a2 | ||
|
|
7106328390 | ||
|
|
338acba5c6 | ||
|
|
dcf6f2431e | ||
|
|
58b520e651 | ||
|
|
8be0846733 | ||
|
|
5b651f2527 | ||
|
|
026ba4fca8 | ||
|
|
4bca07d4ba | ||
|
|
9091f14711 | ||
|
|
8b78bb838e | ||
|
|
d384f7ca78 | ||
|
|
48475b55d5 | ||
|
|
64d7959c75 | ||
|
|
d1856c2325 | ||
|
|
61b7fcdb0a | ||
|
|
2693f13829 | ||
|
|
7fea06861b | ||
|
|
abd155193a | ||
|
|
7a4de0343d |
@@ -0,0 +1,123 @@
|
||||
# Komodo AI Coding Instructions
|
||||
|
||||
## Project Overview
|
||||
Komodo is a distributed system for building and deploying software across multiple servers. It consists of:
|
||||
- **Core** (`bin/core`): Central Rust/Axum API server managing resources, scheduling, monitoring
|
||||
- **Periphery** (`bin/periphery`): Rust agent deployed to managed servers, executes docker/git operations
|
||||
- **Frontend** (`frontend`): React/TypeScript SPA using TanStack Query and shadcn/ui components
|
||||
- **Client Libraries** (`client/core/{rs,ts}`): Type-safe API clients generated from shared entity definitions
|
||||
|
||||
## Architecture Fundamentals
|
||||
|
||||
### Core ↔ Periphery Communication
|
||||
- Core and Periphery communicate via **WebSocket connections** that can be initiated from either direction
|
||||
- Periphery can connect outbound to Core (common) or Core can connect inbound to Periphery (less common)
|
||||
- Connection management is in `bin/core/src/connection` and `bin/periphery/src/connection`
|
||||
- Use `state::periphery_connections()` in Core to send requests to Periphery agents
|
||||
|
||||
### Type Generation Pipeline
|
||||
**Critical**: Rust entities are the source of truth for all types across the stack:
|
||||
1. Entities defined in `client/core/rs/src/entities/` with `#[typeshare]` macro
|
||||
2. Run `node ./client/core/ts/generate_types.mjs` (uses typeshare CLI) → generates `client/core/ts/src/types.ts`
|
||||
3. TypeScript client is built and linked to frontend via `yarn link komodo_client`
|
||||
4. Frontend imports types: `import { Types } from "komodo_client"`
|
||||
|
||||
**When modifying entities**: Always run the `gen-client` (alias: `gc`) runfile task to regenerate TypeScript types.
|
||||
|
||||
### API Pattern (Resolver-Based)
|
||||
Core and Periphery use `mogh_resolver::Resolve` trait for RPC-style endpoints:
|
||||
- Request enums in `client/*/rs/src/api/` implement `Resolve<Response = T, Error = E>`
|
||||
- Core handlers in `bin/core/src/api/{read,write,execute}/*.rs`
|
||||
- Periphery handlers in `bin/periphery/src/api/*.rs`
|
||||
- Router maps `/{variant}` paths to enum variants automatically
|
||||
|
||||
Example request handling:
|
||||
```rust
|
||||
#[derive(Serialize, Deserialize, Resolve)]
|
||||
#[response(MyResponse)]
|
||||
#[error(anyhow::Error)]
|
||||
pub struct MyRequest { /* fields */ }
|
||||
```
|
||||
|
||||
### State Management
|
||||
Core maintains singleton state via `OnceLock` patterns in `bin/core/src/state.rs`:
|
||||
- `db_client()`: MongoDB connection
|
||||
- `periphery_connections()`: WebSocket connections to Periphery agents
|
||||
- `action_states()`, `build_states()`, etc.: In-memory caches using `CloneCache<K, V>`
|
||||
- Always use `state::*()` accessors, never instantiate directly
|
||||
|
||||
## Development Workflows
|
||||
|
||||
### Running Services Locally
|
||||
```bash
|
||||
# Start all local services (MongoDB, Core, Periphery, Frontend)
|
||||
run dc # dev-core: runs Core in release mode with .dev/core.config.toml
|
||||
run dp # dev-periphery: runs Periphery with .dev/periphery.config.toml
|
||||
|
||||
# Or use docker-compose for full stack
|
||||
run dev-compose # deploys dev.compose.yaml with ferretdb
|
||||
```
|
||||
|
||||
### Frontend Development
|
||||
```bash
|
||||
cd frontend && yarn dev # Vite dev server on port 5173
|
||||
|
||||
# After changing Rust entities, regenerate client:
|
||||
run gc # gen-client: typeshare → build TS client → copy to frontend/public/client
|
||||
```
|
||||
|
||||
### Building & Deploying
|
||||
- Deno scripts in `action/` automate version bumps and deployments
|
||||
- `run dk` (deploy-komodo): Full build and deploy via Komodo Action
|
||||
- `run bk` (build-komodo): Build binaries without deploying
|
||||
|
||||
## Code Conventions
|
||||
|
||||
### Error Handling
|
||||
- Use `anyhow::Result<T>` for application code
|
||||
- Use `serror` for serializable errors sent over API boundaries
|
||||
- Response helper: `response::Response::from(value)` for JSON responses
|
||||
- Never use `unwrap()` in production code paths; prefer `context()` for error messages
|
||||
|
||||
### Naming & Structure
|
||||
- **Entities**: CamelCase structs in `client/core/rs/src/entities/*/mod.rs`
|
||||
- **API requests**: CamelCase request types in `client/core/rs/src/api/*.rs`
|
||||
- **Config derivations**: Use `derive_builder::Builder` for config update structs
|
||||
- **Variants**: `derive_variants::EnumVariants` generates `TypeVariant` enums for discriminants
|
||||
|
||||
### Frontend Patterns
|
||||
- **API hooks**: `useRead("GetStack", { stack: "my-stack" })` for queries
|
||||
- **Mutations**: `useWrite("UpdateStack", { onSuccess: () => invalidate(...) })`
|
||||
- **Components**: Located in `frontend/src/components/`, use shadcn/ui primitives from `@ui/*`
|
||||
- **State**: Prefer TanStack Query for server state, Jotai atoms for local UI state
|
||||
|
||||
## Key Files & Directories
|
||||
|
||||
### Must-Read for Context
|
||||
- [readme.md](readme.md): Project overview and deployment info
|
||||
- [runfile.toml](runfile.toml): All development task definitions
|
||||
- [bin/core/src/state.rs](bin/core/src/state.rs): Core's global state singletons
|
||||
- [client/core/rs/src/entities/mod.rs](client/core/rs/src/entities/mod.rs): All entity definitions
|
||||
|
||||
### Common Touch Points
|
||||
- **Adding new resource type**: Update entities, Core API handlers, Frontend pages/components
|
||||
- **Changing API**: Modify request/response types in `client/core/rs/src/api/`, regenerate client
|
||||
- **Frontend features**: Usually start in `frontend/src/pages/`, add hooks in `frontend/src/lib/hooks/`
|
||||
- **Periphery capabilities**: Add to `periphery_client::api` enum and implement handler in `bin/periphery/src/api/`
|
||||
|
||||
## Configuration
|
||||
- Core config: `config/core.config.toml` (see `client/core/rs/src/entities/config/core.rs` for schema)
|
||||
- Periphery config: `config/periphery.config.toml`
|
||||
- Environment variable pattern: Settings support `_FILE` suffix for Docker secrets (see `lib/environment_file`)
|
||||
|
||||
## Testing & Validation
|
||||
- No comprehensive test suite currently
|
||||
- Validate changes by running locally with `run dc` / `run dp`
|
||||
- Check Core logs for startup errors (connection to DB, Periphery agents)
|
||||
- Frontend errors appear in browser console and TanStack Query devtools
|
||||
|
||||
## Common Pitfalls
|
||||
- **Forgetting to regenerate TS types** after Rust entity changes → frontend type errors
|
||||
- **Not using `state::*()` accessors** → panics from uninitialized `OnceLock`
|
||||
- **Breaking WebSocket message format** between Core/Periphery → connection failures
|
||||
- **Yarn link issues**: If frontend can't find `komodo_client`, re-run `run link-client`
|
||||
@@ -6,8 +6,4 @@ deno.lock
|
||||
.env.development
|
||||
.DS_Store
|
||||
.idea
|
||||
|
||||
/frontend/build
|
||||
/lib/ts_client/build
|
||||
|
||||
.dev
|
||||
|
||||
Generated
+926
-851
File diff suppressed because it is too large
Load Diff
+39
-56
@@ -8,7 +8,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "2.0.0-dev-102"
|
||||
version = "2.0.0-dev-112"
|
||||
edition = "2024"
|
||||
authors = ["mbecker20 <becker.maxh@gmail.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -22,106 +22,89 @@ strip = "debuginfo"
|
||||
# LOCAL
|
||||
komodo_client = { path = "client/core/rs" }
|
||||
periphery_client = { path = "client/periphery/rs" }
|
||||
environment_file = { path = "lib/environment_file" }
|
||||
environment = { path = "lib/environment" }
|
||||
interpolate = { path = "lib/interpolate" }
|
||||
secret_file = { path = "lib/secret_file" }
|
||||
validations = { path = "lib/validations" }
|
||||
formatting = { path = "lib/formatting" }
|
||||
rate_limit = { path = "lib/rate_limit" }
|
||||
transport = { path = "lib/transport" }
|
||||
database = { path = "lib/database" }
|
||||
encoding = { path = "lib/encoding" }
|
||||
response = { path = "lib/response" }
|
||||
command = { path = "lib/command" }
|
||||
config = { path = "lib/config" }
|
||||
logger = { path = "lib/logger" }
|
||||
cache = { path = "lib/cache" }
|
||||
noise = { path = "lib/noise" }
|
||||
git = { path = "lib/git" }
|
||||
|
||||
# MOGH
|
||||
serror = { version = "0.5.3", default-features = false }
|
||||
slack = { version = "2.0.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
|
||||
mogh_error = { version = "1.0.3", default-features = false }
|
||||
derive_default_builder = "0.1.8"
|
||||
derive_empty_traits = "0.1.0"
|
||||
async_timing_util = "1.1.0"
|
||||
partial_derive2 = "0.4.3"
|
||||
derive_variants = "1.0.0"
|
||||
mogh_auth_client = "1.2.1"
|
||||
mogh_auth_server = "1.2.4"
|
||||
mogh_secret_file = "1.0.0"
|
||||
mogh_validations = "1.0.0"
|
||||
mogh_rate_limit = "1.0.0"
|
||||
partial_derive2 = "0.4.5"
|
||||
mongo_indexed = "2.0.2"
|
||||
resolver_api = "3.0.0"
|
||||
mogh_resolver = "1.0.0"
|
||||
mogh_config = "1.0.1"
|
||||
mogh_logger = "1.3.0"
|
||||
mogh_server = "1.1.2"
|
||||
toml_pretty = "2.0.0"
|
||||
mogh_cache = "1.1.1"
|
||||
mogh_pki = "1.1.0"
|
||||
mungos = "3.2.2"
|
||||
svi = "1.2.0"
|
||||
|
||||
# ASYNC
|
||||
reqwest = { version = "0.12.24", default-features = false, features = ["json", "stream", "rustls-tls-native-roots"] }
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7.17", features = ["io", "codec"] }
|
||||
tokio-stream = { version = "0.1.17", features = ["sync"] }
|
||||
reqwest = { version = "0.13.1", default-features = false, features = ["json", "stream", "form", "query", "rustls"] }
|
||||
tokio = { version = "1.49.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7.18", features = ["io", "codec"] }
|
||||
tokio-stream = { version = "0.1.18", features = ["sync"] }
|
||||
pin-project-lite = "0.2.16"
|
||||
futures-util = "0.3.31"
|
||||
arc-swap = "1.7.1"
|
||||
arc-swap = "1.8.0"
|
||||
|
||||
# SERVER
|
||||
tokio-tungstenite = { version = "0.28.0", features = ["rustls-tls-native-roots"] }
|
||||
tower-http = { version = "0.6.8", features = ["fs", "cors", "set-header"] }
|
||||
axum = { version = "0.8.7", features = ["ws", "json", "macros"] }
|
||||
axum-extra = { version = "0.12.2", features = ["typed-header"] }
|
||||
axum-server = { version = "0.8.0", features = ["tls-rustls"] }
|
||||
tower-sessions = "0.14.0"
|
||||
axum = { version = "0.8.8", features = ["ws", "json", "macros"] }
|
||||
axum-extra = { version = "0.12.5", features = ["typed-header"] }
|
||||
|
||||
# OPENAPI
|
||||
utoipa-scalar = { version = "0.3.0", features = ["axum"] }
|
||||
utoipa = "5.4.0"
|
||||
|
||||
# SER/DE
|
||||
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
||||
indexmap = { version = "2.12.1", features = ["serde"] }
|
||||
indexmap = { version = "2.13.0", features = ["serde"] }
|
||||
serde = { version = "1.0.227", features = ["derive"] }
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
bson = { version = "2.15.0" } # must keep in sync with mongodb version
|
||||
toml = "0.9.11"
|
||||
serde_yaml_ng = "0.10.0"
|
||||
serde_json = "1.0.145"
|
||||
serde_json = "1.0.148"
|
||||
serde_qs = "0.15.0"
|
||||
toml = "0.9.8"
|
||||
url = "2.5.7"
|
||||
|
||||
# ERROR
|
||||
anyhow = "1.0.100"
|
||||
thiserror = "2.0.17"
|
||||
thiserror = "2.0.18"
|
||||
|
||||
# LOGGING
|
||||
opentelemetry-otlp = { version = "0.31.0", features = ["tls-roots", "reqwest-rustls"] }
|
||||
opentelemetry_sdk = { version = "0.31.0", features = ["rt-tokio"] }
|
||||
tracing-subscriber = { version = "0.3.22", features = ["json"] }
|
||||
opentelemetry-semantic-conventions = "0.31.0"
|
||||
tracing-opentelemetry = "0.32.0"
|
||||
opentelemetry = "0.31.0"
|
||||
tracing = "0.1.43"
|
||||
tracing = "0.1.44"
|
||||
|
||||
# CONFIG
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
clap = { version = "4.5.54", features = ["derive"] }
|
||||
dotenvy = "0.15.7"
|
||||
envy = "0.4.2"
|
||||
|
||||
# CRYPTO / AUTH
|
||||
webauthn-rs = { version = "0.5.4", features = ["danger-allow-state-serialisation"] }
|
||||
openidconnect = { version = "4.0.1", features = ["accept-rfc3339-timestamps"] }
|
||||
uuid = { version = "1.19.0", features = ["v4", "fast-rng", "serde"] }
|
||||
jsonwebtoken = { version = "10.2.0", features = ["aws_lc_rs"] } # locked back with octorust
|
||||
rustls = { version = "0.23.35", features = ["aws-lc-rs"] }
|
||||
pem-rfc7468 = { version = "1.0.0", features = ["alloc"] }
|
||||
totp-rs = { version = "5.7.0", features = ["qr"] }
|
||||
webauthn-rs-proto = "0.5.4"
|
||||
base64urlsafedata = "0.5.4"
|
||||
rustls = { version = "0.23.36", features = ["aws-lc-rs"] }
|
||||
data-encoding = "2.9.0"
|
||||
urlencoding = "2.1.3"
|
||||
bcrypt = "0.17.1"
|
||||
pkcs8 = "0.10.2"
|
||||
snow = "0.10.0"
|
||||
bcrypt = "0.18.0"
|
||||
hmac = "0.12.1"
|
||||
sha1 = "0.10.6"
|
||||
sha2 = "0.10.9"
|
||||
rand = "0.9.2"
|
||||
spki = "0.7.3"
|
||||
der = "0.7.10"
|
||||
hex = "0.4.3"
|
||||
|
||||
# SYSTEM
|
||||
@@ -129,26 +112,26 @@ hickory-resolver = "0.25.2"
|
||||
portable-pty = "0.9.0"
|
||||
shell-escape = "0.1.5"
|
||||
crossterm = "0.29.0"
|
||||
bollard = "0.19.4"
|
||||
bollard = "0.20.0"
|
||||
sysinfo = "0.37.1"
|
||||
shlex = "1.3.0"
|
||||
|
||||
# CLOUD
|
||||
aws-config = "1.8.12"
|
||||
aws-sdk-ec2 = "1.196.0"
|
||||
aws-sdk-ec2 = "1.200.0"
|
||||
aws-credential-types = "1.2.11"
|
||||
|
||||
## CRON
|
||||
english-to-cron = "0.1.7"
|
||||
chrono-tz = "0.10.4"
|
||||
chrono = "0.4.42"
|
||||
chrono = "0.4.43"
|
||||
croner = "3.0.1"
|
||||
|
||||
# MISC
|
||||
async-compression = { version = "0.4.35", features = ["tokio", "gzip"] }
|
||||
async-compression = { version = "0.4.37", features = ["tokio", "gzip"] }
|
||||
derive_builder = "0.20.2"
|
||||
comfy-table = "7.2.1"
|
||||
typeshare = "1.0.4"
|
||||
comfy-table = "7.2.2"
|
||||
typeshare = "1.0.5"
|
||||
wildcard = "0.3.0"
|
||||
colored = "3.0.0"
|
||||
bytes = "1.11.0"
|
||||
|
||||
+5
-5
@@ -14,12 +14,12 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# local
|
||||
environment_file.workspace = true
|
||||
komodo_client.workspace = true
|
||||
komodo_client = { workspace = true, features = ["cli"] }
|
||||
mogh_secret_file.workspace = true
|
||||
mogh_pki.workspace = true
|
||||
database.workspace = true
|
||||
config.workspace = true
|
||||
logger.workspace = true
|
||||
noise.workspace = true
|
||||
mogh_config.workspace = true
|
||||
mogh_logger.workspace = true
|
||||
# external
|
||||
futures-util.workspace = true
|
||||
comfy-table.workspace = true
|
||||
|
||||
@@ -358,6 +358,11 @@ async fn v1_downgrade(yes: bool) -> anyhow::Result<()> {
|
||||
.await
|
||||
.context("Failed to downgrade Server schema")?;
|
||||
|
||||
db.collection::<Document>("Deployment")
|
||||
.update_many(doc! {}, doc! { "$set": { "info": null } })
|
||||
.await
|
||||
.context("Failed to downgrade Deployment schema")?;
|
||||
|
||||
info!(
|
||||
"V1 Downgrade complete. Ready to downgrade to komodo-core:1 ✅"
|
||||
);
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::{path::PathBuf, sync::OnceLock};
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
use environment_file::maybe_read_item_from_file;
|
||||
use komodo_client::entities::{
|
||||
config::{
|
||||
DatabaseConfig,
|
||||
@@ -14,6 +13,7 @@ use komodo_client::entities::{
|
||||
},
|
||||
logger::LogConfig,
|
||||
};
|
||||
use mogh_secret_file::maybe_read_item_from_file;
|
||||
|
||||
pub fn cli_args() -> &'static CliArgs {
|
||||
static CLI_ARGS: OnceLock<CliArgs> = OnceLock::new();
|
||||
@@ -74,7 +74,7 @@ pub fn cli_config() -> &'static CliConfig {
|
||||
"Config File Keywords".dimmed(),
|
||||
);
|
||||
}
|
||||
let mut unparsed_config = (config::ConfigLoader {
|
||||
let mut unparsed_config = (mogh_config::ConfigLoader {
|
||||
paths: &config_paths
|
||||
.iter()
|
||||
.map(PathBuf::as_path)
|
||||
@@ -166,7 +166,7 @@ pub fn cli_config() -> &'static CliConfig {
|
||||
else {
|
||||
panic!("Profile config is not Object type.");
|
||||
};
|
||||
config::merge_config(
|
||||
mogh_config::merge_config(
|
||||
unparsed_config,
|
||||
profile_config.clone(),
|
||||
env.komodo_cli_merge_nested_config,
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ mod config;
|
||||
|
||||
async fn app() -> anyhow::Result<()> {
|
||||
dotenvy::dotenv().ok();
|
||||
logger::init(&config::cli_config().cli_logging)?;
|
||||
mogh_logger::init(&config::cli_config().cli_logging)?;
|
||||
let args = config::cli_args();
|
||||
let env = config::cli_env();
|
||||
let debug_load =
|
||||
@@ -65,7 +65,7 @@ async fn app() -> anyhow::Result<()> {
|
||||
command::terminal::handle_attach(attach).await
|
||||
}
|
||||
args::Command::Key { command } => {
|
||||
noise::key::command::handle(command).await
|
||||
mogh_pki::cli::handle(command, mogh_pki::PkiKind::Mutual).await
|
||||
}
|
||||
args::Command::Database { command } => {
|
||||
command::database::handle(command).await
|
||||
|
||||
+16
-21
@@ -15,50 +15,45 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# local
|
||||
komodo_client = { workspace = true, features = ["mongo"] }
|
||||
komodo_client = { workspace = true, features = ["core"] }
|
||||
periphery_client.workspace = true
|
||||
environment_file.workspace = true
|
||||
mogh_validations.workspace = true
|
||||
interpolate.workspace = true
|
||||
secret_file.workspace = true
|
||||
validations.workspace = true
|
||||
mogh_secret_file.workspace = true
|
||||
formatting.workspace = true
|
||||
rate_limit.workspace = true
|
||||
mogh_rate_limit.workspace = true
|
||||
transport.workspace = true
|
||||
database.workspace = true
|
||||
encoding.workspace = true
|
||||
response.workspace = true
|
||||
command.workspace = true
|
||||
config.workspace = true
|
||||
logger.workspace = true
|
||||
cache.workspace = true
|
||||
noise.workspace = true
|
||||
mogh_config.workspace = true
|
||||
mogh_logger.workspace = true
|
||||
mogh_cache.workspace = true
|
||||
mogh_pki.workspace = true
|
||||
git.workspace = true
|
||||
# mogh
|
||||
serror = { workspace = true, features = ["axum"] }
|
||||
mogh_error = { workspace = true, features = ["axum"] }
|
||||
mogh_auth_client = { workspace = true, features = ["utoipa"] }
|
||||
mogh_auth_server.workspace = true
|
||||
async_timing_util.workspace = true
|
||||
partial_derive2.workspace = true
|
||||
derive_variants.workspace = true
|
||||
resolver_api.workspace = true
|
||||
mogh_resolver.workspace = true
|
||||
mogh_server.workspace = true
|
||||
toml_pretty.workspace = true
|
||||
slack.workspace = true
|
||||
svi.workspace = true
|
||||
# external
|
||||
aws-credential-types.workspace = true
|
||||
english-to-cron.workspace = true
|
||||
tower-sessions.workspace = true
|
||||
openidconnect.workspace = true
|
||||
data-encoding.workspace = true
|
||||
serde_yaml_ng.workspace = true
|
||||
jsonwebtoken.workspace = true
|
||||
utoipa-scalar.workspace = true
|
||||
futures-util.workspace = true
|
||||
axum-server.workspace = true
|
||||
aws-sdk-ec2.workspace = true
|
||||
urlencoding.workspace = true
|
||||
webauthn-rs.workspace = true
|
||||
aws-config.workspace = true
|
||||
tokio-util.workspace = true
|
||||
axum-extra.workspace = true
|
||||
tower-http.workspace = true
|
||||
serde_json.workspace = true
|
||||
typeshare.workspace = true
|
||||
chrono-tz.workspace = true
|
||||
@@ -70,12 +65,12 @@ colored.workspace = true
|
||||
tracing.workspace = true
|
||||
reqwest.workspace = true
|
||||
dotenvy.workspace = true
|
||||
totp-rs.workspace = true
|
||||
anyhow.workspace = true
|
||||
bcrypt.workspace = true
|
||||
croner.workspace = true
|
||||
chrono.workspace = true
|
||||
bcrypt.workspace = true
|
||||
rustls.workspace = true
|
||||
utoipa.workspace = true
|
||||
bytes.workspace = true
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -18,7 +18,7 @@ RUN cargo build -p komodo_core --release && \
|
||||
cargo strip
|
||||
|
||||
# Build Frontend
|
||||
FROM node:20.12-alpine AS frontend-builder
|
||||
FROM node:22.12-alpine AS frontend-builder
|
||||
WORKDIR /builder
|
||||
COPY ./frontend ./frontend
|
||||
COPY ./client/core/ts ./client
|
||||
|
||||
@@ -7,7 +7,7 @@ ARG BINARIES_IMAGE=ghcr.io/moghtech/komodo-binaries:latest
|
||||
FROM ${BINARIES_IMAGE} AS binaries
|
||||
|
||||
# Build Frontend
|
||||
FROM node:20.12-alpine AS frontend-builder
|
||||
FROM node:22.12-alpine AS frontend-builder
|
||||
WORKDIR /builder
|
||||
COPY ./frontend ./frontend
|
||||
COPY ./client/core/ts ./client
|
||||
|
||||
@@ -16,6 +16,24 @@ pub async fn send_alert(
|
||||
"{level} | If you see this message, then Alerter **{name}** is **working**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::SwarmUnhealthy { id, name, err } => {
|
||||
let link = resource_link(ResourceTargetVariant::Swarm, id);
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
format!("{level} | Swarm **{name}** is now **healthy**\n{link}")
|
||||
}
|
||||
SeverityLevel::Critical => {
|
||||
let err = err
|
||||
.as_ref()
|
||||
.map(|e| format!("\n**error**: {e}"))
|
||||
.unwrap_or_default();
|
||||
format!(
|
||||
"{level} | Swarm **{name}** is **unhealthy** ❌\n{link}{err}"
|
||||
)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
AlertData::ServerVersionMismatch {
|
||||
id,
|
||||
name,
|
||||
@@ -108,6 +126,8 @@ pub async fn send_alert(
|
||||
AlertData::ContainerStateChange {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
@@ -115,37 +135,64 @@ pub async fn send_alert(
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
let to = fmt_docker_container_state(to);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: **{swarm}**")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: **{server}**")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"📦 Deployment **{name}** is now **{to}**\nserver: **{server_name}**\nprevious: **{from}**\n{link}"
|
||||
"📦 Deployment **{name}** is now **{to}**{target}\nprevious: **{from}**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::DeploymentImageUpdateAvailable {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: **{swarm}**")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: **{server}**")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"⬆ Deployment **{name}** has an update available\nserver: **{server_name}**\nimage: **{image}**\n{link}"
|
||||
"⬆ Deployment **{name}** has an update available{target}\nimage: **{image}**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::DeploymentAutoUpdated {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: **{swarm}**")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: **{server}**")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"⬆ Deployment **{name}** was updated automatically ⏫\nserver: **{server_name}**\nimage: **{image}**\n{link}"
|
||||
"⬆ Deployment **{name}** was updated automatically ⏫{target}\nimage: **{image}**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::StackStateChange {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
@@ -153,26 +200,44 @@ pub async fn send_alert(
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Stack, id);
|
||||
let to = fmt_stack_state(to);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: **{swarm}**")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: **{server}**")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"🥞 Stack **{name}** is now {to}\nserver: **{server_name}**\nprevious: **{from}**\n{link}"
|
||||
"🥞 Stack **{name}** is now {to}{target}\nprevious: **{from}**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::StackImageUpdateAvailable {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
service,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Stack, id);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: **{swarm}**")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: **{server}**")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"⬆ Stack **{name}** has an update available\nserver: **{server_name}**\nservice: **{service}**\nimage: **{image}**\n{link}"
|
||||
"⬆ Stack **{name}** has an update available{target}\nservice: **{service}**\nimage: **{image}**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::StackAutoUpdated {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
images,
|
||||
@@ -181,8 +246,15 @@ pub async fn send_alert(
|
||||
let images_label =
|
||||
if images.len() > 1 { "images" } else { "image" };
|
||||
let images = images.join(", ");
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: **{swarm}**")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: **{server}**")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"⬆ Stack **{name}** was updated automatically ⏫\nserver: **{server_name}**\n{images_label}: **{images}**\n{link}"
|
||||
"⬆ Stack **{name}** was updated automatically ⏫{target}\n{images_label}: **{images}**\n{link}"
|
||||
)
|
||||
}
|
||||
AlertData::AwsBuilderTerminationFailed {
|
||||
@@ -264,7 +336,7 @@ pub async fn send_alert(
|
||||
let sanitized_error =
|
||||
svi::replace_in_string(&format!("{e:?}"), &replacers);
|
||||
anyhow::Error::msg(format!(
|
||||
"Error with slack request: {sanitized_error}"
|
||||
"Error with request to Discord: {sanitized_error}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
+79
-10
@@ -1,6 +1,5 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::{find::find_collect, mongodb::bson::doc};
|
||||
use derive_variants::ExtractVariant;
|
||||
use futures_util::future::join_all;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::entities::{
|
||||
@@ -81,14 +80,14 @@ pub async fn send_alert_to_alerter(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let alert_type = alert.data.extract_variant();
|
||||
let alert_variant: AlertDataVariant = (&alert.data).into();
|
||||
|
||||
// In the test case, we don't want the filters inside this
|
||||
// block to stop the test from being sent to the alerting endpoint.
|
||||
if alert_type != AlertDataVariant::Test {
|
||||
if alert_variant != AlertDataVariant::Test {
|
||||
// Don't send if alert type not configured on the alerter
|
||||
if !alerter.config.alert_types.is_empty()
|
||||
&& !alerter.config.alert_types.contains(&alert_type)
|
||||
&& !alerter.config.alert_types.contains(&alert_variant)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
@@ -252,6 +251,22 @@ fn standard_alert_content(alert: &Alert) -> String {
|
||||
"{level} | If you see this message, then Alerter {name} is working\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::SwarmUnhealthy { id, name, err } => {
|
||||
let link = resource_link(ResourceTargetVariant::Swarm, id);
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
format!("{level} | Swarm {name} is now healthy\n{link}")
|
||||
}
|
||||
SeverityLevel::Critical => {
|
||||
let err = err
|
||||
.as_ref()
|
||||
.map(|e| format!("\nerror: {e}"))
|
||||
.unwrap_or_default();
|
||||
format!("{level} | Swarm {name} is unhealthy ❌\n{link}{err}")
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
AlertData::ServerVersionMismatch {
|
||||
id,
|
||||
name,
|
||||
@@ -342,6 +357,8 @@ fn standard_alert_content(alert: &Alert) -> String {
|
||||
AlertData::ContainerStateChange {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
@@ -349,37 +366,64 @@ fn standard_alert_content(alert: &Alert) -> String {
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
let to_state = fmt_docker_container_state(to);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: {swarm}")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: {server}")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"📦Deployment {name} is now {to_state}\nserver: {server_name}\nprevious: {from}\n{link}",
|
||||
"📦Deployment {name} is now {to_state}{target}\nprevious: {from}\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::DeploymentImageUpdateAvailable {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: {swarm}")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: {server}")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"⬆ Deployment {name} has an update available\nserver: {server_name}\nimage: {image}\n{link}",
|
||||
"⬆ Deployment {name} has an update available{target}\nimage: {image}\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::DeploymentAutoUpdated {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Deployment, id);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: {swarm}")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: {server}")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"⬆ Deployment {name} was updated automatically\nserver: {server_name}\nimage: {image}\n{link}",
|
||||
"⬆ Deployment {name} was updated automatically{target}\nimage: {image}\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::StackStateChange {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
@@ -387,26 +431,44 @@ fn standard_alert_content(alert: &Alert) -> String {
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Stack, id);
|
||||
let to_state = fmt_stack_state(to);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: {swarm}")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: {server}")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"🥞 Stack {name} is now {to_state}\nserver: {server_name}\nprevious: {from}\n{link}",
|
||||
"🥞 Stack {name} is now {to_state}{target}\nprevious: {from}\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::StackImageUpdateAvailable {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
service,
|
||||
image,
|
||||
} => {
|
||||
let link = resource_link(ResourceTargetVariant::Stack, id);
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: {swarm}")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: {server}")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"⬆ Stack {name} has an update available\nserver: {server_name}\nservice: {service}\nimage: {image}\n{link}",
|
||||
"⬆ Stack {name} has an update available{target}\nservice: {service}\nimage: {image}\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::StackAutoUpdated {
|
||||
id,
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
images,
|
||||
@@ -415,8 +477,15 @@ fn standard_alert_content(alert: &Alert) -> String {
|
||||
let images_label =
|
||||
if images.len() > 1 { "images" } else { "image" };
|
||||
let images_str = images.join(", ");
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("\nswarm: {swarm}")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("\nserver: {server}")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
format!(
|
||||
"⬆ Stack {name} was updated automatically ⏫\nserver: {server_name}\n{images_label}: {images_str}\n{link}",
|
||||
"⬆ Stack {name} was updated automatically ⏫{target}\n{images_label}: {images_str}\n{link}",
|
||||
)
|
||||
}
|
||||
AlertData::AwsBuilderTerminationFailed {
|
||||
|
||||
@@ -31,7 +31,7 @@ pub async fn send_alert(
|
||||
let sanitized_error =
|
||||
svi::replace_in_string(&format!("{e:?}"), &replacers);
|
||||
anyhow::Error::msg(format!(
|
||||
"Error with slack request: {sanitized_error}"
|
||||
"Error with request to Ntfy: {sanitized_error}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ pub async fn send_alert(
|
||||
let sanitized_error =
|
||||
svi::replace_in_string(&format!("{e:?}"), &replacers);
|
||||
anyhow::Error::msg(format!(
|
||||
"Error with slack request: {sanitized_error}"
|
||||
"Error with request to Pushover: {sanitized_error}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
+105
-24
@@ -24,6 +24,41 @@ pub async fn send_alert(
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
AlertData::SwarmUnhealthy { id, name, err } => {
|
||||
match alert.level {
|
||||
SeverityLevel::Ok => {
|
||||
let text =
|
||||
format!("{level} | Swarm *{name}* is now *healthy*");
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"Swarm *{name}* is now *healthy*"
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
SeverityLevel::Critical => {
|
||||
let text =
|
||||
format!("{level} | Swarm *{name}* is *unhealthy* ❌");
|
||||
let err = err
|
||||
.as_ref()
|
||||
.map(|e| format!("\nerror: {e}"))
|
||||
.unwrap_or_default();
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"Swarm *{name}* is *unhealthy* ❌{err}"
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Server,
|
||||
id,
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
AlertData::ServerVersionMismatch {
|
||||
id,
|
||||
name,
|
||||
@@ -67,7 +102,7 @@ pub async fn send_alert(
|
||||
let blocks = vec![
|
||||
Block::header(level),
|
||||
Block::section(format!(
|
||||
"*{name}*{region} is now *connnected*"
|
||||
"*{name}*{region} is now *connected*"
|
||||
)),
|
||||
];
|
||||
(text, blocks.into())
|
||||
@@ -239,19 +274,26 @@ pub async fn send_alert(
|
||||
}
|
||||
AlertData::ContainerStateChange {
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
to,
|
||||
id,
|
||||
..
|
||||
} => {
|
||||
let to = fmt_docker_container_state(to);
|
||||
let text = format!("📦 Container *{name}* is now *{to}*");
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("swarm: *{swarm}*\n")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("server: *{server}*\n")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!(
|
||||
"server: {server_name}\nprevious: {from}",
|
||||
)),
|
||||
Block::section(format!("{target}previous: {from}",)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Deployment,
|
||||
id,
|
||||
@@ -262,17 +304,24 @@ pub async fn send_alert(
|
||||
AlertData::DeploymentImageUpdateAvailable {
|
||||
id,
|
||||
name,
|
||||
server_name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
image,
|
||||
} => {
|
||||
let text =
|
||||
format!("⬆ Deployment *{name}* has an update available");
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("swarm: *{swarm}*\n")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("server: *{server}*\n")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!(
|
||||
"server: *{server_name}*\nimage: *{image}*",
|
||||
)),
|
||||
Block::section(format!("{target}image: *{image}*",)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Deployment,
|
||||
id,
|
||||
@@ -283,17 +332,24 @@ pub async fn send_alert(
|
||||
AlertData::DeploymentAutoUpdated {
|
||||
id,
|
||||
name,
|
||||
server_name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
image,
|
||||
} => {
|
||||
let text =
|
||||
format!("⬆ Deployment *{name}* was updated automatically ⏫");
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("swarm: *{swarm}*\n")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("server: *{server}*\n")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!(
|
||||
"server: *{server_name}*\nimage: *{image}*",
|
||||
)),
|
||||
Block::section(format!("{target}image: *{image}*",)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Deployment,
|
||||
id,
|
||||
@@ -303,19 +359,26 @@ pub async fn send_alert(
|
||||
}
|
||||
AlertData::StackStateChange {
|
||||
name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
from,
|
||||
to,
|
||||
id,
|
||||
..
|
||||
} => {
|
||||
let to = fmt_stack_state(to);
|
||||
let text = format!("🥞 Stack *{name}* is now *{to}*");
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("swarm: *{swarm}*\n")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("server: *{server}*\n")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!(
|
||||
"server: *{server_name}*\nprevious: *{from}*",
|
||||
)),
|
||||
Block::section(format!("{target}previous: *{from}*",)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Stack,
|
||||
id,
|
||||
@@ -326,16 +389,25 @@ pub async fn send_alert(
|
||||
AlertData::StackImageUpdateAvailable {
|
||||
id,
|
||||
name,
|
||||
server_name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
service,
|
||||
image,
|
||||
} => {
|
||||
let text = format!("⬆ Stack *{name}* has an update available");
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("swarm: *{swarm}*\n")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("server: *{server}*\n")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!(
|
||||
"server: *{server_name}*\nservice: *{service}*\nimage: *{image}*",
|
||||
"{target}service: *{service}*\nimage: *{image}*",
|
||||
)),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Stack,
|
||||
@@ -347,8 +419,10 @@ pub async fn send_alert(
|
||||
AlertData::StackAutoUpdated {
|
||||
id,
|
||||
name,
|
||||
server_name,
|
||||
swarm_id: _swarm_id,
|
||||
swarm_name,
|
||||
server_id: _server_id,
|
||||
server_name,
|
||||
images,
|
||||
} => {
|
||||
let text =
|
||||
@@ -356,11 +430,18 @@ pub async fn send_alert(
|
||||
let images_label =
|
||||
if images.len() > 1 { "images" } else { "image" };
|
||||
let images = images.join(", ");
|
||||
let target = if let Some(swarm) = swarm_name {
|
||||
format!("swarm: *{swarm}*\n")
|
||||
} else if let Some(server) = server_name {
|
||||
format!("server: *{server}*\n")
|
||||
} else {
|
||||
format!("")
|
||||
};
|
||||
let blocks = vec![
|
||||
Block::header(text.clone()),
|
||||
Block::section(format!(
|
||||
"server: *{server_name}*\n{images_label}: *{images}*",
|
||||
)),
|
||||
Block::section(
|
||||
format!("{target}{images_label}: *{images}*",),
|
||||
),
|
||||
Block::section(resource_link(
|
||||
ResourceTargetVariant::Stack,
|
||||
id,
|
||||
@@ -491,7 +572,7 @@ pub async fn send_alert(
|
||||
let sanitized_error =
|
||||
svi::replace_in_string(&format!("{e:?}"), &replacers);
|
||||
anyhow::Error::msg(format!(
|
||||
"Error with slack request: {sanitized_error}"
|
||||
"Error with request to Slack: {sanitized_error}"
|
||||
))
|
||||
})?;
|
||||
Ok(())
|
||||
|
||||
@@ -1,353 +0,0 @@
|
||||
use std::{
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::OnceLock,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{ConnectInfo, Path},
|
||||
http::HeaderMap,
|
||||
routing::post,
|
||||
};
|
||||
use data_encoding::BASE32_NOPAD;
|
||||
use database::{
|
||||
bson::{doc, to_bson},
|
||||
mungos::by_id::update_one_by_id,
|
||||
};
|
||||
use derive_variants::{EnumVariants, ExtractVariant};
|
||||
use komodo_client::{api::auth::*, entities::user::User};
|
||||
use rate_limit::WithFailureRateLimit;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use response::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serror::{AddStatusCode, AddStatusCodeError, Json};
|
||||
use tower_sessions::Session;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
auth::{
|
||||
SessionPasskeyLogin, SessionTotpLogin, SessionUserId,
|
||||
get_user_id_from_headers,
|
||||
github::{self, client::github_oauth_client},
|
||||
google::{self, client::google_oauth_client},
|
||||
oidc::{self, client::oidc_client},
|
||||
totp::make_totp,
|
||||
},
|
||||
config::core_config,
|
||||
helpers::query::get_user,
|
||||
state::{auth_rate_limiter, db_client, jwt_client, webauthn},
|
||||
};
|
||||
|
||||
use super::Variant;
|
||||
|
||||
pub struct AuthArgs {
|
||||
pub headers: HeaderMap,
|
||||
/// Prefer extracting IP from headers.
|
||||
/// This IP will be the IP of reverse proxy itself.
|
||||
pub ip: IpAddr,
|
||||
/// Per-client session state
|
||||
pub session: Option<Session>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
|
||||
)]
|
||||
#[args(AuthArgs)]
|
||||
#[response(Response)]
|
||||
#[error(serror::Error)]
|
||||
#[variant_derive(Debug)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
#[allow(clippy::enum_variant_names, clippy::large_enum_variant)]
|
||||
pub enum AuthRequest {
|
||||
GetLoginOptions(GetLoginOptions),
|
||||
SignUpLocalUser(SignUpLocalUser),
|
||||
LoginLocalUser(LoginLocalUser),
|
||||
ExchangeForJwt(ExchangeForJwt),
|
||||
CompleteTotpLogin(CompleteTotpLogin),
|
||||
CompletePasskeyLogin(CompletePasskeyLogin),
|
||||
GetUser(GetUser),
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
let mut router = Router::new()
|
||||
.route("/", post(handler))
|
||||
.route("/{variant}", post(variant_handler));
|
||||
|
||||
if core_config().local_auth {
|
||||
info!("🔑 Local Login Enabled");
|
||||
}
|
||||
|
||||
if github_oauth_client().is_some() {
|
||||
info!("🔑 Github Login Enabled");
|
||||
router = router.nest("/github", github::router())
|
||||
}
|
||||
|
||||
if google_oauth_client().is_some() {
|
||||
info!("🔑 Google Login Enabled");
|
||||
router = router.nest("/google", google::router())
|
||||
}
|
||||
|
||||
if core_config().oidc_enabled {
|
||||
info!("🔑 OIDC Login Enabled");
|
||||
router = router.nest("/oidc", oidc::router())
|
||||
}
|
||||
|
||||
router
|
||||
}
|
||||
|
||||
async fn variant_handler(
|
||||
headers: HeaderMap,
|
||||
session: Session,
|
||||
info: ConnectInfo<SocketAddr>,
|
||||
Path(Variant { variant }): Path<Variant>,
|
||||
Json(params): Json<serde_json::Value>,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
let req: AuthRequest = serde_json::from_value(json!({
|
||||
"type": variant,
|
||||
"params": params,
|
||||
}))?;
|
||||
handler(headers, session, info, Json(req)).await
|
||||
}
|
||||
|
||||
async fn handler(
|
||||
headers: HeaderMap,
|
||||
session: Session,
|
||||
ConnectInfo(info): ConnectInfo<SocketAddr>,
|
||||
Json(request): Json<AuthRequest>,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
let timer = Instant::now();
|
||||
let req_id = Uuid::new_v4();
|
||||
debug!(
|
||||
"/auth request {req_id} | METHOD: {:?}",
|
||||
request.extract_variant()
|
||||
);
|
||||
let res = request
|
||||
.resolve(&AuthArgs {
|
||||
headers,
|
||||
ip: info.ip(),
|
||||
session: Some(session),
|
||||
})
|
||||
.await;
|
||||
if let Err(e) = &res {
|
||||
debug!("/auth request {req_id} | error: {:#}", e.error);
|
||||
}
|
||||
let elapsed = timer.elapsed();
|
||||
debug!("/auth request {req_id} | resolve time: {elapsed:?}");
|
||||
res.map(|res| res.0)
|
||||
}
|
||||
|
||||
fn login_options_reponse() -> &'static GetLoginOptionsResponse {
|
||||
static GET_LOGIN_OPTIONS_RESPONSE: OnceLock<
|
||||
GetLoginOptionsResponse,
|
||||
> = OnceLock::new();
|
||||
GET_LOGIN_OPTIONS_RESPONSE.get_or_init(|| {
|
||||
let config = core_config();
|
||||
GetLoginOptionsResponse {
|
||||
local: config.local_auth,
|
||||
github: github_oauth_client().is_some(),
|
||||
google: google_oauth_client().is_some(),
|
||||
oidc: oidc_client().load().is_some(),
|
||||
registration_disabled: config.disable_user_registration,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl Resolve<AuthArgs> for GetLoginOptions {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &AuthArgs,
|
||||
) -> serror::Result<GetLoginOptionsResponse> {
|
||||
Ok(*login_options_reponse())
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<AuthArgs> for ExchangeForJwt {
|
||||
async fn resolve(
|
||||
self,
|
||||
AuthArgs {
|
||||
headers,
|
||||
ip,
|
||||
session,
|
||||
}: &AuthArgs,
|
||||
) -> serror::Result<ExchangeForJwtResponse> {
|
||||
async {
|
||||
let session = session.as_ref().context(
|
||||
"Method called in invalid context. This should not happen",
|
||||
)?;
|
||||
|
||||
let SessionUserId(user_id) = session
|
||||
.remove(SessionUserId::KEY)
|
||||
.await
|
||||
.context("Internal session type error")?
|
||||
.context("Authentication steps must be completed before JWT can be retrieved")?;
|
||||
|
||||
jwt_client().encode(user_id).map_err(Into::into)
|
||||
}
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(*ip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<AuthArgs> for CompleteTotpLogin {
|
||||
async fn resolve(
|
||||
self,
|
||||
AuthArgs {
|
||||
headers,
|
||||
ip,
|
||||
session,
|
||||
}: &AuthArgs,
|
||||
) -> serror::Result<CompleteTotpLoginResponse> {
|
||||
async {
|
||||
let session = session.as_ref().context(
|
||||
"Method called in invalid context. This should not happen",
|
||||
)?;
|
||||
|
||||
let SessionTotpLogin { user_id } = session
|
||||
.get(SessionTotpLogin::KEY)
|
||||
.await
|
||||
.context("Internal session type error")?
|
||||
.context(
|
||||
"Totp login has not been initiated for this session",
|
||||
)?;
|
||||
|
||||
let user = get_user(&user_id)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
if user.totp.secret.is_empty() {
|
||||
return Err(
|
||||
anyhow!("User is not enrolled in totp")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
}
|
||||
|
||||
let secret_bytes = BASE32_NOPAD
|
||||
.decode(user.totp.secret.as_bytes())
|
||||
.context("Failed to decode totp secret to bytes")?;
|
||||
|
||||
let totp = make_totp(secret_bytes, None)?;
|
||||
|
||||
let valid = totp
|
||||
.check_current(&self.code)
|
||||
.context("Failed to check TOTP code validity")?;
|
||||
|
||||
if !valid {
|
||||
return Err(
|
||||
anyhow!("Invalid totp code")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
);
|
||||
}
|
||||
|
||||
jwt_client().encode(user_id).map_err(Into::into)
|
||||
}
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(*ip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<AuthArgs> for CompletePasskeyLogin {
|
||||
async fn resolve(
|
||||
self,
|
||||
AuthArgs {
|
||||
headers,
|
||||
ip,
|
||||
session,
|
||||
}: &AuthArgs,
|
||||
) -> serror::Result<CompletePasskeyLoginResponse> {
|
||||
async {
|
||||
let session = session.as_ref().context(
|
||||
"Method called in invalid context. This should not happen",
|
||||
)?;
|
||||
|
||||
let webauthn = webauthn().context(
|
||||
"No webauthn provider available, invalid KOMODO_HOST config",
|
||||
)?;
|
||||
|
||||
let SessionPasskeyLogin { user_id, state } = session
|
||||
.get(SessionPasskeyLogin::KEY)
|
||||
.await
|
||||
.context("Internal session type error")?
|
||||
.context(
|
||||
"Passkey login has not been initiated for this session",
|
||||
)?;
|
||||
|
||||
// The result of this call must be used to
|
||||
// update the stored passkey info on database.
|
||||
let update = webauthn
|
||||
.finish_passkey_authentication(&self.credential, &state)
|
||||
.context("Failed to validate passkey")?;
|
||||
|
||||
let mut passkey = get_user(&user_id)
|
||||
.await?
|
||||
.passkey
|
||||
.passkey
|
||||
.context("Could not find passkey on database.")?;
|
||||
|
||||
passkey.update_credential(&update);
|
||||
|
||||
let passkey = to_bson(&passkey)
|
||||
.context("Failed to serialize passkey to BSON")?;
|
||||
|
||||
let update = doc! { "$set": { "passkey.passkey": passkey } };
|
||||
|
||||
let _ =
|
||||
update_one_by_id(&db_client().users, &user_id, update, None)
|
||||
.await
|
||||
.context(
|
||||
"Failed to update user passkey on database after login",
|
||||
)
|
||||
.inspect_err(|e| warn!("{e:#}"));
|
||||
|
||||
jwt_client().encode(user_id).map_err(Into::into)
|
||||
}
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(*ip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<AuthArgs> for GetUser {
|
||||
async fn resolve(
|
||||
self,
|
||||
AuthArgs {
|
||||
headers,
|
||||
ip,
|
||||
session: _,
|
||||
}: &AuthArgs,
|
||||
) -> serror::Result<User> {
|
||||
async {
|
||||
let user_id = get_user_id_from_headers(headers)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
let mut user = get_user(&user_id)
|
||||
.await
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
// Sanitize before sending to client.
|
||||
user.sanitize();
|
||||
Ok(user)
|
||||
}
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(*ip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,12 @@ use std::{
|
||||
|
||||
use anyhow::Context;
|
||||
use command::run_komodo_standard_command;
|
||||
use config::merge_objects;
|
||||
use database::mungos::{
|
||||
by_id::update_one_by_id, mongodb::bson::to_document,
|
||||
};
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
api::{
|
||||
execute::{BatchExecutionResponse, BatchRunAction, RunAction},
|
||||
user::{CreateApiKey, CreateApiKeyResponse, DeleteApiKey},
|
||||
},
|
||||
api::execute::{BatchExecutionResponse, BatchRunAction, RunAction},
|
||||
entities::{
|
||||
FileFormat, JsonObject,
|
||||
action::Action,
|
||||
@@ -29,12 +25,20 @@ use komodo_client::{
|
||||
},
|
||||
parsers::parse_key_value_list,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_auth_client::api::manage::{
|
||||
CreateApiKey, CreateApiKeyResponse,
|
||||
};
|
||||
use mogh_auth_server::api::manage::api_key::{
|
||||
create_api_key, delete_api_key,
|
||||
};
|
||||
use mogh_config::merge_objects;
|
||||
use mogh_resolver::Resolve;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
api::{execute::ExecuteRequest, user::UserArgs},
|
||||
api::execute::ExecuteRequest,
|
||||
auth::KomodoAuthImpl,
|
||||
config::core_config,
|
||||
helpers::{
|
||||
query::{VariablesAndSecrets, get_variables_and_secrets},
|
||||
@@ -70,7 +74,7 @@ impl Resolve<ExecuteArgs> for BatchRunAction {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> serror::Result<BatchExecutionResponse> {
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchRunAction>(&self.pattern, user)
|
||||
.await?,
|
||||
@@ -92,7 +96,7 @@ impl Resolve<ExecuteArgs> for RunAction {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let mut action = get_check_permissions::<Action>(
|
||||
&self.action,
|
||||
user,
|
||||
@@ -135,74 +139,87 @@ impl Resolve<ExecuteArgs> for RunAction {
|
||||
let args = serde_json::to_string(&args)
|
||||
.context("Failed to serialize action run arguments")?;
|
||||
|
||||
let CreateApiKeyResponse { key, secret } = CreateApiKey {
|
||||
name: update.id.clone(),
|
||||
expires: 0,
|
||||
}
|
||||
.resolve(&UserArgs {
|
||||
user: action_user().to_owned(),
|
||||
session: None,
|
||||
})
|
||||
let CreateApiKeyResponse { key, secret } = create_api_key(
|
||||
&KomodoAuthImpl,
|
||||
action_user().id.clone(),
|
||||
CreateApiKey {
|
||||
name: update.id.clone(),
|
||||
expires: 0,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let contents = &mut action.config.file_contents;
|
||||
// Do next steps in seperate error handling block,
|
||||
// and delete the API key before unwrapping the error.
|
||||
// If Komodo shuts down during these steps, there will
|
||||
// be a dangling api key in the DB with user_id: "000000000000000000000002".
|
||||
// These need to be
|
||||
let res = async {
|
||||
let contents = &mut action.config.file_contents;
|
||||
|
||||
// Wrap the file contents in the execution context.
|
||||
*contents = full_contents(contents, &args, &key, &secret);
|
||||
// Wrap the file contents in the execution context.
|
||||
*contents = full_contents(contents, &args, &key, &secret);
|
||||
|
||||
let replacers =
|
||||
interpolate(contents, &mut update, key.clone(), secret.clone())
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let replacers = interpolate(
|
||||
contents,
|
||||
&mut update,
|
||||
key.clone(),
|
||||
secret.clone(),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let file = format!("{}.ts", random_string(10));
|
||||
let path = core_config().action_directory.join(&file);
|
||||
let file = format!("{}.ts", random_string(10));
|
||||
let path = core_config().action_directory.join(&file);
|
||||
|
||||
secret_file::write_async(&path, contents)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Failed to write action file to {path:?}")
|
||||
})?;
|
||||
mogh_secret_file::write_async(&path, contents)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Failed to write action file to {path:?}")
|
||||
})?;
|
||||
|
||||
let CoreConfig { ssl_enabled, .. } = core_config();
|
||||
let CoreConfig { ssl_enabled, .. } = core_config();
|
||||
|
||||
let https_cert_flag = if *ssl_enabled {
|
||||
" --unsafely-ignore-certificate-errors=localhost"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let https_cert_flag = if *ssl_enabled {
|
||||
" --unsafely-ignore-certificate-errors=localhost"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let reload = if action.config.reload_deno_deps {
|
||||
" --reload"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let reload = if action.config.reload_deno_deps {
|
||||
" --reload"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let mut res = run_komodo_standard_command(
|
||||
// Keep this stage name as is, the UI will find the latest update log by matching the stage name
|
||||
"Execute Action",
|
||||
None,
|
||||
format!(
|
||||
"deno run --allow-all{https_cert_flag}{reload} {}",
|
||||
path.display()
|
||||
),
|
||||
)
|
||||
let mut res = run_komodo_standard_command(
|
||||
// Keep this stage name as is, the UI will find the latest update log by matching the stage name
|
||||
"Execute Action",
|
||||
None,
|
||||
format!(
|
||||
"deno run --allow-all{https_cert_flag}{reload} {}",
|
||||
path.display()
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
res.stdout = svi::replace_in_string(&res.stdout, &replacers)
|
||||
.replace(&key, "<ACTION_API_KEY>");
|
||||
res.stderr = svi::replace_in_string(&res.stderr, &replacers)
|
||||
.replace(&secret, "<ACTION_API_SECRET>");
|
||||
|
||||
cleanup_run(file + ".js", &path).await;
|
||||
|
||||
update.logs.push(res);
|
||||
update.finalize();
|
||||
|
||||
mogh_error::Ok(update)
|
||||
}
|
||||
.await;
|
||||
|
||||
res.stdout = svi::replace_in_string(&res.stdout, &replacers)
|
||||
.replace(&key, "<ACTION_API_KEY>");
|
||||
res.stderr = svi::replace_in_string(&res.stderr, &replacers)
|
||||
.replace(&secret, "<ACTION_API_SECRET>");
|
||||
|
||||
cleanup_run(file + ".js", &path).await;
|
||||
|
||||
if let Err(e) = (DeleteApiKey { key })
|
||||
.resolve(&UserArgs {
|
||||
user: action_user().to_owned(),
|
||||
session: None,
|
||||
})
|
||||
.await
|
||||
if let Err(e) =
|
||||
delete_api_key(&KomodoAuthImpl, &action_user().id, key).await
|
||||
{
|
||||
warn!(
|
||||
"Failed to delete API key after action execution | {:#}",
|
||||
@@ -210,8 +227,7 @@ impl Resolve<ExecuteArgs> for RunAction {
|
||||
);
|
||||
};
|
||||
|
||||
update.logs.push(res);
|
||||
update.finalize();
|
||||
let update = res?;
|
||||
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with update_update.
|
||||
@@ -259,7 +275,7 @@ async fn interpolate(
|
||||
update: &mut Update,
|
||||
key: String,
|
||||
secret: String,
|
||||
) -> serror::Result<HashSet<(String, String)>> {
|
||||
) -> mogh_error::Result<HashSet<(String, String)>> {
|
||||
let VariablesAndSecrets {
|
||||
variables,
|
||||
mut secrets,
|
||||
|
||||
@@ -12,9 +12,9 @@ use komodo_client::{
|
||||
permission::PermissionLevel,
|
||||
},
|
||||
};
|
||||
use mogh_error::AddStatusCodeError;
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError;
|
||||
|
||||
use crate::{
|
||||
alert::send_alert_to_alerter, helpers::update::update_update,
|
||||
|
||||
@@ -37,8 +37,8 @@ use komodo_client::{
|
||||
user::auto_redeploy_user,
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -83,7 +83,7 @@ impl Resolve<ExecuteArgs> for BatchRunBuild {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> serror::Result<BatchExecutionResponse> {
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchRunBuild>(&self.pattern, user)
|
||||
.await?,
|
||||
@@ -105,7 +105,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let mut build = get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
user,
|
||||
@@ -431,7 +431,7 @@ async fn handle_early_return(
|
||||
build_id: String,
|
||||
build_name: String,
|
||||
is_cancel: bool,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
update.finalize();
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
@@ -531,7 +531,7 @@ impl Resolve<ExecuteArgs> for CancelBuild {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let build = get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
user,
|
||||
@@ -660,7 +660,7 @@ async fn validate_account_extract_registry_tokens(
|
||||
..
|
||||
}: &Build,
|
||||
// Maps (domain, account) -> token
|
||||
) -> serror::Result<Vec<(String, String, String)>> {
|
||||
) -> mogh_error::Result<Vec<(String, String, String)>> {
|
||||
let mut res = HashMap::with_capacity(image_registry.capacity());
|
||||
|
||||
for (domain, account) in image_registry
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use cache::TimeoutCache;
|
||||
use formatting::format_serror;
|
||||
use interpolate::Interpolator;
|
||||
use komodo_client::{
|
||||
@@ -10,7 +9,8 @@ use komodo_client::{
|
||||
SwarmOrServer, Version,
|
||||
build::{Build, ImageRegistryConfig},
|
||||
deployment::{
|
||||
Deployment, DeploymentImage, extract_registry_domain,
|
||||
Deployment, DeploymentImage, DeploymentInfo,
|
||||
extract_registry_domain,
|
||||
},
|
||||
komodo_timestamp, optional_string,
|
||||
permission::PermissionLevel,
|
||||
@@ -18,10 +18,11 @@ use komodo_client::{
|
||||
update::{Log, Update},
|
||||
},
|
||||
};
|
||||
use mogh_cache::TimeoutCache;
|
||||
use mogh_error::AddStatusCodeError;
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
@@ -62,7 +63,7 @@ impl Resolve<ExecuteArgs> for BatchDeploy {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> serror::Result<BatchExecutionResponse> {
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchDeploy>(&self.pattern, user)
|
||||
.await?,
|
||||
@@ -86,7 +87,7 @@ impl Resolve<ExecuteArgs> for Deploy {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (mut deployment, swarm_or_server) =
|
||||
setup_deployment_execution(
|
||||
&self.deployment,
|
||||
@@ -95,6 +96,8 @@ impl Resolve<ExecuteArgs> for Deploy {
|
||||
)
|
||||
.await?;
|
||||
|
||||
swarm_or_server.verify_has_target()?;
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
@@ -203,7 +206,10 @@ impl Resolve<ExecuteArgs> for Deploy {
|
||||
update.version = version;
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let deployment_id = deployment.id.clone();
|
||||
|
||||
match swarm_or_server {
|
||||
SwarmOrServer::None => unreachable!(),
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
@@ -253,6 +259,20 @@ impl Resolve<ExecuteArgs> for Deploy {
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = resource::update_info::<Deployment>(
|
||||
&deployment_id,
|
||||
&DeploymentInfo {
|
||||
latest_image_digest: Default::default(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Failed to update deployment {} info after deploy | {e:#}",
|
||||
deployment_id
|
||||
);
|
||||
}
|
||||
|
||||
update.finalize();
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
@@ -407,7 +427,7 @@ impl Resolve<ExecuteArgs> for PullDeployment {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
@@ -461,7 +481,7 @@ impl Resolve<ExecuteArgs> for StartDeployment {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
@@ -529,7 +549,7 @@ impl Resolve<ExecuteArgs> for RestartDeployment {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
@@ -599,7 +619,7 @@ impl Resolve<ExecuteArgs> for PauseDeployment {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
@@ -667,7 +687,7 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
@@ -739,7 +759,7 @@ impl Resolve<ExecuteArgs> for StopDeployment {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
@@ -825,7 +845,7 @@ impl Resolve<ExecuteArgs> for BatchDestroyDeployment {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> serror::Result<BatchExecutionResponse> {
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchDestroyDeployment>(
|
||||
&self.pattern,
|
||||
@@ -852,7 +872,7 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
@@ -860,6 +880,8 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
|
||||
)
|
||||
.await?;
|
||||
|
||||
swarm_or_server.verify_has_target()?;
|
||||
|
||||
// get the action state for the deployment (or insert default).
|
||||
let action_state = action_states()
|
||||
.deployment
|
||||
@@ -877,6 +899,7 @@ impl Resolve<ExecuteArgs> for DestroyDeployment {
|
||||
update_update(update.clone()).await?;
|
||||
|
||||
let log = match swarm_or_server {
|
||||
SwarmOrServer::None => unreachable!(),
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
match swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
|
||||
@@ -14,22 +14,28 @@ use komodo_client::{
|
||||
RotateAllServerKeys, RotateCoreKeys,
|
||||
},
|
||||
entities::{
|
||||
deployment::DeploymentState, server::ServerState,
|
||||
SwarmOrServer, deployment::DeploymentState, server::ServerState,
|
||||
stack::StackState,
|
||||
},
|
||||
};
|
||||
use mogh_error::AddStatusCodeError;
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
api::execute::{
|
||||
ExecuteArgs, pull_deployment_inner, pull_stack_inner,
|
||||
api::{
|
||||
execute::ExecuteArgs,
|
||||
write::{
|
||||
check_deployment_for_update_inner, check_stack_for_update_inner,
|
||||
},
|
||||
},
|
||||
config::{core_config, core_keys},
|
||||
helpers::{periphery_client, update::update_update},
|
||||
helpers::{
|
||||
periphery_client, query::find_swarm_or_server,
|
||||
update::update_update,
|
||||
},
|
||||
resource::rotate_server_keys,
|
||||
state::{
|
||||
db_client, deployment_status_cache, server_status_cache,
|
||||
@@ -209,6 +215,9 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
|
||||
let servers = find_collect(&db_client().servers, None, None)
|
||||
.await
|
||||
.context("Failed to query for servers from database")?;
|
||||
let swarms = find_collect(&db_client().swarms, None, None)
|
||||
.await
|
||||
.context("Failed to query for swarms from database")?;
|
||||
|
||||
let query = doc! {
|
||||
"$or": [
|
||||
@@ -217,11 +226,10 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
|
||||
]
|
||||
};
|
||||
|
||||
let (stacks, repos) = tokio::try_join!(
|
||||
find_collect(&db_client().stacks, query.clone(), None),
|
||||
find_collect(&db_client().repos, None, None)
|
||||
)
|
||||
.context("Failed to query for resources from database")?;
|
||||
let stacks =
|
||||
find_collect(&db_client().stacks, query.clone(), None)
|
||||
.await
|
||||
.context("Failed to query for stacks from database")?;
|
||||
|
||||
let server_status_cache = server_status_cache();
|
||||
let stack_status_cache = stack_status_cache();
|
||||
@@ -234,10 +242,23 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Only pull running stacks.
|
||||
if !matches!(status.curr.state, StackState::Running) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let swarm_or_server = find_swarm_or_server(
|
||||
&stack.config.swarm_id,
|
||||
&swarms,
|
||||
&stack.config.server_id,
|
||||
&servers,
|
||||
)?;
|
||||
|
||||
if let SwarmOrServer::None = &swarm_or_server {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(server) =
|
||||
servers.iter().find(|s| s.id == stack.config.server_id)
|
||||
// This check is probably redundant along with running check
|
||||
@@ -248,39 +269,28 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
|
||||
.map(|s| matches!(s.state, ServerState::Ok))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
let name = stack.name.clone();
|
||||
let repo = if stack.config.linked_repo.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let Some(repo) =
|
||||
repos.iter().find(|r| r.id == stack.config.linked_repo)
|
||||
else {
|
||||
update.push_error_log(
|
||||
&format!("Pull Stack {name}"),
|
||||
format!(
|
||||
"Did not find any Repo matching {}",
|
||||
stack.config.linked_repo
|
||||
),
|
||||
);
|
||||
continue;
|
||||
};
|
||||
Some(repo.clone())
|
||||
};
|
||||
if let Err(e) =
|
||||
pull_stack_inner(stack, Vec::new(), server, repo, None)
|
||||
.await
|
||||
if let Err(e) = check_stack_for_update_inner(
|
||||
stack.id,
|
||||
&swarm_or_server,
|
||||
self.skip_auto_update,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
update.push_error_log(
|
||||
&format!("Pull Stack {name}"),
|
||||
&format!("Check Stack {}", stack.name),
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
} else {
|
||||
if !update.logs[0].stdout.is_empty() {
|
||||
update.logs[0].stdout.push('\n');
|
||||
}
|
||||
update.logs[0]
|
||||
.stdout
|
||||
.push_str(&format!("Pulled Stack {} ✅", bold(name)));
|
||||
|
||||
update.logs[0].stdout.push_str(&format!(
|
||||
"Checked Stack {} ✅",
|
||||
bold(&stack.name)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -290,43 +300,50 @@ impl Resolve<ExecuteArgs> for GlobalAutoUpdate {
|
||||
find_collect(&db_client().deployments, query, None)
|
||||
.await
|
||||
.context("Failed to query for deployments from database")?;
|
||||
|
||||
for deployment in deployments {
|
||||
let Some(status) =
|
||||
deployment_status_cache.get(&deployment.id).await
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Only pull running deployments.
|
||||
if !matches!(status.curr.state, DeploymentState::Running) {
|
||||
continue;
|
||||
}
|
||||
if let Some(server) =
|
||||
servers.iter().find(|s| s.id == deployment.config.server_id)
|
||||
// This check is probably redundant along with running check
|
||||
// but shouldn't hurt
|
||||
&& server_status_cache
|
||||
.get(&server.id)
|
||||
.await
|
||||
.map(|s| matches!(s.state, ServerState::Ok))
|
||||
.unwrap_or_default()
|
||||
|
||||
let swarm_or_server = find_swarm_or_server(
|
||||
&deployment.config.swarm_id,
|
||||
&swarms,
|
||||
&deployment.config.server_id,
|
||||
&servers,
|
||||
)?;
|
||||
|
||||
if let SwarmOrServer::None = &swarm_or_server {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = deployment.name.clone();
|
||||
if let Err(e) = check_deployment_for_update_inner(
|
||||
deployment,
|
||||
&swarm_or_server,
|
||||
self.skip_auto_update,
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let name = deployment.name.clone();
|
||||
if let Err(e) =
|
||||
pull_deployment_inner(deployment, server).await
|
||||
{
|
||||
update.push_error_log(
|
||||
&format!("Pull Deployment {name}"),
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
} else {
|
||||
if !update.logs[0].stdout.is_empty() {
|
||||
update.logs[0].stdout.push('\n');
|
||||
}
|
||||
update.logs[0].stdout.push_str(&format!(
|
||||
"Pulled Deployment {} ✅",
|
||||
bold(name)
|
||||
));
|
||||
update.push_error_log(
|
||||
&format!("Check Deployment {name}"),
|
||||
format_serror(&e.into()),
|
||||
);
|
||||
} else {
|
||||
if !update.logs[0].stdout.is_empty() {
|
||||
update.logs[0].stdout.push('\n');
|
||||
}
|
||||
update.logs[0]
|
||||
.stdout
|
||||
.push_str(&format!("Checked Deployment {} ✅", bold(name)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,7 +540,10 @@ impl Resolve<ExecuteArgs> for RotateCoreKeys {
|
||||
);
|
||||
}
|
||||
|
||||
let public_key = core_keys.rotate().await?.into_inner();
|
||||
let public_key = core_keys
|
||||
.rotate(mogh_pki::PkiKind::Mutual)
|
||||
.await?
|
||||
.into_inner();
|
||||
|
||||
info!("New Public Key: {public_key}");
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ use axum::{
|
||||
};
|
||||
use axum_extra::{TypedHeader, headers::ContentType};
|
||||
use database::mungos::by_id::find_one_by_id;
|
||||
use derive_variants::{EnumVariants, ExtractVariant};
|
||||
use formatting::format_serror;
|
||||
use futures_util::future::join_all;
|
||||
use komodo_client::{
|
||||
@@ -18,17 +17,18 @@ use komodo_client::{
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use response::JsonString;
|
||||
use mogh_auth_server::middleware::authenticate_request;
|
||||
use mogh_error::Json;
|
||||
use mogh_error::JsonString;
|
||||
use mogh_resolver::Resolve;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serror::Json;
|
||||
use strum::Display;
|
||||
use strum::{Display, EnumDiscriminants};
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
auth::auth_request,
|
||||
auth::KomodoAuthImpl,
|
||||
helpers::update::{init_execution_update, update_update},
|
||||
resource::{KomodoResource, list_full_for_user_using_pattern},
|
||||
state::db_client,
|
||||
@@ -48,10 +48,6 @@ mod sync;
|
||||
|
||||
use super::Variant;
|
||||
|
||||
pub use {
|
||||
deployment::pull_deployment_inner, stack::pull_stack_inner,
|
||||
};
|
||||
|
||||
pub struct ExecuteArgs {
|
||||
/// The execution id.
|
||||
/// Unique for every /execute call.
|
||||
@@ -62,12 +58,12 @@ pub struct ExecuteArgs {
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EnumDiscriminants,
|
||||
)]
|
||||
#[variant_derive(Debug, Display)]
|
||||
#[strum_discriminants(name(ExecuteRequestVariant), derive(Display))]
|
||||
#[args(ExecuteArgs)]
|
||||
#[response(JsonString)]
|
||||
#[error(serror::Error)]
|
||||
#[error(mogh_error::Error)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
pub enum ExecuteRequest {
|
||||
// ==== STACK ====
|
||||
@@ -173,14 +169,16 @@ pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.route("/{variant}", post(variant_handler))
|
||||
.layer(middleware::from_fn(auth_request))
|
||||
.layer(middleware::from_fn(
|
||||
authenticate_request::<KomodoAuthImpl, true>,
|
||||
))
|
||||
}
|
||||
|
||||
async fn variant_handler(
|
||||
user: Extension<User>,
|
||||
Path(Variant { variant }): Path<Variant>,
|
||||
Json(params): Json<serde_json::Value>,
|
||||
) -> serror::Result<(TypedHeader<ContentType>, String)> {
|
||||
) -> mogh_error::Result<(TypedHeader<ContentType>, String)> {
|
||||
let req: ExecuteRequest = serde_json::from_value(json!({
|
||||
"type": variant,
|
||||
"params": params,
|
||||
@@ -191,7 +189,7 @@ async fn variant_handler(
|
||||
async fn handler(
|
||||
Extension(user): Extension<User>,
|
||||
Json(request): Json<ExecuteRequest>,
|
||||
) -> serror::Result<(TypedHeader<ContentType>, String)> {
|
||||
) -> mogh_error::Result<(TypedHeader<ContentType>, String)> {
|
||||
let res = match inner_handler(request, user).await? {
|
||||
ExecutionResult::Single(update) => serde_json::to_string(&update)
|
||||
.context("Failed to serialize Update")?,
|
||||
@@ -269,8 +267,8 @@ pub fn inner_handler(
|
||||
let mut update =
|
||||
find_one_by_id(&db_client().updates, &update_id)
|
||||
.await
|
||||
.context("failed to query to db")?
|
||||
.context("no update exists with given id")?;
|
||||
.context("Failed to query to db")?
|
||||
.context("No update exists with given id")?;
|
||||
update.logs.push(log);
|
||||
update.finalize();
|
||||
update_update(update).await
|
||||
@@ -279,7 +277,7 @@ pub fn inner_handler(
|
||||
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
"failed to update update with task error log | {e:#}"
|
||||
"Failed to update update with task error log | {e:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -295,11 +293,11 @@ async fn task(
|
||||
user: User,
|
||||
update: Update,
|
||||
) -> anyhow::Result<String> {
|
||||
let variant = request.extract_variant();
|
||||
let variant: ExecuteRequestVariant = (&request).into();
|
||||
|
||||
info!(
|
||||
"/execute request {id} | {variant} | user: {}",
|
||||
user.username
|
||||
"EXECUTE REQUEST {id} | METHOD: {variant} | USER: {} ({})",
|
||||
user.username, user.id
|
||||
);
|
||||
|
||||
let res =
|
||||
@@ -313,7 +311,7 @@ async fn task(
|
||||
};
|
||||
|
||||
if let Err(e) = &res {
|
||||
warn!("/execute request {id} error: {e:#}");
|
||||
warn!("EXECUTE REQUEST {id} | METHOD: {variant} | ERROR: {e:#}");
|
||||
}
|
||||
|
||||
res
|
||||
|
||||
@@ -17,7 +17,7 @@ use komodo_client::{
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
@@ -46,7 +46,7 @@ impl Resolve<ExecuteArgs> for BatchRunProcedure {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, .. }: &ExecuteArgs,
|
||||
) -> serror::Result<BatchExecutionResponse> {
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchRunProcedure>(&self.pattern, user)
|
||||
.await?,
|
||||
@@ -68,7 +68,7 @@ impl Resolve<ExecuteArgs> for RunProcedure {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
Ok(
|
||||
resolve_inner(self.procedure, user.clone(), update.clone())
|
||||
.await?,
|
||||
|
||||
@@ -22,8 +22,8 @@ use komodo_client::{
|
||||
update::{Log, Update},
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{
|
||||
@@ -63,7 +63,7 @@ impl Resolve<ExecuteArgs> for BatchCloneRepo {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> serror::Result<BatchExecutionResponse> {
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchCloneRepo>(&self.pattern, user)
|
||||
.await?,
|
||||
@@ -85,7 +85,7 @@ impl Resolve<ExecuteArgs> for CloneRepo {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let mut repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
@@ -194,7 +194,7 @@ impl Resolve<ExecuteArgs> for BatchPullRepo {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> serror::Result<BatchExecutionResponse> {
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchPullRepo>(&self.pattern, user)
|
||||
.await?,
|
||||
@@ -216,7 +216,7 @@ impl Resolve<ExecuteArgs> for PullRepo {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let mut repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
@@ -316,7 +316,7 @@ impl Resolve<ExecuteArgs> for PullRepo {
|
||||
)]
|
||||
async fn handle_repo_update_return(
|
||||
update: Update,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
// The Err case of to_document should be unreachable,
|
||||
@@ -371,7 +371,7 @@ impl Resolve<ExecuteArgs> for BatchBuildRepo {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> serror::Result<BatchExecutionResponse> {
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchBuildRepo>(&self.pattern, user)
|
||||
.await?,
|
||||
@@ -393,7 +393,7 @@ impl Resolve<ExecuteArgs> for BuildRepo {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let mut repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
@@ -614,7 +614,7 @@ async fn handle_builder_early_return(
|
||||
repo_id: String,
|
||||
repo_name: String,
|
||||
is_cancel: bool,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
update.finalize();
|
||||
// Need to manually update the update before cache refresh,
|
||||
// and before broadcast with add_update.
|
||||
@@ -714,7 +714,7 @@ impl Resolve<ExecuteArgs> for CancelRepoBuild {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
|
||||
@@ -9,8 +9,8 @@ use komodo_client::{
|
||||
update::{Log, Update},
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{periphery_client, update::update_update},
|
||||
@@ -36,7 +36,7 @@ impl Resolve<ExecuteArgs> for StartContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -100,7 +100,7 @@ impl Resolve<ExecuteArgs> for RestartContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -166,7 +166,7 @@ impl Resolve<ExecuteArgs> for PauseContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -230,7 +230,7 @@ impl Resolve<ExecuteArgs> for UnpauseContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -298,7 +298,7 @@ impl Resolve<ExecuteArgs> for StopContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -366,7 +366,7 @@ impl Resolve<ExecuteArgs> for DestroyContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let DestroyContainer {
|
||||
server,
|
||||
container,
|
||||
@@ -439,7 +439,7 @@ impl Resolve<ExecuteArgs> for StartAllContainers {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -499,7 +499,7 @@ impl Resolve<ExecuteArgs> for RestartAllContainers {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -561,7 +561,7 @@ impl Resolve<ExecuteArgs> for PauseAllContainers {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -621,7 +621,7 @@ impl Resolve<ExecuteArgs> for UnpauseAllContainers {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -683,7 +683,7 @@ impl Resolve<ExecuteArgs> for StopAllContainers {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -743,7 +743,7 @@ impl Resolve<ExecuteArgs> for PruneContainers {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -809,7 +809,7 @@ impl Resolve<ExecuteArgs> for DeleteNetwork {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -869,7 +869,7 @@ impl Resolve<ExecuteArgs> for PruneNetworks {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -933,7 +933,7 @@ impl Resolve<ExecuteArgs> for DeleteImage {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -990,7 +990,7 @@ impl Resolve<ExecuteArgs> for PruneImages {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -1052,7 +1052,7 @@ impl Resolve<ExecuteArgs> for DeleteVolume {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -1112,7 +1112,7 @@ impl Resolve<ExecuteArgs> for PruneVolumes {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -1173,7 +1173,7 @@ impl Resolve<ExecuteArgs> for PruneDockerBuilders {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -1234,7 +1234,7 @@ impl Resolve<ExecuteArgs> for PruneBuildx {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -1295,7 +1295,7 @@ impl Resolve<ExecuteArgs> for PruneSystem {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
|
||||
@@ -20,12 +20,12 @@ use komodo_client::{
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mogh_error::AddStatusCodeError as _;
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api::{
|
||||
DeployStackResponse, compose::*, swarm::DeploySwarmStack,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError as _;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
@@ -77,7 +77,7 @@ impl Resolve<ExecuteArgs> for BatchDeployStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> serror::Result<BatchExecutionResponse> {
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchDeployStack>(&self.pattern, user)
|
||||
.await?,
|
||||
@@ -101,7 +101,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (mut stack, swarm_or_server) = setup_stack_execution(
|
||||
&self.stack,
|
||||
user,
|
||||
@@ -109,6 +109,8 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
)
|
||||
.await?;
|
||||
|
||||
swarm_or_server.verify_has_target()?;
|
||||
|
||||
let mut repo = if !stack.config.files_on_host
|
||||
&& !stack.config.linked_repo.is_empty()
|
||||
{
|
||||
@@ -185,6 +187,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
commit_hash,
|
||||
commit_message,
|
||||
} = match &swarm_or_server {
|
||||
SwarmOrServer::None => unreachable!(),
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
@@ -308,6 +311,7 @@ impl Resolve<ExecuteArgs> for DeployStack {
|
||||
|
||||
// Ensure cached stack state up to date by updating server cache
|
||||
match swarm_or_server {
|
||||
SwarmOrServer::None => unreachable!(),
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
update_cache_for_swarm(&swarm, true).await;
|
||||
}
|
||||
@@ -346,7 +350,7 @@ impl Resolve<ExecuteArgs> for BatchDeployStackIfChanged {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> serror::Result<BatchExecutionResponse> {
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchDeployStackIfChanged>(
|
||||
&self.pattern,
|
||||
@@ -372,7 +376,7 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let stack = get_check_permissions::<Stack>(
|
||||
&self.stack,
|
||||
user,
|
||||
@@ -551,7 +555,7 @@ async fn deploy_services(
|
||||
stack: String,
|
||||
services: Vec<String>,
|
||||
user: &User,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
// The existing update is initialized to DeployStack,
|
||||
// but also has not been created on database.
|
||||
// Setup a new update here.
|
||||
@@ -585,7 +589,7 @@ async fn restart_services(
|
||||
stack: String,
|
||||
services: Vec<String>,
|
||||
user: &User,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
// The existing update is initialized to DeployStack,
|
||||
// but also has not been created on database.
|
||||
// Setup a new update here.
|
||||
@@ -770,7 +774,7 @@ impl Resolve<ExecuteArgs> for BatchPullStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> serror::Result<BatchExecutionResponse> {
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
Ok(
|
||||
super::batch_execute::<BatchPullStack>(&self.pattern, user)
|
||||
.await?,
|
||||
@@ -894,7 +898,7 @@ impl Resolve<ExecuteArgs> for PullStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&self.stack,
|
||||
user,
|
||||
@@ -965,7 +969,7 @@ impl Resolve<ExecuteArgs> for StartStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
execute_compose::<StartStack>(
|
||||
&self.stack,
|
||||
self.services,
|
||||
@@ -994,7 +998,7 @@ impl Resolve<ExecuteArgs> for RestartStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
execute_compose::<RestartStack>(
|
||||
&self.stack,
|
||||
self.services,
|
||||
@@ -1025,7 +1029,7 @@ impl Resolve<ExecuteArgs> for PauseStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
execute_compose::<PauseStack>(
|
||||
&self.stack,
|
||||
self.services,
|
||||
@@ -1054,7 +1058,7 @@ impl Resolve<ExecuteArgs> for UnpauseStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
execute_compose::<UnpauseStack>(
|
||||
&self.stack,
|
||||
self.services,
|
||||
@@ -1083,7 +1087,7 @@ impl Resolve<ExecuteArgs> for StopStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
execute_compose::<StopStack>(
|
||||
&self.stack,
|
||||
self.services,
|
||||
@@ -1122,7 +1126,7 @@ impl Resolve<ExecuteArgs> for BatchDestroyStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, id, .. }: &ExecuteArgs,
|
||||
) -> serror::Result<BatchExecutionResponse> {
|
||||
) -> mogh_error::Result<BatchExecutionResponse> {
|
||||
super::batch_execute::<BatchDestroyStack>(&self.pattern, user)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
@@ -1146,7 +1150,7 @@ impl Resolve<ExecuteArgs> for DestroyStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&self.stack,
|
||||
user,
|
||||
@@ -1154,7 +1158,10 @@ impl Resolve<ExecuteArgs> for DestroyStack {
|
||||
)
|
||||
.await?;
|
||||
|
||||
swarm_or_server.verify_has_target()?;
|
||||
|
||||
match swarm_or_server {
|
||||
SwarmOrServer::None => unreachable!(),
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
if !self.services.is_empty() {
|
||||
return Err(
|
||||
@@ -1237,7 +1244,7 @@ impl Resolve<ExecuteArgs> for RunStackService {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let (mut stack, swarm_or_server) = setup_stack_execution(
|
||||
&self.stack,
|
||||
user,
|
||||
|
||||
@@ -7,7 +7,7 @@ use komodo_client::{
|
||||
},
|
||||
entities::{permission::PermissionLevel, swarm::Swarm},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
api::execute::ExecuteArgs,
|
||||
|
||||
@@ -26,7 +26,7 @@ use komodo_client::{
|
||||
user::sync_user,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
api::write::WriteArgs,
|
||||
@@ -64,7 +64,7 @@ impl Resolve<ExecuteArgs> for RunSync {
|
||||
async fn resolve(
|
||||
self,
|
||||
ExecuteArgs { user, update, id }: &ExecuteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let RunSync {
|
||||
sync,
|
||||
resource_type: match_resource_type,
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::{Router, http::HeaderMap};
|
||||
use cache::CloneCache;
|
||||
use komodo_client::entities::resource::Resource;
|
||||
use mogh_cache::CloneCache;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::resource::KomodoResource;
|
||||
|
||||
@@ -11,7 +11,7 @@ use komodo_client::{
|
||||
stack::Stack, sync::ResourceSync, user::git_webhook_user,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
@@ -241,11 +241,11 @@ fn stack_locks() -> &'static ListenerLockCache {
|
||||
}
|
||||
|
||||
pub trait StackExecution {
|
||||
async fn resolve(stack: Stack) -> serror::Result<()>;
|
||||
async fn resolve(stack: Stack) -> mogh_error::Result<()>;
|
||||
}
|
||||
|
||||
impl StackExecution for RefreshStackCache {
|
||||
async fn resolve(stack: Stack) -> serror::Result<()> {
|
||||
async fn resolve(stack: Stack) -> mogh_error::Result<()> {
|
||||
RefreshStackCache { stack: stack.id }
|
||||
.resolve(&WriteArgs {
|
||||
user: git_webhook_user().to_owned(),
|
||||
@@ -256,7 +256,7 @@ impl StackExecution for RefreshStackCache {
|
||||
}
|
||||
|
||||
impl StackExecution for DeployStack {
|
||||
async fn resolve(stack: Stack) -> serror::Result<()> {
|
||||
async fn resolve(stack: Stack) -> mogh_error::Result<()> {
|
||||
let user = git_webhook_user().to_owned();
|
||||
if stack.config.webhook_force_deploy {
|
||||
let req = ExecuteRequest::DeployStack(DeployStack {
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::net::IpAddr;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{ConnectInfo, Path},
|
||||
http::HeaderMap,
|
||||
routing::post,
|
||||
};
|
||||
use axum::{Router, extract::Path, http::HeaderMap, routing::post};
|
||||
use komodo_client::entities::{
|
||||
action::Action, build::Build, procedure::Procedure, repo::Repo,
|
||||
resource::Resource, stack::Stack, sync::ResourceSync,
|
||||
};
|
||||
use rate_limit::WithFailureRateLimit;
|
||||
use mogh_auth_server::request_ip::RequestIp;
|
||||
use mogh_error::AddStatusCode;
|
||||
use mogh_rate_limit::WithFailureRateLimit;
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
use serror::AddStatusCode;
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::{resource::KomodoResource, state::auth_rate_limiter};
|
||||
use crate::{auth::GENERAL_RATE_LIMITER, resource::KomodoResource};
|
||||
|
||||
use super::{
|
||||
CustomSecret, ExtractBranch, VerifySecret,
|
||||
@@ -55,9 +51,9 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.route(
|
||||
"/build/{id}",
|
||||
post(
|
||||
|Path(Id { id }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
|Path(Id { id }), RequestIp(ip), headers: HeaderMap, body: String| async move {
|
||||
let build =
|
||||
auth_webhook::<P, Build>(&id, &headers, info.ip(), &body).await?;
|
||||
auth_webhook::<P, Build>(&id, &headers, ip, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("BuildWebhook", id);
|
||||
async {
|
||||
@@ -74,16 +70,16 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
mogh_error::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/repo/{id}/{option}",
|
||||
post(
|
||||
|Path(IdAndOption::<RepoWebhookOption> { id, option }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
|Path(IdAndOption::<RepoWebhookOption> { id, option }), RequestIp(ip), headers: HeaderMap, body: String| async move {
|
||||
let repo =
|
||||
auth_webhook::<P, Repo>(&id, &headers, info.ip(), &body).await?;
|
||||
auth_webhook::<P, Repo>(&id, &headers, ip, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("RepoWebhook", id);
|
||||
async {
|
||||
@@ -100,16 +96,16 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
mogh_error::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/stack/{id}/{option}",
|
||||
post(
|
||||
|Path(IdAndOption::<StackWebhookOption> { id, option }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
|Path(IdAndOption::<StackWebhookOption> { id, option }), RequestIp(ip), headers: HeaderMap, body: String| async move {
|
||||
let stack =
|
||||
auth_webhook::<P, Stack>(&id, &headers, info.ip(), &body).await?;
|
||||
auth_webhook::<P, Stack>(&id, &headers, ip, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("StackWebhook", id);
|
||||
async {
|
||||
@@ -126,16 +122,16 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
mogh_error::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/sync/{id}/{option}",
|
||||
post(
|
||||
|Path(IdAndOption::<SyncWebhookOption> { id, option }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
|Path(IdAndOption::<SyncWebhookOption> { id, option }), RequestIp(ip), headers: HeaderMap, body: String| async move {
|
||||
let sync =
|
||||
auth_webhook::<P, ResourceSync>(&id, &headers, info.ip(), &body).await?;
|
||||
auth_webhook::<P, ResourceSync>(&id, &headers, ip, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("ResourceSyncWebhook", id);
|
||||
async {
|
||||
@@ -152,16 +148,16 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
mogh_error::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/procedure/{id}/{branch}",
|
||||
post(
|
||||
|Path(IdAndBranch { id, branch }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
|Path(IdAndBranch { id, branch }), RequestIp(ip), headers: HeaderMap, body: String| async move {
|
||||
let procedure =
|
||||
auth_webhook::<P, Procedure>(&id, &headers, info.ip(), &body).await?;
|
||||
auth_webhook::<P, Procedure>(&id, &headers, ip, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("ProcedureWebhook", id);
|
||||
async {
|
||||
@@ -178,16 +174,16 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
mogh_error::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/action/{id}/{branch}",
|
||||
post(
|
||||
|Path(IdAndBranch { id, branch }), headers: HeaderMap, ConnectInfo(info): ConnectInfo<SocketAddr>, body: String| async move {
|
||||
|Path(IdAndBranch { id, branch }), RequestIp(ip), headers: HeaderMap, body: String| async move {
|
||||
let action =
|
||||
auth_webhook::<P, Action>(&id, &headers, info.ip(), &body).await?;
|
||||
auth_webhook::<P, Action>(&id, &headers, ip, &body).await?;
|
||||
tokio::spawn(async move {
|
||||
let span = info_span!("ActionWebhook", id);
|
||||
async {
|
||||
@@ -204,7 +200,7 @@ pub fn router<P: VerifySecret + ExtractBranch>() -> Router {
|
||||
.instrument(span)
|
||||
.await
|
||||
});
|
||||
serror::Result::Ok(())
|
||||
mogh_error::Result::Ok(())
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -215,7 +211,7 @@ async fn auth_webhook<P, R>(
|
||||
headers: &HeaderMap,
|
||||
ip: IpAddr,
|
||||
body: &str,
|
||||
) -> serror::Result<Resource<R::Config, R::Info>>
|
||||
) -> mogh_error::Result<Resource<R::Config, R::Info>>
|
||||
where
|
||||
P: VerifySecret,
|
||||
R: KomodoResource + CustomSecret,
|
||||
@@ -226,12 +222,8 @@ where
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
P::verify_secret(headers, body, R::custom_secret(&resource))
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
serror::Result::Ok(resource)
|
||||
mogh_error::Result::Ok(resource)
|
||||
}
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(ip),
|
||||
)
|
||||
.with_failure_rate_limit_using_ip(&GENERAL_RATE_LIMITER, &ip)
|
||||
.await
|
||||
}
|
||||
|
||||
+24
-63
@@ -1,29 +1,20 @@
|
||||
use axum::{
|
||||
Router,
|
||||
http::{HeaderName, HeaderValue},
|
||||
routing::get,
|
||||
};
|
||||
use tower_http::{
|
||||
services::{ServeDir, ServeFile},
|
||||
set_header::SetResponseHeaderLayer,
|
||||
};
|
||||
use tower_sessions::{
|
||||
Expiry, MemoryStore, SessionManagerLayer,
|
||||
cookie::{SameSite, time::Duration},
|
||||
use axum::{Extension, Router, routing::get};
|
||||
use komodo_client::entities::user::User;
|
||||
use mogh_auth_server::middleware::authenticate_request;
|
||||
use mogh_error::Json;
|
||||
use mogh_server::{
|
||||
cors::cors_layer, session::memory_session_layer,
|
||||
ui::serve_static_ui,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::{core_config, core_host, cors_layer},
|
||||
ts_client,
|
||||
};
|
||||
use crate::{auth::KomodoAuthImpl, config::core_config, ts_client};
|
||||
|
||||
pub mod auth;
|
||||
pub mod execute;
|
||||
pub mod read;
|
||||
pub mod user;
|
||||
pub mod write;
|
||||
|
||||
mod listener;
|
||||
mod openapi;
|
||||
mod terminal;
|
||||
mod ws;
|
||||
|
||||
@@ -34,18 +25,11 @@ struct Variant {
|
||||
|
||||
pub fn app() -> Router {
|
||||
let config = core_config();
|
||||
|
||||
// Setup static frontend services
|
||||
let frontend_path = &config.frontend_path;
|
||||
let frontend_index =
|
||||
ServeFile::new(format!("{frontend_path}/index.html"));
|
||||
let serve_frontend = ServeDir::new(frontend_path)
|
||||
.not_found_service(frontend_index.clone());
|
||||
|
||||
Router::new()
|
||||
.merge(openapi::serve_docs())
|
||||
.route("/version", get(|| async { env!("CARGO_PKG_VERSION") }))
|
||||
.nest("/auth", auth::router())
|
||||
.nest("/user", user::router())
|
||||
.nest("/auth", mogh_auth_server::api::router::<KomodoAuthImpl>())
|
||||
.nest("/user", user_router())
|
||||
.nest("/read", read::router())
|
||||
.nest("/write", write::router())
|
||||
.nest("/execute", execute::router())
|
||||
@@ -53,41 +37,18 @@ pub fn app() -> Router {
|
||||
.nest("/listener", listener::router())
|
||||
.nest("/ws", ws::router())
|
||||
.nest("/client", ts_client::router())
|
||||
.layer(memory_session_layer())
|
||||
.fallback_service(serve_frontend)
|
||||
.layer(cors_layer())
|
||||
.layer(SetResponseHeaderLayer::overriding(
|
||||
HeaderName::from_static("x-content-type-options"),
|
||||
HeaderValue::from_static("nosniff"),
|
||||
))
|
||||
.layer(SetResponseHeaderLayer::overriding(
|
||||
HeaderName::from_static("x-frame-options"),
|
||||
HeaderValue::from_static("DENY"),
|
||||
))
|
||||
.layer(SetResponseHeaderLayer::overriding(
|
||||
HeaderName::from_static("x-xss-protection"),
|
||||
HeaderValue::from_static("1; mode=block"),
|
||||
))
|
||||
.layer(SetResponseHeaderLayer::overriding(
|
||||
HeaderName::from_static("referrer-policy"),
|
||||
HeaderValue::from_static("strict-origin-when-cross-origin"),
|
||||
))
|
||||
.layer(memory_session_layer(config))
|
||||
.fallback_service(serve_static_ui(&config.frontend_path))
|
||||
.layer(cors_layer(config))
|
||||
}
|
||||
|
||||
const MEMORY_SESSION_EXPIRY_SECONDS: i64 = 60;
|
||||
|
||||
fn memory_session_layer() -> SessionManagerLayer<MemoryStore> {
|
||||
let config = core_config();
|
||||
let mut layer = SessionManagerLayer::new(MemoryStore::default())
|
||||
.with_expiry(Expiry::OnInactivity(Duration::seconds(
|
||||
MEMORY_SESSION_EXPIRY_SECONDS,
|
||||
)))
|
||||
.with_secure(config.host.starts_with("https://"))
|
||||
// Needs Lax in order for sessions to work
|
||||
// accross oauth redirects.
|
||||
.with_same_site(SameSite::Lax);
|
||||
if let Some(domain) = core_host().and_then(|url| url.domain()) {
|
||||
layer = layer.with_domain(domain);
|
||||
}
|
||||
layer
|
||||
fn user_router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/",
|
||||
get(|Extension(user): Extension<User>| async { Json(user) }),
|
||||
)
|
||||
.layer(axum::middleware::from_fn(
|
||||
authenticate_request::<KomodoAuthImpl, false>,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Komodo API Docs</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<script id="api-reference" type="application/json">
|
||||
$spec
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
use komodo_client::openapi::KomodoApi;
|
||||
use utoipa::OpenApi as _;
|
||||
use utoipa_scalar::{Scalar, Servable as _};
|
||||
|
||||
pub fn serve_docs() -> Scalar<utoipa::openapi::OpenApi> {
|
||||
Scalar::with_url("/docs", KomodoApi::openapi())
|
||||
.custom_html(include_str!("docs.html"))
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use komodo_client::{
|
||||
permission::PermissionLevel,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
@@ -23,7 +23,7 @@ impl Resolve<ReadArgs> for GetAction {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Action> {
|
||||
) -> mogh_error::Result<Action> {
|
||||
Ok(
|
||||
get_check_permissions::<Action>(
|
||||
&self.action,
|
||||
@@ -39,7 +39,7 @@ impl Resolve<ReadArgs> for ListActions {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Vec<ActionListItem>> {
|
||||
) -> mogh_error::Result<Vec<ActionListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -61,7 +61,7 @@ impl Resolve<ReadArgs> for ListFullActions {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListFullActionsResponse> {
|
||||
) -> mogh_error::Result<ListFullActionsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -83,7 +83,7 @@ impl Resolve<ReadArgs> for GetActionActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ActionActionState> {
|
||||
) -> mogh_error::Result<ActionActionState> {
|
||||
let action = get_check_permissions::<Action>(
|
||||
&self.action,
|
||||
user,
|
||||
@@ -104,7 +104,7 @@ impl Resolve<ReadArgs> for GetActionsSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetActionsSummaryResponse> {
|
||||
) -> mogh_error::Result<GetActionsSummaryResponse> {
|
||||
let actions = resource::list_full_for_user::<Action>(
|
||||
Default::default(),
|
||||
user,
|
||||
|
||||
@@ -10,7 +10,7 @@ use komodo_client::{
|
||||
},
|
||||
entities::permission::PermissionLevel,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
@@ -28,7 +28,7 @@ impl Resolve<ReadArgs> for ListAlerts {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListAlertsResponse> {
|
||||
) -> mogh_error::Result<ListAlertsResponse> {
|
||||
// Alerts
|
||||
let query = user_resource_target_query(user, self.query)
|
||||
.await?
|
||||
@@ -62,7 +62,7 @@ impl Resolve<ReadArgs> for GetAlert {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetAlertResponse> {
|
||||
) -> mogh_error::Result<GetAlertResponse> {
|
||||
let alert = find_one_by_id(&db_client().alerts, &self.id)
|
||||
.await
|
||||
.context("failed to query db for alert")?
|
||||
|
||||
@@ -8,7 +8,7 @@ use komodo_client::{
|
||||
permission::PermissionLevel,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
@@ -23,7 +23,7 @@ impl Resolve<ReadArgs> for GetAlerter {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Alerter> {
|
||||
) -> mogh_error::Result<Alerter> {
|
||||
Ok(
|
||||
get_check_permissions::<Alerter>(
|
||||
&self.alerter,
|
||||
@@ -39,7 +39,7 @@ impl Resolve<ReadArgs> for ListAlerters {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Vec<AlerterListItem>> {
|
||||
) -> mogh_error::Result<Vec<AlerterListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -61,7 +61,7 @@ impl Resolve<ReadArgs> for ListFullAlerters {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListFullAlertersResponse> {
|
||||
) -> mogh_error::Result<ListFullAlertersResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -83,7 +83,7 @@ impl Resolve<ReadArgs> for GetAlertersSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetAlertersSummaryResponse> {
|
||||
) -> mogh_error::Result<GetAlertersSummaryResponse> {
|
||||
let query = match list_resource_ids_for_user::<Alerter>(
|
||||
None,
|
||||
user,
|
||||
|
||||
@@ -16,7 +16,7 @@ use komodo_client::{
|
||||
update::UpdateStatus,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
@@ -31,7 +31,7 @@ impl Resolve<ReadArgs> for GetBuild {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Build> {
|
||||
) -> mogh_error::Result<Build> {
|
||||
Ok(
|
||||
get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
@@ -47,7 +47,7 @@ impl Resolve<ReadArgs> for ListBuilds {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Vec<BuildListItem>> {
|
||||
) -> mogh_error::Result<Vec<BuildListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -69,7 +69,7 @@ impl Resolve<ReadArgs> for ListFullBuilds {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListFullBuildsResponse> {
|
||||
) -> mogh_error::Result<ListFullBuildsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -91,7 +91,7 @@ impl Resolve<ReadArgs> for GetBuildActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<BuildActionState> {
|
||||
) -> mogh_error::Result<BuildActionState> {
|
||||
let build = get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
user,
|
||||
@@ -112,7 +112,7 @@ impl Resolve<ReadArgs> for GetBuildsSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetBuildsSummaryResponse> {
|
||||
) -> mogh_error::Result<GetBuildsSummaryResponse> {
|
||||
let builds = resource::list_full_for_user::<Build>(
|
||||
Default::default(),
|
||||
user,
|
||||
@@ -160,7 +160,7 @@ impl Resolve<ReadArgs> for GetBuildMonthlyStats {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &ReadArgs,
|
||||
) -> serror::Result<GetBuildMonthlyStatsResponse> {
|
||||
) -> mogh_error::Result<GetBuildMonthlyStatsResponse> {
|
||||
let curr_ts = unix_timestamp_ms() as i64;
|
||||
let next_day = curr_ts - curr_ts % ONE_DAY_MS + ONE_DAY_MS;
|
||||
|
||||
@@ -216,7 +216,7 @@ impl Resolve<ReadArgs> for ListBuildVersions {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Vec<BuildVersionResponseItem>> {
|
||||
) -> mogh_error::Result<Vec<BuildVersionResponseItem>> {
|
||||
let ListBuildVersions {
|
||||
build,
|
||||
major,
|
||||
@@ -273,7 +273,7 @@ impl Resolve<ReadArgs> for ListCommonBuildExtraArgs {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListCommonBuildExtraArgsResponse> {
|
||||
) -> mogh_error::Result<ListCommonBuildExtraArgsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,7 @@ use komodo_client::{
|
||||
permission::PermissionLevel,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
@@ -23,7 +23,7 @@ impl Resolve<ReadArgs> for GetBuilder {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Builder> {
|
||||
) -> mogh_error::Result<Builder> {
|
||||
Ok(
|
||||
get_check_permissions::<Builder>(
|
||||
&self.builder,
|
||||
@@ -39,7 +39,7 @@ impl Resolve<ReadArgs> for ListBuilders {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Vec<BuilderListItem>> {
|
||||
) -> mogh_error::Result<Vec<BuilderListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -61,7 +61,7 @@ impl Resolve<ReadArgs> for ListFullBuilders {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListFullBuildersResponse> {
|
||||
) -> mogh_error::Result<ListFullBuildersResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -83,7 +83,7 @@ impl Resolve<ReadArgs> for GetBuildersSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetBuildersSummaryResponse> {
|
||||
) -> mogh_error::Result<GetBuildersSummaryResponse> {
|
||||
let query = match list_resource_ids_for_user::<Builder>(
|
||||
None,
|
||||
user,
|
||||
|
||||
@@ -18,10 +18,10 @@ use komodo_client::{
|
||||
update::Log,
|
||||
},
|
||||
};
|
||||
use mogh_error::AddStatusCodeError as _;
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api::{self, container::InspectContainer};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError as _;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
@@ -40,7 +40,7 @@ impl Resolve<ReadArgs> for GetDeployment {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Deployment> {
|
||||
) -> mogh_error::Result<Deployment> {
|
||||
Ok(
|
||||
get_check_permissions::<Deployment>(
|
||||
&self.deployment,
|
||||
@@ -56,7 +56,7 @@ impl Resolve<ReadArgs> for ListDeployments {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Vec<DeploymentListItem>> {
|
||||
) -> mogh_error::Result<Vec<DeploymentListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -86,7 +86,7 @@ impl Resolve<ReadArgs> for ListFullDeployments {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListFullDeploymentsResponse> {
|
||||
) -> mogh_error::Result<ListFullDeploymentsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -108,7 +108,7 @@ impl Resolve<ReadArgs> for GetDeploymentContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetDeploymentContainerResponse> {
|
||||
) -> mogh_error::Result<GetDeploymentContainerResponse> {
|
||||
let deployment = get_check_permissions::<Deployment>(
|
||||
&self.deployment,
|
||||
user,
|
||||
@@ -133,7 +133,7 @@ impl Resolve<ReadArgs> for GetDeploymentLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Log> {
|
||||
) -> mogh_error::Result<Log> {
|
||||
let GetDeploymentLog {
|
||||
deployment,
|
||||
tail,
|
||||
@@ -147,7 +147,10 @@ impl Resolve<ReadArgs> for GetDeploymentLog {
|
||||
)
|
||||
.await?;
|
||||
|
||||
swarm_or_server.verify_has_target()?;
|
||||
|
||||
let log = match swarm_or_server {
|
||||
SwarmOrServer::None => unreachable!(),
|
||||
SwarmOrServer::Swarm(swarm) => swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::GetSwarmServiceLog {
|
||||
@@ -180,7 +183,7 @@ impl Resolve<ReadArgs> for SearchDeploymentLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Log> {
|
||||
) -> mogh_error::Result<Log> {
|
||||
let SearchDeploymentLog {
|
||||
deployment,
|
||||
terms,
|
||||
@@ -196,7 +199,10 @@ impl Resolve<ReadArgs> for SearchDeploymentLog {
|
||||
)
|
||||
.await?;
|
||||
|
||||
swarm_or_server.verify_has_target()?;
|
||||
|
||||
let log = match swarm_or_server {
|
||||
SwarmOrServer::None => unreachable!(),
|
||||
SwarmOrServer::Swarm(swarm) => swarm_request(
|
||||
&swarm.config.server_ids,
|
||||
periphery_client::api::swarm::GetSwarmServiceLogSearch {
|
||||
@@ -233,7 +239,7 @@ impl Resolve<ReadArgs> for InspectDeploymentContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Container> {
|
||||
) -> mogh_error::Result<Container> {
|
||||
let InspectDeploymentContainer { deployment } = self;
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&deployment,
|
||||
@@ -280,7 +286,7 @@ impl Resolve<ReadArgs> for InspectDeploymentSwarmService {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<SwarmService> {
|
||||
) -> mogh_error::Result<SwarmService> {
|
||||
let InspectDeploymentSwarmService { deployment } = self;
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&deployment,
|
||||
@@ -314,7 +320,7 @@ impl Resolve<ReadArgs> for GetDeploymentStats {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ContainerStats> {
|
||||
) -> mogh_error::Result<ContainerStats> {
|
||||
let Deployment {
|
||||
name,
|
||||
config: DeploymentConfig { server_id, .. },
|
||||
@@ -344,7 +350,7 @@ impl Resolve<ReadArgs> for GetDeploymentActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<DeploymentActionState> {
|
||||
) -> mogh_error::Result<DeploymentActionState> {
|
||||
let deployment = get_check_permissions::<Deployment>(
|
||||
&self.deployment,
|
||||
user,
|
||||
@@ -365,7 +371,7 @@ impl Resolve<ReadArgs> for GetDeploymentsSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetDeploymentsSummaryResponse> {
|
||||
) -> mogh_error::Result<GetDeploymentsSummaryResponse> {
|
||||
let deployments = resource::list_full_for_user::<Deployment>(
|
||||
Default::default(),
|
||||
user,
|
||||
@@ -408,7 +414,7 @@ impl Resolve<ReadArgs> for ListCommonDeploymentExtraArgs {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListCommonDeploymentExtraArgsResponse> {
|
||||
) -> mogh_error::Result<ListCommonDeploymentExtraArgsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{collections::HashSet, time::Instant};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
@@ -18,16 +18,19 @@ use komodo_client::{
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use response::Response;
|
||||
use mogh_auth_server::middleware::authenticate_request;
|
||||
use mogh_error::Response;
|
||||
use mogh_error::{AddStatusCodeError, Json};
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serror::Json;
|
||||
use strum::{Display, EnumDiscriminants};
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
auth::auth_request,
|
||||
auth::KomodoAuthImpl,
|
||||
config::{core_config, core_keys},
|
||||
helpers::periphery_client,
|
||||
resource,
|
||||
@@ -64,10 +67,13 @@ pub struct ReadArgs {
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EnumDiscriminants,
|
||||
)]
|
||||
#[strum_discriminants(name(ReadRequestVariant), derive(Display))]
|
||||
#[args(ReadArgs)]
|
||||
#[response(Response)]
|
||||
#[error(serror::Error)]
|
||||
#[error(mogh_error::Error)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
enum ReadRequest {
|
||||
GetVersion(GetVersion),
|
||||
@@ -263,14 +269,16 @@ pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.route("/{variant}", post(variant_handler))
|
||||
.layer(middleware::from_fn(auth_request))
|
||||
.layer(middleware::from_fn(
|
||||
authenticate_request::<KomodoAuthImpl, true>,
|
||||
))
|
||||
}
|
||||
|
||||
async fn variant_handler(
|
||||
user: Extension<User>,
|
||||
Path(Variant { variant }): Path<Variant>,
|
||||
Json(params): Json<serde_json::Value>,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
) -> mogh_error::Result<axum::response::Response> {
|
||||
let req: ReadRequest = serde_json::from_value(json!({
|
||||
"type": variant,
|
||||
"params": params,
|
||||
@@ -281,16 +289,24 @@ async fn variant_handler(
|
||||
async fn handler(
|
||||
Extension(user): Extension<User>,
|
||||
Json(request): Json<ReadRequest>,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
let timer = Instant::now();
|
||||
) -> mogh_error::Result<axum::response::Response> {
|
||||
let req_id = Uuid::new_v4();
|
||||
debug!("/read request | user: {}", user.username);
|
||||
let variant: ReadRequestVariant = (&request).into();
|
||||
|
||||
debug!(
|
||||
"READ REQUEST {req_id} | METHOD: {variant} | USER: {} ({})",
|
||||
user.username, user.id
|
||||
);
|
||||
|
||||
let res = request.resolve(&ReadArgs { user }).await;
|
||||
|
||||
if let Err(e) = &res {
|
||||
debug!("/read request {req_id} error: {:#}", e.error);
|
||||
debug!(
|
||||
"READ REQUEST {req_id} | METHOD: {variant} | ERROR: {:#}",
|
||||
e.error
|
||||
);
|
||||
}
|
||||
let elapsed = timer.elapsed();
|
||||
debug!("/read request {req_id} | resolve time: {elapsed:?}");
|
||||
|
||||
res.map(|res| res.0)
|
||||
}
|
||||
|
||||
@@ -298,18 +314,20 @@ impl Resolve<ReadArgs> for GetVersion {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &ReadArgs,
|
||||
) -> serror::Result<GetVersionResponse> {
|
||||
) -> mogh_error::Result<GetVersionResponse> {
|
||||
Ok(GetVersionResponse {
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<ReadArgs> for GetCoreInfo {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &ReadArgs,
|
||||
) -> serror::Result<GetCoreInfoResponse> {
|
||||
) -> mogh_error::Result<GetCoreInfoResponse> {
|
||||
let config = core_config();
|
||||
let info = GetCoreInfoResponse {
|
||||
title: config.title.clone(),
|
||||
@@ -332,11 +350,13 @@ impl Resolve<ReadArgs> for GetCoreInfo {
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<ReadArgs> for ListSecrets {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &ReadArgs,
|
||||
) -> serror::Result<ListSecretsResponse> {
|
||||
) -> mogh_error::Result<ListSecretsResponse> {
|
||||
let mut secrets = core_config()
|
||||
.secrets
|
||||
.keys()
|
||||
@@ -358,7 +378,8 @@ impl Resolve<ReadArgs> for ListSecrets {
|
||||
}
|
||||
_ => {
|
||||
return Err(
|
||||
anyhow!("target must be `Server` or `Builder`").into(),
|
||||
anyhow!("target must be `Server` or `Builder`")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -385,11 +406,13 @@ impl Resolve<ReadArgs> for ListSecrets {
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListGitProvidersFromConfigResponse> {
|
||||
) -> mogh_error::Result<ListGitProvidersFromConfigResponse> {
|
||||
let mut providers = core_config().git_providers.clone();
|
||||
|
||||
if let Some(target) = self.target {
|
||||
@@ -417,7 +440,8 @@ impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
|
||||
}
|
||||
_ => {
|
||||
return Err(
|
||||
anyhow!("target must be `Server` or `Builder`").into(),
|
||||
anyhow!("target must be `Server` or `Builder`")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -487,11 +511,13 @@ impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<ReadArgs> for ListDockerRegistriesFromConfig {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &ReadArgs,
|
||||
) -> serror::Result<ListDockerRegistriesFromConfigResponse> {
|
||||
) -> mogh_error::Result<ListDockerRegistriesFromConfigResponse> {
|
||||
let mut registries = core_config().docker_registries.clone();
|
||||
|
||||
if let Some(target) = self.target {
|
||||
@@ -535,7 +561,7 @@ impl Resolve<ReadArgs> for ListDockerRegistriesFromConfig {
|
||||
async fn merge_git_providers_for_server(
|
||||
providers: &mut Vec<GitProvider>,
|
||||
server_id: &str,
|
||||
) -> serror::Result<()> {
|
||||
) -> mogh_error::Result<()> {
|
||||
let server = resource::get::<Server>(server_id).await?;
|
||||
let more = periphery_client(&server)
|
||||
.await?
|
||||
@@ -574,7 +600,7 @@ fn merge_git_providers(
|
||||
async fn merge_docker_registries_for_server(
|
||||
registries: &mut Vec<DockerRegistry>,
|
||||
server_id: &str,
|
||||
) -> serror::Result<()> {
|
||||
) -> mogh_error::Result<()> {
|
||||
let server = resource::get::<Server>(server_id).await?;
|
||||
let more = periphery_client(&server)
|
||||
.await?
|
||||
|
||||
@@ -5,9 +5,9 @@ use database::mungos::find::find_collect;
|
||||
use komodo_client::api::read::{
|
||||
ListOnboardingKeys, ListOnboardingKeysResponse,
|
||||
};
|
||||
use mogh_error::AddStatusCodeError;
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError;
|
||||
|
||||
use crate::{api::read::ReadArgs, state::db_client};
|
||||
|
||||
@@ -17,7 +17,7 @@ impl Resolve<ReadArgs> for ListOnboardingKeys {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user: admin }: &ReadArgs,
|
||||
) -> serror::Result<ListOnboardingKeysResponse> {
|
||||
) -> mogh_error::Result<ListOnboardingKeysResponse> {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This call is admin only")
|
||||
|
||||
@@ -8,7 +8,7 @@ use komodo_client::{
|
||||
},
|
||||
entities::permission::PermissionLevel,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_user_permission_on_target, state::db_client,
|
||||
@@ -20,7 +20,7 @@ impl Resolve<ReadArgs> for ListPermissions {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListPermissionsResponse> {
|
||||
) -> mogh_error::Result<ListPermissionsResponse> {
|
||||
let res = find_collect(
|
||||
&db_client().permissions,
|
||||
doc! {
|
||||
@@ -39,7 +39,7 @@ impl Resolve<ReadArgs> for GetPermission {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetPermissionResponse> {
|
||||
) -> mogh_error::Result<GetPermissionResponse> {
|
||||
if user.admin {
|
||||
return Ok(PermissionLevel::Write.all());
|
||||
}
|
||||
@@ -51,7 +51,7 @@ impl Resolve<ReadArgs> for ListUserTargetPermissions {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListUserTargetPermissionsResponse> {
|
||||
) -> mogh_error::Result<ListUserTargetPermissionsResponse> {
|
||||
if !user.admin {
|
||||
return Err(anyhow!("this method is admin only").into());
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use komodo_client::{
|
||||
procedure::{Procedure, ProcedureState},
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
@@ -21,7 +21,7 @@ impl Resolve<ReadArgs> for GetProcedure {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetProcedureResponse> {
|
||||
) -> mogh_error::Result<GetProcedureResponse> {
|
||||
Ok(
|
||||
get_check_permissions::<Procedure>(
|
||||
&self.procedure,
|
||||
@@ -37,7 +37,7 @@ impl Resolve<ReadArgs> for ListProcedures {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListProceduresResponse> {
|
||||
) -> mogh_error::Result<ListProceduresResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -59,7 +59,7 @@ impl Resolve<ReadArgs> for ListFullProcedures {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListFullProceduresResponse> {
|
||||
) -> mogh_error::Result<ListFullProceduresResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -81,7 +81,7 @@ impl Resolve<ReadArgs> for GetProceduresSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetProceduresSummaryResponse> {
|
||||
) -> mogh_error::Result<GetProceduresSummaryResponse> {
|
||||
let procedures = resource::list_full_for_user::<Procedure>(
|
||||
Default::default(),
|
||||
user,
|
||||
@@ -127,7 +127,7 @@ impl Resolve<ReadArgs> for GetProcedureActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetProcedureActionStateResponse> {
|
||||
) -> mogh_error::Result<GetProcedureActionStateResponse> {
|
||||
let procedure = get_check_permissions::<Procedure>(
|
||||
&self.procedure,
|
||||
user,
|
||||
|
||||
@@ -5,7 +5,7 @@ use database::mungos::{
|
||||
mongodb::options::FindOptions,
|
||||
};
|
||||
use komodo_client::api::read::*;
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::state::db_client;
|
||||
|
||||
@@ -15,7 +15,7 @@ impl Resolve<ReadArgs> for GetGitProviderAccount {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetGitProviderAccountResponse> {
|
||||
) -> mogh_error::Result<GetGitProviderAccountResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can read git provider accounts").into(),
|
||||
@@ -35,7 +35,7 @@ impl Resolve<ReadArgs> for ListGitProviderAccounts {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListGitProviderAccountsResponse> {
|
||||
) -> mogh_error::Result<ListGitProviderAccountsResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can read git provider accounts").into(),
|
||||
@@ -65,7 +65,7 @@ impl Resolve<ReadArgs> for GetDockerRegistryAccount {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetDockerRegistryAccountResponse> {
|
||||
) -> mogh_error::Result<GetDockerRegistryAccountResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can read docker registry accounts")
|
||||
@@ -87,7 +87,7 @@ impl Resolve<ReadArgs> for ListDockerRegistryAccounts {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListDockerRegistryAccountsResponse> {
|
||||
) -> mogh_error::Result<ListDockerRegistryAccountsResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can read docker registry accounts")
|
||||
|
||||
@@ -6,7 +6,7 @@ use komodo_client::{
|
||||
repo::{Repo, RepoActionState, RepoListItem, RepoState},
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags,
|
||||
@@ -21,7 +21,7 @@ impl Resolve<ReadArgs> for GetRepo {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Repo> {
|
||||
) -> mogh_error::Result<Repo> {
|
||||
Ok(
|
||||
get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
@@ -37,7 +37,7 @@ impl Resolve<ReadArgs> for ListRepos {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Vec<RepoListItem>> {
|
||||
) -> mogh_error::Result<Vec<RepoListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -59,7 +59,7 @@ impl Resolve<ReadArgs> for ListFullRepos {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListFullReposResponse> {
|
||||
) -> mogh_error::Result<ListFullReposResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -81,7 +81,7 @@ impl Resolve<ReadArgs> for GetRepoActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<RepoActionState> {
|
||||
) -> mogh_error::Result<RepoActionState> {
|
||||
let repo = get_check_permissions::<Repo>(
|
||||
&self.repo,
|
||||
user,
|
||||
@@ -102,7 +102,7 @@ impl Resolve<ReadArgs> for GetReposSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetReposSummaryResponse> {
|
||||
) -> mogh_error::Result<GetReposSummaryResponse> {
|
||||
let repos = resource::list_full_for_user::<Repo>(
|
||||
Default::default(),
|
||||
user,
|
||||
|
||||
@@ -10,7 +10,7 @@ use komodo_client::{
|
||||
schedule::Schedule,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::{get_all_tags, get_last_run_at},
|
||||
@@ -24,7 +24,7 @@ impl Resolve<ReadArgs> for ListSchedules {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &ReadArgs,
|
||||
) -> serror::Result<Vec<Schedule>> {
|
||||
) -> mogh_error::Result<Vec<Schedule>> {
|
||||
let all_tags = get_all_tags(None).await?;
|
||||
let (actions, procedures) = tokio::try_join!(
|
||||
list_full_for_user::<Action>(
|
||||
|
||||
@@ -35,6 +35,8 @@ use komodo_client::{
|
||||
update::Log,
|
||||
},
|
||||
};
|
||||
use mogh_error::AddStatusCode;
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api::{
|
||||
self as periphery,
|
||||
container::InspectContainer,
|
||||
@@ -43,8 +45,6 @@ use periphery_client::api::{
|
||||
},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCode;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
@@ -61,7 +61,7 @@ impl Resolve<ReadArgs> for GetServersSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetServersSummaryResponse> {
|
||||
) -> mogh_error::Result<GetServersSummaryResponse> {
|
||||
let servers = resource::list_for_user::<Server>(
|
||||
Default::default(),
|
||||
user,
|
||||
@@ -103,7 +103,7 @@ impl Resolve<ReadArgs> for GetServer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Server> {
|
||||
) -> mogh_error::Result<Server> {
|
||||
Ok(
|
||||
get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
@@ -119,7 +119,7 @@ impl Resolve<ReadArgs> for ListServers {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Vec<ServerListItem>> {
|
||||
) -> mogh_error::Result<Vec<ServerListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -141,7 +141,7 @@ impl Resolve<ReadArgs> for ListFullServers {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListFullServersResponse> {
|
||||
) -> mogh_error::Result<ListFullServersResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -163,7 +163,7 @@ impl Resolve<ReadArgs> for GetServerState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetServerStateResponse> {
|
||||
) -> mogh_error::Result<GetServerStateResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -185,7 +185,7 @@ impl Resolve<ReadArgs> for GetServerActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ServerActionState> {
|
||||
) -> mogh_error::Result<ServerActionState> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -206,7 +206,7 @@ impl Resolve<ReadArgs> for GetPeripheryInformation {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetPeripheryInformationResponse> {
|
||||
) -> mogh_error::Result<GetPeripheryInformationResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -229,7 +229,7 @@ impl Resolve<ReadArgs> for GetSystemInformation {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<SystemInformation> {
|
||||
) -> mogh_error::Result<SystemInformation> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -253,7 +253,7 @@ impl Resolve<ReadArgs> for GetSystemStats {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetSystemStatsResponse> {
|
||||
) -> mogh_error::Result<GetSystemStatsResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -285,7 +285,7 @@ impl Resolve<ReadArgs> for ListSystemProcesses {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSystemProcessesResponse> {
|
||||
) -> mogh_error::Result<ListSystemProcessesResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -320,7 +320,7 @@ impl Resolve<ReadArgs> for GetHistoricalServerStats {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetHistoricalServerStatsResponse> {
|
||||
) -> mogh_error::Result<GetHistoricalServerStatsResponse> {
|
||||
let GetHistoricalServerStats {
|
||||
server,
|
||||
granularity,
|
||||
@@ -373,7 +373,7 @@ impl Resolve<ReadArgs> for ListDockerContainers {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListDockerContainersResponse> {
|
||||
) -> mogh_error::Result<ListDockerContainersResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -395,7 +395,7 @@ impl Resolve<ReadArgs> for ListAllDockerContainers {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListAllDockerContainersResponse> {
|
||||
) -> mogh_error::Result<ListAllDockerContainersResponse> {
|
||||
let servers = resource::list_for_user::<Server>(
|
||||
ServerQuery::builder().names(self.servers.clone()).build(),
|
||||
user,
|
||||
@@ -432,7 +432,7 @@ impl Resolve<ReadArgs> for GetDockerContainersSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetDockerContainersSummaryResponse> {
|
||||
) -> mogh_error::Result<GetDockerContainersSummaryResponse> {
|
||||
let servers = resource::list_full_for_user::<Server>(
|
||||
Default::default(),
|
||||
user,
|
||||
@@ -472,7 +472,7 @@ impl Resolve<ReadArgs> for InspectDockerContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Container> {
|
||||
) -> mogh_error::Result<Container> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -507,7 +507,7 @@ impl Resolve<ReadArgs> for GetContainerLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Log> {
|
||||
) -> mogh_error::Result<Log> {
|
||||
let GetContainerLog {
|
||||
server,
|
||||
container,
|
||||
@@ -537,7 +537,7 @@ impl Resolve<ReadArgs> for SearchContainerLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Log> {
|
||||
) -> mogh_error::Result<Log> {
|
||||
let SearchContainerLog {
|
||||
server,
|
||||
container,
|
||||
@@ -571,7 +571,7 @@ impl Resolve<ReadArgs> for GetResourceMatchingContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetResourceMatchingContainerResponse> {
|
||||
) -> mogh_error::Result<GetResourceMatchingContainerResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -632,7 +632,7 @@ impl Resolve<ReadArgs> for ListDockerNetworks {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListDockerNetworksResponse> {
|
||||
) -> mogh_error::Result<ListDockerNetworksResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -654,7 +654,7 @@ impl Resolve<ReadArgs> for InspectDockerNetwork {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Network> {
|
||||
) -> mogh_error::Result<Network> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -685,7 +685,7 @@ impl Resolve<ReadArgs> for ListDockerImages {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListDockerImagesResponse> {
|
||||
) -> mogh_error::Result<ListDockerImagesResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -707,7 +707,7 @@ impl Resolve<ReadArgs> for InspectDockerImage {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Image> {
|
||||
) -> mogh_error::Result<Image> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -735,7 +735,7 @@ impl Resolve<ReadArgs> for ListDockerImageHistory {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Vec<ImageHistoryResponseItem>> {
|
||||
) -> mogh_error::Result<Vec<ImageHistoryResponseItem>> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -766,7 +766,7 @@ impl Resolve<ReadArgs> for ListDockerVolumes {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListDockerVolumesResponse> {
|
||||
) -> mogh_error::Result<ListDockerVolumesResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -788,7 +788,7 @@ impl Resolve<ReadArgs> for InspectDockerVolume {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Volume> {
|
||||
) -> mogh_error::Result<Volume> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -816,7 +816,7 @@ impl Resolve<ReadArgs> for ListComposeProjects {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListComposeProjectsResponse> {
|
||||
) -> mogh_error::Result<ListComposeProjectsResponse> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
|
||||
@@ -12,13 +12,13 @@ use komodo_client::{
|
||||
stack::{Stack, StackActionState, StackListItem, StackState},
|
||||
},
|
||||
};
|
||||
use mogh_error::AddStatusCodeError as _;
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api::{
|
||||
compose::{GetComposeLog, GetComposeLogSearch},
|
||||
container::InspectContainer,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError as _;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
@@ -36,7 +36,7 @@ impl Resolve<ReadArgs> for GetStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Stack> {
|
||||
) -> mogh_error::Result<Stack> {
|
||||
Ok(
|
||||
get_check_permissions::<Stack>(
|
||||
&self.stack,
|
||||
@@ -52,7 +52,7 @@ impl Resolve<ReadArgs> for ListStackServices {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListStackServicesResponse> {
|
||||
) -> mogh_error::Result<ListStackServicesResponse> {
|
||||
let stack = get_check_permissions::<Stack>(
|
||||
&self.stack,
|
||||
user,
|
||||
@@ -76,7 +76,7 @@ impl Resolve<ReadArgs> for GetStackLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetStackLogResponse> {
|
||||
) -> mogh_error::Result<GetStackLogResponse> {
|
||||
let GetStackLog {
|
||||
stack,
|
||||
mut services,
|
||||
@@ -90,7 +90,10 @@ impl Resolve<ReadArgs> for GetStackLog {
|
||||
)
|
||||
.await?;
|
||||
|
||||
swarm_or_server.verify_has_target()?;
|
||||
|
||||
let log = match swarm_or_server {
|
||||
SwarmOrServer::None => unreachable!(),
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
let service = services.pop().context(
|
||||
"Must pass single service for Swarm mode Stack logs",
|
||||
@@ -133,7 +136,7 @@ impl Resolve<ReadArgs> for SearchStackLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<SearchStackLogResponse> {
|
||||
) -> mogh_error::Result<SearchStackLogResponse> {
|
||||
let SearchStackLog {
|
||||
stack,
|
||||
mut services,
|
||||
@@ -149,7 +152,10 @@ impl Resolve<ReadArgs> for SearchStackLog {
|
||||
)
|
||||
.await?;
|
||||
|
||||
swarm_or_server.verify_has_target()?;
|
||||
|
||||
let log = match swarm_or_server {
|
||||
SwarmOrServer::None => unreachable!(),
|
||||
SwarmOrServer::Swarm(swarm) => {
|
||||
let service = services.pop().context(
|
||||
"Must pass single service for Swarm mode Stack logs",
|
||||
@@ -192,7 +198,7 @@ impl Resolve<ReadArgs> for InspectStackContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Container> {
|
||||
) -> mogh_error::Result<Container> {
|
||||
let InspectStackContainer { stack, service } = self;
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&stack,
|
||||
@@ -241,7 +247,7 @@ impl Resolve<ReadArgs> for InspectStackSwarmService {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<SwarmService> {
|
||||
) -> mogh_error::Result<SwarmService> {
|
||||
let InspectStackSwarmService { stack, service } = self;
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&stack,
|
||||
@@ -292,7 +298,7 @@ impl Resolve<ReadArgs> for InspectStackSwarmInfo {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<SwarmStack> {
|
||||
) -> mogh_error::Result<SwarmStack> {
|
||||
let (stack, swarm_or_server) = setup_stack_execution(
|
||||
&self.stack,
|
||||
user,
|
||||
@@ -325,7 +331,7 @@ impl Resolve<ReadArgs> for ListCommonStackExtraArgs {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListCommonStackExtraArgsResponse> {
|
||||
) -> mogh_error::Result<ListCommonStackExtraArgsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -359,7 +365,7 @@ impl Resolve<ReadArgs> for ListCommonStackBuildExtraArgs {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListCommonStackBuildExtraArgsResponse> {
|
||||
) -> mogh_error::Result<ListCommonStackBuildExtraArgsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -393,7 +399,7 @@ impl Resolve<ReadArgs> for ListStacks {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Vec<StackListItem>> {
|
||||
) -> mogh_error::Result<Vec<StackListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -429,7 +435,7 @@ impl Resolve<ReadArgs> for ListFullStacks {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListFullStacksResponse> {
|
||||
) -> mogh_error::Result<ListFullStacksResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -451,7 +457,7 @@ impl Resolve<ReadArgs> for GetStackActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<StackActionState> {
|
||||
) -> mogh_error::Result<StackActionState> {
|
||||
let stack = get_check_permissions::<Stack>(
|
||||
&self.stack,
|
||||
user,
|
||||
@@ -472,7 +478,7 @@ impl Resolve<ReadArgs> for GetStacksSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetStacksSummaryResponse> {
|
||||
) -> mogh_error::Result<GetStacksSummaryResponse> {
|
||||
let stacks = resource::list_full_for_user::<Stack>(
|
||||
Default::default(),
|
||||
user,
|
||||
|
||||
@@ -6,7 +6,7 @@ use komodo_client::{
|
||||
swarm::{Swarm, SwarmActionState, SwarmListItem, SwarmState},
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{query::get_all_tags, swarm::swarm_request},
|
||||
@@ -21,7 +21,7 @@ impl Resolve<ReadArgs> for GetSwarm {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Swarm> {
|
||||
) -> mogh_error::Result<Swarm> {
|
||||
Ok(
|
||||
get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
@@ -37,7 +37,7 @@ impl Resolve<ReadArgs> for ListSwarms {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Vec<SwarmListItem>> {
|
||||
) -> mogh_error::Result<Vec<SwarmListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -59,7 +59,7 @@ impl Resolve<ReadArgs> for ListFullSwarms {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListFullSwarmsResponse> {
|
||||
) -> mogh_error::Result<ListFullSwarmsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -81,7 +81,7 @@ impl Resolve<ReadArgs> for GetSwarmActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<SwarmActionState> {
|
||||
) -> mogh_error::Result<SwarmActionState> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
@@ -102,7 +102,7 @@ impl Resolve<ReadArgs> for GetSwarmsSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetSwarmsSummaryResponse> {
|
||||
) -> mogh_error::Result<GetSwarmsSummaryResponse> {
|
||||
let swarms = resource::list_full_for_user::<Swarm>(
|
||||
Default::default(),
|
||||
user,
|
||||
@@ -134,6 +134,9 @@ impl Resolve<ReadArgs> for GetSwarmsSummary {
|
||||
SwarmState::Unhealthy => {
|
||||
res.unhealthy += 1;
|
||||
}
|
||||
SwarmState::Down => {
|
||||
res.down += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +148,7 @@ impl Resolve<ReadArgs> for InspectSwarm {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<InspectSwarmResponse> {
|
||||
) -> mogh_error::Result<InspectSwarmResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
@@ -167,7 +170,7 @@ impl Resolve<ReadArgs> for ListSwarmNodes {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSwarmNodesResponse> {
|
||||
) -> mogh_error::Result<ListSwarmNodesResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
@@ -188,7 +191,7 @@ impl Resolve<ReadArgs> for InspectSwarmNode {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<InspectSwarmNodeResponse> {
|
||||
) -> mogh_error::Result<InspectSwarmNodeResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
@@ -210,7 +213,7 @@ impl Resolve<ReadArgs> for ListSwarmServices {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSwarmServicesResponse> {
|
||||
) -> mogh_error::Result<ListSwarmServicesResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
@@ -231,7 +234,7 @@ impl Resolve<ReadArgs> for InspectSwarmService {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<InspectSwarmServiceResponse> {
|
||||
) -> mogh_error::Result<InspectSwarmServiceResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
@@ -253,7 +256,7 @@ impl Resolve<ReadArgs> for GetSwarmServiceLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetSwarmServiceLogResponse> {
|
||||
) -> mogh_error::Result<GetSwarmServiceLogResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
@@ -280,7 +283,7 @@ impl Resolve<ReadArgs> for SearchSwarmServiceLog {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<SearchSwarmServiceLogResponse> {
|
||||
) -> mogh_error::Result<SearchSwarmServiceLogResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
@@ -309,7 +312,7 @@ impl Resolve<ReadArgs> for ListSwarmTasks {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSwarmTasksResponse> {
|
||||
) -> mogh_error::Result<ListSwarmTasksResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
@@ -330,7 +333,7 @@ impl Resolve<ReadArgs> for InspectSwarmTask {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<InspectSwarmTaskResponse> {
|
||||
) -> mogh_error::Result<InspectSwarmTaskResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
@@ -352,7 +355,7 @@ impl Resolve<ReadArgs> for ListSwarmSecrets {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSwarmSecretsResponse> {
|
||||
) -> mogh_error::Result<ListSwarmSecretsResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
@@ -373,7 +376,7 @@ impl Resolve<ReadArgs> for InspectSwarmSecret {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<InspectSwarmSecretResponse> {
|
||||
) -> mogh_error::Result<InspectSwarmSecretResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
@@ -395,7 +398,7 @@ impl Resolve<ReadArgs> for ListSwarmConfigs {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSwarmConfigsResponse> {
|
||||
) -> mogh_error::Result<ListSwarmConfigsResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
@@ -416,7 +419,7 @@ impl Resolve<ReadArgs> for InspectSwarmConfig {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<InspectSwarmConfigResponse> {
|
||||
) -> mogh_error::Result<InspectSwarmConfigResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
@@ -438,7 +441,7 @@ impl Resolve<ReadArgs> for ListSwarmStacks {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListSwarmStacksResponse> {
|
||||
) -> mogh_error::Result<ListSwarmStacksResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
@@ -459,7 +462,7 @@ impl Resolve<ReadArgs> for InspectSwarmStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<InspectSwarmStackResponse> {
|
||||
) -> mogh_error::Result<InspectSwarmStackResponse> {
|
||||
let swarm = get_check_permissions::<Swarm>(
|
||||
&self.swarm,
|
||||
user,
|
||||
|
||||
@@ -8,7 +8,7 @@ use komodo_client::{
|
||||
},
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::get_all_tags, permission::get_check_permissions,
|
||||
@@ -21,7 +21,7 @@ impl Resolve<ReadArgs> for GetResourceSync {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ResourceSync> {
|
||||
) -> mogh_error::Result<ResourceSync> {
|
||||
Ok(
|
||||
get_check_permissions::<ResourceSync>(
|
||||
&self.sync,
|
||||
@@ -37,7 +37,7 @@ impl Resolve<ReadArgs> for ListResourceSyncs {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Vec<ResourceSyncListItem>> {
|
||||
) -> mogh_error::Result<Vec<ResourceSyncListItem>> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -59,7 +59,7 @@ impl Resolve<ReadArgs> for ListFullResourceSyncs {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListFullResourceSyncsResponse> {
|
||||
) -> mogh_error::Result<ListFullResourceSyncsResponse> {
|
||||
let all_tags = if self.query.tags.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -81,7 +81,7 @@ impl Resolve<ReadArgs> for GetResourceSyncActionState {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ResourceSyncActionState> {
|
||||
) -> mogh_error::Result<ResourceSyncActionState> {
|
||||
let sync = get_check_permissions::<ResourceSync>(
|
||||
&self.sync,
|
||||
user,
|
||||
@@ -102,7 +102,7 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetResourceSyncsSummaryResponse> {
|
||||
) -> mogh_error::Result<GetResourceSyncsSummaryResponse> {
|
||||
let resource_syncs =
|
||||
resource::list_full_for_user::<ResourceSync>(
|
||||
Default::default(),
|
||||
|
||||
@@ -7,20 +7,23 @@ use komodo_client::{
|
||||
api::read::{GetTag, ListTags},
|
||||
entities::tag::Tag,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{helpers::query::get_tag, state::db_client};
|
||||
|
||||
use super::ReadArgs;
|
||||
|
||||
impl Resolve<ReadArgs> for GetTag {
|
||||
async fn resolve(self, _: &ReadArgs) -> serror::Result<Tag> {
|
||||
async fn resolve(self, _: &ReadArgs) -> mogh_error::Result<Tag> {
|
||||
Ok(get_tag(&self.tag).await?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<ReadArgs> for ListTags {
|
||||
async fn resolve(self, _: &ReadArgs) -> serror::Result<Vec<Tag>> {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &ReadArgs,
|
||||
) -> mogh_error::Result<Vec<Tag>> {
|
||||
let res = find_collect(
|
||||
&db_client().tags,
|
||||
self.query,
|
||||
|
||||
@@ -13,9 +13,9 @@ use komodo_client::{
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mogh_error::AddStatusCode;
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCode;
|
||||
|
||||
use crate::{
|
||||
helpers::periphery_client, permission::get_check_permissions,
|
||||
@@ -30,7 +30,7 @@ impl Resolve<ReadArgs> for ListTerminals {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListTerminalsResponse> {
|
||||
) -> mogh_error::Result<ListTerminalsResponse> {
|
||||
let Some(target) = self.target else {
|
||||
return list_all_terminals_for_user(user, self.use_names).await;
|
||||
};
|
||||
@@ -88,7 +88,7 @@ impl Resolve<ReadArgs> for ListTerminals {
|
||||
async fn list_all_terminals_for_user(
|
||||
user: &User,
|
||||
use_names: bool,
|
||||
) -> serror::Result<Vec<Terminal>> {
|
||||
) -> mogh_error::Result<Vec<Terminal>> {
|
||||
let (mut servers, stacks, deployments) = tokio::try_join!(
|
||||
resource::list_full_for_user::<Server>(
|
||||
Default::default(),
|
||||
@@ -230,7 +230,7 @@ async fn list_all_terminals_for_user(
|
||||
async fn list_terminals_on_server(
|
||||
server: &Server,
|
||||
target: Option<TerminalTarget>,
|
||||
) -> serror::Result<Vec<Terminal>> {
|
||||
) -> mogh_error::Result<Vec<Terminal>> {
|
||||
periphery_client(server)
|
||||
.await?
|
||||
.request(periphery_client::api::terminal::ListTerminals {
|
||||
|
||||
@@ -15,7 +15,7 @@ use komodo_client::{
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::query::{
|
||||
@@ -161,7 +161,7 @@ impl Resolve<ReadArgs> for ExportAllResourcesToToml {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &ReadArgs,
|
||||
) -> serror::Result<ExportAllResourcesToTomlResponse> {
|
||||
) -> mogh_error::Result<ExportAllResourcesToTomlResponse> {
|
||||
let targets = if self.include_resources {
|
||||
get_all_targets(&self.tags, &args.user).await?
|
||||
} else {
|
||||
@@ -197,7 +197,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &ReadArgs,
|
||||
) -> serror::Result<ExportResourcesToTomlResponse> {
|
||||
) -> mogh_error::Result<ExportResourcesToTomlResponse> {
|
||||
let ExportResourcesToToml {
|
||||
targets,
|
||||
user_groups,
|
||||
|
||||
@@ -14,7 +14,7 @@ use komodo_client::{
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
@@ -32,7 +32,7 @@ impl Resolve<ReadArgs> for ListUpdates {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListUpdatesResponse> {
|
||||
) -> mogh_error::Result<ListUpdatesResponse> {
|
||||
let query = user_resource_target_query(user, self.query).await?;
|
||||
|
||||
let usernames = find_collect(&db_client().users, None, None)
|
||||
@@ -92,7 +92,7 @@ impl Resolve<ReadArgs> for GetUpdate {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let update = find_one_by_id(&db_client().updates, &self.id)
|
||||
.await
|
||||
.context("failed to query to db")?
|
||||
|
||||
@@ -13,7 +13,7 @@ use komodo_client::{
|
||||
},
|
||||
entities::user::{UserConfig, admin_service_user},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{helpers::query::get_user, state::db_client};
|
||||
|
||||
@@ -23,7 +23,7 @@ impl Resolve<ReadArgs> for GetUsername {
|
||||
async fn resolve(
|
||||
self,
|
||||
_: &ReadArgs,
|
||||
) -> serror::Result<GetUsernameResponse> {
|
||||
) -> mogh_error::Result<GetUsernameResponse> {
|
||||
if let Some(user) = admin_service_user(&self.user_id) {
|
||||
return Ok(GetUsernameResponse {
|
||||
username: user.username,
|
||||
@@ -53,7 +53,7 @@ impl Resolve<ReadArgs> for FindUser {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user: admin }: &ReadArgs,
|
||||
) -> serror::Result<FindUserResponse> {
|
||||
) -> mogh_error::Result<FindUserResponse> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This method is admin only.").into());
|
||||
}
|
||||
@@ -65,7 +65,7 @@ impl Resolve<ReadArgs> for ListUsers {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListUsersResponse> {
|
||||
) -> mogh_error::Result<ListUsersResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("this route is only accessable by admins").into(),
|
||||
@@ -87,7 +87,7 @@ impl Resolve<ReadArgs> for ListApiKeys {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListApiKeysResponse> {
|
||||
) -> mogh_error::Result<ListApiKeysResponse> {
|
||||
let api_keys = find_collect(
|
||||
&db_client().api_keys,
|
||||
doc! { "user_id": &user.id },
|
||||
@@ -109,7 +109,7 @@ impl Resolve<ReadArgs> for ListApiKeysForServiceUser {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user: admin }: &ReadArgs,
|
||||
) -> serror::Result<ListApiKeysForServiceUserResponse> {
|
||||
) -> mogh_error::Result<ListApiKeysForServiceUserResponse> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("This method is admin only.").into());
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use database::mungos::{
|
||||
},
|
||||
};
|
||||
use komodo_client::api::read::*;
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::state::db_client;
|
||||
|
||||
@@ -19,7 +19,7 @@ impl Resolve<ReadArgs> for GetUserGroup {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetUserGroupResponse> {
|
||||
) -> mogh_error::Result<GetUserGroupResponse> {
|
||||
let mut filter = match ObjectId::from_str(&self.user_group) {
|
||||
Ok(id) => doc! { "_id": id },
|
||||
Err(_) => doc! { "name": &self.user_group },
|
||||
@@ -43,7 +43,7 @@ impl Resolve<ReadArgs> for ListUserGroups {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListUserGroupsResponse> {
|
||||
) -> mogh_error::Result<ListUserGroupsResponse> {
|
||||
let mut filter = Document::new();
|
||||
if !user.admin {
|
||||
filter.insert("users", &user.id);
|
||||
|
||||
@@ -4,7 +4,7 @@ use database::mungos::{
|
||||
find::find_collect, mongodb::options::FindOptions,
|
||||
};
|
||||
use komodo_client::api::read::*;
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{helpers::query::get_variable, state::db_client};
|
||||
|
||||
@@ -14,7 +14,7 @@ impl Resolve<ReadArgs> for GetVariable {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<GetVariableResponse> {
|
||||
) -> mogh_error::Result<GetVariableResponse> {
|
||||
let mut variable = get_variable(&self.name).await?;
|
||||
if !variable.is_secret || user.admin {
|
||||
return Ok(variable);
|
||||
@@ -28,7 +28,7 @@ impl Resolve<ReadArgs> for ListVariables {
|
||||
async fn resolve(
|
||||
self,
|
||||
ReadArgs { user }: &ReadArgs,
|
||||
) -> serror::Result<ListVariablesResponse> {
|
||||
) -> mogh_error::Result<ListVariablesResponse> {
|
||||
let variables = find_collect(
|
||||
&db_client().variables,
|
||||
None,
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
use anyhow::Context;
|
||||
use axum::{Extension, Router, middleware, routing::post};
|
||||
use komodo_client::{api::terminal::*, entities::user::User};
|
||||
use serror::Json;
|
||||
use mogh_auth_server::middleware::authenticate_request;
|
||||
use mogh_error::Json;
|
||||
|
||||
use crate::{
|
||||
auth::auth_request, helpers::terminal::setup_target_for_user,
|
||||
auth::KomodoAuthImpl, helpers::terminal::setup_target_for_user,
|
||||
};
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/execute", post(execute_terminal))
|
||||
.layer(middleware::from_fn(auth_request))
|
||||
.layer(middleware::from_fn(
|
||||
authenticate_request::<KomodoAuthImpl, true>,
|
||||
))
|
||||
}
|
||||
|
||||
// =================
|
||||
@@ -35,8 +38,11 @@ async fn execute_terminal(
|
||||
command,
|
||||
init,
|
||||
}): Json<ExecuteTerminalBody>,
|
||||
) -> serror::Result<axum::body::Body> {
|
||||
info!("/terminal/execute request | user: {}", user.username);
|
||||
) -> mogh_error::Result<axum::body::Body> {
|
||||
info!(
|
||||
"TERMINAL EXECUTE REQUEST | USER: {} ({})",
|
||||
user.username, user.id
|
||||
);
|
||||
|
||||
let (target, terminal, periphery) =
|
||||
setup_target_for_user(target, terminal, init, &user).await?;
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use database::bson::doc;
|
||||
use komodo_client::{
|
||||
api::user::{
|
||||
CreateApiKey, CreateApiKeyResponse, DeleteApiKey,
|
||||
DeleteApiKeyResponse,
|
||||
},
|
||||
entities::{api_key::ApiKey, komodo_timestamp, random_string},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::{AddStatusCode as _, AddStatusCodeError as _};
|
||||
|
||||
use crate::{
|
||||
helpers::{query::get_user, validations::validate_api_key_name},
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
use super::UserArgs;
|
||||
|
||||
const SECRET_LENGTH: usize = 40;
|
||||
const BCRYPT_COST: u32 = 10;
|
||||
|
||||
impl Resolve<UserArgs> for CreateApiKey {
|
||||
#[instrument(
|
||||
"CreateApiKey",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, .. }: &UserArgs,
|
||||
) -> serror::Result<CreateApiKeyResponse> {
|
||||
let user = get_user(&user.id).await?;
|
||||
|
||||
validate_api_key_name(&self.name)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
|
||||
let key = format!("K_{}_K", random_string(SECRET_LENGTH));
|
||||
let secret = format!("S_{}_S", random_string(SECRET_LENGTH));
|
||||
let secret_hash = bcrypt::hash(&secret, BCRYPT_COST)
|
||||
.context("Failed at hashing secret string")?;
|
||||
|
||||
let api_key = ApiKey {
|
||||
name: self.name,
|
||||
key: key.clone(),
|
||||
secret: secret_hash,
|
||||
user_id: user.id.clone(),
|
||||
created_at: komodo_timestamp(),
|
||||
expires: self.expires,
|
||||
};
|
||||
|
||||
db_client()
|
||||
.api_keys
|
||||
.insert_one(api_key)
|
||||
.await
|
||||
.context("Failed to create api key on database")?;
|
||||
|
||||
Ok(CreateApiKeyResponse { key, secret })
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<UserArgs> for DeleteApiKey {
|
||||
#[instrument(
|
||||
"DeleteApiKey",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, .. }: &UserArgs,
|
||||
) -> serror::Result<DeleteApiKeyResponse> {
|
||||
let client = db_client();
|
||||
|
||||
let key = client
|
||||
.api_keys
|
||||
.find_one(doc! { "key": &self.key })
|
||||
.await
|
||||
.context("Failed at database query")?
|
||||
.context("No api key with key found")?;
|
||||
|
||||
if user.id != key.user_id {
|
||||
return Err(
|
||||
anyhow!("Api key does not belong to user")
|
||||
.status_code(StatusCode::FORBIDDEN),
|
||||
);
|
||||
}
|
||||
|
||||
client
|
||||
.api_keys
|
||||
.delete_one(doc! { "key": key.key })
|
||||
.await
|
||||
.context("Failed to delete api key from database")?;
|
||||
|
||||
Ok(DeleteApiKeyResponse {})
|
||||
}
|
||||
}
|
||||
@@ -1,367 +0,0 @@
|
||||
use std::{collections::VecDeque, time::Instant};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
Extension, Json, Router, extract::Path, middleware, routing::post,
|
||||
};
|
||||
use database::{
|
||||
bson::{doc, to_bson},
|
||||
mungos::by_id::update_one_by_id,
|
||||
};
|
||||
use derive_variants::EnumVariants;
|
||||
use komodo_client::{
|
||||
api::user::*,
|
||||
entities::{NoData, komodo_timestamp, user::User},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use response::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serror::{AddStatusCode as _, AddStatusCodeError};
|
||||
use tower_sessions::Session;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
auth::auth_request,
|
||||
config::core_config,
|
||||
helpers::{
|
||||
query::get_user,
|
||||
validations::{validate_password, validate_username},
|
||||
},
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
use super::Variant;
|
||||
|
||||
mod api_key;
|
||||
mod passkey;
|
||||
mod totp;
|
||||
|
||||
pub struct UserArgs {
|
||||
pub user: User,
|
||||
/// Per-client session state
|
||||
pub session: Option<Session>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Debug, Clone, Serialize, Deserialize, Resolve, EnumVariants,
|
||||
)]
|
||||
#[args(UserArgs)]
|
||||
#[response(Response)]
|
||||
#[error(serror::Error)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
enum UserRequest {
|
||||
PushRecentlyViewed(PushRecentlyViewed),
|
||||
SetLastSeenUpdate(SetLastSeenUpdate),
|
||||
UpdateUsername(UpdateUsername),
|
||||
UpdatePassword(UpdatePassword),
|
||||
CreateApiKey(CreateApiKey),
|
||||
DeleteApiKey(DeleteApiKey),
|
||||
BeginTotpEnrollment(BeginTotpEnrollment),
|
||||
ConfirmTotpEnrollment(ConfirmTotpEnrollment),
|
||||
UnenrollTotp(UnenrollTotp),
|
||||
BeginPasskeyEnrollment(BeginPasskeyEnrollment),
|
||||
ConfirmPasskeyEnrollment(ConfirmPasskeyEnrollment),
|
||||
UnenrollPasskey(UnenrollPasskey),
|
||||
BeginThirdPartyLoginLink(BeginThirdPartyLoginLink),
|
||||
UnlinkLogin(UnlinkLogin),
|
||||
UpdateThirdPartySkip2fa(UpdateThirdPartySkip2fa),
|
||||
}
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.route("/{variant}", post(variant_handler))
|
||||
.layer(middleware::from_fn(auth_request))
|
||||
}
|
||||
|
||||
async fn variant_handler(
|
||||
session: Session,
|
||||
user: Extension<User>,
|
||||
Path(Variant { variant }): Path<Variant>,
|
||||
Json(params): Json<serde_json::Value>,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
let req: UserRequest = serde_json::from_value(json!({
|
||||
"type": variant,
|
||||
"params": params,
|
||||
}))?;
|
||||
handler(session, user, Json(req)).await
|
||||
}
|
||||
|
||||
async fn handler(
|
||||
session: Session,
|
||||
Extension(user): Extension<User>,
|
||||
Json(request): Json<UserRequest>,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
let timer = Instant::now();
|
||||
let req_id = Uuid::new_v4();
|
||||
debug!(
|
||||
"/user request {req_id} | user: {} ({})",
|
||||
user.username, user.id
|
||||
);
|
||||
let res = request
|
||||
.resolve(&UserArgs {
|
||||
user,
|
||||
session: Some(session),
|
||||
})
|
||||
.await;
|
||||
if let Err(e) = &res {
|
||||
warn!("/user request {req_id} error: {:#}", e.error);
|
||||
}
|
||||
let elapsed = timer.elapsed();
|
||||
debug!("/user request {req_id} | resolve time: {elapsed:?}");
|
||||
res.map(|res| res.0)
|
||||
}
|
||||
|
||||
const RECENTLY_VIEWED_MAX: usize = 10;
|
||||
|
||||
impl Resolve<UserArgs> for PushRecentlyViewed {
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, .. }: &UserArgs,
|
||||
) -> serror::Result<PushRecentlyViewedResponse> {
|
||||
let user = get_user(&user.id).await?;
|
||||
|
||||
let (resource_type, id) = self.resource.extract_variant_id();
|
||||
|
||||
let field = format!("recents.{resource_type}");
|
||||
|
||||
let update = match user.recents.get(&resource_type) {
|
||||
Some(recents) => {
|
||||
let mut recents = recents
|
||||
.iter()
|
||||
.filter(|_id| !id.eq(*_id))
|
||||
.take(RECENTLY_VIEWED_MAX - 1)
|
||||
.collect::<VecDeque<_>>();
|
||||
|
||||
recents.push_front(id);
|
||||
|
||||
doc! { &field: to_bson(&recents)? }
|
||||
}
|
||||
None => {
|
||||
doc! { &field: [id] }
|
||||
}
|
||||
};
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().users,
|
||||
&user.id,
|
||||
database::mungos::update::Update::Set(update),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("Failed to update user '{field}'"))?;
|
||||
|
||||
Ok(PushRecentlyViewedResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<UserArgs> for SetLastSeenUpdate {
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, .. }: &UserArgs,
|
||||
) -> serror::Result<SetLastSeenUpdateResponse> {
|
||||
update_one_by_id(
|
||||
&db_client().users,
|
||||
&user.id,
|
||||
database::mungos::update::Update::Set(doc! {
|
||||
"last_update_view": komodo_timestamp()
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("Failed to update user 'last_update_view'")?;
|
||||
|
||||
Ok(SetLastSeenUpdateResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<UserArgs> for UpdateUsername {
|
||||
#[instrument(
|
||||
"UpdateUsername",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
new_username = self.username,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, .. }: &UserArgs,
|
||||
) -> serror::Result<UpdateUsernameResponse> {
|
||||
check_locked(&user.username)?;
|
||||
|
||||
validate_username(&self.username)?;
|
||||
|
||||
let db = db_client();
|
||||
|
||||
if db
|
||||
.users
|
||||
.find_one(doc! { "username": &self.username })
|
||||
.await
|
||||
.context("Failed to query for existing users")?
|
||||
.is_some()
|
||||
{
|
||||
return Err(anyhow!("Username already taken.").into());
|
||||
}
|
||||
|
||||
let update = doc! { "$set": { "username": self.username } };
|
||||
|
||||
update_one_by_id(&db.users, &user.id, update, None)
|
||||
.await
|
||||
.context("Failed to update user username on database.")?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<UserArgs> for UpdatePassword {
|
||||
#[instrument(
|
||||
"UpdatePassword",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, .. }: &UserArgs,
|
||||
) -> serror::Result<UpdatePasswordResponse> {
|
||||
check_locked(&user.username)?;
|
||||
|
||||
validate_password(&self.password)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
|
||||
db_client().set_user_password(user, &self.password).await?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SessionThirdPartyLinkInfo {
|
||||
pub user_id: String,
|
||||
}
|
||||
|
||||
impl SessionThirdPartyLinkInfo {
|
||||
pub const KEY: &str = "third-party-link-info";
|
||||
}
|
||||
|
||||
impl Resolve<UserArgs> for BeginThirdPartyLoginLink {
|
||||
#[instrument(
|
||||
"BeginThirdPartyLoginLink",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, session }: &UserArgs,
|
||||
) -> serror::Result<BeginThirdPartyLoginLinkResponse> {
|
||||
check_locked(&user.username)?;
|
||||
|
||||
let session = session.as_ref().context(
|
||||
"Method called in invalid context. This should not happen",
|
||||
)?;
|
||||
|
||||
session
|
||||
.insert(
|
||||
SessionThirdPartyLinkInfo::KEY,
|
||||
SessionThirdPartyLinkInfo {
|
||||
user_id: user.id.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Failed to insert third party link info into client session",
|
||||
)?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<UserArgs> for UnlinkLogin {
|
||||
#[instrument(
|
||||
"UnlinkLogin",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, .. }: &UserArgs,
|
||||
) -> serror::Result<UnlinkLoginResponse> {
|
||||
check_locked(&user.username)?;
|
||||
|
||||
let field = match self.provider.to_lowercase().as_str() {
|
||||
"local" => "linked_logins.Local",
|
||||
"github" => "linked_logins.Github",
|
||||
"google" => "linked_logins.Google",
|
||||
"oidc" => "linked_logins.Oidc",
|
||||
_ => {
|
||||
return Err(
|
||||
anyhow!(
|
||||
"Unrecognized third party login provider: {}",
|
||||
self.provider
|
||||
)
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let update = doc! {
|
||||
"$unset": {
|
||||
field: ""
|
||||
}
|
||||
};
|
||||
|
||||
update_one_by_id(&db_client().users, &user.id, update, None)
|
||||
.await
|
||||
.context("Failed to unlink third partly login on database")?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<UserArgs> for UpdateThirdPartySkip2fa {
|
||||
#[instrument(
|
||||
"UpdateThirdPartySkip2fa",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, .. }: &UserArgs,
|
||||
) -> serror::Result<UpdateThirdPartySkip2faResponse> {
|
||||
check_locked(&user.username)?;
|
||||
|
||||
let update = doc! {
|
||||
"$set": {
|
||||
"third_party_skip_2fa": self.skip
|
||||
}
|
||||
};
|
||||
|
||||
update_one_by_id(&db_client().users, &user.id, update, None)
|
||||
.await
|
||||
.context("Failed to unlink third partly login on database")?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
fn check_locked(username: &str) -> serror::Result<()> {
|
||||
for locked_username in &core_config().lock_login_credentials_for {
|
||||
if locked_username == "__ALL__" || *locked_username == username {
|
||||
return Err(
|
||||
anyhow!("User login credentials are locked.")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
use anyhow::Context as _;
|
||||
use database::{
|
||||
bson::{doc, to_bson},
|
||||
mungos::by_id::update_one_by_id,
|
||||
};
|
||||
use komodo_client::{
|
||||
api::user::{
|
||||
BeginPasskeyEnrollment, BeginPasskeyEnrollmentResponse,
|
||||
ConfirmPasskeyEnrollment, ConfirmPasskeyEnrollmentResponse,
|
||||
UnenrollPasskey, UnenrollPasskeyResponse,
|
||||
},
|
||||
entities::komodo_timestamp,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use webauthn_rs::prelude::PasskeyRegistration;
|
||||
|
||||
use crate::state::{db_client, webauthn};
|
||||
|
||||
use super::UserArgs;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SessionPasskeyEnrollment {
|
||||
user_id: String,
|
||||
state: PasskeyRegistration,
|
||||
}
|
||||
impl SessionPasskeyEnrollment {
|
||||
const KEY: &str = "passkey-enrollment";
|
||||
}
|
||||
|
||||
impl Resolve<UserArgs> for BeginPasskeyEnrollment {
|
||||
#[instrument(
|
||||
"BeginPasskeyEnrollment",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, session }: &UserArgs,
|
||||
) -> serror::Result<BeginPasskeyEnrollmentResponse> {
|
||||
super::check_locked(&user.username)?;
|
||||
|
||||
let session = session.as_ref().context(
|
||||
"Method called in invalid context. This should not happen",
|
||||
)?;
|
||||
|
||||
let webauthn = webauthn().context(
|
||||
"No webauthn provider available, invalid KOMODO_HOST config",
|
||||
)?;
|
||||
|
||||
// Get two parts from this, the first is returned to the client.
|
||||
// The second must stay server side and is used in confirmation flow.
|
||||
let (challenge, state) = webauthn.start_passkey_registration(
|
||||
Uuid::new_v4(),
|
||||
&user.username,
|
||||
&user.username,
|
||||
None,
|
||||
)?;
|
||||
|
||||
session
|
||||
.insert(
|
||||
SessionPasskeyEnrollment::KEY,
|
||||
SessionPasskeyEnrollment {
|
||||
user_id: user.id.clone(),
|
||||
state
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Failed to store passkey enrollment state in server side client session",
|
||||
)?;
|
||||
|
||||
Ok(challenge)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<UserArgs> for ConfirmPasskeyEnrollment {
|
||||
#[instrument(
|
||||
"ConfirmPasskeyEnrollment",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, session }: &UserArgs,
|
||||
) -> serror::Result<ConfirmPasskeyEnrollmentResponse> {
|
||||
let session = session.as_ref().context(
|
||||
"Method called in invalid context. This should not happen",
|
||||
)?;
|
||||
|
||||
let webauthn = webauthn().context(
|
||||
"No webauthn provider available, invalid KOMODO_HOST config",
|
||||
)?;
|
||||
|
||||
let SessionPasskeyEnrollment { user_id, state } = session
|
||||
.remove(SessionPasskeyEnrollment::KEY)
|
||||
.await
|
||||
.context("Passkey enrollment was not initiated correctly")?
|
||||
.context(
|
||||
"Passkey enrollment was not initiated correctly or timed out",
|
||||
)?;
|
||||
|
||||
let passkey = webauthn
|
||||
.finish_passkey_registration(&self.credential, &state)
|
||||
.context("Failed to finish passkey registration")?;
|
||||
|
||||
let passkey = to_bson(&passkey)
|
||||
.context("Failed to serialize passkey to BSON")?;
|
||||
|
||||
let update = doc! {
|
||||
"$set": {
|
||||
"passkey.passkey": passkey,
|
||||
"passkey.created_at": komodo_timestamp()
|
||||
}
|
||||
};
|
||||
|
||||
update_one_by_id(&db_client().users, &user_id, update, None)
|
||||
.await
|
||||
.context("Failed to update user passkey options on database")?;
|
||||
|
||||
Ok(ConfirmPasskeyEnrollmentResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<UserArgs> for UnenrollPasskey {
|
||||
#[instrument(
|
||||
"UnenrollPasskey",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, .. }: &UserArgs,
|
||||
) -> serror::Result<UnenrollPasskeyResponse> {
|
||||
let update = doc! {
|
||||
"$set": {
|
||||
"passkey.passkey": null,
|
||||
"passkey.created_at": 0
|
||||
}
|
||||
};
|
||||
update_one_by_id(&db_client().users, &user.id, update, None)
|
||||
.await
|
||||
.context("Failed to update user passkey options on database")?;
|
||||
Ok(UnenrollPasskeyResponse {})
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use data_encoding::BASE32_NOPAD;
|
||||
use database::{
|
||||
bson::doc, hash_password, mungos::by_id::update_one_by_id,
|
||||
};
|
||||
use komodo_client::{
|
||||
api::user::{
|
||||
BeginTotpEnrollment, BeginTotpEnrollmentResponse,
|
||||
ConfirmTotpEnrollment, ConfirmTotpEnrollmentResponse,
|
||||
UnenrollTotp, UnenrollTotpResponse,
|
||||
},
|
||||
entities::{komodo_timestamp, random_bytes, random_string},
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serror::AddStatusCodeError as _;
|
||||
|
||||
use crate::{auth::totp::make_totp, state::db_client};
|
||||
|
||||
use super::UserArgs;
|
||||
|
||||
const TOTP_ENROLLMENT_SECRET_LENGTH: usize = 20;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SessionTotpEnrollment {
|
||||
secret: Vec<u8>,
|
||||
}
|
||||
impl SessionTotpEnrollment {
|
||||
const KEY: &str = "totp-enrollment";
|
||||
}
|
||||
|
||||
impl Resolve<UserArgs> for BeginTotpEnrollment {
|
||||
#[instrument(
|
||||
"BeginTotpEnrollment",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, session }: &UserArgs,
|
||||
) -> serror::Result<BeginTotpEnrollmentResponse> {
|
||||
super::check_locked(&user.username)?;
|
||||
|
||||
let session = session.as_ref().context(
|
||||
"Method called in invalid context. This should not happen",
|
||||
)?;
|
||||
|
||||
let secret = random_bytes(TOTP_ENROLLMENT_SECRET_LENGTH);
|
||||
let totp = make_totp(secret.clone(), user.id.clone())?;
|
||||
let png = totp
|
||||
.get_qr_base64()
|
||||
.map_err(anyhow::Error::msg)
|
||||
.context("Failed to generate QR code png")?;
|
||||
session
|
||||
.insert(
|
||||
SessionTotpEnrollment::KEY,
|
||||
SessionTotpEnrollment { secret },
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(BeginTotpEnrollmentResponse {
|
||||
uri: totp.get_url(),
|
||||
png,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<UserArgs> for ConfirmTotpEnrollment {
|
||||
#[instrument(
|
||||
"ConfirmTotpEnrollment",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, session }: &UserArgs,
|
||||
) -> serror::Result<ConfirmTotpEnrollmentResponse> {
|
||||
let session = session.as_ref().context(
|
||||
"Method called in invalid context. This should not happen",
|
||||
)?;
|
||||
|
||||
let SessionTotpEnrollment { secret } = session
|
||||
.remove(SessionTotpEnrollment::KEY)
|
||||
.await
|
||||
.context("Totp enrollment was not initiated correctly")?
|
||||
.context(
|
||||
"Totp enrollment was not initiated correctly or timed out",
|
||||
)?;
|
||||
|
||||
let encoded_secret = BASE32_NOPAD.encode(&secret);
|
||||
|
||||
let totp = make_totp(secret, None)?;
|
||||
|
||||
let valid = totp
|
||||
.check_current(&self.code)
|
||||
.context("Failed to check code validity")?;
|
||||
|
||||
if !valid {
|
||||
return Err(anyhow!(
|
||||
"The provided code was not valid. Please try BeginTotpEnrollment flow again."
|
||||
).status_code(StatusCode::BAD_REQUEST));
|
||||
}
|
||||
|
||||
let recovery_codes =
|
||||
(0..10).map(|_| random_string(20)).collect::<Vec<_>>();
|
||||
let hashed_recovery_codes = recovery_codes
|
||||
.iter()
|
||||
.map(hash_password)
|
||||
.collect::<anyhow::Result<Vec<_>>>()
|
||||
.context("Failed to generate valid recovery codes")?;
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().users,
|
||||
&user.id,
|
||||
doc! {
|
||||
"$set": {
|
||||
"totp.secret": encoded_secret,
|
||||
"totp.confirmed_at": komodo_timestamp(),
|
||||
"totp.recovery_codes": hashed_recovery_codes,
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("Failed to update user totp fields on database")?;
|
||||
|
||||
Ok(ConfirmTotpEnrollmentResponse { recovery_codes })
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolve<UserArgs> for UnenrollTotp {
|
||||
#[instrument(
|
||||
"UnenrollTotp",
|
||||
skip_all,
|
||||
fields(operator = user.id)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
UserArgs { user, .. }: &UserArgs,
|
||||
) -> serror::Result<UnenrollTotpResponse> {
|
||||
update_one_by_id(
|
||||
&db_client().users,
|
||||
&user.id,
|
||||
doc! {
|
||||
"$set": {
|
||||
"totp.secret": "",
|
||||
"totp.confirmed_at": 0,
|
||||
"totp.recovery_codes": [],
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("Failed to clear user totp fields on database")?;
|
||||
Ok(UnenrollTotpResponse {})
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use komodo_client::{
|
||||
action::Action, permission::PermissionLevel, update::Update,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{permission::get_check_permissions, resource};
|
||||
|
||||
@@ -23,7 +23,7 @@ impl Resolve<WriteArgs> for CreateAction {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Action> {
|
||||
) -> mogh_error::Result<Action> {
|
||||
resource::create::<Action>(&self.name, self.config, None, user)
|
||||
.await
|
||||
}
|
||||
@@ -42,7 +42,7 @@ impl Resolve<WriteArgs> for CopyAction {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Action> {
|
||||
) -> mogh_error::Result<Action> {
|
||||
let Action { config, .. } = get_check_permissions::<Action>(
|
||||
&self.id,
|
||||
user,
|
||||
@@ -67,7 +67,7 @@ impl Resolve<WriteArgs> for UpdateAction {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Action> {
|
||||
) -> mogh_error::Result<Action> {
|
||||
Ok(resource::update::<Action>(&self.id, self.config, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ impl Resolve<WriteArgs> for RenameAction {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
Ok(resource::rename::<Action>(&self.id, &self.name, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,7 @@ impl Resolve<WriteArgs> for DeleteAction {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Action> {
|
||||
) -> mogh_error::Result<Action> {
|
||||
Ok(resource::delete::<Action>(&self.id, user).await?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ use std::str::FromStr;
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::mongodb::bson::{doc, oid::ObjectId};
|
||||
use komodo_client::{api::write::CloseAlert, entities::NoData};
|
||||
use mogh_error::AddStatusCodeError;
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError;
|
||||
|
||||
use crate::{api::write::WriteArgs, state::db_client};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use komodo_client::{
|
||||
alerter::Alerter, permission::PermissionLevel, update::Update,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{permission::get_check_permissions, resource};
|
||||
|
||||
@@ -23,7 +23,7 @@ impl Resolve<WriteArgs> for CreateAlerter {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Alerter> {
|
||||
) -> mogh_error::Result<Alerter> {
|
||||
resource::create::<Alerter>(&self.name, self.config, None, user)
|
||||
.await
|
||||
}
|
||||
@@ -42,7 +42,7 @@ impl Resolve<WriteArgs> for CopyAlerter {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Alerter> {
|
||||
) -> mogh_error::Result<Alerter> {
|
||||
let Alerter { config, .. } = get_check_permissions::<Alerter>(
|
||||
&self.id,
|
||||
user,
|
||||
@@ -66,7 +66,7 @@ impl Resolve<WriteArgs> for DeleteAlerter {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Alerter> {
|
||||
) -> mogh_error::Result<Alerter> {
|
||||
Ok(resource::delete::<Alerter>(&self.id, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ impl Resolve<WriteArgs> for UpdateAlerter {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Alerter> {
|
||||
) -> mogh_error::Result<Alerter> {
|
||||
Ok(
|
||||
resource::update::<Alerter>(&self.id, self.config, user)
|
||||
.await?,
|
||||
@@ -105,7 +105,7 @@ impl Resolve<WriteArgs> for RenameAlerter {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
Ok(resource::rename::<Alerter>(&self.id, &self.name, user).await?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ use komodo_client::{
|
||||
update::Update,
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api::build::{
|
||||
GetDockerfileContentsOnHost, WriteDockerfileContentsToHost,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::{
|
||||
@@ -54,7 +54,7 @@ impl Resolve<WriteArgs> for CreateBuild {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Build> {
|
||||
) -> mogh_error::Result<Build> {
|
||||
resource::create::<Build>(&self.name, self.config, None, user)
|
||||
.await
|
||||
}
|
||||
@@ -73,7 +73,7 @@ impl Resolve<WriteArgs> for CopyBuild {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Build> {
|
||||
) -> mogh_error::Result<Build> {
|
||||
let Build { mut config, .. } = get_check_permissions::<Build>(
|
||||
&self.id,
|
||||
user,
|
||||
@@ -99,7 +99,7 @@ impl Resolve<WriteArgs> for DeleteBuild {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Build> {
|
||||
) -> mogh_error::Result<Build> {
|
||||
Ok(resource::delete::<Build>(&self.id, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -117,7 +117,7 @@ impl Resolve<WriteArgs> for UpdateBuild {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Build> {
|
||||
) -> mogh_error::Result<Build> {
|
||||
Ok(resource::update::<Build>(&self.id, self.config, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ impl Resolve<WriteArgs> for RenameBuild {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
Ok(resource::rename::<Build>(&self.id, &self.name, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -149,7 +149,10 @@ impl Resolve<WriteArgs> for WriteBuildFileContents {
|
||||
build = self.build,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &WriteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let build = get_check_permissions::<Build>(
|
||||
&self.build,
|
||||
&args.user,
|
||||
@@ -226,7 +229,7 @@ async fn write_dockerfile_contents_git(
|
||||
args: &WriteArgs,
|
||||
build: Build,
|
||||
mut update: Update,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let WriteBuildFileContents { build: _, contents } = req;
|
||||
|
||||
let mut repo_args: RepoExecutionArgs = if !build
|
||||
@@ -318,7 +321,7 @@ async fn write_dockerfile_contents_git(
|
||||
return Ok(update);
|
||||
}
|
||||
|
||||
if let Err(e) = secret_file::write_async(&full_path, &contents)
|
||||
if let Err(e) = mogh_secret_file::write_async(&full_path, &contents)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Failed to write dockerfile contents to {full_path:?}")
|
||||
@@ -370,7 +373,7 @@ impl Resolve<WriteArgs> for RefreshBuildCache {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<NoData> {
|
||||
) -> mogh_error::Result<NoData> {
|
||||
// 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 = get_check_permissions::<Build>(
|
||||
|
||||
@@ -4,7 +4,7 @@ use komodo_client::{
|
||||
builder::Builder, permission::PermissionLevel, update::Update,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{permission::get_check_permissions, resource};
|
||||
|
||||
@@ -23,7 +23,7 @@ impl Resolve<WriteArgs> for CreateBuilder {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Builder> {
|
||||
) -> mogh_error::Result<Builder> {
|
||||
resource::create::<Builder>(&self.name, self.config, None, user)
|
||||
.await
|
||||
}
|
||||
@@ -42,7 +42,7 @@ impl Resolve<WriteArgs> for CopyBuilder {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Builder> {
|
||||
) -> mogh_error::Result<Builder> {
|
||||
let Builder { config, .. } = get_check_permissions::<Builder>(
|
||||
&self.id,
|
||||
user,
|
||||
@@ -66,7 +66,7 @@ impl Resolve<WriteArgs> for DeleteBuilder {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Builder> {
|
||||
) -> mogh_error::Result<Builder> {
|
||||
Ok(resource::delete::<Builder>(&self.id, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ impl Resolve<WriteArgs> for UpdateBuilder {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Builder> {
|
||||
) -> mogh_error::Result<Builder> {
|
||||
Ok(
|
||||
resource::update::<Builder>(&self.id, self.config, user)
|
||||
.await?,
|
||||
@@ -105,7 +105,7 @@ impl Resolve<WriteArgs> for RenameBuilder {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
Ok(resource::rename::<Builder>(&self.id, &self.name, user).await?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,44 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::{by_id::update_one_by_id, mongodb::bson::doc};
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
api::{execute::Deploy, write::*},
|
||||
entities::{
|
||||
Operation,
|
||||
NoData, Operation, ResourceTarget, SwarmOrServer,
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
deployment::{
|
||||
Deployment, DeploymentImage, DeploymentState,
|
||||
PartialDeploymentConfig, RestartMode,
|
||||
Deployment, DeploymentImage, DeploymentInfo, DeploymentState,
|
||||
PartialDeploymentConfig, RestartMode, extract_registry_domain,
|
||||
},
|
||||
docker::container::RestartPolicyNameEnum,
|
||||
komodo_timestamp,
|
||||
komodo_timestamp, optional_string,
|
||||
permission::PermissionLevel,
|
||||
server::{Server, ServerState},
|
||||
to_container_compatible_name,
|
||||
update::Update,
|
||||
user::{auto_redeploy_user, system_user},
|
||||
},
|
||||
};
|
||||
use mogh_cache::SetCache;
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api::{self, container::InspectContainer};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
api::execute::{self, ExecuteRequest},
|
||||
helpers::{
|
||||
periphery_client,
|
||||
query::get_deployment_state,
|
||||
registry_token,
|
||||
update::{add_update, make_update},
|
||||
},
|
||||
permission::get_check_permissions,
|
||||
resource,
|
||||
state::{action_states, db_client, server_status_cache},
|
||||
resource::{self, setup_deployment_execution},
|
||||
state::{
|
||||
action_states, db_client, deployment_status_cache,
|
||||
image_digest_cache, server_status_cache,
|
||||
},
|
||||
};
|
||||
|
||||
use super::WriteArgs;
|
||||
@@ -45,7 +56,7 @@ impl Resolve<WriteArgs> for CreateDeployment {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Deployment> {
|
||||
) -> mogh_error::Result<Deployment> {
|
||||
resource::create::<Deployment>(
|
||||
&self.name,
|
||||
self.config,
|
||||
@@ -69,7 +80,7 @@ impl Resolve<WriteArgs> for CopyDeployment {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Deployment> {
|
||||
) -> mogh_error::Result<Deployment> {
|
||||
let Deployment { config, .. } =
|
||||
get_check_permissions::<Deployment>(
|
||||
&self.id,
|
||||
@@ -100,7 +111,7 @@ impl Resolve<WriteArgs> for CreateDeploymentFromContainer {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Deployment> {
|
||||
) -> mogh_error::Result<Deployment> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
@@ -201,7 +212,7 @@ impl Resolve<WriteArgs> for DeleteDeployment {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Deployment> {
|
||||
) -> mogh_error::Result<Deployment> {
|
||||
Ok(resource::delete::<Deployment>(&self.id, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -219,11 +230,35 @@ impl Resolve<WriteArgs> for UpdateDeployment {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Deployment> {
|
||||
Ok(
|
||||
) -> mogh_error::Result<Deployment> {
|
||||
// If the update changes image,
|
||||
// also update the stored latest image digest.
|
||||
let image_update = self
|
||||
.config
|
||||
.image
|
||||
.as_ref()
|
||||
.map(|image| image.as_image().is_some())
|
||||
.unwrap_or_default();
|
||||
|
||||
let deployment =
|
||||
resource::update::<Deployment>(&self.id, self.config, user)
|
||||
.await?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if image_update {
|
||||
tokio::spawn(async move {
|
||||
let _ = (CheckDeploymentForUpdate {
|
||||
deployment: self.id,
|
||||
skip_auto_update: false,
|
||||
wait_for_auto_update: false,
|
||||
})
|
||||
.resolve(&WriteArgs {
|
||||
user: system_user().to_owned(),
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
Ok(deployment)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,7 +275,7 @@ impl Resolve<WriteArgs> for RenameDeployment {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let deployment = get_check_permissions::<Deployment>(
|
||||
&self.id,
|
||||
user,
|
||||
@@ -314,3 +349,228 @@ impl Resolve<WriteArgs> for RenameDeployment {
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<WriteArgs> for CheckDeploymentForUpdate {
|
||||
#[instrument(
|
||||
"CheckDeploymentForUpdate",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
deployment = self.deployment,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> mogh_error::Result<NoData> {
|
||||
// Even though this is a write request, this doesn't change any config. Anyone that can execute the
|
||||
// deployment should be able to do this.
|
||||
let (deployment, swarm_or_server) = setup_deployment_execution(
|
||||
&self.deployment,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
swarm_or_server.verify_has_target()?;
|
||||
|
||||
check_deployment_for_update_inner(
|
||||
deployment,
|
||||
&swarm_or_server,
|
||||
self.skip_auto_update,
|
||||
self.wait_for_auto_update,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
/// If it goes down the "update available" path,
|
||||
/// only send alert if deployment id is not in this cache.
|
||||
/// If alert is sent, add ID to cache.
|
||||
/// If later it goes down non "update available" path,
|
||||
/// remove the id from cache, so next time it does another alert
|
||||
/// will be sent.
|
||||
fn deployment_alert_sent_cache() -> &'static SetCache<String> {
|
||||
static CACHE: OnceLock<SetCache<String>> = OnceLock::new();
|
||||
CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// Checks remote registry for latest image digest,
|
||||
/// and saves it to database associated with the deployment.
|
||||
///
|
||||
/// Returns true if update is available and auto deploy is false.
|
||||
/// If auto deploy is true, this will deploy.
|
||||
#[instrument(
|
||||
"CheckDeploymentForUpdateInner",
|
||||
skip_all,
|
||||
fields(
|
||||
deployment = deployment.id,
|
||||
)
|
||||
)]
|
||||
pub async fn check_deployment_for_update_inner(
|
||||
deployment: Deployment,
|
||||
swarm_or_server: &SwarmOrServer,
|
||||
skip_auto_update: bool,
|
||||
// Otherwise spawns task to run in background
|
||||
wait_for_auto_update: bool,
|
||||
) -> anyhow::Result<bool> {
|
||||
let alert_cache = deployment_alert_sent_cache();
|
||||
|
||||
let (image, account, token) = match deployment.config.image {
|
||||
DeploymentImage::Image { image } => {
|
||||
if image.contains('@') {
|
||||
// Images with a hardcoded digest can't have update.
|
||||
return Ok(false)
|
||||
}
|
||||
let domain = extract_registry_domain(&image)?;
|
||||
let account =
|
||||
optional_string(&deployment.config.image_registry_account);
|
||||
let token = if let Some(account) = &account {
|
||||
registry_token(&domain, account).await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(image, account, token)
|
||||
}
|
||||
DeploymentImage::Build { .. } => {
|
||||
alert_cache.remove(&deployment.id).await;
|
||||
// This method not used for build based deployments
|
||||
// as deployed version vs built version can be inferred from Updates.
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
let latest_digest = image_digest_cache()
|
||||
.get(&swarm_or_server, &image, account, token)
|
||||
.await?;
|
||||
|
||||
resource::update_info::<Deployment>(
|
||||
&deployment.id,
|
||||
&DeploymentInfo {
|
||||
latest_image_digest: latest_digest.clone(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let Some((state, Some(current_digest))) = deployment_status_cache()
|
||||
.get(&deployment.id)
|
||||
.await
|
||||
.map(|s| (s.curr.state, s.curr.image_digest.clone()))
|
||||
else {
|
||||
alert_cache.remove(&deployment.id).await;
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
// If not running or latest digest matches current, early return
|
||||
if !matches!(state, DeploymentState::Running)
|
||||
|| !latest_digest.update_available(¤t_digest)
|
||||
{
|
||||
alert_cache.remove(&deployment.id).await;
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if !skip_auto_update && deployment.config.auto_update {
|
||||
// Trigger deploy + alert
|
||||
let swarm_id = swarm_or_server.swarm_id().map(str::to_string);
|
||||
let swarm_name = swarm_or_server.swarm_name().map(str::to_string);
|
||||
let server_id = swarm_or_server.server_id().map(str::to_string);
|
||||
let server_name =
|
||||
swarm_or_server.server_name().map(str::to_string);
|
||||
|
||||
// Conservatively remove from alert cache so 'skip_auto_update'
|
||||
// doesn't cause alerts not to be sent on subsequent calls.
|
||||
alert_cache.remove(&deployment.id).await;
|
||||
|
||||
let run = async move {
|
||||
match execute::inner_handler(
|
||||
ExecuteRequest::Deploy(Deploy {
|
||||
deployment: deployment.name.clone(),
|
||||
stop_signal: None,
|
||||
stop_time: None,
|
||||
}),
|
||||
auto_redeploy_user().to_owned(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let ts = komodo_timestamp();
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: true,
|
||||
resolved_ts: ts.into(),
|
||||
level: SeverityLevel::Ok,
|
||||
target: ResourceTarget::Deployment(deployment.id.clone()),
|
||||
data: AlertData::DeploymentAutoUpdated {
|
||||
id: deployment.id.clone(),
|
||||
name: deployment.name.clone(),
|
||||
swarm_id,
|
||||
swarm_name,
|
||||
server_id,
|
||||
server_name,
|
||||
image: image.clone(),
|
||||
},
|
||||
};
|
||||
let res = db_client().alerts.insert_one(&alert).await;
|
||||
if let Err(e) = res {
|
||||
error!(
|
||||
"Failed to record DeploymentAutoUpdated to db | {e:#}"
|
||||
);
|
||||
}
|
||||
send_alerts(&[alert]).await;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to auto update Deployment {} | {e:#}",
|
||||
deployment.name
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if wait_for_auto_update {
|
||||
run.await
|
||||
} else {
|
||||
tokio::spawn(run);
|
||||
}
|
||||
} else {
|
||||
// Avoid spamming alerts
|
||||
if alert_cache.contains(&deployment.id).await {
|
||||
return Ok(true);
|
||||
}
|
||||
alert_cache.insert(deployment.id.clone()).await;
|
||||
let ts = komodo_timestamp();
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: true,
|
||||
resolved_ts: ts.into(),
|
||||
level: SeverityLevel::Ok,
|
||||
target: ResourceTarget::Deployment(deployment.id.clone()),
|
||||
data: AlertData::DeploymentImageUpdateAvailable {
|
||||
id: deployment.id.clone(),
|
||||
name: deployment.name.clone(),
|
||||
swarm_id: swarm_or_server.swarm_id().map(str::to_string),
|
||||
swarm_name: swarm_or_server.swarm_name().map(str::to_string),
|
||||
server_id: swarm_or_server.server_id().map(str::to_string),
|
||||
server_name: swarm_or_server
|
||||
.server_name()
|
||||
.map(str::to_string),
|
||||
image: image.clone(),
|
||||
},
|
||||
};
|
||||
let res = db_client().alerts.insert_one(&alert).await;
|
||||
if let Err(e) = res {
|
||||
error!(
|
||||
"Failed to record DeploymentImageUpdateAvailable to db | {e:#}"
|
||||
);
|
||||
}
|
||||
send_alerts(&[alert]).await;
|
||||
}
|
||||
|
||||
Ok(!deployment.config.auto_update)
|
||||
}
|
||||
|
||||
@@ -2,18 +2,19 @@ use anyhow::Context;
|
||||
use axum::{
|
||||
Extension, Router, extract::Path, middleware, routing::post,
|
||||
};
|
||||
use derive_variants::{EnumVariants, ExtractVariant};
|
||||
use komodo_client::{api::write::*, entities::user::User};
|
||||
use resolver_api::Resolve;
|
||||
use response::Response;
|
||||
use mogh_auth_server::middleware::authenticate_request;
|
||||
use mogh_error::Json;
|
||||
use mogh_error::Response;
|
||||
use mogh_resolver::Resolve;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serror::Json;
|
||||
use strum::Display;
|
||||
use strum::EnumDiscriminants;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::auth_request;
|
||||
use crate::auth::KomodoAuthImpl;
|
||||
|
||||
use super::Variant;
|
||||
|
||||
@@ -40,18 +41,23 @@ mod user;
|
||||
mod user_group;
|
||||
mod variable;
|
||||
|
||||
pub use {
|
||||
deployment::check_deployment_for_update_inner,
|
||||
stack::check_stack_for_update_inner,
|
||||
};
|
||||
|
||||
pub struct WriteArgs {
|
||||
pub user: User,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EnumVariants,
|
||||
Serialize, Deserialize, Debug, Clone, Resolve, EnumDiscriminants,
|
||||
)]
|
||||
#[variant_derive(Debug, Display)]
|
||||
#[strum_discriminants(name(WriteRequestVariant), derive(Display))]
|
||||
#[args(WriteArgs)]
|
||||
#[response(Response)]
|
||||
#[error(serror::Error)]
|
||||
#[error(mogh_error::Error)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
pub enum WriteRequest {
|
||||
// ==== RESOURCE ====
|
||||
@@ -88,6 +94,7 @@ pub enum WriteRequest {
|
||||
RenameStack(RenameStack),
|
||||
WriteStackFileContents(WriteStackFileContents),
|
||||
RefreshStackCache(RefreshStackCache),
|
||||
CheckStackForUpdate(CheckStackForUpdate),
|
||||
|
||||
// ==== DEPLOYMENT ====
|
||||
CreateDeployment(CreateDeployment),
|
||||
@@ -96,6 +103,7 @@ pub enum WriteRequest {
|
||||
DeleteDeployment(DeleteDeployment),
|
||||
UpdateDeployment(UpdateDeployment),
|
||||
RenameDeployment(RenameDeployment),
|
||||
CheckDeploymentForUpdate(CheckDeploymentForUpdate),
|
||||
|
||||
// ==== BUILD ====
|
||||
CreateBuild(CreateBuild),
|
||||
@@ -158,6 +166,8 @@ pub enum WriteRequest {
|
||||
DeleteOnboardingKey(DeleteOnboardingKey),
|
||||
|
||||
// ==== USER ====
|
||||
PushRecentlyViewed(PushRecentlyViewed),
|
||||
SetLastSeenUpdate(SetLastSeenUpdate),
|
||||
CreateLocalUser(CreateLocalUser),
|
||||
DeleteUser(DeleteUser),
|
||||
|
||||
@@ -211,14 +221,16 @@ pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/", post(handler))
|
||||
.route("/{variant}", post(variant_handler))
|
||||
.layer(middleware::from_fn(auth_request))
|
||||
.layer(middleware::from_fn(
|
||||
authenticate_request::<KomodoAuthImpl, true>,
|
||||
))
|
||||
}
|
||||
|
||||
async fn variant_handler(
|
||||
user: Extension<User>,
|
||||
Path(Variant { variant }): Path<Variant>,
|
||||
Json(params): Json<serde_json::Value>,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
) -> mogh_error::Result<axum::response::Response> {
|
||||
let req: WriteRequest = serde_json::from_value(json!({
|
||||
"type": variant,
|
||||
"params": params,
|
||||
@@ -229,10 +241,8 @@ async fn variant_handler(
|
||||
async fn handler(
|
||||
Extension(user): Extension<User>,
|
||||
Json(request): Json<WriteRequest>,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
let req_id = Uuid::new_v4();
|
||||
|
||||
let res = tokio::spawn(task(req_id, request, user))
|
||||
) -> mogh_error::Result<axum::response::Response> {
|
||||
let res = tokio::spawn(task(request, user))
|
||||
.await
|
||||
.context("failure in spawned task");
|
||||
|
||||
@@ -240,18 +250,22 @@ async fn handler(
|
||||
}
|
||||
|
||||
async fn task(
|
||||
req_id: Uuid,
|
||||
request: WriteRequest,
|
||||
user: User,
|
||||
) -> serror::Result<axum::response::Response> {
|
||||
let variant = request.extract_variant();
|
||||
info!("/write request | {variant} | user: {}", user.username);
|
||||
) -> mogh_error::Result<axum::response::Response> {
|
||||
let req_id = Uuid::new_v4();
|
||||
let variant: WriteRequestVariant = (&request).into();
|
||||
|
||||
info!(
|
||||
"WRITE REQUEST {req_id} | METHOD: {variant} | USER: {} ({})",
|
||||
user.username, user.id
|
||||
);
|
||||
|
||||
let res = request.resolve(&WriteArgs { user }).await;
|
||||
|
||||
if let Err(e) = &res {
|
||||
warn!(
|
||||
"/write request {req_id} | {variant} | error: {:#}",
|
||||
"WRITE REQUEST {req_id} | METHOD: {variant} | ERROR: {:#}",
|
||||
e.error
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ use komodo_client::{
|
||||
komodo_timestamp, onboarding_key::OnboardingKey, random_string,
|
||||
},
|
||||
};
|
||||
use noise::key::EncodedKeyPair;
|
||||
use mogh_error::{AddStatusCode, AddStatusCodeError};
|
||||
use mogh_pki::EncodedKeyPair;
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::{AddStatusCode, AddStatusCodeError};
|
||||
|
||||
use crate::{api::write::WriteArgs, state::db_client};
|
||||
|
||||
@@ -35,7 +35,7 @@ impl Resolve<WriteArgs> for CreateOnboardingKey {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<CreateOnboardingKeyResponse> {
|
||||
) -> mogh_error::Result<CreateOnboardingKeyResponse> {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This call is admin only")
|
||||
@@ -47,9 +47,12 @@ impl Resolve<WriteArgs> for CreateOnboardingKey {
|
||||
} else {
|
||||
format!("O_{}_O", random_string(28))
|
||||
};
|
||||
let public_key = EncodedKeyPair::from_private_key(&private_key)?
|
||||
.public
|
||||
.into_inner();
|
||||
let public_key = EncodedKeyPair::from_private_key(
|
||||
mogh_pki::PkiKind::Mutual,
|
||||
&private_key,
|
||||
)?
|
||||
.public
|
||||
.into_inner();
|
||||
let onboarding_key = OnboardingKey {
|
||||
public_key,
|
||||
name: self.name,
|
||||
@@ -58,6 +61,7 @@ impl Resolve<WriteArgs> for CreateOnboardingKey {
|
||||
created_at: komodo_timestamp(),
|
||||
expires: self.expires,
|
||||
tags: self.tags,
|
||||
fix_existing_servers: self.fix_existing_servers,
|
||||
copy_server: self.copy_server,
|
||||
create_builder: self.create_builder,
|
||||
};
|
||||
@@ -99,7 +103,7 @@ impl Resolve<WriteArgs> for UpdateOnboardingKey {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UpdateOnboardingKeyResponse> {
|
||||
) -> mogh_error::Result<UpdateOnboardingKeyResponse> {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This call is admin only")
|
||||
@@ -138,6 +142,10 @@ impl Resolve<WriteArgs> for UpdateOnboardingKey {
|
||||
update.insert("tags", tags);
|
||||
}
|
||||
|
||||
if let Some(fix_existing_servers) = self.fix_existing_servers {
|
||||
update.insert("fix_existing_servers", fix_existing_servers);
|
||||
}
|
||||
|
||||
if let Some(copy_server) = self.copy_server {
|
||||
update.insert("copy_server", copy_server);
|
||||
}
|
||||
@@ -176,7 +184,7 @@ impl Resolve<WriteArgs> for DeleteOnboardingKey {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<DeleteOnboardingKeyResponse> {
|
||||
) -> mogh_error::Result<DeleteOnboardingKeyResponse> {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This call is admin only")
|
||||
|
||||
@@ -8,7 +8,6 @@ use database::mungos::{
|
||||
options::UpdateOptions,
|
||||
},
|
||||
};
|
||||
use derive_variants::ExtractVariant as _;
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
@@ -16,7 +15,7 @@ use komodo_client::{
|
||||
permission::{UserTarget, UserTargetVariant},
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{helpers::query::get_user, state::db_client};
|
||||
|
||||
@@ -35,7 +34,7 @@ impl Resolve<WriteArgs> for UpdateUserAdmin {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user: super_admin }: &WriteArgs,
|
||||
) -> serror::Result<UpdateUserAdminResponse> {
|
||||
) -> mogh_error::Result<UpdateUserAdminResponse> {
|
||||
if !super_admin.super_admin {
|
||||
return Err(
|
||||
anyhow!("Only super admins can call this method.").into(),
|
||||
@@ -83,7 +82,7 @@ impl Resolve<WriteArgs> for UpdateUserBasePermissions {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UpdateUserBasePermissionsResponse> {
|
||||
) -> mogh_error::Result<UpdateUserBasePermissionsResponse> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("this method is admin only").into());
|
||||
}
|
||||
@@ -149,7 +148,7 @@ impl Resolve<WriteArgs> for UpdatePermissionOnResourceType {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UpdatePermissionOnResourceTypeResponse> {
|
||||
) -> mogh_error::Result<UpdatePermissionOnResourceTypeResponse> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("this method is admin only").into());
|
||||
}
|
||||
@@ -227,7 +226,7 @@ impl Resolve<WriteArgs> for UpdatePermissionOnTarget {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UpdatePermissionOnTargetResponse> {
|
||||
) -> mogh_error::Result<UpdatePermissionOnTargetResponse> {
|
||||
if !admin.admin {
|
||||
return Err(anyhow!("this method is admin only").into());
|
||||
}
|
||||
@@ -296,7 +295,7 @@ impl Resolve<WriteArgs> for UpdatePermissionOnTarget {
|
||||
/// checks if inner id is actually a `name`, and replaces it with id if so.
|
||||
async fn extract_user_target_with_validation(
|
||||
user_target: &UserTarget,
|
||||
) -> serror::Result<(UserTargetVariant, String)> {
|
||||
) -> mogh_error::Result<(UserTargetVariant, String)> {
|
||||
match user_target {
|
||||
UserTarget::User(ident) => {
|
||||
let filter = match ObjectId::from_str(ident) {
|
||||
@@ -332,7 +331,7 @@ async fn extract_user_target_with_validation(
|
||||
/// checks if inner id is actually a `name`, and replaces it with id if so.
|
||||
async fn extract_resource_target_with_validation(
|
||||
resource_target: &ResourceTarget,
|
||||
) -> serror::Result<(ResourceTargetVariant, String)> {
|
||||
) -> mogh_error::Result<(ResourceTargetVariant, String)> {
|
||||
match resource_target {
|
||||
ResourceTarget::System(_) => {
|
||||
let res = resource_target.extract_variant_id();
|
||||
|
||||
@@ -4,7 +4,7 @@ use komodo_client::{
|
||||
permission::PermissionLevel, procedure::Procedure, update::Update,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{permission::get_check_permissions, resource};
|
||||
|
||||
@@ -23,7 +23,7 @@ impl Resolve<WriteArgs> for CreateProcedure {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<CreateProcedureResponse> {
|
||||
) -> mogh_error::Result<CreateProcedureResponse> {
|
||||
resource::create::<Procedure>(&self.name, self.config, None, user)
|
||||
.await
|
||||
}
|
||||
@@ -42,7 +42,7 @@ impl Resolve<WriteArgs> for CopyProcedure {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<CopyProcedureResponse> {
|
||||
) -> mogh_error::Result<CopyProcedureResponse> {
|
||||
let Procedure { config, .. } =
|
||||
get_check_permissions::<Procedure>(
|
||||
&self.id,
|
||||
@@ -73,7 +73,7 @@ impl Resolve<WriteArgs> for UpdateProcedure {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<UpdateProcedureResponse> {
|
||||
) -> mogh_error::Result<UpdateProcedureResponse> {
|
||||
Ok(
|
||||
resource::update::<Procedure>(&self.id, self.config, user)
|
||||
.await?,
|
||||
@@ -94,7 +94,7 @@ impl Resolve<WriteArgs> for RenameProcedure {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
Ok(
|
||||
resource::rename::<Procedure>(&self.id, &self.name, user)
|
||||
.await?,
|
||||
@@ -114,7 +114,7 @@ impl Resolve<WriteArgs> for DeleteProcedure {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<DeleteProcedureResponse> {
|
||||
) -> mogh_error::Result<DeleteProcedureResponse> {
|
||||
Ok(resource::delete::<Procedure>(&self.id, user).await?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ use komodo_client::{
|
||||
provider::{DockerRegistryAccount, GitProviderAccount},
|
||||
},
|
||||
};
|
||||
use mogh_error::AddStatusCodeError;
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError;
|
||||
|
||||
use crate::{
|
||||
helpers::update::{add_update, make_update},
|
||||
@@ -35,7 +35,7 @@ impl Resolve<WriteArgs> for CreateGitProviderAccount {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<CreateGitProviderAccountResponse> {
|
||||
) -> mogh_error::Result<CreateGitProviderAccountResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can create git provider accounts")
|
||||
@@ -111,7 +111,7 @@ impl Resolve<WriteArgs> for UpdateGitProviderAccount {
|
||||
async fn resolve(
|
||||
mut self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<UpdateGitProviderAccountResponse> {
|
||||
) -> mogh_error::Result<UpdateGitProviderAccountResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can update git provider accounts")
|
||||
@@ -199,7 +199,7 @@ impl Resolve<WriteArgs> for DeleteGitProviderAccount {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<DeleteGitProviderAccountResponse> {
|
||||
) -> mogh_error::Result<DeleteGitProviderAccountResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can delete git provider accounts")
|
||||
@@ -261,7 +261,7 @@ impl Resolve<WriteArgs> for CreateDockerRegistryAccount {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<CreateDockerRegistryAccountResponse> {
|
||||
) -> mogh_error::Result<CreateDockerRegistryAccountResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!(
|
||||
@@ -340,7 +340,7 @@ impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
|
||||
async fn resolve(
|
||||
mut self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<UpdateDockerRegistryAccountResponse> {
|
||||
) -> mogh_error::Result<UpdateDockerRegistryAccountResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can update docker registry accounts")
|
||||
@@ -435,7 +435,7 @@ impl Resolve<WriteArgs> for DeleteDockerRegistryAccount {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<DeleteDockerRegistryAccountResponse> {
|
||||
) -> mogh_error::Result<DeleteDockerRegistryAccountResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only admins can delete docker registry accounts")
|
||||
|
||||
@@ -15,8 +15,8 @@ use komodo_client::{
|
||||
update::{Log, Update},
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
@@ -44,7 +44,7 @@ impl Resolve<WriteArgs> for CreateRepo {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Repo> {
|
||||
) -> mogh_error::Result<Repo> {
|
||||
resource::create::<Repo>(&self.name, self.config, None, user)
|
||||
.await
|
||||
}
|
||||
@@ -63,7 +63,7 @@ impl Resolve<WriteArgs> for CopyRepo {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Repo> {
|
||||
) -> mogh_error::Result<Repo> {
|
||||
let Repo { config, .. } = get_check_permissions::<Repo>(
|
||||
&self.id,
|
||||
user,
|
||||
@@ -87,7 +87,7 @@ impl Resolve<WriteArgs> for DeleteRepo {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Repo> {
|
||||
) -> mogh_error::Result<Repo> {
|
||||
Ok(resource::delete::<Repo>(&self.id, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@ impl Resolve<WriteArgs> for UpdateRepo {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Repo> {
|
||||
) -> mogh_error::Result<Repo> {
|
||||
Ok(resource::update::<Repo>(&self.id, self.config, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,7 @@ impl Resolve<WriteArgs> for RenameRepo {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let repo = get_check_permissions::<Repo>(
|
||||
&self.id,
|
||||
user,
|
||||
@@ -199,7 +199,7 @@ impl Resolve<WriteArgs> for RefreshRepoCache {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<NoData> {
|
||||
) -> mogh_error::Result<NoData> {
|
||||
// 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 = get_check_permissions::<Repo>(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use anyhow::anyhow;
|
||||
use derive_variants::ExtractVariant as _;
|
||||
use komodo_client::{
|
||||
api::write::{UpdateResourceMeta, UpdateResourceMetaResponse},
|
||||
entities::{
|
||||
@@ -9,9 +8,9 @@ use komodo_client::{
|
||||
sync::ResourceSync,
|
||||
},
|
||||
};
|
||||
use mogh_error::AddStatusCodeError;
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError;
|
||||
|
||||
use crate::resource::{self, ResourceMetaUpdate};
|
||||
|
||||
@@ -33,7 +32,7 @@ impl Resolve<WriteArgs> for UpdateResourceMeta {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &WriteArgs,
|
||||
) -> serror::Result<UpdateResourceMetaResponse> {
|
||||
) -> mogh_error::Result<UpdateResourceMetaResponse> {
|
||||
let meta = ResourceMetaUpdate {
|
||||
description: self.description,
|
||||
template: self.template,
|
||||
|
||||
@@ -10,8 +10,8 @@ use komodo_client::{
|
||||
update::{Update, UpdateStatus},
|
||||
},
|
||||
};
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api;
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
@@ -37,7 +37,7 @@ impl Resolve<WriteArgs> for CreateServer {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Server> {
|
||||
) -> mogh_error::Result<Server> {
|
||||
resource::create::<Server>(
|
||||
&self.name,
|
||||
self.config,
|
||||
@@ -64,7 +64,7 @@ impl Resolve<WriteArgs> for CopyServer {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Server> {
|
||||
) -> mogh_error::Result<Server> {
|
||||
let Server { config, .. } = get_check_permissions::<Server>(
|
||||
&self.id,
|
||||
user,
|
||||
@@ -97,7 +97,7 @@ impl Resolve<WriteArgs> for DeleteServer {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Server> {
|
||||
) -> mogh_error::Result<Server> {
|
||||
Ok(resource::delete::<Server>(&self.id, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ impl Resolve<WriteArgs> for UpdateServer {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Server> {
|
||||
) -> mogh_error::Result<Server> {
|
||||
Ok(resource::update::<Server>(&self.id, self.config, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,7 @@ impl Resolve<WriteArgs> for RenameServer {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
Ok(resource::rename::<Server>(&self.id, &self.name, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -151,7 +151,7 @@ impl Resolve<WriteArgs> for CreateNetwork {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::{by_id::find_one_by_id, mongodb::bson::doc};
|
||||
use komodo_client::{
|
||||
api::{user::CreateApiKey, write::*},
|
||||
api::write::*,
|
||||
entities::{
|
||||
komodo_timestamp,
|
||||
user::{User, UserConfig},
|
||||
user::{NewUserParams, User, UserConfig},
|
||||
},
|
||||
};
|
||||
use mogh_auth_client::api::manage::CreateApiKey;
|
||||
use mogh_auth_server::api::manage::api_key::create_api_key;
|
||||
use mogh_error::{AddStatusCode as _, AddStatusCodeError as _};
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::{AddStatusCode as _, AddStatusCodeError as _};
|
||||
|
||||
use crate::{
|
||||
api::user::UserArgs,
|
||||
helpers::validations::{validate_api_key_name, validate_username},
|
||||
auth::KomodoAuthImpl, helpers::validations::validate_username,
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
@@ -32,7 +33,7 @@ impl Resolve<WriteArgs> for CreateServiceUser {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<CreateServiceUserResponse> {
|
||||
) -> mogh_error::Result<CreateServiceUserResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only Admins can manage Service Users")
|
||||
@@ -47,24 +48,14 @@ impl Resolve<WriteArgs> for CreateServiceUser {
|
||||
description: self.description,
|
||||
};
|
||||
|
||||
let mut user = User {
|
||||
id: Default::default(),
|
||||
let mut user = User::new(NewUserParams {
|
||||
username: self.username,
|
||||
config,
|
||||
totp: Default::default(),
|
||||
passkey: Default::default(),
|
||||
third_party_skip_2fa: Default::default(),
|
||||
enabled: true,
|
||||
admin: false,
|
||||
super_admin: false,
|
||||
create_server_permissions: false,
|
||||
create_build_permissions: false,
|
||||
last_update_view: 0,
|
||||
linked_logins: Default::default(),
|
||||
recents: Default::default(),
|
||||
all: Default::default(),
|
||||
config,
|
||||
updated_at: komodo_timestamp(),
|
||||
};
|
||||
});
|
||||
|
||||
user.id = db_client()
|
||||
.users
|
||||
@@ -93,7 +84,7 @@ impl Resolve<WriteArgs> for UpdateServiceUserDescription {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<UpdateServiceUserDescriptionResponse> {
|
||||
) -> mogh_error::Result<UpdateServiceUserDescriptionResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only Admins can manage Service Users")
|
||||
@@ -150,7 +141,7 @@ impl Resolve<WriteArgs> for CreateApiKeyForServiceUser {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<CreateApiKeyForServiceUserResponse> {
|
||||
) -> mogh_error::Result<CreateApiKeyForServiceUserResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only Admins can manage Service Users")
|
||||
@@ -158,9 +149,6 @@ impl Resolve<WriteArgs> for CreateApiKeyForServiceUser {
|
||||
);
|
||||
}
|
||||
|
||||
validate_api_key_name(&self.name)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
|
||||
let service_user =
|
||||
find_one_by_id(&db_client().users, &self.user_id)
|
||||
.await
|
||||
@@ -174,14 +162,14 @@ impl Resolve<WriteArgs> for CreateApiKeyForServiceUser {
|
||||
);
|
||||
};
|
||||
|
||||
CreateApiKey {
|
||||
name: self.name,
|
||||
expires: self.expires,
|
||||
}
|
||||
.resolve(&UserArgs {
|
||||
user: service_user,
|
||||
session: None,
|
||||
})
|
||||
create_api_key(
|
||||
&KomodoAuthImpl,
|
||||
service_user.id,
|
||||
CreateApiKey {
|
||||
name: self.name,
|
||||
expires: self.expires as u64,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -198,7 +186,7 @@ impl Resolve<WriteArgs> for DeleteApiKeyForServiceUser {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<DeleteApiKeyForServiceUserResponse> {
|
||||
) -> mogh_error::Result<DeleteApiKeyForServiceUserResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only Admins can manage Service Users")
|
||||
|
||||
+416
-30
@@ -1,27 +1,38 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::mongodb::bson::{doc, to_document};
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
FileContents, NoData, Operation, RepoExecutionArgs,
|
||||
all_logs_success,
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
stack::{Stack, StackInfo},
|
||||
update::Update,
|
||||
user::stack_user,
|
||||
use database::{
|
||||
bson::to_bson,
|
||||
mungos::{
|
||||
by_id::update_one_by_id,
|
||||
mongodb::bson::{doc, to_document},
|
||||
},
|
||||
};
|
||||
use formatting::format_serror;
|
||||
use komodo_client::{
|
||||
api::{execute::DeployStack, write::*},
|
||||
entities::{
|
||||
FileContents, NoData, Operation, RepoExecutionArgs,
|
||||
ResourceTarget, SwarmOrServer,
|
||||
alert::{Alert, AlertData, SeverityLevel},
|
||||
all_logs_success, komodo_timestamp,
|
||||
permission::PermissionLevel,
|
||||
repo::Repo,
|
||||
stack::{Stack, StackInfo, StackServiceWithUpdate, StackState},
|
||||
update::Update,
|
||||
user::{auto_redeploy_user, stack_user, system_user},
|
||||
},
|
||||
};
|
||||
use mogh_cache::SetCache;
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api::compose::{
|
||||
GetComposeContentsOnHost, GetComposeContentsOnHostResponse,
|
||||
WriteComposeContentsToHost,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
alert::send_alerts,
|
||||
api::execute::{self, ExecuteRequest},
|
||||
config::core_config,
|
||||
helpers::{
|
||||
query::get_swarm_or_server,
|
||||
@@ -32,9 +43,12 @@ use crate::{
|
||||
resource,
|
||||
stack::{
|
||||
remote::{RemoteComposeContents, get_repo_compose_contents},
|
||||
services::extract_services_into_res,
|
||||
services::{
|
||||
extract_services_from_stack, extract_services_into_res,
|
||||
},
|
||||
setup_stack_execution,
|
||||
},
|
||||
state::db_client,
|
||||
state::{db_client, image_digest_cache, stack_status_cache},
|
||||
};
|
||||
|
||||
use super::WriteArgs;
|
||||
@@ -52,7 +66,7 @@ impl Resolve<WriteArgs> for CreateStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Stack> {
|
||||
) -> mogh_error::Result<Stack> {
|
||||
resource::create::<Stack>(&self.name, self.config, None, user)
|
||||
.await
|
||||
}
|
||||
@@ -71,7 +85,7 @@ impl Resolve<WriteArgs> for CopyStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Stack> {
|
||||
) -> mogh_error::Result<Stack> {
|
||||
let Stack { config, .. } = get_check_permissions::<Stack>(
|
||||
&self.id,
|
||||
user,
|
||||
@@ -96,7 +110,7 @@ impl Resolve<WriteArgs> for DeleteStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Stack> {
|
||||
) -> mogh_error::Result<Stack> {
|
||||
Ok(resource::delete::<Stack>(&self.id, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -114,8 +128,31 @@ impl Resolve<WriteArgs> for UpdateStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Stack> {
|
||||
Ok(resource::update::<Stack>(&self.id, self.config, user).await?)
|
||||
) -> mogh_error::Result<Stack> {
|
||||
let compose_update = self.config.linked_repo.is_some()
|
||||
|| self.config.repo.is_some()
|
||||
|| self.config.files_on_host.is_some()
|
||||
|| self.config.file_contents.is_some();
|
||||
|
||||
let stack =
|
||||
resource::update::<Stack>(&self.id, self.config, user).await?;
|
||||
|
||||
if compose_update {
|
||||
tokio::spawn(async move {
|
||||
let _ = (CheckStackForUpdate {
|
||||
stack: self.id,
|
||||
skip_auto_update: false,
|
||||
wait_for_auto_update: false,
|
||||
skip_cache_refresh: true,
|
||||
})
|
||||
.resolve(&WriteArgs {
|
||||
user: system_user().to_owned(),
|
||||
})
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
Ok(stack)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +169,7 @@ impl Resolve<WriteArgs> for RenameStack {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
Ok(resource::rename::<Stack>(&self.id, &self.name, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -150,7 +187,7 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let WriteStackFileContents {
|
||||
stack,
|
||||
file_path,
|
||||
@@ -177,11 +214,13 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
|
||||
|
||||
update.push_simple_log("File contents to write", &contents);
|
||||
|
||||
if stack.config.files_on_host {
|
||||
let id = stack.id.clone();
|
||||
|
||||
let update = if stack.config.files_on_host {
|
||||
write_stack_file_contents_on_host(
|
||||
stack, file_path, contents, update,
|
||||
)
|
||||
.await
|
||||
.await?
|
||||
} else {
|
||||
write_stack_file_contents_git(
|
||||
stack,
|
||||
@@ -190,8 +229,23 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
|
||||
&user.username,
|
||||
update,
|
||||
)
|
||||
.await
|
||||
}
|
||||
.await?
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = (CheckStackForUpdate {
|
||||
stack: id,
|
||||
skip_auto_update: false,
|
||||
wait_for_auto_update: false,
|
||||
skip_cache_refresh: true,
|
||||
})
|
||||
.resolve(&WriteArgs {
|
||||
user: system_user().to_owned(),
|
||||
})
|
||||
.await;
|
||||
});
|
||||
|
||||
Ok(update)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +255,7 @@ async fn write_stack_file_contents_on_host(
|
||||
file_path: String,
|
||||
contents: String,
|
||||
mut update: Update,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let swarm_or_server = get_swarm_or_server(
|
||||
&stack.config.swarm_id,
|
||||
&stack.config.server_id,
|
||||
@@ -267,7 +321,7 @@ async fn write_stack_file_contents_git(
|
||||
contents: &str,
|
||||
username: &str,
|
||||
mut update: Update,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let mut repo = if !stack.config.linked_repo.is_empty() {
|
||||
crate::resource::get::<Repo>(&stack.config.linked_repo)
|
||||
.await?
|
||||
@@ -405,7 +459,7 @@ impl Resolve<WriteArgs> for RefreshStackCache {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<NoData> {
|
||||
) -> mogh_error::Result<NoData> {
|
||||
// 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 = get_check_permissions::<Stack>(
|
||||
@@ -438,6 +492,15 @@ impl Resolve<WriteArgs> for RefreshStackCache {
|
||||
}
|
||||
|
||||
let mut missing_files = Vec::new();
|
||||
let service_image_digests = stack
|
||||
.info
|
||||
.latest_services
|
||||
.iter()
|
||||
.filter_map(|s| {
|
||||
let digest = s.image_digest.clone()?;
|
||||
Some((s.service_name.clone(), digest))
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let (
|
||||
latest_services,
|
||||
@@ -487,6 +550,7 @@ impl Resolve<WriteArgs> for RefreshStackCache {
|
||||
if let Err(e) = extract_services_into_res(
|
||||
&project_name,
|
||||
&contents.contents,
|
||||
&service_image_digests,
|
||||
&mut services,
|
||||
) {
|
||||
warn!(
|
||||
@@ -529,6 +593,7 @@ impl Resolve<WriteArgs> for RefreshStackCache {
|
||||
if let Err(e) = extract_services_into_res(
|
||||
&project_name,
|
||||
&contents.contents,
|
||||
&service_image_digests,
|
||||
&mut services,
|
||||
) {
|
||||
warn!(
|
||||
@@ -554,6 +619,7 @@ impl Resolve<WriteArgs> for RefreshStackCache {
|
||||
// this should latest (not deployed), so make the project name fresh.
|
||||
&stack.project_name(true),
|
||||
&stack.config.file_contents,
|
||||
&service_image_digests,
|
||||
&mut services,
|
||||
) {
|
||||
warn!(
|
||||
@@ -595,3 +661,323 @@ impl Resolve<WriteArgs> for RefreshStackCache {
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<WriteArgs> for CheckStackForUpdate {
|
||||
#[instrument(
|
||||
"CheckStackForUpdate",
|
||||
skip_all,
|
||||
fields(
|
||||
operator = user.id,
|
||||
stack = self.stack,
|
||||
)
|
||||
)]
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> mogh_error::Result<NoData> {
|
||||
// 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, swarm_or_server) = setup_stack_execution(
|
||||
&self.stack,
|
||||
user,
|
||||
PermissionLevel::Execute.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
swarm_or_server.verify_has_target()?;
|
||||
|
||||
check_stack_for_update_inner(
|
||||
stack.id,
|
||||
&swarm_or_server,
|
||||
self.skip_auto_update,
|
||||
self.wait_for_auto_update,
|
||||
self.skip_cache_refresh,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(NoData {})
|
||||
}
|
||||
}
|
||||
|
||||
/// If it goes down the "update available" path,
|
||||
/// only send alert if stack id + service is not in this cache.
|
||||
/// If alert is sent, add ID to cache.
|
||||
/// If later it goes down non "update available" path,
|
||||
/// remove the id from cache, so next time it does another alert
|
||||
/// will be sent.
|
||||
fn stack_alert_sent_cache() -> &'static SetCache<(String, String)> {
|
||||
static CACHE: OnceLock<SetCache<(String, String)>> =
|
||||
OnceLock::new();
|
||||
CACHE.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// First refresh stack cache, then save
|
||||
/// latest available 'image_digest' for stack services
|
||||
/// to database.
|
||||
#[instrument(
|
||||
"CheckStackForUpdateInner",
|
||||
skip_all,
|
||||
fields(
|
||||
stack = stack,
|
||||
)
|
||||
)]
|
||||
pub async fn check_stack_for_update_inner(
|
||||
// ID or name.
|
||||
stack: String,
|
||||
swarm_or_server: &SwarmOrServer,
|
||||
skip_auto_update: bool,
|
||||
// Otherwise spawns task to run in background
|
||||
wait_for_auto_update: bool,
|
||||
skip_cache_refresh: bool,
|
||||
) -> anyhow::Result<Vec<StackServiceWithUpdate>> {
|
||||
if !skip_cache_refresh {
|
||||
(RefreshStackCache {
|
||||
stack: stack.clone(),
|
||||
})
|
||||
.resolve(&WriteArgs {
|
||||
user: system_user().to_owned(),
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.error)
|
||||
.context("Failed to refresh stack cache before update check")?;
|
||||
}
|
||||
|
||||
// Query again after refresh
|
||||
let mut stack = resource::get::<Stack>(&stack).await?;
|
||||
|
||||
let cache = image_digest_cache();
|
||||
|
||||
for service in &mut stack.info.latest_services {
|
||||
if service.image.is_empty() ||
|
||||
// Images with a hardcoded digest can't have update.
|
||||
service.image.contains('@')
|
||||
{
|
||||
service.image_digest = None;
|
||||
continue;
|
||||
}
|
||||
match cache
|
||||
.get(&swarm_or_server, &service.image, None, None)
|
||||
.await
|
||||
{
|
||||
Ok(digest) => service.image_digest = Some(digest),
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to check for update | Stack: {} | Service: {} | Error: {e:#}",
|
||||
stack.name, service.service_name
|
||||
);
|
||||
service.image_digest = None;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let latest_services = to_bson(&stack.info.latest_services)
|
||||
.context("Failed to serialize stack latest services to BSON")?;
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().stacks,
|
||||
&stack.id,
|
||||
doc! { "$set": { "info.latest_services": latest_services } },
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let alert_cache = stack_alert_sent_cache();
|
||||
|
||||
let Some(status) = stack_status_cache().get(&stack.id).await else {
|
||||
alert_cache
|
||||
.retain(|(stack_id, _)| stack_id != &stack.id)
|
||||
.await;
|
||||
return Ok(
|
||||
extract_services_from_stack(&stack)
|
||||
.into_iter()
|
||||
.map(|service| StackServiceWithUpdate {
|
||||
service: service.service_name,
|
||||
image: service.image,
|
||||
update_available: false,
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
};
|
||||
|
||||
let StackState::Running = status.curr.state else {
|
||||
alert_cache
|
||||
.retain(|(stack_id, _)| stack_id != &stack.id)
|
||||
.await;
|
||||
return Ok(
|
||||
status
|
||||
.curr
|
||||
.services
|
||||
.iter()
|
||||
.map(|service| StackServiceWithUpdate {
|
||||
service: service.service.clone(),
|
||||
image: service.image.clone(),
|
||||
update_available: false,
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
};
|
||||
|
||||
let mut services = Vec::new();
|
||||
|
||||
for service in status.curr.services.iter() {
|
||||
let mut service_with_update = StackServiceWithUpdate {
|
||||
service: service.service.clone(),
|
||||
image: service.image.clone(),
|
||||
update_available: false,
|
||||
};
|
||||
|
||||
let Some(current_digest) = &service.image_digest else {
|
||||
services.push(service_with_update);
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(latest_digest) =
|
||||
stack.info.latest_services.iter().find_map(|s| {
|
||||
if s.service_name == service.service {
|
||||
s.image_digest.clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
else {
|
||||
services.push(service_with_update);
|
||||
continue;
|
||||
};
|
||||
|
||||
service_with_update.update_available =
|
||||
latest_digest.update_available(¤t_digest.0);
|
||||
|
||||
if service_with_update.update_available
|
||||
&& (skip_auto_update || !stack.config.auto_update)
|
||||
&& !alert_cache
|
||||
.contains(&(stack.id.clone(), service.service.clone()))
|
||||
.await
|
||||
{
|
||||
// Send service update available alert
|
||||
alert_cache
|
||||
.insert((stack.id.clone(), service.service.clone()))
|
||||
.await;
|
||||
let ts = komodo_timestamp();
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: true,
|
||||
resolved_ts: ts.into(),
|
||||
level: SeverityLevel::Ok,
|
||||
target: ResourceTarget::Stack(stack.id.clone()),
|
||||
data: AlertData::StackImageUpdateAvailable {
|
||||
id: stack.id.clone(),
|
||||
name: stack.name.clone(),
|
||||
swarm_name: swarm_or_server
|
||||
.swarm_name()
|
||||
.map(str::to_string),
|
||||
swarm_id: swarm_or_server.swarm_id().map(str::to_string),
|
||||
server_name: swarm_or_server
|
||||
.server_name()
|
||||
.map(str::to_string),
|
||||
server_id: swarm_or_server.server_id().map(str::to_string),
|
||||
service: service.service.clone(),
|
||||
image: service.image.clone(),
|
||||
},
|
||||
};
|
||||
let res = db_client().alerts.insert_one(&alert).await;
|
||||
if let Err(e) = res {
|
||||
error!(
|
||||
"Failed to record StackImageUpdateAvailable to db | {e:#}"
|
||||
);
|
||||
}
|
||||
send_alerts(&[alert]).await;
|
||||
}
|
||||
|
||||
services.push(service_with_update);
|
||||
}
|
||||
|
||||
let services_with_update = services
|
||||
.iter()
|
||||
.filter(|service| service.update_available)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if skip_auto_update
|
||||
|| !stack.config.auto_update
|
||||
|| services_with_update.is_empty()
|
||||
{
|
||||
return Ok(services);
|
||||
}
|
||||
|
||||
// Conservatively remove from alert cache so 'skip_auto_update'
|
||||
// doesn't cause alerts not to be sent on subsequent calls.
|
||||
alert_cache
|
||||
.retain(|(stack_id, _)| stack_id != &stack.id)
|
||||
.await;
|
||||
|
||||
let deploy_services = if stack.config.auto_update_all_services {
|
||||
Vec::new()
|
||||
} else {
|
||||
services_with_update
|
||||
.iter()
|
||||
.map(|service| service.service.clone())
|
||||
.collect()
|
||||
};
|
||||
|
||||
let swarm_id = swarm_or_server.swarm_id().map(str::to_string);
|
||||
let swarm_name = swarm_or_server.swarm_name().map(str::to_string);
|
||||
let server_id = swarm_or_server.server_id().map(str::to_string);
|
||||
let server_name = swarm_or_server.server_name().map(str::to_string);
|
||||
|
||||
let run = async move {
|
||||
match execute::inner_handler(
|
||||
ExecuteRequest::DeployStack(DeployStack {
|
||||
stack: stack.id.clone(),
|
||||
services: deploy_services,
|
||||
stop_time: None,
|
||||
}),
|
||||
auto_redeploy_user().to_owned(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let ts = komodo_timestamp();
|
||||
let alert = Alert {
|
||||
id: Default::default(),
|
||||
ts,
|
||||
resolved: true,
|
||||
resolved_ts: ts.into(),
|
||||
level: SeverityLevel::Ok,
|
||||
target: ResourceTarget::Stack(stack.id.clone()),
|
||||
data: AlertData::StackAutoUpdated {
|
||||
id: stack.id.clone(),
|
||||
name: stack.name.clone(),
|
||||
swarm_id,
|
||||
swarm_name,
|
||||
server_id,
|
||||
server_name,
|
||||
images: services_with_update
|
||||
.iter()
|
||||
.map(|service| service.image.clone())
|
||||
.collect(),
|
||||
},
|
||||
};
|
||||
let res = db_client().alerts.insert_one(&alert).await;
|
||||
if let Err(e) = res {
|
||||
error!("Failed to record StackAutoUpdated to db | {e:#}");
|
||||
}
|
||||
send_alerts(&[alert]).await;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to auto update Stack {} | {e:#}", stack.name)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if wait_for_auto_update {
|
||||
run.await
|
||||
} else {
|
||||
tokio::spawn(run);
|
||||
}
|
||||
|
||||
Ok(services)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use komodo_client::{
|
||||
permission::PermissionLevel, swarm::Swarm, update::Update,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
|
||||
use crate::{permission::get_check_permissions, resource};
|
||||
|
||||
@@ -23,7 +23,7 @@ impl Resolve<WriteArgs> for CreateSwarm {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Swarm> {
|
||||
) -> mogh_error::Result<Swarm> {
|
||||
resource::create::<Swarm>(&self.name, self.config, None, user)
|
||||
.await
|
||||
}
|
||||
@@ -42,7 +42,7 @@ impl Resolve<WriteArgs> for CopySwarm {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Swarm> {
|
||||
) -> mogh_error::Result<Swarm> {
|
||||
let Swarm { config, .. } = get_check_permissions::<Swarm>(
|
||||
&self.id,
|
||||
user,
|
||||
@@ -66,7 +66,7 @@ impl Resolve<WriteArgs> for DeleteSwarm {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Swarm> {
|
||||
) -> mogh_error::Result<Swarm> {
|
||||
Ok(resource::delete::<Swarm>(&self.id, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ impl Resolve<WriteArgs> for UpdateSwarm {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Swarm> {
|
||||
) -> mogh_error::Result<Swarm> {
|
||||
Ok(resource::update::<Swarm>(&self.id, self.config, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,7 @@ impl Resolve<WriteArgs> for RenameSwarm {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
Ok(resource::rename::<Swarm>(&self.id, &self.name, user).await?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ use komodo_client::{
|
||||
user::sync_user,
|
||||
},
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
use mogh_resolver::Resolve;
|
||||
use tracing::Instrument;
|
||||
|
||||
use crate::{
|
||||
@@ -69,7 +69,7 @@ impl Resolve<WriteArgs> for CreateResourceSync {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<ResourceSync> {
|
||||
) -> mogh_error::Result<ResourceSync> {
|
||||
resource::create::<ResourceSync>(
|
||||
&self.name,
|
||||
self.config,
|
||||
@@ -93,7 +93,7 @@ impl Resolve<WriteArgs> for CopyResourceSync {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<ResourceSync> {
|
||||
) -> mogh_error::Result<ResourceSync> {
|
||||
let ResourceSync { config, .. } =
|
||||
get_check_permissions::<ResourceSync>(
|
||||
&self.id,
|
||||
@@ -123,7 +123,7 @@ impl Resolve<WriteArgs> for DeleteResourceSync {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<ResourceSync> {
|
||||
) -> mogh_error::Result<ResourceSync> {
|
||||
Ok(resource::delete::<ResourceSync>(&self.id, user).await?)
|
||||
}
|
||||
}
|
||||
@@ -141,7 +141,7 @@ impl Resolve<WriteArgs> for UpdateResourceSync {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<ResourceSync> {
|
||||
) -> mogh_error::Result<ResourceSync> {
|
||||
Ok(
|
||||
resource::update::<ResourceSync>(&self.id, self.config, user)
|
||||
.await?,
|
||||
@@ -162,7 +162,7 @@ impl Resolve<WriteArgs> for RenameResourceSync {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
Ok(
|
||||
resource::rename::<ResourceSync>(&self.id, &self.name, user)
|
||||
.await?,
|
||||
@@ -181,7 +181,10 @@ impl Resolve<WriteArgs> for WriteSyncFileContents {
|
||||
file_path = self.file_path,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &WriteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let sync = get_check_permissions::<ResourceSync>(
|
||||
&self.sync,
|
||||
&args.user,
|
||||
@@ -231,7 +234,7 @@ async fn write_sync_file_contents_on_host(
|
||||
args: &WriteArgs,
|
||||
sync: ResourceSync,
|
||||
mut update: Update,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let WriteSyncFileContents {
|
||||
sync: _,
|
||||
resource_path,
|
||||
@@ -249,7 +252,7 @@ async fn write_sync_file_contents_on_host(
|
||||
.context("Invalid resource path")?;
|
||||
let full_path = root.join(&resource_path).join(&file_path);
|
||||
|
||||
if let Err(e) = secret_file::write_async(&full_path, &contents)
|
||||
if let Err(e) = mogh_secret_file::write_async(&full_path, &contents)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
@@ -295,7 +298,7 @@ async fn write_sync_file_contents_git(
|
||||
sync: ResourceSync,
|
||||
repo: Option<Repo>,
|
||||
mut update: Update,
|
||||
) -> serror::Result<Update> {
|
||||
) -> mogh_error::Result<Update> {
|
||||
let WriteSyncFileContents {
|
||||
sync: _,
|
||||
resource_path,
|
||||
@@ -448,7 +451,10 @@ impl Resolve<WriteArgs> for CommitSync {
|
||||
sync = self.sync,
|
||||
)
|
||||
)]
|
||||
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
|
||||
async fn resolve(
|
||||
self,
|
||||
args: &WriteArgs,
|
||||
) -> mogh_error::Result<Update> {
|
||||
let WriteArgs { user } = args;
|
||||
|
||||
let sync = get_check_permissions::<entities::sync::ResourceSync>(
|
||||
@@ -535,12 +541,13 @@ impl Resolve<WriteArgs> for CommitSync {
|
||||
.join(to_path_compatible_name(&sync.name))
|
||||
.join(&resource_path);
|
||||
let span = info_span!("CommitSyncOnHost");
|
||||
if let Err(e) = secret_file::write_async(&file_path, &res.toml)
|
||||
.instrument(span)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Failed to write resource file to {file_path:?}",)
|
||||
})
|
||||
if let Err(e) =
|
||||
mogh_secret_file::write_async(&file_path, &res.toml)
|
||||
.instrument(span)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("Failed to write resource file to {file_path:?}",)
|
||||
})
|
||||
{
|
||||
update.push_error_log(
|
||||
"Write resource file",
|
||||
@@ -677,7 +684,7 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<ResourceSync> {
|
||||
) -> mogh_error::Result<ResourceSync> {
|
||||
// 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 =
|
||||
|
||||
@@ -13,9 +13,9 @@ use komodo_client::{
|
||||
server::Server, stack::Stack, sync::ResourceSync, tag::Tag,
|
||||
},
|
||||
};
|
||||
use mogh_error::AddStatusCodeError;
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
@@ -39,7 +39,7 @@ impl Resolve<WriteArgs> for CreateTag {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Tag> {
|
||||
) -> mogh_error::Result<Tag> {
|
||||
if core_config().disable_non_admin_create && !user.admin {
|
||||
return Err(
|
||||
anyhow!("Non admins cannot create tags")
|
||||
@@ -88,7 +88,7 @@ impl Resolve<WriteArgs> for RenameTag {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Tag> {
|
||||
) -> mogh_error::Result<Tag> {
|
||||
if ObjectId::from_str(&self.name).is_ok() {
|
||||
return Err(anyhow!("tag name cannot be ObjectId").into());
|
||||
}
|
||||
@@ -121,7 +121,7 @@ impl Resolve<WriteArgs> for UpdateTagColor {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Tag> {
|
||||
) -> mogh_error::Result<Tag> {
|
||||
let tag = get_tag_check_owner(&self.tag, user).await?;
|
||||
|
||||
update_one_by_id(
|
||||
@@ -149,7 +149,7 @@ impl Resolve<WriteArgs> for DeleteTag {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<Tag> {
|
||||
) -> mogh_error::Result<Tag> {
|
||||
let tag = get_tag_check_owner(&self.id, user).await?;
|
||||
|
||||
tokio::try_join!(
|
||||
|
||||
@@ -8,10 +8,10 @@ use komodo_client::{
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
use mogh_error::AddStatusCode;
|
||||
use mogh_resolver::Resolve;
|
||||
use periphery_client::api;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCode;
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
@@ -47,7 +47,7 @@ impl Resolve<WriteArgs> for CreateTerminal {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<NoData> {
|
||||
) -> mogh_error::Result<NoData> {
|
||||
match self.target.clone() {
|
||||
TerminalTarget::Server { server } => {
|
||||
let server = server
|
||||
@@ -159,7 +159,7 @@ impl Resolve<WriteArgs> for DeleteTerminal {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<NoData> {
|
||||
) -> mogh_error::Result<NoData> {
|
||||
let server = match &self.target {
|
||||
TerminalTarget::Server { server } => {
|
||||
let server = server
|
||||
@@ -233,7 +233,7 @@ impl Resolve<WriteArgs> for DeleteAllTerminals {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<NoData> {
|
||||
) -> mogh_error::Result<NoData> {
|
||||
let server = get_check_permissions::<Server>(
|
||||
&self.server,
|
||||
user,
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
use std::str::FromStr;
|
||||
use std::{collections::VecDeque, str::FromStr};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use database::{
|
||||
bson::to_bson,
|
||||
hash_password,
|
||||
mungos::mongodb::bson::{doc, oid::ObjectId},
|
||||
mungos::{
|
||||
by_id::update_one_by_id,
|
||||
mongodb::bson::{doc, oid::ObjectId},
|
||||
},
|
||||
};
|
||||
use komodo_client::{
|
||||
api::write::*,
|
||||
entities::user::{User, UserConfig},
|
||||
entities::{
|
||||
komodo_timestamp,
|
||||
user::{NewUserParams, User, UserConfig},
|
||||
},
|
||||
};
|
||||
use mogh_error::{AddStatusCode as _, AddStatusCodeError};
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::{AddStatusCode as _, AddStatusCodeError};
|
||||
|
||||
use crate::{
|
||||
helpers::validations::{validate_password, validate_username},
|
||||
helpers::{
|
||||
query::get_user,
|
||||
validations::{validate_password, validate_username},
|
||||
},
|
||||
state::db_client,
|
||||
};
|
||||
|
||||
@@ -23,6 +33,73 @@ use super::WriteArgs;
|
||||
|
||||
//
|
||||
|
||||
const RECENTLY_VIEWED_MAX: usize = 10;
|
||||
|
||||
impl Resolve<WriteArgs> for PushRecentlyViewed {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user, .. }: &WriteArgs,
|
||||
) -> mogh_error::Result<PushRecentlyViewedResponse> {
|
||||
let user = get_user(&user.id).await?;
|
||||
|
||||
let (resource_type, id) = self.resource.extract_variant_id();
|
||||
|
||||
let field = format!("recents.{resource_type}");
|
||||
|
||||
let update = match user.recents.get(&resource_type) {
|
||||
Some(recents) => {
|
||||
let mut recents = recents
|
||||
.iter()
|
||||
.filter(|_id| !id.eq(*_id))
|
||||
.take(RECENTLY_VIEWED_MAX - 1)
|
||||
.collect::<VecDeque<_>>();
|
||||
|
||||
recents.push_front(id);
|
||||
|
||||
doc! { &field: to_bson(&recents)? }
|
||||
}
|
||||
None => {
|
||||
doc! { &field: [id] }
|
||||
}
|
||||
};
|
||||
|
||||
update_one_by_id(
|
||||
&db_client().users,
|
||||
&user.id,
|
||||
database::mungos::update::Update::Set(update),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("Failed to update user '{field}'"))?;
|
||||
|
||||
Ok(PushRecentlyViewedResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<WriteArgs> for SetLastSeenUpdate {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user, .. }: &WriteArgs,
|
||||
) -> mogh_error::Result<SetLastSeenUpdateResponse> {
|
||||
update_one_by_id(
|
||||
&db_client().users,
|
||||
&user.id,
|
||||
database::mungos::update::Update::Set(doc! {
|
||||
"last_update_view": komodo_timestamp()
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("Failed to update user 'last_update_view'")?;
|
||||
|
||||
Ok(SetLastSeenUpdateResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<WriteArgs> for CreateLocalUser {
|
||||
#[instrument(
|
||||
"CreateLocalUser",
|
||||
@@ -35,7 +112,7 @@ impl Resolve<WriteArgs> for CreateLocalUser {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<CreateLocalUserResponse> {
|
||||
) -> mogh_error::Result<CreateLocalUserResponse> {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This method is Admin Only.")
|
||||
@@ -63,26 +140,16 @@ impl Resolve<WriteArgs> for CreateLocalUser {
|
||||
let ts = unix_timestamp_ms() as i64;
|
||||
let hashed_password = hash_password(self.password)?;
|
||||
|
||||
let mut user = User {
|
||||
id: Default::default(),
|
||||
let mut user = User::new(NewUserParams {
|
||||
username: self.username,
|
||||
enabled: true,
|
||||
admin: false,
|
||||
super_admin: false,
|
||||
create_server_permissions: false,
|
||||
create_build_permissions: false,
|
||||
updated_at: ts,
|
||||
last_update_view: 0,
|
||||
recents: Default::default(),
|
||||
all: Default::default(),
|
||||
config: UserConfig::Local {
|
||||
password: hashed_password,
|
||||
},
|
||||
linked_logins: Default::default(),
|
||||
totp: Default::default(),
|
||||
passkey: Default::default(),
|
||||
third_party_skip_2fa: Default::default(),
|
||||
};
|
||||
updated_at: ts,
|
||||
});
|
||||
|
||||
user.id = db_client()
|
||||
.users
|
||||
@@ -114,7 +181,7 @@ impl Resolve<WriteArgs> for DeleteUser {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<DeleteUserResponse> {
|
||||
) -> mogh_error::Result<DeleteUserResponse> {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This method is admin-only.")
|
||||
|
||||
@@ -10,9 +10,9 @@ use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{komodo_timestamp, user_group::UserGroup},
|
||||
};
|
||||
use mogh_error::AddStatusCodeError;
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::AddStatusCodeError;
|
||||
|
||||
use crate::state::db_client;
|
||||
|
||||
@@ -30,7 +30,7 @@ impl Resolve<WriteArgs> for CreateUserGroup {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UserGroup> {
|
||||
) -> mogh_error::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This call is admin only")
|
||||
@@ -76,7 +76,7 @@ impl Resolve<WriteArgs> for RenameUserGroup {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UserGroup> {
|
||||
) -> mogh_error::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This call is admin only")
|
||||
@@ -112,7 +112,7 @@ impl Resolve<WriteArgs> for DeleteUserGroup {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UserGroup> {
|
||||
) -> mogh_error::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This call is admin only")
|
||||
@@ -156,7 +156,7 @@ impl Resolve<WriteArgs> for AddUserToUserGroup {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UserGroup> {
|
||||
) -> mogh_error::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This call is admin only")
|
||||
@@ -211,7 +211,7 @@ impl Resolve<WriteArgs> for RemoveUserFromUserGroup {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UserGroup> {
|
||||
) -> mogh_error::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This call is admin only")
|
||||
@@ -266,7 +266,7 @@ impl Resolve<WriteArgs> for SetUsersInUserGroup {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UserGroup> {
|
||||
) -> mogh_error::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This call is admin only")
|
||||
@@ -324,7 +324,7 @@ impl Resolve<WriteArgs> for SetEveryoneUserGroup {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user: admin }: &WriteArgs,
|
||||
) -> serror::Result<UserGroup> {
|
||||
) -> mogh_error::Result<UserGroup> {
|
||||
if !admin.admin {
|
||||
return Err(
|
||||
anyhow!("This call is admin only")
|
||||
|
||||
@@ -4,9 +4,9 @@ use komodo_client::{
|
||||
api::write::*,
|
||||
entities::{Operation, ResourceTarget, variable::Variable},
|
||||
};
|
||||
use mogh_error::{AddStatusCode as _, AddStatusCodeError};
|
||||
use mogh_resolver::Resolve;
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::{AddStatusCode as _, AddStatusCodeError};
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
@@ -33,7 +33,7 @@ impl Resolve<WriteArgs> for CreateVariable {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<CreateVariableResponse> {
|
||||
) -> mogh_error::Result<CreateVariableResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only Admins can create Variables")
|
||||
@@ -95,7 +95,7 @@ impl Resolve<WriteArgs> for UpdateVariableValue {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<UpdateVariableValueResponse> {
|
||||
) -> mogh_error::Result<UpdateVariableValueResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only Admins can update Variables")
|
||||
@@ -165,7 +165,7 @@ impl Resolve<WriteArgs> for UpdateVariableDescription {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<UpdateVariableDescriptionResponse> {
|
||||
) -> mogh_error::Result<UpdateVariableDescriptionResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only Admins can update Variables")
|
||||
@@ -199,7 +199,7 @@ impl Resolve<WriteArgs> for UpdateVariableIsSecret {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<UpdateVariableIsSecretResponse> {
|
||||
) -> mogh_error::Result<UpdateVariableIsSecretResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only Admins can update Variables")
|
||||
@@ -232,7 +232,7 @@ impl Resolve<WriteArgs> for DeleteVariable {
|
||||
async fn resolve(
|
||||
self,
|
||||
WriteArgs { user }: &WriteArgs,
|
||||
) -> serror::Result<DeleteVariableResponse> {
|
||||
) -> mogh_error::Result<DeleteVariableResponse> {
|
||||
if !user.admin {
|
||||
return Err(
|
||||
anyhow!("Only Admins can delete Variables")
|
||||
|
||||
+14
-15
@@ -1,21 +1,25 @@
|
||||
use std::net::IpAddr;
|
||||
|
||||
use crate::{
|
||||
auth::{auth_api_key_check_enabled, auth_jwt_check_enabled},
|
||||
helpers::query::get_user,
|
||||
state::auth_rate_limiter,
|
||||
};
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::ws::{self, WebSocket},
|
||||
http::HeaderMap,
|
||||
routing::get,
|
||||
};
|
||||
use komodo_client::{entities::user::User, ws::WsLoginMessage};
|
||||
use rate_limit::WithFailureRateLimit;
|
||||
use mogh_error::{AddStatusCode, AddStatusCodeError};
|
||||
use mogh_rate_limit::WithFailureRateLimit;
|
||||
use reqwest::StatusCode;
|
||||
use serror::{AddStatusCode, AddStatusCodeError};
|
||||
|
||||
use crate::{
|
||||
auth::{
|
||||
GENERAL_RATE_LIMITER,
|
||||
middleware::{
|
||||
auth_api_key_check_enabled, auth_jwt_check_enabled,
|
||||
},
|
||||
},
|
||||
helpers::query::get_user,
|
||||
};
|
||||
|
||||
mod terminal;
|
||||
mod update;
|
||||
@@ -31,8 +35,7 @@ pub fn router() -> Router {
|
||||
|
||||
async fn user_ws_login(
|
||||
mut socket: WebSocket,
|
||||
headers: &HeaderMap,
|
||||
fallback_ip: IpAddr,
|
||||
ip: IpAddr,
|
||||
) -> Option<(WebSocket, User)> {
|
||||
let res = async {
|
||||
let message = match socket
|
||||
@@ -69,11 +72,7 @@ async fn user_ws_login(
|
||||
}
|
||||
}
|
||||
}
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(fallback_ip),
|
||||
)
|
||||
.with_failure_rate_limit_using_ip(&GENERAL_RATE_LIMITER, &ip)
|
||||
.await;
|
||||
match res {
|
||||
Ok(user) => {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
extract::{ConnectInfo, FromRequestParts, WebSocketUpgrade, ws},
|
||||
http::{HeaderMap, request},
|
||||
extract::{FromRequestParts, WebSocketUpgrade, ws},
|
||||
http::request,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
@@ -11,6 +9,7 @@ use futures_util::{SinkExt, StreamExt as _};
|
||||
use komodo_client::{
|
||||
api::terminal::ConnectTerminalQuery, entities::user::User,
|
||||
};
|
||||
use mogh_auth_server::request_ip::RequestIp;
|
||||
use periphery_client::api::terminal::DisconnectTerminal;
|
||||
use serde::de::DeserializeOwned;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -23,15 +22,13 @@ use crate::{
|
||||
|
||||
#[instrument("ConnectTerminal", skip(ws))]
|
||||
pub async fn handler(
|
||||
RequestIp(ip): RequestIp,
|
||||
Qs(query): Qs<ConnectTerminalQuery>,
|
||||
ConnectInfo(info): ConnectInfo<SocketAddr>,
|
||||
headers: HeaderMap,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> impl IntoResponse {
|
||||
let ip = info.ip();
|
||||
ws.on_upgrade(move |socket| async move {
|
||||
let Some((mut client_socket, user)) =
|
||||
super::user_ws_login(socket, &headers, ip).await
|
||||
super::user_ws_login(socket, ip).await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
extract::{ConnectInfo, WebSocketUpgrade, ws::Message},
|
||||
http::HeaderMap,
|
||||
extract::{WebSocketUpgrade, ws::Message},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use komodo_client::entities::{
|
||||
ResourceTarget, permission::PermissionLevel, user::User,
|
||||
};
|
||||
use mogh_auth_server::request_ip::RequestIp;
|
||||
use mogh_error::serialize_error;
|
||||
use serde_json::json;
|
||||
use serror::serialize_error;
|
||||
use tokio::select;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
@@ -20,18 +18,16 @@ use crate::helpers::{
|
||||
};
|
||||
|
||||
pub async fn handler(
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(info): ConnectInfo<SocketAddr>,
|
||||
RequestIp(ip): RequestIp,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> impl IntoResponse {
|
||||
// get a reveiver for internal update messages.
|
||||
let mut receiver = update_channel().receiver.resubscribe();
|
||||
let ip = info.ip();
|
||||
|
||||
// handle http -> ws updgrade
|
||||
ws.on_upgrade(move |socket| async move {
|
||||
let Some((client_socket, user)) =
|
||||
super::user_ws_login(socket, &headers, ip).await
|
||||
super::user_ws_login(socket, ip).await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
use anyhow::Context as _;
|
||||
use database::bson::doc;
|
||||
use komodo_client::entities::{api_key::ApiKey, komodo_timestamp};
|
||||
use mogh_auth_client::api::manage::CreateApiKey;
|
||||
|
||||
use crate::state::db_client;
|
||||
|
||||
pub async fn create_api_key(
|
||||
user_id: String,
|
||||
CreateApiKey { name, expires }: CreateApiKey,
|
||||
key: String,
|
||||
hashed_secret: String,
|
||||
) -> anyhow::Result<()> {
|
||||
let api_key = ApiKey {
|
||||
name,
|
||||
key,
|
||||
secret: hashed_secret,
|
||||
user_id,
|
||||
created_at: komodo_timestamp(),
|
||||
expires: expires as i64,
|
||||
};
|
||||
|
||||
db_client()
|
||||
.api_keys
|
||||
.insert_one(api_key)
|
||||
.await
|
||||
.context("Failed to create api key on database")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_api_key(key: &str) -> anyhow::Result<()> {
|
||||
db_client()
|
||||
.api_keys
|
||||
.delete_one(doc! { "key": key })
|
||||
.await
|
||||
.context("Failed to delete api key from database")?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use komodo_client::entities::{
|
||||
config::core::{CoreConfig, OauthCredentials},
|
||||
random_string,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
|
||||
use crate::{auth::STATE_PREFIX_LENGTH, config::core_config};
|
||||
|
||||
pub fn github_oauth_client() -> Option<&'static GithubOauthClient> {
|
||||
static GITHUB_OAUTH_CLIENT: OnceLock<Option<GithubOauthClient>> =
|
||||
OnceLock::new();
|
||||
GITHUB_OAUTH_CLIENT
|
||||
.get_or_init(|| GithubOauthClient::new(core_config()))
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
pub struct GithubOauthClient {
|
||||
http: reqwest::Client,
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
redirect_uri: String,
|
||||
scopes: String,
|
||||
user_agent: String,
|
||||
}
|
||||
|
||||
impl GithubOauthClient {
|
||||
pub fn new(
|
||||
CoreConfig {
|
||||
github_oauth:
|
||||
OauthCredentials {
|
||||
enabled,
|
||||
id,
|
||||
secret,
|
||||
},
|
||||
host,
|
||||
..
|
||||
}: &CoreConfig,
|
||||
) -> Option<GithubOauthClient> {
|
||||
if !enabled {
|
||||
return None;
|
||||
}
|
||||
if host.is_empty() {
|
||||
warn!(
|
||||
"github oauth is enabled, but 'config.host' is not configured"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
if id.is_empty() {
|
||||
warn!(
|
||||
"github oauth is enabled, but 'config.github_oauth.id' is not configured"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
if secret.is_empty() {
|
||||
warn!(
|
||||
"github oauth is enabled, but 'config.github_oauth.secret' is not configured"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
GithubOauthClient {
|
||||
http: reqwest::Client::new(),
|
||||
client_id: id.clone(),
|
||||
client_secret: secret.clone(),
|
||||
redirect_uri: format!("{host}/auth/github/callback"),
|
||||
user_agent: Default::default(),
|
||||
scopes: Default::default(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub async fn get_state_and_login_redirect_url(
|
||||
&self,
|
||||
redirect: Option<String>,
|
||||
) -> (String, String) {
|
||||
let state_prefix = random_string(STATE_PREFIX_LENGTH);
|
||||
let state = match redirect {
|
||||
Some(redirect) => format!("{state_prefix}{redirect}"),
|
||||
None => state_prefix,
|
||||
};
|
||||
let redirect_url = format!(
|
||||
"https://github.com/login/oauth/authorize?state={state}&client_id={}&redirect_uri={}&scope={}",
|
||||
self.client_id, self.redirect_uri, self.scopes
|
||||
);
|
||||
(state, redirect_url)
|
||||
}
|
||||
|
||||
pub async fn get_access_token(
|
||||
&self,
|
||||
code: &str,
|
||||
) -> anyhow::Result<AccessTokenResponse> {
|
||||
self
|
||||
.post::<(), _>(
|
||||
"https://github.com/login/oauth/access_token",
|
||||
&[
|
||||
("client_id", self.client_id.as_str()),
|
||||
("client_secret", self.client_secret.as_str()),
|
||||
("redirect_uri", self.redirect_uri.as_str()),
|
||||
("code", code),
|
||||
],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to get github access token using code")
|
||||
}
|
||||
|
||||
pub async fn get_github_user(
|
||||
&self,
|
||||
token: &str,
|
||||
) -> anyhow::Result<GithubUserResponse> {
|
||||
self
|
||||
.get("https://api.github.com/user", &[], Some(token))
|
||||
.await
|
||||
.context("failed to get github user using access token")
|
||||
}
|
||||
|
||||
async fn get<R: DeserializeOwned>(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
query: &[(&str, &str)],
|
||||
bearer_token: Option<&str>,
|
||||
) -> anyhow::Result<R> {
|
||||
let mut req = self
|
||||
.http
|
||||
.get(endpoint)
|
||||
.query(query)
|
||||
.header("User-Agent", &self.user_agent);
|
||||
|
||||
if let Some(bearer_token) = bearer_token {
|
||||
req =
|
||||
req.header("Authorization", format!("Bearer {bearer_token}"));
|
||||
}
|
||||
|
||||
let res = req.send().await.context("failed to reach github")?;
|
||||
|
||||
let status = res.status();
|
||||
|
||||
if status == StatusCode::OK {
|
||||
let body = res
|
||||
.json()
|
||||
.await
|
||||
.context("failed to parse body into expected type")?;
|
||||
Ok(body)
|
||||
} else {
|
||||
let text = res.text().await.context(format!(
|
||||
"status: {status} | failed to get response text"
|
||||
))?;
|
||||
Err(anyhow!("status: {status} | text: {text}"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn post<B: Serialize, R: DeserializeOwned>(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
query: &[(&str, &str)],
|
||||
body: Option<&B>,
|
||||
bearer_token: Option<&str>,
|
||||
) -> anyhow::Result<R> {
|
||||
let mut req = self
|
||||
.http
|
||||
.post(endpoint)
|
||||
.query(query)
|
||||
.header("Accept", "application/json")
|
||||
.header("User-Agent", &self.user_agent);
|
||||
|
||||
if let Some(body) = body {
|
||||
req = req.json(body);
|
||||
}
|
||||
|
||||
if let Some(bearer_token) = bearer_token {
|
||||
req =
|
||||
req.header("Authorization", format!("Bearer {bearer_token}"));
|
||||
}
|
||||
|
||||
let res = req.send().await.context("failed to reach github")?;
|
||||
|
||||
let status = res.status();
|
||||
|
||||
if status == StatusCode::OK {
|
||||
let body = res
|
||||
.json()
|
||||
.await
|
||||
.context("failed to parse POST body into expected type")?;
|
||||
Ok(body)
|
||||
} else {
|
||||
let text = res.text().await.with_context(|| format!(
|
||||
"method: POST | status: {status} | failed to get response text"
|
||||
))?;
|
||||
Err(anyhow!("method: POST | status: {status} | text: {text}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AccessTokenResponse {
|
||||
pub access_token: String,
|
||||
// pub scope: String,
|
||||
// pub token_type: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct GithubUserResponse {
|
||||
pub login: String,
|
||||
pub id: u128,
|
||||
pub avatar_url: String,
|
||||
// pub email: Option<String>,
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{ConnectInfo, Query},
|
||||
http::HeaderMap,
|
||||
response::Redirect,
|
||||
routing::get,
|
||||
};
|
||||
use database::mungos::mongodb::bson::doc;
|
||||
use database::{
|
||||
mongo_indexed::Document, mungos::by_id::update_one_by_id,
|
||||
};
|
||||
use futures_util::TryFutureExt;
|
||||
use komodo_client::{
|
||||
api::auth::UserIdOrTwoFactor,
|
||||
entities::{
|
||||
komodo_timestamp, random_string,
|
||||
user::{User, UserConfig},
|
||||
},
|
||||
};
|
||||
use rate_limit::WithFailureRateLimit;
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serror::{AddStatusCode, AddStatusCodeError as _};
|
||||
use tower_sessions::Session;
|
||||
|
||||
use crate::{
|
||||
api::user::SessionThirdPartyLinkInfo,
|
||||
auth::{
|
||||
SessionPasskeyLogin, SessionTotpLogin, SessionUserId,
|
||||
format_redirect, github::client::GithubOauthClient,
|
||||
},
|
||||
config::core_config,
|
||||
helpers::query::{find_github_user, get_user},
|
||||
state::{auth_rate_limiter, db_client, webauthn},
|
||||
};
|
||||
|
||||
use self::client::github_oauth_client;
|
||||
|
||||
use super::{RedirectQuery, STATE_PREFIX_LENGTH};
|
||||
|
||||
pub mod client;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/login", get(login))
|
||||
.route("/link", get(link))
|
||||
.route(
|
||||
"/callback",
|
||||
get(
|
||||
|query,
|
||||
session: Session,
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(info): ConnectInfo<SocketAddr>| async move {
|
||||
callback(query, session)
|
||||
.map_err(|e| e.status_code(StatusCode::UNAUTHORIZED))
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
&headers,
|
||||
Some(info.ip()),
|
||||
)
|
||||
.await
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SessionGithubVerificationInfo {
|
||||
state: String,
|
||||
}
|
||||
|
||||
impl SessionGithubVerificationInfo {
|
||||
const KEY: &str = "github-verification-info";
|
||||
}
|
||||
|
||||
async fn login(
|
||||
session: Session,
|
||||
Query(query): Query<RedirectQuery>,
|
||||
) -> serror::Result<Redirect> {
|
||||
let (state, uri) = github_oauth_client()
|
||||
.as_ref()
|
||||
.context("Github Oauth not configured")
|
||||
.status_code(StatusCode::UNAUTHORIZED)?
|
||||
.get_state_and_login_redirect_url(query.redirect)
|
||||
.await;
|
||||
session
|
||||
.insert(
|
||||
SessionGithubVerificationInfo::KEY,
|
||||
SessionGithubVerificationInfo { state },
|
||||
)
|
||||
.await
|
||||
.context("Failed to insert github oauth session state")?;
|
||||
Ok(Redirect::to(&uri))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SessionGithubLinkInfo {
|
||||
user_id: String,
|
||||
state: String,
|
||||
}
|
||||
|
||||
impl SessionGithubLinkInfo {
|
||||
const KEY: &str = "github-link-info";
|
||||
}
|
||||
|
||||
async fn link(session: Session) -> serror::Result<Redirect> {
|
||||
let SessionThirdPartyLinkInfo { user_id } = session
|
||||
.remove(SessionThirdPartyLinkInfo::KEY)
|
||||
.await
|
||||
.context("Invalid session third party link info.")?
|
||||
.context("Missing session third party link info")?;
|
||||
let (state, uri) = github_oauth_client()
|
||||
.as_ref()
|
||||
.context("Github Oauth not configured")
|
||||
.status_code(StatusCode::UNAUTHORIZED)?
|
||||
.get_state_and_login_redirect_url(None)
|
||||
.await;
|
||||
session
|
||||
.insert(
|
||||
SessionGithubLinkInfo::KEY,
|
||||
SessionGithubLinkInfo { user_id, state },
|
||||
)
|
||||
.await
|
||||
.context("Failed to insert session link info")?;
|
||||
Ok(Redirect::to(&uri))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CallbackQuery {
|
||||
state: String,
|
||||
code: String,
|
||||
}
|
||||
|
||||
async fn callback(
|
||||
Query(query): Query<CallbackQuery>,
|
||||
session: Session,
|
||||
) -> anyhow::Result<Redirect> {
|
||||
let client =
|
||||
github_oauth_client().context("Missing github oauth client")?;
|
||||
|
||||
// Check first if this is a link callback
|
||||
// and use the linking handler if so.
|
||||
if let Ok(Some(info)) =
|
||||
session.remove(SessionGithubLinkInfo::KEY).await
|
||||
{
|
||||
return link_github_callback(client, info, query).await;
|
||||
}
|
||||
|
||||
let SessionGithubVerificationInfo { state } = session
|
||||
.remove(SessionGithubVerificationInfo::KEY)
|
||||
.await
|
||||
.context("Invalid github oauth session state")?
|
||||
.context("Missing github oauth session state")?;
|
||||
|
||||
if query.state != state {
|
||||
return Err(anyhow!("State mismatch"));
|
||||
}
|
||||
|
||||
let token = client.get_access_token(&query.code).await?;
|
||||
|
||||
let github_user =
|
||||
client.get_github_user(&token.access_token).await?;
|
||||
|
||||
let github_id = github_user.id.to_string();
|
||||
|
||||
let db_client = db_client();
|
||||
|
||||
let user = find_github_user(&github_id).await?;
|
||||
|
||||
let user_id_or_two_factor = match user {
|
||||
Some(user) => {
|
||||
match (
|
||||
user.third_party_skip_2fa,
|
||||
user.passkey.passkey,
|
||||
user.totp.enrolled(),
|
||||
) {
|
||||
// Skip / No 2FA
|
||||
(true, _, _) | (false, None, false) => {
|
||||
session
|
||||
.insert(
|
||||
SessionUserId::KEY,
|
||||
SessionUserId(user.id.clone()),
|
||||
)
|
||||
.await
|
||||
.context("Failed to store user id for client session")?;
|
||||
UserIdOrTwoFactor::UserId(user.id)
|
||||
}
|
||||
// WebAuthn Passkey 2FA
|
||||
(false, Some(passkey), _) => {
|
||||
let webauthn = webauthn().context(
|
||||
"No webauthn provider available, invalid KOMODO_HOST config",
|
||||
)?;
|
||||
let (response, server_state) = webauthn
|
||||
.start_passkey_authentication(&[passkey])
|
||||
.context("Failed to start passkey authentication flow")?;
|
||||
session
|
||||
.insert(
|
||||
SessionPasskeyLogin::KEY,
|
||||
SessionPasskeyLogin {
|
||||
user_id: user.id,
|
||||
state: server_state,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
UserIdOrTwoFactor::Passkey(response)
|
||||
}
|
||||
// TOTP 2FA
|
||||
(false, None, true) => {
|
||||
session
|
||||
.insert(
|
||||
SessionTotpLogin::KEY,
|
||||
SessionTotpLogin { user_id: user.id },
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Failed to store totp login state in for user session",
|
||||
)?;
|
||||
UserIdOrTwoFactor::Totp {}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let ts = komodo_timestamp();
|
||||
let no_users_exist =
|
||||
db_client.users.find_one(Document::new()).await?.is_none();
|
||||
let core_config = core_config();
|
||||
if !no_users_exist && core_config.disable_user_registration {
|
||||
return Err(anyhow!("User registration is disabled"));
|
||||
}
|
||||
|
||||
let mut username = github_user.login;
|
||||
// Modify username if it already exists
|
||||
if db_client
|
||||
.users
|
||||
.find_one(doc! { "username": &username })
|
||||
.await
|
||||
.context("Failed to query users collection")?
|
||||
.is_some()
|
||||
{
|
||||
username += "-";
|
||||
username += &random_string(5);
|
||||
};
|
||||
|
||||
let user = User {
|
||||
id: Default::default(),
|
||||
username,
|
||||
enabled: no_users_exist || core_config.enable_new_users,
|
||||
admin: no_users_exist,
|
||||
super_admin: no_users_exist,
|
||||
create_server_permissions: no_users_exist,
|
||||
create_build_permissions: no_users_exist,
|
||||
updated_at: ts,
|
||||
last_update_view: 0,
|
||||
recents: Default::default(),
|
||||
all: Default::default(),
|
||||
config: UserConfig::Github {
|
||||
github_id,
|
||||
avatar: github_user.avatar_url,
|
||||
},
|
||||
linked_logins: Default::default(),
|
||||
totp: Default::default(),
|
||||
passkey: Default::default(),
|
||||
third_party_skip_2fa: Default::default(),
|
||||
};
|
||||
let user_id = db_client
|
||||
.users
|
||||
.insert_one(user)
|
||||
.await
|
||||
.context("Failed to create user on mongo")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
UserIdOrTwoFactor::UserId(user_id)
|
||||
}
|
||||
};
|
||||
let redirect = Some(&query.state[STATE_PREFIX_LENGTH..]);
|
||||
match user_id_or_two_factor {
|
||||
UserIdOrTwoFactor::UserId(_) => {
|
||||
Ok(format_redirect(redirect, "redeem_ready=true"))
|
||||
}
|
||||
UserIdOrTwoFactor::Totp {} => {
|
||||
Ok(format_redirect(redirect, "totp=true"))
|
||||
}
|
||||
UserIdOrTwoFactor::Passkey(passkey) => {
|
||||
let passkey = serde_json::to_string(&passkey)
|
||||
.context("Failed to serialize passkey response")?;
|
||||
let passkey = urlencoding::encode(&passkey);
|
||||
Ok(format_redirect(redirect, &format!("passkey={passkey}")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This intercepts during the normal oauth callback if
|
||||
/// 'github-link-info' is found on session.
|
||||
async fn link_github_callback(
|
||||
client: &GithubOauthClient,
|
||||
SessionGithubLinkInfo { user_id, state }: SessionGithubLinkInfo,
|
||||
query: CallbackQuery,
|
||||
) -> anyhow::Result<Redirect> {
|
||||
if query.state != state {
|
||||
return Err(anyhow!("State mismatch"));
|
||||
}
|
||||
|
||||
let token = client.get_access_token(&query.code).await?;
|
||||
|
||||
let github_user =
|
||||
client.get_github_user(&token.access_token).await?;
|
||||
let github_id = github_user.id.to_string();
|
||||
|
||||
if let Some(existing_user) = find_github_user(&github_id).await? {
|
||||
if existing_user.id == user_id {
|
||||
// Link is already complete, this is a no-op
|
||||
return Ok(Redirect::to(&format!(
|
||||
"{}/settings",
|
||||
core_config().host,
|
||||
)));
|
||||
} else {
|
||||
return Err(anyhow!("Account already linked to another user."));
|
||||
}
|
||||
}
|
||||
|
||||
let user = get_user(&user_id).await?;
|
||||
|
||||
if let UserConfig::Github { .. } = &user.config {
|
||||
return Err(anyhow!(
|
||||
"User is primary Github user, cannot link another Github login."
|
||||
));
|
||||
}
|
||||
|
||||
let update = doc! {
|
||||
"$set": {
|
||||
"linked_logins.Github.type": "Github",
|
||||
"linked_logins.Github.data.github_id": &github_id,
|
||||
"linked_logins.Github.data.avatar": &github_user.avatar_url,
|
||||
}
|
||||
};
|
||||
|
||||
update_one_by_id(&db_client().users, &user_id, update, None)
|
||||
.await
|
||||
.context(
|
||||
"Failed to link Github login to existing user on database",
|
||||
)?;
|
||||
|
||||
Ok(Redirect::to(&format!("{}/settings", core_config().host,)))
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use jsonwebtoken::dangerous::insecure_decode;
|
||||
use komodo_client::entities::{
|
||||
config::core::{CoreConfig, OauthCredentials},
|
||||
random_string,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, de::DeserializeOwned};
|
||||
|
||||
use crate::{auth::STATE_PREFIX_LENGTH, config::core_config};
|
||||
|
||||
pub fn google_oauth_client() -> Option<&'static GoogleOauthClient> {
|
||||
static GOOGLE_OAUTH_CLIENT: OnceLock<Option<GoogleOauthClient>> =
|
||||
OnceLock::new();
|
||||
GOOGLE_OAUTH_CLIENT
|
||||
.get_or_init(|| GoogleOauthClient::new(core_config()))
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
pub struct GoogleOauthClient {
|
||||
http: reqwest::Client,
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
redirect_uri: String,
|
||||
scopes: String,
|
||||
user_agent: String,
|
||||
}
|
||||
|
||||
impl GoogleOauthClient {
|
||||
pub fn new(
|
||||
CoreConfig {
|
||||
google_oauth:
|
||||
OauthCredentials {
|
||||
enabled,
|
||||
id,
|
||||
secret,
|
||||
},
|
||||
host,
|
||||
..
|
||||
}: &CoreConfig,
|
||||
) -> Option<GoogleOauthClient> {
|
||||
if !enabled {
|
||||
return None;
|
||||
}
|
||||
if host.is_empty() {
|
||||
warn!(
|
||||
"google oauth is enabled, but 'config.host' is not configured"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
if id.is_empty() {
|
||||
warn!(
|
||||
"google oauth is enabled, but 'config.google_oauth.id' is not configured"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
if secret.is_empty() {
|
||||
warn!(
|
||||
"google oauth is enabled, but 'config.google_oauth.secret' is not configured"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let scopes = urlencoding::encode(
|
||||
&[
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
]
|
||||
.join(" "),
|
||||
)
|
||||
.to_string();
|
||||
GoogleOauthClient {
|
||||
http: Default::default(),
|
||||
client_id: id.clone(),
|
||||
client_secret: secret.clone(),
|
||||
redirect_uri: format!("{host}/auth/google/callback"),
|
||||
user_agent: String::from("komodo"),
|
||||
scopes,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub async fn get_state_and_login_redirect_url(
|
||||
&self,
|
||||
redirect: Option<String>,
|
||||
) -> (String, String) {
|
||||
let state_prefix = random_string(STATE_PREFIX_LENGTH);
|
||||
let state = match redirect {
|
||||
Some(redirect) => format!("{state_prefix}{redirect}"),
|
||||
None => state_prefix,
|
||||
};
|
||||
let redirect_url = format!(
|
||||
"https://accounts.google.com/o/oauth2/v2/auth?response_type=code&state={state}&client_id={}&redirect_uri={}&scope={}",
|
||||
self.client_id, self.redirect_uri, self.scopes
|
||||
);
|
||||
(state, redirect_url)
|
||||
}
|
||||
|
||||
pub async fn get_access_token(
|
||||
&self,
|
||||
code: &str,
|
||||
) -> anyhow::Result<AccessTokenResponse> {
|
||||
self
|
||||
.post::<_>(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
&[
|
||||
("client_id", self.client_id.as_str()),
|
||||
("client_secret", self.client_secret.as_str()),
|
||||
("redirect_uri", self.redirect_uri.as_str()),
|
||||
("code", code),
|
||||
("grant_type", "authorization_code"),
|
||||
],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.context("failed to get google access token using code")
|
||||
}
|
||||
|
||||
pub fn get_google_user(
|
||||
&self,
|
||||
id_token: &str,
|
||||
) -> anyhow::Result<GoogleUser> {
|
||||
let res = insecure_decode::<GoogleUser>(id_token)
|
||||
.context("failed to decode google id token")?;
|
||||
Ok(res.claims)
|
||||
}
|
||||
|
||||
async fn post<R: DeserializeOwned>(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
body: &[(&str, &str)],
|
||||
bearer_token: Option<&str>,
|
||||
) -> anyhow::Result<R> {
|
||||
let mut req = self
|
||||
.http
|
||||
.post(endpoint)
|
||||
.form(body)
|
||||
.header("Accept", "application/json")
|
||||
.header("User-Agent", &self.user_agent);
|
||||
|
||||
if let Some(bearer_token) = bearer_token {
|
||||
req =
|
||||
req.header("Authorization", format!("Bearer {bearer_token}"));
|
||||
}
|
||||
|
||||
let res = req.send().await.context("failed to reach google")?;
|
||||
|
||||
let status = res.status();
|
||||
|
||||
if status == StatusCode::OK {
|
||||
let body = res
|
||||
.json()
|
||||
.await
|
||||
.context("failed to parse POST body into expected type")?;
|
||||
Ok(body)
|
||||
} else {
|
||||
let text = res.text().await.context(format!(
|
||||
"method: POST | status: {status} | failed to get response text"
|
||||
))?;
|
||||
Err(anyhow!("method: POST | status: {status} | text: {text}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AccessTokenResponse {
|
||||
// pub access_token: String,
|
||||
pub id_token: String,
|
||||
// pub scope: String,
|
||||
// pub token_type: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct GoogleUser {
|
||||
#[serde(rename = "sub")]
|
||||
pub id: String,
|
||||
pub email: String,
|
||||
#[serde(default)]
|
||||
pub picture: String,
|
||||
}
|
||||
@@ -1,364 +0,0 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{ConnectInfo, Query},
|
||||
http::HeaderMap,
|
||||
response::Redirect,
|
||||
routing::get,
|
||||
};
|
||||
use database::mungos::mongodb::bson::doc;
|
||||
use database::{
|
||||
mongo_indexed::Document, mungos::by_id::update_one_by_id,
|
||||
};
|
||||
use futures_util::TryFutureExt;
|
||||
use komodo_client::{
|
||||
api::auth::UserIdOrTwoFactor,
|
||||
entities::{
|
||||
random_string,
|
||||
user::{User, UserConfig},
|
||||
},
|
||||
};
|
||||
use rate_limit::WithFailureRateLimit;
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serror::{AddStatusCode, AddStatusCodeError as _};
|
||||
use tower_sessions::Session;
|
||||
|
||||
use crate::{
|
||||
api::user::SessionThirdPartyLinkInfo,
|
||||
auth::{
|
||||
SessionPasskeyLogin, SessionTotpLogin, SessionUserId,
|
||||
format_redirect, google::client::GoogleOauthClient,
|
||||
},
|
||||
config::core_config,
|
||||
helpers::query::{find_google_user, get_user},
|
||||
state::{auth_rate_limiter, db_client, webauthn},
|
||||
};
|
||||
|
||||
use self::client::google_oauth_client;
|
||||
|
||||
use super::{RedirectQuery, STATE_PREFIX_LENGTH};
|
||||
|
||||
pub mod client;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/login", get(login))
|
||||
.route("/link", get(link))
|
||||
.route(
|
||||
"/callback",
|
||||
get(
|
||||
|query,
|
||||
session,
|
||||
headers: HeaderMap,
|
||||
ConnectInfo(info): ConnectInfo<SocketAddr>| async move {
|
||||
callback(session, query)
|
||||
.map_err(|e| e.status_code(StatusCode::UNAUTHORIZED))
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
&headers,
|
||||
Some(info.ip()),
|
||||
)
|
||||
.await
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SessionGoogleVerificationInfo {
|
||||
state: String,
|
||||
}
|
||||
|
||||
impl SessionGoogleVerificationInfo {
|
||||
const KEY: &str = "google-verification-info";
|
||||
}
|
||||
|
||||
async fn login(
|
||||
session: Session,
|
||||
Query(query): Query<RedirectQuery>,
|
||||
) -> serror::Result<Redirect> {
|
||||
let (state, uri) = google_oauth_client()
|
||||
.as_ref()
|
||||
.context("Google Oauth not configured")
|
||||
.status_code(StatusCode::UNAUTHORIZED)?
|
||||
.get_state_and_login_redirect_url(query.redirect)
|
||||
.await;
|
||||
session
|
||||
.insert(
|
||||
SessionGoogleVerificationInfo::KEY,
|
||||
SessionGoogleVerificationInfo { state },
|
||||
)
|
||||
.await
|
||||
.context("Failed to insert google oauth session state")?;
|
||||
Ok(Redirect::to(&uri))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SessionGoogleLinkInfo {
|
||||
user_id: String,
|
||||
state: String,
|
||||
}
|
||||
|
||||
impl SessionGoogleLinkInfo {
|
||||
const KEY: &str = "google-link-info";
|
||||
}
|
||||
|
||||
async fn link(session: Session) -> serror::Result<Redirect> {
|
||||
let SessionThirdPartyLinkInfo { user_id } = session
|
||||
.remove(SessionThirdPartyLinkInfo::KEY)
|
||||
.await
|
||||
.context("Invalid session third party link info.")?
|
||||
.context("Missing session third party link info")?;
|
||||
let (state, uri) = google_oauth_client()
|
||||
.as_ref()
|
||||
.context("Google Oauth not configured")
|
||||
.status_code(StatusCode::UNAUTHORIZED)?
|
||||
.get_state_and_login_redirect_url(None)
|
||||
.await;
|
||||
session
|
||||
.insert(
|
||||
SessionGoogleLinkInfo::KEY,
|
||||
SessionGoogleLinkInfo { user_id, state },
|
||||
)
|
||||
.await
|
||||
.context("Failed to insert session link info")?;
|
||||
Ok(Redirect::to(&uri))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CallbackQuery {
|
||||
state: Option<String>,
|
||||
code: Option<String>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
async fn callback(
|
||||
session: Session,
|
||||
Query(query): Query<CallbackQuery>,
|
||||
) -> anyhow::Result<Redirect> {
|
||||
let client =
|
||||
google_oauth_client().context("Missing google oauth client")?;
|
||||
|
||||
if let Some(error) = query.error {
|
||||
return Err(anyhow!("Auth error from google: {error}"));
|
||||
}
|
||||
|
||||
let client_state = query
|
||||
.state
|
||||
.context("Callback query does not contain state")?;
|
||||
let code =
|
||||
query.code.context("callback query does not contain code")?;
|
||||
|
||||
// Check first if this is a link callback
|
||||
// and use the linking handler if so.
|
||||
if let Ok(Some(info)) =
|
||||
session.remove(SessionGoogleLinkInfo::KEY).await
|
||||
{
|
||||
return link_google_callback(client, info, client_state, code)
|
||||
.await;
|
||||
}
|
||||
|
||||
let SessionGoogleVerificationInfo { state } = session
|
||||
.get(SessionGoogleVerificationInfo::KEY)
|
||||
.await
|
||||
.context("Invalid google oauth session state")?
|
||||
.context("Missing google oauth session state")?;
|
||||
|
||||
if client_state != state {
|
||||
return Err(anyhow!("State mismatch"));
|
||||
}
|
||||
|
||||
let token = client.get_access_token(&code).await?;
|
||||
|
||||
let google_user = client.get_google_user(&token.id_token)?;
|
||||
let google_id = google_user.id.to_string();
|
||||
|
||||
let user = find_google_user(&google_id).await?;
|
||||
|
||||
let user_id_or_two_factor = match user {
|
||||
Some(user) => {
|
||||
match (
|
||||
user.third_party_skip_2fa,
|
||||
user.passkey.passkey,
|
||||
user.totp.enrolled(),
|
||||
) {
|
||||
// Skip / No 2FA
|
||||
(true, _, _) | (false, None, false) => {
|
||||
session
|
||||
.insert(
|
||||
SessionUserId::KEY,
|
||||
SessionUserId(user.id.clone()),
|
||||
)
|
||||
.await
|
||||
.context("Failed to store user id for client session")?;
|
||||
UserIdOrTwoFactor::UserId(user.id)
|
||||
}
|
||||
// WebAuthn Passkey 2FA
|
||||
(false, Some(passkey), _) => {
|
||||
let webauthn = webauthn().context(
|
||||
"No webauthn provider available, invalid KOMODO_HOST config",
|
||||
)?;
|
||||
let (response, server_state) = webauthn
|
||||
.start_passkey_authentication(&[passkey])
|
||||
.context("Failed to start passkey authentication flow")?;
|
||||
session
|
||||
.insert(
|
||||
SessionPasskeyLogin::KEY,
|
||||
SessionPasskeyLogin {
|
||||
user_id: user.id,
|
||||
state: server_state,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
UserIdOrTwoFactor::Passkey(response)
|
||||
}
|
||||
// TOTP 2FA
|
||||
(false, None, true) => {
|
||||
session
|
||||
.insert(
|
||||
SessionTotpLogin::KEY,
|
||||
SessionTotpLogin { user_id: user.id },
|
||||
)
|
||||
.await
|
||||
.context(
|
||||
"Failed to store totp login state in for user session",
|
||||
)?;
|
||||
UserIdOrTwoFactor::Totp {}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let db_client = db_client();
|
||||
let ts = unix_timestamp_ms() as i64;
|
||||
let no_users_exist =
|
||||
db_client.users.find_one(Document::new()).await?.is_none();
|
||||
let core_config = core_config();
|
||||
if !no_users_exist && core_config.disable_user_registration {
|
||||
return Err(anyhow!("User registration is disabled"));
|
||||
}
|
||||
let mut username = google_user
|
||||
.email
|
||||
.split('@')
|
||||
.collect::<Vec<&str>>()
|
||||
.first()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
// Modify username if it already exists
|
||||
if db_client
|
||||
.users
|
||||
.find_one(doc! { "username": &username })
|
||||
.await
|
||||
.context("Failed to query users collection")?
|
||||
.is_some()
|
||||
{
|
||||
username += "-";
|
||||
username += &random_string(5);
|
||||
};
|
||||
|
||||
let user = User {
|
||||
id: Default::default(),
|
||||
username,
|
||||
enabled: no_users_exist || core_config.enable_new_users,
|
||||
admin: no_users_exist,
|
||||
super_admin: no_users_exist,
|
||||
create_server_permissions: no_users_exist,
|
||||
create_build_permissions: no_users_exist,
|
||||
updated_at: ts,
|
||||
last_update_view: 0,
|
||||
recents: Default::default(),
|
||||
all: Default::default(),
|
||||
config: UserConfig::Google {
|
||||
google_id,
|
||||
avatar: google_user.picture,
|
||||
},
|
||||
linked_logins: Default::default(),
|
||||
totp: Default::default(),
|
||||
passkey: Default::default(),
|
||||
third_party_skip_2fa: Default::default(),
|
||||
};
|
||||
let user_id = db_client
|
||||
.users
|
||||
.insert_one(user)
|
||||
.await
|
||||
.context("Failed to create user on mongo")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("inserted_id is not ObjectId")?
|
||||
.to_string();
|
||||
UserIdOrTwoFactor::UserId(user_id)
|
||||
}
|
||||
};
|
||||
let redirect = Some(&state[STATE_PREFIX_LENGTH..]);
|
||||
match user_id_or_two_factor {
|
||||
UserIdOrTwoFactor::UserId(_) => {
|
||||
Ok(format_redirect(redirect, "redeem_ready=true"))
|
||||
}
|
||||
UserIdOrTwoFactor::Totp {} => {
|
||||
Ok(format_redirect(redirect, "totp=true"))
|
||||
}
|
||||
UserIdOrTwoFactor::Passkey(passkey) => {
|
||||
let passkey = serde_json::to_string(&passkey)
|
||||
.context("Failed to serialize passkey response")?;
|
||||
let passkey = urlencoding::encode(&passkey);
|
||||
Ok(format_redirect(redirect, &format!("passkey={passkey}")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This intercepts during the normal oauth callback if
|
||||
/// 'google-link-info' is found on session.
|
||||
async fn link_google_callback(
|
||||
client: &GoogleOauthClient,
|
||||
SessionGoogleLinkInfo { user_id, state }: SessionGoogleLinkInfo,
|
||||
client_state: String,
|
||||
code: String,
|
||||
) -> anyhow::Result<Redirect> {
|
||||
if client_state != state {
|
||||
return Err(anyhow!("State mismatch"));
|
||||
}
|
||||
|
||||
let token = client.get_access_token(&code).await?;
|
||||
|
||||
let google_user = client.get_google_user(&token.id_token)?;
|
||||
let google_id = google_user.id.to_string();
|
||||
|
||||
if let Some(existing_user) = find_google_user(&google_id).await? {
|
||||
if existing_user.id == user_id {
|
||||
// Link is already complete, this is a no-op
|
||||
return Ok(Redirect::to(&format!(
|
||||
"{}/settings",
|
||||
core_config().host,
|
||||
)));
|
||||
} else {
|
||||
return Err(anyhow!("Account already linked to another user."));
|
||||
}
|
||||
}
|
||||
|
||||
let user = get_user(&user_id).await?;
|
||||
|
||||
if let UserConfig::Google { .. } = &user.config {
|
||||
return Err(anyhow!(
|
||||
"User is primary Google user, cannot link another Google login."
|
||||
));
|
||||
}
|
||||
|
||||
let update = doc! {
|
||||
"$set": {
|
||||
"linked_logins.Google.type": "Google",
|
||||
"linked_logins.Google.data.google_id": &google_id,
|
||||
"linked_logins.Google.data.avatar": &google_user.picture,
|
||||
}
|
||||
};
|
||||
|
||||
update_one_by_id(&db_client().users, &user_id, update, None)
|
||||
.await
|
||||
.context(
|
||||
"Failed to link Google login to existing user on database",
|
||||
)?;
|
||||
|
||||
Ok(Redirect::to(&format!("{}/settings", core_config().host,)))
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
use anyhow::Context;
|
||||
use async_timing_util::{get_timelength_in_ms, unix_timestamp_ms};
|
||||
use database::mungos::mongodb::bson::doc;
|
||||
use jsonwebtoken::{
|
||||
DecodingKey, EncodingKey, Header, Validation, decode, encode,
|
||||
};
|
||||
use komodo_client::{
|
||||
api::auth::JwtResponse,
|
||||
entities::{config::core::CoreConfig, random_string},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct JwtClaims {
|
||||
pub id: String,
|
||||
pub iat: u128,
|
||||
pub exp: u128,
|
||||
}
|
||||
|
||||
pub struct JwtClient {
|
||||
header: Header,
|
||||
validation: Validation,
|
||||
encoding_key: EncodingKey,
|
||||
decoding_key: DecodingKey,
|
||||
ttl_ms: u128,
|
||||
}
|
||||
|
||||
impl JwtClient {
|
||||
pub fn new(config: &CoreConfig) -> anyhow::Result<JwtClient> {
|
||||
let secret = if config.jwt_secret.is_empty() {
|
||||
random_string(40)
|
||||
} else {
|
||||
config.jwt_secret.clone()
|
||||
};
|
||||
Ok(JwtClient {
|
||||
header: Header::default(),
|
||||
validation: Validation::new(Default::default()),
|
||||
encoding_key: EncodingKey::from_secret(secret.as_bytes()),
|
||||
decoding_key: DecodingKey::from_secret(secret.as_bytes()),
|
||||
ttl_ms: get_timelength_in_ms(
|
||||
config.jwt_ttl.to_string().parse()?,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encode(
|
||||
&self,
|
||||
user_id: String,
|
||||
) -> anyhow::Result<JwtResponse> {
|
||||
let iat = unix_timestamp_ms();
|
||||
let exp = iat + self.ttl_ms;
|
||||
let claims = JwtClaims {
|
||||
id: user_id.clone(),
|
||||
iat,
|
||||
exp,
|
||||
};
|
||||
let jwt = encode(&self.header, &claims, &self.encoding_key)
|
||||
.context("Failed at signing claim")?;
|
||||
Ok(JwtResponse { user_id, jwt })
|
||||
}
|
||||
|
||||
pub fn decode(&self, jwt: &str) -> anyhow::Result<JwtClaims> {
|
||||
decode::<JwtClaims>(jwt, &self.decoding_key, &self.validation)
|
||||
.map(|res| res.claims)
|
||||
.context("Failed to decode token claims")
|
||||
}
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use async_timing_util::unix_timestamp_ms;
|
||||
use database::{
|
||||
hash_password,
|
||||
mungos::mongodb::bson::{Document, doc},
|
||||
};
|
||||
use komodo_client::{
|
||||
api::auth::{
|
||||
LoginLocalUser, LoginLocalUserResponse, SignUpLocalUser,
|
||||
SignUpLocalUserResponse,
|
||||
},
|
||||
entities::user::{User, UserConfig, UserConfigVariant},
|
||||
};
|
||||
use rate_limit::{RateLimiter, WithFailureRateLimit};
|
||||
use reqwest::StatusCode;
|
||||
use resolver_api::Resolve;
|
||||
use serror::{AddStatusCode as _, AddStatusCodeError};
|
||||
use tower_sessions::Session;
|
||||
|
||||
use crate::{
|
||||
api::auth::AuthArgs,
|
||||
auth::{SessionPasskeyLogin, SessionTotpLogin},
|
||||
config::core_config,
|
||||
helpers::validations::{validate_password, validate_username},
|
||||
state::{auth_rate_limiter, db_client, jwt_client, webauthn},
|
||||
};
|
||||
|
||||
impl Resolve<AuthArgs> for SignUpLocalUser {
|
||||
#[instrument("SignUpLocalUser", skip(self))]
|
||||
async fn resolve(
|
||||
self,
|
||||
AuthArgs {
|
||||
headers,
|
||||
ip,
|
||||
session,
|
||||
}: &AuthArgs,
|
||||
) -> serror::Result<SignUpLocalUserResponse> {
|
||||
sign_up_local_user(self)
|
||||
.with_failure_rate_limit_using_headers(
|
||||
auth_rate_limiter(),
|
||||
headers,
|
||||
Some(*ip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn sign_up_local_user(
|
||||
req: SignUpLocalUser,
|
||||
) -> serror::Result<SignUpLocalUserResponse> {
|
||||
let config = core_config();
|
||||
|
||||
if !config.local_auth {
|
||||
return Err(anyhow!("Local auth is not enabled").into());
|
||||
}
|
||||
|
||||
validate_username(&req.username)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
validate_password(&req.password)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
|
||||
let db = db_client();
|
||||
|
||||
let no_users_exist =
|
||||
db.users.find_one(Document::new()).await?.is_none();
|
||||
|
||||
if !no_users_exist && config.disable_user_registration {
|
||||
return Err(
|
||||
anyhow!("User registration is disabled")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
);
|
||||
}
|
||||
|
||||
if db
|
||||
.users
|
||||
.find_one(doc! { "username": &req.username })
|
||||
.await
|
||||
.context("Failed to query for existing users")?
|
||||
.is_some()
|
||||
{
|
||||
// When user registration is enabled, there is no way around allowing
|
||||
// potential attackers to gain some insight about which usernames exist
|
||||
// if they are allowed to register accounts. Since this can be easily inferred,
|
||||
// might as well be clear. The auth rate limiter is critical here.
|
||||
return Err(
|
||||
anyhow!("Username already taken.")
|
||||
.status_code(StatusCode::BAD_REQUEST),
|
||||
);
|
||||
}
|
||||
|
||||
let ts = unix_timestamp_ms() as i64;
|
||||
let hashed_password = hash_password(req.password)?;
|
||||
|
||||
let user = User {
|
||||
id: Default::default(),
|
||||
username: req.username,
|
||||
enabled: no_users_exist || config.enable_new_users,
|
||||
admin: no_users_exist,
|
||||
super_admin: no_users_exist,
|
||||
create_server_permissions: no_users_exist,
|
||||
create_build_permissions: no_users_exist,
|
||||
updated_at: ts,
|
||||
last_update_view: 0,
|
||||
recents: Default::default(),
|
||||
all: Default::default(),
|
||||
config: UserConfig::Local {
|
||||
password: hashed_password,
|
||||
},
|
||||
linked_logins: Default::default(),
|
||||
totp: Default::default(),
|
||||
passkey: Default::default(),
|
||||
third_party_skip_2fa: Default::default(),
|
||||
};
|
||||
|
||||
let user_id = db_client()
|
||||
.users
|
||||
.insert_one(user)
|
||||
.await
|
||||
.context("Failed to create user on database")?
|
||||
.inserted_id
|
||||
.as_object_id()
|
||||
.context("The 'inserted_id' is not ObjectId")?
|
||||
.to_string();
|
||||
|
||||
jwt_client()
|
||||
.encode(user_id)
|
||||
.context("Failed to generate JWT for user")
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Local login method has a dedicated rate limiter
|
||||
/// so the UI background calls using existing JWT do
|
||||
/// not influence the number of attempts user has
|
||||
/// to log in.
|
||||
fn login_local_user_rate_limiter() -> &'static RateLimiter {
|
||||
static LOGIN_LOCAL_USER_RATE_LIMITER: OnceLock<Arc<RateLimiter>> =
|
||||
OnceLock::new();
|
||||
LOGIN_LOCAL_USER_RATE_LIMITER.get_or_init(|| {
|
||||
let config = core_config();
|
||||
RateLimiter::new(
|
||||
config.auth_rate_limit_disabled,
|
||||
config.auth_rate_limit_max_attempts as usize,
|
||||
config.auth_rate_limit_window_seconds,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
impl Resolve<AuthArgs> for LoginLocalUser {
|
||||
async fn resolve(
|
||||
self,
|
||||
AuthArgs {
|
||||
headers,
|
||||
ip,
|
||||
session,
|
||||
}: &AuthArgs,
|
||||
) -> serror::Result<LoginLocalUserResponse> {
|
||||
login_local_user(
|
||||
self,
|
||||
session
|
||||
.as_ref()
|
||||
.context("Method called in context without session")?,
|
||||
)
|
||||
.with_failure_rate_limit_using_headers(
|
||||
login_local_user_rate_limiter(),
|
||||
headers,
|
||||
Some(*ip),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn login_local_user(
|
||||
req: LoginLocalUser,
|
||||
session: &Session,
|
||||
) -> serror::Result<LoginLocalUserResponse> {
|
||||
if !core_config().local_auth {
|
||||
return Err(
|
||||
anyhow!("Local auth is not enabled")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
);
|
||||
}
|
||||
|
||||
validate_username(&req.username)
|
||||
.status_code(StatusCode::BAD_REQUEST)?;
|
||||
|
||||
let user = db_client()
|
||||
.users
|
||||
.find_one(doc! { "username": &req.username })
|
||||
.await
|
||||
.context("Failed at db query for users")?
|
||||
.context("Invalid login credentials")
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
let user_pw_hash =
|
||||
if let UserConfig::Local { password } = &user.config {
|
||||
password
|
||||
} else if let Some(UserConfig::Local { password }) =
|
||||
user.linked_logins.get(UserConfigVariant::Local)
|
||||
{
|
||||
password
|
||||
} else {
|
||||
return Err(
|
||||
anyhow!("Invalid login credentials")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
);
|
||||
};
|
||||
|
||||
let verified = bcrypt::verify(req.password, user_pw_hash)
|
||||
.context("Invalid login credentials")
|
||||
.status_code(StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
if !verified {
|
||||
return Err(
|
||||
anyhow!("Invalid login credentials")
|
||||
.status_code(StatusCode::UNAUTHORIZED),
|
||||
);
|
||||
}
|
||||
|
||||
match (user.passkey.passkey, user.totp.enrolled()) {
|
||||
// WebAuthn 2FA
|
||||
(Some(passkey), _) => {
|
||||
let webauthn = webauthn().context(
|
||||
"No webauthn provider available, invalid KOMODO_HOST config",
|
||||
)?;
|
||||
let (response, server_state) = webauthn
|
||||
.start_passkey_authentication(&[passkey])
|
||||
.context("Failed to start passkey authentication flow")?;
|
||||
session
|
||||
.insert(
|
||||
SessionPasskeyLogin::KEY,
|
||||
SessionPasskeyLogin {
|
||||
user_id: user.id,
|
||||
state: server_state,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(LoginLocalUserResponse::Passkey(response))
|
||||
}
|
||||
// TOTP 2FA
|
||||
(None, true) => {
|
||||
session
|
||||
.insert(
|
||||
SessionTotpLogin::KEY,
|
||||
SessionTotpLogin { user_id: user.id },
|
||||
)
|
||||
.await?;
|
||||
Ok(LoginLocalUserResponse::Totp {})
|
||||
}
|
||||
// No 2FA, can return JWT immediately
|
||||
(None, false) => {
|
||||
jwt_client()
|
||||
.encode(user.id)
|
||||
// This is in internal error (500), not auth error
|
||||
.context("Failed to generate JWT for user")
|
||||
.map(LoginLocalUserResponse::Jwt)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use database::mungos::mongodb::bson::doc;
|
||||
use komodo_client::entities::{komodo_timestamp, user::User};
|
||||
use mogh_auth_server::RequestAuthentication;
|
||||
|
||||
use crate::{
|
||||
auth::JWT_PROVIDER, helpers::query::get_user, state::db_client,
|
||||
};
|
||||
|
||||
pub async fn extract_user_from_auth(
|
||||
auth: RequestAuthentication,
|
||||
require_user_enabled: bool,
|
||||
) -> anyhow::Result<User> {
|
||||
let user_id = match auth {
|
||||
RequestAuthentication::UserId(user_id) => user_id,
|
||||
RequestAuthentication::KeyAndSecret { key, secret } => {
|
||||
auth_api_key_get_user_id(&key, &secret).await?
|
||||
}
|
||||
RequestAuthentication::PublicKey(_) => todo!(),
|
||||
};
|
||||
if require_user_enabled {
|
||||
check_enabled(&user_id).await
|
||||
} else {
|
||||
get_user(&user_id).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn auth_jwt_check_enabled(
|
||||
jwt: &str,
|
||||
) -> anyhow::Result<User> {
|
||||
let user_id = JWT_PROVIDER.decode_sub(jwt)?;
|
||||
check_enabled(&user_id).await
|
||||
}
|
||||
|
||||
pub async fn auth_api_key_check_enabled(
|
||||
key: &str,
|
||||
secret: &str,
|
||||
) -> anyhow::Result<User> {
|
||||
let user_id = auth_api_key_get_user_id(key, secret).await?;
|
||||
check_enabled(&user_id).await
|
||||
}
|
||||
|
||||
/// Api Key Clock skew tolerance in milliseconds (5 minutes for Api Keys)
|
||||
const API_KEY_CLOCK_SKEW_TOLERANCE_MS: i64 = 5 * 60 * 1000;
|
||||
|
||||
pub async fn auth_api_key_get_user_id(
|
||||
key: &str,
|
||||
secret: &str,
|
||||
) -> anyhow::Result<String> {
|
||||
let key = db_client()
|
||||
.api_keys
|
||||
.find_one(doc! { "key": key })
|
||||
.await
|
||||
.context("Failed to query db")?
|
||||
.context("Invalid user credentials")?;
|
||||
// Apply clock skew tolerance.
|
||||
// Token is invalid if expiration is less than (now - tolerance)
|
||||
if key.expires != 0
|
||||
&& key.expires
|
||||
< komodo_timestamp()
|
||||
.saturating_sub(API_KEY_CLOCK_SKEW_TOLERANCE_MS)
|
||||
{
|
||||
return Err(anyhow!("Invalid user credentials"));
|
||||
}
|
||||
if bcrypt::verify(secret, &key.secret)
|
||||
.map_err(|_| anyhow!("Invalid user credentials"))?
|
||||
{
|
||||
// secret matches
|
||||
Ok(key.user_id)
|
||||
} else {
|
||||
// secret mismatch
|
||||
Err(anyhow!("Invalid user credentials"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_enabled(user_id: &str) -> anyhow::Result<User> {
|
||||
let user = get_user(user_id).await?;
|
||||
if user.enabled {
|
||||
Ok(user)
|
||||
} else {
|
||||
Err(anyhow!("Invalid user credentials"))
|
||||
}
|
||||
}
|
||||
+795
-201
File diff suppressed because it is too large
Load Diff
@@ -1,217 +0,0 @@
|
||||
use std::{sync::OnceLock, time::Duration};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use arc_swap::ArcSwapOption;
|
||||
use openidconnect::{
|
||||
AccessToken, AccessTokenHash, AuthorizationCode, Client, ClientId,
|
||||
ClientSecret, CsrfToken, EmptyAdditionalClaims, EndpointMaybeSet,
|
||||
EndpointNotSet, EndpointSet, IssuerUrl, Nonce, OAuth2TokenResponse,
|
||||
PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, Scope,
|
||||
StandardErrorResponse, SubjectIdentifier, TokenResponse as _,
|
||||
UserInfoClaims, core::*,
|
||||
};
|
||||
|
||||
use crate::config::core_config;
|
||||
|
||||
static APP_USER_AGENT: &str =
|
||||
concat!("Komodo/", env!("CARGO_PKG_VERSION"),);
|
||||
|
||||
fn oidc_reqwest_client() -> &'static reqwest::Client {
|
||||
static REQWEST: OnceLock<reqwest::Client> = OnceLock::new();
|
||||
REQWEST.get_or_init(|| {
|
||||
reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.user_agent(APP_USER_AGENT)
|
||||
.build()
|
||||
.expect("Invalid OIDC reqwest client")
|
||||
})
|
||||
}
|
||||
|
||||
pub type InnerOidcClient = Client<
|
||||
EmptyAdditionalClaims,
|
||||
CoreAuthDisplay,
|
||||
CoreGenderClaim,
|
||||
CoreJweContentEncryptionAlgorithm,
|
||||
CoreJsonWebKey,
|
||||
CoreAuthPrompt,
|
||||
StandardErrorResponse<CoreErrorResponseType>,
|
||||
CoreTokenResponse,
|
||||
CoreTokenIntrospectionResponse,
|
||||
CoreRevocableToken,
|
||||
CoreRevocationErrorResponse,
|
||||
EndpointSet,
|
||||
EndpointNotSet,
|
||||
EndpointNotSet,
|
||||
EndpointNotSet,
|
||||
EndpointMaybeSet,
|
||||
EndpointMaybeSet,
|
||||
>;
|
||||
|
||||
pub struct OidcClient(InnerOidcClient);
|
||||
|
||||
pub fn oidc_client() -> &'static ArcSwapOption<OidcClient> {
|
||||
static OIDC_CLIENT: OnceLock<ArcSwapOption<OidcClient>> =
|
||||
OnceLock::new();
|
||||
OIDC_CLIENT.get_or_init(Default::default)
|
||||
}
|
||||
|
||||
/// The OIDC client must be reinitialized to
|
||||
/// pick up the latest provider JWKs. This
|
||||
/// function spawns a management thread to do this
|
||||
/// on a loop.
|
||||
pub async fn spawn_oidc_client_management() {
|
||||
let config = core_config();
|
||||
if !config.oidc_enabled
|
||||
|| config.oidc_provider.is_empty()
|
||||
|| config.oidc_client_id.is_empty()
|
||||
{
|
||||
return;
|
||||
}
|
||||
if let Err(e) = reset_oidc_client().await {
|
||||
error!("Failed to initialize OIDC client | {e:#}");
|
||||
}
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
if let Err(e) = reset_oidc_client().await {
|
||||
warn!("Failed to reinitialize OIDC client | {e:#}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn reset_oidc_client() -> anyhow::Result<()> {
|
||||
let config = core_config();
|
||||
// Use OpenID Connect Discovery to fetch the provider metadata.
|
||||
let provider_metadata = CoreProviderMetadata::discover_async(
|
||||
IssuerUrl::new(config.oidc_provider.clone())?,
|
||||
oidc_reqwest_client(),
|
||||
)
|
||||
.await
|
||||
.context("Failed to get OIDC /.well-known/openid-configuration")?;
|
||||
|
||||
let client = CoreClient::from_provider_metadata(
|
||||
provider_metadata,
|
||||
ClientId::new(config.oidc_client_id.to_string()),
|
||||
// The secret may be empty / ommitted if auth provider supports PKCE
|
||||
if config.oidc_client_secret.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ClientSecret::new(config.oidc_client_secret.to_string()))
|
||||
},
|
||||
)
|
||||
// Set the URL the user will be redirected to after the authorization process.
|
||||
.set_redirect_uri(RedirectUrl::new(format!(
|
||||
"{}/auth/oidc/callback",
|
||||
core_config().host
|
||||
))?);
|
||||
|
||||
oidc_client().store(Some(OidcClient(client).into()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl OidcClient {
|
||||
pub fn authorize_url(
|
||||
&self,
|
||||
pkce_challenge: PkceCodeChallenge,
|
||||
) -> (url::Url, CsrfToken, Nonce) {
|
||||
self
|
||||
.0
|
||||
.authorize_url(
|
||||
CoreAuthenticationFlow::AuthorizationCode,
|
||||
CsrfToken::new_random,
|
||||
Nonce::new_random,
|
||||
)
|
||||
.set_pkce_challenge(pkce_challenge)
|
||||
.add_scope(Scope::new("openid".to_string()))
|
||||
.add_scope(Scope::new("profile".to_string()))
|
||||
.add_scope(Scope::new("email".to_string()))
|
||||
.url()
|
||||
}
|
||||
|
||||
/// Applied security validations and extracts the
|
||||
/// oidc user id.
|
||||
pub async fn validate_extract_subject_and_token(
|
||||
&self,
|
||||
(client, server): (CsrfToken, String),
|
||||
code: String,
|
||||
pkce_verifier: PkceCodeVerifier,
|
||||
nonce: Nonce,
|
||||
) -> anyhow::Result<(SubjectIdentifier, AccessToken)> {
|
||||
// Validate CSRF tokens match
|
||||
if client.secret() != &server {
|
||||
return Err(anyhow!("CSRF token invalid"));
|
||||
}
|
||||
|
||||
let reqwest_client = oidc_reqwest_client();
|
||||
let token_response = self
|
||||
.0
|
||||
.exchange_code(AuthorizationCode::new(code))
|
||||
.context("Failed to get Oauth token at exchange code")?
|
||||
.set_pkce_verifier(pkce_verifier)
|
||||
.request_async(reqwest_client)
|
||||
.await
|
||||
.context("Failed to get Oauth token")?;
|
||||
|
||||
// Extract the ID token claims after verifying its authenticity and nonce.
|
||||
let id_token = token_response
|
||||
.id_token()
|
||||
.context("OIDC Server did not return an ID token")?;
|
||||
|
||||
// Some providers attach additional audiences, they must be added here
|
||||
// so token verification succeeds.
|
||||
let verifier = self.0.id_token_verifier();
|
||||
let additional_audiences =
|
||||
&core_config().oidc_additional_audiences;
|
||||
let verifier = if additional_audiences.is_empty() {
|
||||
verifier
|
||||
} else {
|
||||
verifier.set_other_audience_verifier_fn(|aud| {
|
||||
additional_audiences.contains(aud)
|
||||
})
|
||||
};
|
||||
|
||||
let claims = id_token
|
||||
.claims(&verifier, &nonce)
|
||||
.context("Failed to verify token claims. This issue may be temporary (60 seconds max).")?;
|
||||
|
||||
// Verify the access token hash to ensure that the access token hasn't been substituted for
|
||||
// another user's.
|
||||
if let Some(expected_access_token_hash) =
|
||||
claims.access_token_hash()
|
||||
{
|
||||
let actual_access_token_hash = AccessTokenHash::from_token(
|
||||
&token_response.access_token().clone(),
|
||||
id_token.signing_alg()?,
|
||||
id_token.signing_key(&verifier)?,
|
||||
)?;
|
||||
if actual_access_token_hash != *expected_access_token_hash {
|
||||
return Err(anyhow!("Invalid access token"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
claims.subject().clone(),
|
||||
token_response.access_token().clone(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn fetch_user_info(
|
||||
&self,
|
||||
access_token: AccessToken,
|
||||
subject: SubjectIdentifier,
|
||||
) -> anyhow::Result<
|
||||
UserInfoClaims<EmptyAdditionalClaims, CoreGenderClaim>,
|
||||
> {
|
||||
self
|
||||
.0
|
||||
.user_info(access_token, Some(subject))
|
||||
.context("Invalid user info request")?
|
||||
.request_async::<EmptyAdditionalClaims, _, CoreGenderClaim>(
|
||||
oidc_reqwest_client(),
|
||||
)
|
||||
.await
|
||||
.context("Failed to fetch OIDC user info")
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user