database config provider

This commit is contained in:
Eugene Pankov 2022-09-02 14:00:08 +02:00
parent 739df5a507
commit 51df7083de
No known key found for this signature in database
GPG key ID: 5896FCBBDD1CF4F4
144 changed files with 5903 additions and 1461 deletions

View file

@ -15,6 +15,10 @@ replace = version = "{new_version}"
search = version = "{current_version}"
replace = version = "{new_version}"
[bumpversion:file:warpgate-config/Cargo.toml]
search = version = "{current_version}"
replace = version = "{new_version}"
[bumpversion:file:warpgate-database-protocols/Cargo.toml]
search = version = "{current_version}"
replace = version = "{new_version}"

View file

@ -17,14 +17,9 @@ jobs:
target: x86_64-unknown-linux-gnu
override: true
- name: Cache
uses: actions/cache@v2
- uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
key: "test"
- name: Install just
run: |

View file

@ -10,14 +10,9 @@ jobs:
with:
submodules: recursive
- name: Cache
uses: actions/cache@v2
- uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
key: "test"
- name: Install build deps
run: |
@ -41,13 +36,14 @@ jobs:
- name: Install deps
working-directory: tests
run: |
sudo apt install -y gnome-keyring
pip3 install poetry
poetry install
- name: Run
working-directory: tests
run: |
poetry run ./run.sh
TIMEOUT=120 poetry run ./run.sh
cargo llvm-cov --no-run --hide-instantiations --lcov > coverage.lcov
- name: SonarCloud Scan

52
Cargo.lock generated
View file

@ -4796,13 +4796,18 @@ dependencies = [
"qrcode",
"rcgen",
"sd-notify",
"sea-orm",
"serde_json",
"serde_yaml",
"time 0.3.11",
"tokio",
"tracing",
"tracing-subscriber",
"uuid",
"warpgate-admin",
"warpgate-common",
"warpgate-core",
"warpgate-db-entities",
"warpgate-protocol-http",
"warpgate-protocol-mysql",
"warpgate-protocol-ssh",
@ -4832,6 +4837,7 @@ dependencies = [
"tracing",
"uuid",
"warpgate-common",
"warpgate-core",
"warpgate-db-entities",
"warpgate-protocol-ssh",
]
@ -4839,6 +4845,44 @@ dependencies = [
[[package]]
name = "warpgate-common"
version = "0.5.0"
dependencies = [
"anyhow",
"argon2",
"async-trait",
"bytes 1.2.1",
"chrono",
"data-encoding",
"delegate",
"futures",
"humantime-serde",
"lazy_static",
"once_cell",
"packet",
"password-hash 0.4.1",
"poem",
"poem-openapi",
"rand 0.8.5",
"rand_chacha 0.3.1",
"rand_core 0.6.3",
"rustls",
"rustls-pemfile",
"sea-orm",
"serde",
"serde_json",
"thiserror",
"tokio",
"totp-rs",
"tracing",
"tracing-core",
"url",
"uuid",
"warpgate-sso",
"webpki",
]
[[package]]
name = "warpgate-core"
version = "0.5.0"
dependencies = [
"anyhow",
"argon2",
@ -4870,6 +4914,7 @@ dependencies = [
"tracing-subscriber",
"url",
"uuid",
"warpgate-common",
"warpgate-db-entities",
"warpgate-db-migrations",
"warpgate-sso",
@ -4899,6 +4944,7 @@ dependencies = [
"serde",
"serde_json",
"uuid",
"warpgate-common",
]
[[package]]
@ -4909,6 +4955,7 @@ dependencies = [
"chrono",
"sea-orm",
"sea-orm-migration",
"serde_json",
"uuid",
]
@ -4938,6 +4985,7 @@ dependencies = [
"uuid",
"warpgate-admin",
"warpgate-common",
"warpgate-core",
"warpgate-db-entities",
"warpgate-sso",
"warpgate-web",
@ -4964,6 +5012,7 @@ dependencies = [
"tracing",
"uuid",
"warpgate-common",
"warpgate-core",
"warpgate-database-protocols",
"warpgate-db-entities",
"webpki",
@ -4989,6 +5038,7 @@ dependencies = [
"tracing",
"uuid",
"warpgate-common",
"warpgate-core",
"warpgate-db-entities",
"zeroize",
]
@ -4997,7 +5047,6 @@ dependencies = [
name = "warpgate-sso"
version = "0.5.0"
dependencies = [
"async-trait",
"bytes 1.2.1",
"once_cell",
"openidconnect",
@ -5006,7 +5055,6 @@ dependencies = [
"thiserror",
"tokio",
"tracing",
"uuid",
]
[[package]]

View file

@ -5,6 +5,7 @@ members = [
"warpgate",
"warpgate-admin",
"warpgate-common",
"warpgate-core",
"warpgate-db-migrations",
"warpgate-db-entities",
"warpgate-database-protocols",

View file

@ -1,4 +1,4 @@
projects := "warpgate warpgate-admin warpgate-common warpgate-db-entities warpgate-db-migrations warpgate-database-protocols warpgate-protocol-ssh warpgate-protocol-mysql warpgate-sso"
projects := "warpgate warpgate-admin warpgate-common warpgate-db-entities warpgate-db-migrations warpgate-database-protocols warpgate-protocol-ssh warpgate-protocol-mysql warpgate-protocol-http warpgate-core warpgate-sso"
run *ARGS:
RUST_BACKTRACE=1 RUST_LOG=warpgate cd warpgate && cargo run -- --config ../config.yaml {{ARGS}}

2
russh

@ -1 +1 @@
Subproject commit 9ac839abf415817d5622e54d12943e2033a9e9f4
Subproject commit 811680a3bf04c563fd1a4cfa39b05224d08e85c0

View file

@ -167,6 +167,7 @@ class ProcessManager:
roles:
- name: role
- name: admin
- name: warpgate:admin
'''
) + config
)
@ -220,6 +221,12 @@ class ProcessManager:
return p
@pytest.fixture(scope='session')
def timeout():
t = os.getenv('TIMEOUT', '10')
return int(t)
@pytest.fixture(scope='session')
def ctx():
with tempfile.TemporaryDirectory() as tmpdir:

View file

@ -3,7 +3,7 @@ set -e
cd ..
rm target/llvm-cov-target/* || true
cargo llvm-cov clean --workspace
cargo llvm-cov --no-report --workspace --all-features
cargo llvm-cov --no-report --workspace --all-features -- --skip agent
cd tests
poetry run pytest $@
RUST_BACKTRACE=1 poetry run pytest -s $@
cargo llvm-cov --no-run --hide-instantiations --html

View file

@ -19,12 +19,12 @@ def http_common_wg_port(processes, echo_server_port, password_123_hash, otp_key_
allow_roles: [role]
http:
url: http://localhostfoobar
- name: admin
- name: warpgate:admin
allow_roles: [admin]
web_admin: {{}}
users:
- username: admin
roles: [admin]
roles: [admin, warpgate:admin]
credentials:
- type: password
hash: '{password_123_hash}'

View file

@ -6,6 +6,7 @@ class Test:
self,
processes,
echo_server_port,
timeout,
):
proc, _ = processes.start_wg(
config=dedent(
@ -20,10 +21,10 @@ class Test:
),
args=['test-target', 'target'],
)
proc.wait(timeout=5)
proc.wait(timeout=timeout)
assert proc.returncode == 0
def test_fail_no_connection(self, processes):
def test_fail_no_connection(self, processes, timeout):
proc, _ = processes.start_wg(
config=dedent(
'''\
@ -37,5 +38,5 @@ class Test:
),
args=['test-target', 'target'],
)
proc.wait(timeout=5)
proc.wait(timeout=timeout)
assert proc.returncode != 0

View file

@ -6,7 +6,7 @@ from .util import wait_port, wait_mysql_port, mysql_client_ssl_opt, mysql_client
class Test:
def test(self, processes: ProcessManager, password_123_hash):
def test(self, processes: ProcessManager, password_123_hash, timeout):
db_port = processes.start_mysql_server()
_, wg_ports = processes.start_wg(
@ -50,7 +50,7 @@ class Test:
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
assert b'\ndb\n' in client.communicate(b'show schemas;')[0]
assert b'\ndb\n' in client.communicate(b'show schemas;', timeout=timeout)[0]
assert client.returncode == 0
client = processes.start(
@ -70,5 +70,5 @@ class Test:
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
client.communicate(b'show schemas;')
client.communicate(b'show schemas;', timeout=timeout)
assert client.returncode != 0

View file

@ -7,7 +7,7 @@ from .util import alloc_port, wait_port
class Test:
def test_success(
self, processes: ProcessManager, wg_c_ed25519_pubkey: Path
self, processes: ProcessManager, wg_c_ed25519_pubkey: Path, timeout
):
ssh_port = processes.start_ssh_server(
trusted_keys=[wg_c_ed25519_pubkey.read_text()]
@ -27,10 +27,10 @@ class Test:
),
args=['test-target', 'ssh'],
)
proc.wait(timeout=5)
proc.wait(timeout=timeout)
assert proc.returncode == 0
def test_fail(self, processes: ProcessManager):
def test_fail(self, processes: ProcessManager, timeout):
ssh_port = alloc_port()
proc, _ = processes.start_wg(
config=dedent(
@ -46,5 +46,5 @@ class Test:
),
args=['test-target', 'ssh'],
)
proc.wait(timeout=5)
proc.wait(timeout=timeout)
assert proc.returncode != 0

View file

@ -57,6 +57,7 @@ class Test:
self,
processes: ProcessManager,
wg_port,
timeout,
):
ssh_client = processes.start_ssh_client(
'-p',
@ -69,7 +70,7 @@ class Test:
stderr=subprocess.PIPE,
)
stdout, stderr = ssh_client.communicate(timeout=10)
stdout, stderr = ssh_client.communicate(timeout=timeout)
assert b'stdout' == stdout
assert stderr.endswith(b'stderr')
@ -77,6 +78,7 @@ class Test:
self,
processes: ProcessManager,
wg_port,
timeout,
):
ssh_client = processes.start_ssh_client(
'-p',
@ -88,7 +90,7 @@ class Test:
password='123',
)
output = ssh_client.communicate()[0]
output = ssh_client.communicate(timeout=timeout)[0]
assert b'Warpgate' in output
assert b'Selected target:' in output
assert b'hello\r\n' in output
@ -134,10 +136,11 @@ class Test:
self,
processes: ProcessManager,
wg_port,
timeout,
):
script = dedent(
f'''
set timeout 10
set timeout {timeout - 5}
spawn ssh -tt user:ssh@localhost -p {wg_port} -o StrictHostKeychecking=no -o UserKnownHostsFile=/dev/null -o PreferredAuthentications=password
@ -163,7 +166,7 @@ class Test:
['expect', '-d'], stdin=subprocess.PIPE, stdout=subprocess.PIPE
)
output = ssh_client.communicate(script.encode())[0]
output = ssh_client.communicate(script.encode(), timeout=timeout)[0]
assert ssh_client.returncode == 0, output
def test_connection_error(

View file

@ -14,6 +14,7 @@ class Test:
wg_c_ed25519_pubkey: Path,
otp_key_base32: str,
otp_key_base64: str,
timeout,
):
ssh_port = processes.start_ssh_server(
trusted_keys=[wg_c_ed25519_pubkey.read_text()]
@ -49,7 +50,7 @@ class Test:
script = dedent(
f'''
set timeout 10
set timeout {timeout - 5}
spawn ssh user:ssh@localhost -p {wg_ports['ssh']} -o StrictHostKeychecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o IdentityFile=ssh-keys/id_ed25519 -o PreferredAuthentications=publickey,keyboard-interactive ls /bin/sh
@ -68,12 +69,12 @@ class Test:
['expect'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
output, stderr = ssh_client.communicate(script.encode())
output, stderr = ssh_client.communicate(script.encode(), timeout=timeout)
assert ssh_client.returncode == 0, output + stderr
script = dedent(
f'''
set timeout 10
set timeout {timeout - 5}
spawn ssh user:ssh@localhost -p {[wg_ports['ssh']]} -o StrictHostKeychecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o IdentityFile=ssh-keys/id_ed25519 -o PreferredAuthentications=publickey,keyboard-interactive ls /bin/sh
@ -93,5 +94,5 @@ class Test:
['expect'], stdin=subprocess.PIPE, stdout=subprocess.PIPE
)
output = ssh_client.communicate(script.encode())[0]
output = ssh_client.communicate(script.encode(), timeout=timeout)[0]
assert ssh_client.returncode != 0, output

View file

@ -7,7 +7,7 @@ from .util import wait_port
class Test:
def test(
self, processes: ProcessManager, wg_c_ed25519_pubkey: Path, password_123_hash
self, processes: ProcessManager, wg_c_ed25519_pubkey: Path, password_123_hash, timeout
):
ssh_port = processes.start_ssh_server(
trusted_keys=[wg_c_ed25519_pubkey.read_text()]
@ -37,6 +37,7 @@ class Test:
ssh_client = processes.start_ssh_client(
'user:ssh@localhost',
'-v',
'-p',
str(wg_ports['ssh']),
'-i',
@ -47,7 +48,7 @@ class Test:
'/bin/sh',
password='123',
)
assert ssh_client.communicate()[0] == b'/bin/sh\n'
assert ssh_client.communicate(timeout=timeout)[0] == b'/bin/sh\n'
assert ssh_client.returncode == 0
ssh_client = processes.start_ssh_client(
@ -62,5 +63,5 @@ class Test:
'/bin/sh',
password='321',
)
ssh_client.communicate()
ssh_client.communicate(timeout=timeout)
assert ssh_client.returncode != 0

View file

@ -7,7 +7,7 @@ from .util import wait_port
class Test:
def test_ed25519(
self, processes: ProcessManager, wg_c_ed25519_pubkey: Path
self, processes: ProcessManager, wg_c_ed25519_pubkey: Path, timeout
):
ssh_port = processes.start_ssh_server(
trusted_keys=[wg_c_ed25519_pubkey.read_text()]
@ -43,10 +43,11 @@ class Test:
'IdentityFile=ssh-keys/id_ed25519',
'-o',
'PreferredAuthentications=publickey',
# 'sh', '-c', '"ls /bin/sh;sleep 1"',
'ls',
'/bin/sh',
)
assert ssh_client.communicate()[0] == b'/bin/sh\n'
assert ssh_client.communicate(timeout=timeout)[0] == b'/bin/sh\n'
assert ssh_client.returncode == 0
ssh_client = processes.start_ssh_client(
@ -60,11 +61,11 @@ class Test:
'ls',
'/bin/sh',
)
assert ssh_client.communicate()[0] == b''
assert ssh_client.communicate(timeout=timeout)[0] == b''
assert ssh_client.returncode != 0
def test_rsa(
self, processes: ProcessManager, wg_c_ed25519_pubkey: Path
self, processes: ProcessManager, wg_c_ed25519_pubkey: Path, timeout
):
ssh_port = processes.start_ssh_server(
trusted_keys=[wg_c_ed25519_pubkey.read_text()]
@ -105,7 +106,7 @@ class Test:
'ls',
'/bin/sh',
)
assert ssh_client.communicate(timeout=10)[0] == b'/bin/sh\n'
assert ssh_client.communicate(timeout=timeout)[0] == b'/bin/sh\n'
assert ssh_client.returncode == 0
ssh_client = processes.start_ssh_client(
@ -120,5 +121,5 @@ class Test:
'ls',
'/bin/sh',
)
assert ssh_client.communicate(timeout=10)[0] == b''
assert ssh_client.communicate(timeout=timeout)[0] == b''
assert ssh_client.returncode != 0

View file

@ -7,7 +7,7 @@ from .util import create_ticket, wait_port
class Test:
def test(
self, processes: ProcessManager, wg_c_ed25519_pubkey: Path, password_123_hash
self, processes: ProcessManager, wg_c_ed25519_pubkey: Path, password_123_hash, timeout
):
ssh_port = processes.start_ssh_server(
trusted_keys=[wg_c_ed25519_pubkey.read_text()]
@ -22,7 +22,7 @@ class Test:
ssh:
host: localhost
port: {ssh_port}
- name: admin
- name: warpgate:admin
allow_roles: [admin]
web_admin: {{}}
users:
@ -32,7 +32,7 @@ class Test:
- type: password
hash: '{password_123_hash}'
- username: admin
roles: [admin]
roles: [warpgate:admin]
credentials:
- type: password
hash: '{password_123_hash}'
@ -59,5 +59,5 @@ class Test:
'/bin/sh',
password='123',
)
assert ssh_client.communicate()[0] == b'/bin/sh\n'
assert ssh_client.communicate(timeout=timeout)[0] == b'/bin/sh\n'
assert ssh_client.returncode == 0

View file

@ -34,7 +34,10 @@ def wait_port(port, recv=True):
try:
s = socket.create_connection(('localhost', port), timeout=5)
if recv:
data = s.recv(100)
while True:
data = s.recv(100)
if data:
break
else:
data = b''
s.close()

View file

@ -38,6 +38,7 @@ tokio = {version = "1.20", features = ["tracing"]}
tracing = "0.1"
uuid = { version = "1.0", features = ["v4", "serde"] }
warpgate-common = { version = "*", path = "../warpgate-common" }
warpgate-core = { version = "*", path = "../warpgate-core" }
warpgate-db-entities = { version = "*", path = "../warpgate-db-entities" }
warpgate-protocol-ssh = { version = "*", path = "../warpgate-protocol-ssh" }
regex = "1.6"

View file

@ -1,25 +1,28 @@
use poem_openapi::OpenApi;
pub mod known_hosts_detail;
pub mod known_hosts_list;
pub mod logs;
mod known_hosts_detail;
mod known_hosts_list;
mod logs;
mod pagination;
pub mod recordings_detail;
pub mod sessions_detail;
mod roles;
mod sessions_detail;
pub mod sessions_list;
pub mod ssh_keys;
pub mod targets_list;
pub mod tickets_detail;
pub mod tickets_list;
pub mod users_list;
mod ssh_keys;
mod targets;
mod tickets_detail;
mod tickets_list;
mod users;
pub fn get() -> impl OpenApi {
(
sessions_list::Api,
sessions_detail::Api,
recordings_detail::Api,
users_list::Api,
targets_list::Api,
roles::ListApi,
roles::DetailApi,
(targets::ListApi, targets::DetailApi, targets::RolesApi),
(users::ListApi, users::DetailApi, users::RolesApi),
tickets_list::Api,
tickets_detail::Api,
known_hosts_list::Api,

View file

@ -16,7 +16,7 @@ use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::sync::Mutex;
use tracing::*;
use uuid::Uuid;
use warpgate_common::recordings::{AsciiCast, SessionRecordings, TerminalRecordingItem};
use warpgate_core::recordings::{AsciiCast, SessionRecordings, TerminalRecordingItem};
use warpgate_db_entities::Recording::{self, RecordingKind};
pub struct Api;

View file

@ -0,0 +1,187 @@
use std::sync::Arc;
use poem::web::Data;
use poem_openapi::param::Path;
use poem_openapi::payload::Json;
use poem_openapi::{ApiResponse, Object, OpenApi};
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryOrder, Set};
use tokio::sync::Mutex;
use uuid::Uuid;
use warpgate_common::{Role as RoleConfig, WarpgateError};
use warpgate_core::consts::BUILTIN_ADMIN_ROLE_NAME;
use warpgate_db_entities::Role;
#[derive(Object)]
struct RoleDataRequest {
name: String,
}
#[derive(ApiResponse)]
enum GetRolesResponse {
#[oai(status = 200)]
Ok(Json<Vec<RoleConfig>>),
}
#[derive(ApiResponse)]
enum CreateRoleResponse {
#[oai(status = 201)]
Created(Json<RoleConfig>),
#[oai(status = 400)]
BadRequest(Json<String>),
}
pub struct ListApi;
#[OpenApi]
impl ListApi {
#[oai(path = "/roles", method = "get", operation_id = "get_roles")]
async fn api_get_all_roles(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
) -> poem::Result<GetRolesResponse> {
let db = db.lock().await;
let roles = Role::Entity::find()
.order_by_asc(Role::Column::Name)
.all(&*db)
.await
.map_err(poem::error::InternalServerError)?;
Ok(GetRolesResponse::Ok(Json(
roles.into_iter().map(Into::into).collect(),
)))
}
#[oai(path = "/roles", method = "post", operation_id = "create_role")]
async fn api_create_role(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
body: Json<RoleDataRequest>,
) -> poem::Result<CreateRoleResponse> {
use warpgate_db_entities::Role;
if body.name.is_empty() {
return Ok(CreateRoleResponse::BadRequest(Json("name".into())));
}
let db = db.lock().await;
let values = Role::ActiveModel {
id: Set(Uuid::new_v4()),
name: Set(body.name.clone()),
};
let role = values.insert(&*db).await.map_err(WarpgateError::from)?;
Ok(CreateRoleResponse::Created(Json(role.into())))
}
}
#[derive(ApiResponse)]
enum GetRoleResponse {
#[oai(status = 200)]
Ok(Json<RoleConfig>),
#[oai(status = 404)]
NotFound,
}
#[derive(ApiResponse)]
enum UpdateRoleResponse {
#[oai(status = 200)]
Ok(Json<RoleConfig>),
#[oai(status = 403)]
Forbidden,
#[oai(status = 404)]
NotFound,
}
#[derive(ApiResponse)]
enum DeleteRoleResponse {
#[oai(status = 204)]
Deleted,
#[oai(status = 403)]
Forbidden,
#[oai(status = 404)]
NotFound,
}
pub struct DetailApi;
#[OpenApi]
impl DetailApi {
#[oai(path = "/role/:id", method = "get", operation_id = "get_role")]
async fn api_get_role(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
id: Path<Uuid>,
) -> poem::Result<GetRoleResponse> {
let db = db.lock().await;
let role = Role::Entity::find_by_id(id.0)
.one(&*db)
.await
.map_err(poem::error::InternalServerError)?;
Ok(match role {
Some(role) => GetRoleResponse::Ok(Json(
role.try_into().map_err(poem::error::InternalServerError)?,
)),
None => GetRoleResponse::NotFound,
})
}
#[oai(path = "/role/:id", method = "put", operation_id = "update_role")]
async fn api_update_role(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
body: Json<RoleDataRequest>,
id: Path<Uuid>,
) -> poem::Result<UpdateRoleResponse> {
let db = db.lock().await;
let Some(role) = Role::Entity::find_by_id(id.0)
.one(&*db)
.await
.map_err(poem::error::InternalServerError)? else {
return Ok(UpdateRoleResponse::NotFound);
};
if role.name == BUILTIN_ADMIN_ROLE_NAME {
return Ok(UpdateRoleResponse::Forbidden);
}
let mut model: Role::ActiveModel = role.into();
model.name = Set(body.name.clone());
let role = model
.update(&*db)
.await
.map_err(poem::error::InternalServerError)?;
Ok(UpdateRoleResponse::Ok(Json(role.into())))
}
#[oai(path = "/role/:id", method = "delete", operation_id = "delete_role")]
async fn api_delete_role(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
id: Path<Uuid>,
) -> poem::Result<DeleteRoleResponse> {
let db = db.lock().await;
let Some(role) = Role::Entity::find_by_id(id.0)
.one(&*db)
.await
.map_err(poem::error::InternalServerError)? else {
return Ok(DeleteRoleResponse::NotFound);
};
if role.name == BUILTIN_ADMIN_ROLE_NAME {
return Ok(DeleteRoleResponse::Forbidden);
}
role.delete(&*db)
.await
.map_err(poem::error::InternalServerError)?;
Ok(DeleteRoleResponse::Deleted)
}
}

View file

@ -7,7 +7,7 @@ use poem_openapi::{ApiResponse, OpenApi};
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder};
use tokio::sync::Mutex;
use uuid::Uuid;
use warpgate_common::{SessionSnapshot, State};
use warpgate_core::{SessionSnapshot, State};
use warpgate_db_entities::{Recording, Session};
pub struct Api;

View file

@ -10,7 +10,7 @@ use poem_openapi::payload::Json;
use poem_openapi::{ApiResponse, OpenApi};
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder};
use tokio::sync::Mutex;
use warpgate_common::{SessionSnapshot, State};
use warpgate_core::{SessionSnapshot, State};
use super::pagination::{PaginatedResponse, PaginationParams};

View file

@ -0,0 +1,349 @@
use std::sync::Arc;
use poem::web::Data;
use poem_openapi::param::Path;
use poem_openapi::payload::Json;
use poem_openapi::{ApiResponse, Object, OpenApi};
use sea_orm::{
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter,
QueryOrder, Set,
};
use tokio::sync::Mutex;
use uuid::Uuid;
use warpgate_common::{Role as RoleConfig, Target as TargetConfig, TargetOptions, WarpgateError};
use warpgate_core::consts::BUILTIN_ADMIN_ROLE_NAME;
use warpgate_db_entities::Target::TargetKind;
use warpgate_db_entities::{Role, Target, TargetRoleAssignment};
#[derive(Object)]
struct TargetDataRequest {
name: String,
options: TargetOptions,
}
#[derive(ApiResponse)]
enum GetTargetsResponse {
#[oai(status = 200)]
Ok(Json<Vec<TargetConfig>>),
}
#[derive(ApiResponse)]
enum CreateTargetResponse {
#[oai(status = 201)]
Created(Json<TargetConfig>),
#[oai(status = 400)]
BadRequest(Json<String>),
}
pub struct ListApi;
#[OpenApi]
impl ListApi {
#[oai(path = "/targets", method = "get", operation_id = "get_targets")]
async fn api_get_all_targets(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
) -> poem::Result<GetTargetsResponse> {
let db = db.lock().await;
let targets = Target::Entity::find()
.order_by_asc(Target::Column::Name)
.all(&*db)
.await
.map_err(WarpgateError::from)?;
let targets: Result<Vec<TargetConfig>, _> =
targets.into_iter().map(|t| t.try_into()).collect();
let targets = targets.map_err(WarpgateError::from)?;
Ok(GetTargetsResponse::Ok(Json(targets)))
}
#[oai(path = "/targets", method = "post", operation_id = "create_target")]
async fn api_create_target(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
body: Json<TargetDataRequest>,
) -> poem::Result<CreateTargetResponse> {
if body.name.is_empty() {
return Ok(CreateTargetResponse::BadRequest(Json("name".into())));
}
if let TargetOptions::WebAdmin(_) = body.options {
return Ok(CreateTargetResponse::BadRequest(Json("kind".into())));
}
let db = db.lock().await;
let values = Target::ActiveModel {
id: Set(Uuid::new_v4()),
name: Set(body.name.clone()),
kind: Set((&body.options).into()),
options: Set(serde_json::to_value(body.options.clone()).map_err(WarpgateError::from)?),
};
let target = values.insert(&*db).await.map_err(WarpgateError::from)?;
Ok(CreateTargetResponse::Created(Json(
target.try_into().map_err(WarpgateError::from)?,
)))
}
}
#[derive(ApiResponse)]
enum GetTargetResponse {
#[oai(status = 200)]
Ok(Json<TargetConfig>),
#[oai(status = 404)]
NotFound,
}
#[derive(ApiResponse)]
enum UpdateTargetResponse {
#[oai(status = 200)]
Ok(Json<TargetConfig>),
#[oai(status = 400)]
BadRequest,
#[oai(status = 404)]
NotFound,
}
#[derive(ApiResponse)]
enum DeleteTargetResponse {
#[oai(status = 204)]
Deleted,
#[oai(status = 403)]
Forbidden,
#[oai(status = 404)]
NotFound,
}
pub struct DetailApi;
#[OpenApi]
impl DetailApi {
#[oai(path = "/targets/:id", method = "get", operation_id = "get_target")]
async fn api_get_target(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
id: Path<Uuid>,
) -> poem::Result<GetTargetResponse> {
let db = db.lock().await;
let Some(target) = Target::Entity::find_by_id(id.0)
.one(&*db)
.await
.map_err(poem::error::InternalServerError)? else {
return Ok(GetTargetResponse::NotFound);
};
Ok(GetTargetResponse::Ok(Json(
target
.try_into()
.map_err(poem::error::InternalServerError)?,
)))
}
#[oai(path = "/targets/:id", method = "put", operation_id = "update_target")]
async fn api_update_target(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
body: Json<TargetDataRequest>,
id: Path<Uuid>,
) -> poem::Result<UpdateTargetResponse> {
let db = db.lock().await;
let Some(target) = Target::Entity::find_by_id(id.0)
.one(&*db)
.await
.map_err(poem::error::InternalServerError)? else {
return Ok(UpdateTargetResponse::NotFound);
};
if target.kind != (&body.options).into() {
return Ok(UpdateTargetResponse::BadRequest);
}
let mut model: Target::ActiveModel = target.into();
model.name = Set(body.name.clone());
model.options =
Set(serde_json::to_value(body.options.clone()).map_err(WarpgateError::from)?);
let target = model
.update(&*db)
.await
.map_err(poem::error::InternalServerError)?;
Ok(UpdateTargetResponse::Ok(Json(
target.try_into().map_err(WarpgateError::from)?,
)))
}
#[oai(
path = "/targets/:id",
method = "delete",
operation_id = "delete_target"
)]
async fn api_delete_target(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
id: Path<Uuid>,
) -> poem::Result<DeleteTargetResponse> {
let db = db.lock().await;
let Some(target) = Target::Entity::find_by_id(id.0)
.one(&*db)
.await
.map_err(poem::error::InternalServerError)? else {
return Ok(DeleteTargetResponse::NotFound);
};
if target.kind == TargetKind::WebAdmin {
return Ok(DeleteTargetResponse::Forbidden);
}
target
.delete(&*db)
.await
.map_err(poem::error::InternalServerError)?;
Ok(DeleteTargetResponse::Deleted)
}
}
#[derive(ApiResponse)]
enum GetTargetRolesResponse {
#[oai(status = 200)]
Ok(Json<Vec<RoleConfig>>),
#[oai(status = 404)]
NotFound,
}
#[derive(ApiResponse)]
enum AddTargetRoleResponse {
#[oai(status = 201)]
Created,
#[oai(status = 409)]
AlreadyExists,
}
#[derive(ApiResponse)]
enum DeleteTargetRoleResponse {
#[oai(status = 204)]
Deleted,
#[oai(status = 403)]
Forbidden,
#[oai(status = 404)]
NotFound,
}
pub struct RolesApi;
#[OpenApi]
impl RolesApi {
#[oai(
path = "/targets/:id/roles",
method = "get",
operation_id = "get_target_roles"
)]
async fn api_get_target_roles(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
id: Path<Uuid>,
) -> poem::Result<GetTargetRolesResponse> {
let db = db.lock().await;
let Some((_, roles)) = Target::Entity::find_by_id(*id)
.find_with_related(Role::Entity)
.all(&*db)
.await
.map(|x| x.into_iter().next())
.map_err(WarpgateError::from)? else {
return Ok(GetTargetRolesResponse::NotFound)
};
Ok(GetTargetRolesResponse::Ok(Json(
roles.into_iter().map(|x| x.into()).collect(),
)))
}
#[oai(
path = "/targets/:id/roles/:role_id",
method = "post",
operation_id = "add_target_role"
)]
async fn api_add_target_role(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
id: Path<Uuid>,
role_id: Path<Uuid>,
) -> poem::Result<AddTargetRoleResponse> {
let db = db.lock().await;
if !TargetRoleAssignment::Entity::find()
.filter(TargetRoleAssignment::Column::TargetId.eq(id.0))
.filter(TargetRoleAssignment::Column::RoleId.eq(role_id.0))
.all(&*db)
.await
.map_err(WarpgateError::from)?
.is_empty()
{
return Ok(AddTargetRoleResponse::AlreadyExists);
}
let values = TargetRoleAssignment::ActiveModel {
target_id: Set(id.0),
role_id: Set(role_id.0),
..Default::default()
};
values.insert(&*db).await.map_err(WarpgateError::from)?;
Ok(AddTargetRoleResponse::Created)
}
#[oai(
path = "/targets/:id/roles/:role_id",
method = "delete",
operation_id = "delete_target_role"
)]
async fn api_delete_target_role(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
id: Path<Uuid>,
role_id: Path<Uuid>,
) -> poem::Result<DeleteTargetRoleResponse> {
let db = db.lock().await;
let Some(target) = Target::Entity::find_by_id(id.0)
.one(&*db)
.await
.map_err(poem::error::InternalServerError)? else {
return Ok(DeleteTargetRoleResponse::NotFound);
};
let Some(role) = Role::Entity::find_by_id(role_id.0)
.one(&*db)
.await
.map_err(poem::error::InternalServerError)? else {
return Ok(DeleteTargetRoleResponse::NotFound);
};
if role.name == BUILTIN_ADMIN_ROLE_NAME && target.kind == TargetKind::WebAdmin {
return Ok(DeleteTargetRoleResponse::Forbidden);
}
let Some(model) = TargetRoleAssignment::Entity::find()
.filter(TargetRoleAssignment::Column::TargetId.eq(id.0))
.filter(TargetRoleAssignment::Column::RoleId.eq(role_id.0))
.one(&*db)
.await
.map_err(WarpgateError::from)? else {
return Ok(DeleteTargetRoleResponse::NotFound);
};
model.delete(&*db).await.map_err(WarpgateError::from)?;
Ok(DeleteTargetRoleResponse::Deleted)
}
}

View file

@ -1,28 +0,0 @@
use std::sync::Arc;
use poem::web::Data;
use poem_openapi::payload::Json;
use poem_openapi::{ApiResponse, OpenApi};
use tokio::sync::Mutex;
use warpgate_common::{ConfigProvider, Target};
pub struct Api;
#[derive(ApiResponse)]
enum GetTargetsResponse {
#[oai(status = 200)]
Ok(Json<Vec<Target>>),
}
#[OpenApi]
impl Api {
#[oai(path = "/targets", method = "get", operation_id = "get_targets")]
async fn api_get_all_targets(
&self,
config_provider: Data<&Arc<Mutex<dyn ConfigProvider + Send>>>,
) -> poem::Result<GetTargetsResponse> {
let mut targets = config_provider.lock().await.list_targets().await?;
targets.sort_by(|a, b| a.name.cmp(&b.name));
Ok(GetTargetsResponse::Ok(Json(targets)))
}
}

View file

@ -0,0 +1,326 @@
use std::sync::Arc;
use poem::web::Data;
use poem_openapi::param::Path;
use poem_openapi::payload::Json;
use poem_openapi::{ApiResponse, Object, OpenApi};
use sea_orm::{
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter,
QueryOrder, Set,
};
use tokio::sync::Mutex;
use uuid::Uuid;
use warpgate_common::{
Role as RoleConfig, User as UserConfig, UserAuthCredential, UserRequireCredentialsPolicy,
WarpgateError,
};
use warpgate_db_entities::{Role, User, UserRoleAssignment};
#[derive(Object)]
struct UserDataRequest {
username: String,
credentials: Vec<UserAuthCredential>,
credential_policy: Option<UserRequireCredentialsPolicy>,
}
#[derive(ApiResponse)]
enum GetUsersResponse {
#[oai(status = 200)]
Ok(Json<Vec<UserConfig>>),
}
#[derive(ApiResponse)]
enum CreateUserResponse {
#[oai(status = 201)]
Created(Json<UserConfig>),
#[oai(status = 400)]
BadRequest(Json<String>),
}
pub struct ListApi;
#[OpenApi]
impl ListApi {
#[oai(path = "/users", method = "get", operation_id = "get_users")]
async fn api_get_all_users(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
) -> poem::Result<GetUsersResponse> {
let db = db.lock().await;
let users = User::Entity::find()
.order_by_asc(User::Column::Username)
.all(&*db)
.await
.map_err(WarpgateError::from)?;
let users: Result<Vec<UserConfig>, _> = users.into_iter().map(|t| t.try_into()).collect();
let users = users.map_err(WarpgateError::from)?;
Ok(GetUsersResponse::Ok(Json(users)))
}
#[oai(path = "/users", method = "post", operation_id = "create_user")]
async fn api_create_user(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
body: Json<UserDataRequest>,
) -> poem::Result<CreateUserResponse> {
if body.username.is_empty() {
return Ok(CreateUserResponse::BadRequest(Json("name".into())));
}
let db = db.lock().await;
let values = User::ActiveModel {
id: Set(Uuid::new_v4()),
username: Set(body.username.clone()),
credentials: Set(
serde_json::to_value(body.credentials.clone()).map_err(WarpgateError::from)?
),
credential_policy: Set(serde_json::to_value(body.credential_policy.clone())
.map_err(WarpgateError::from)?),
};
let user = values.insert(&*db).await.map_err(WarpgateError::from)?;
Ok(CreateUserResponse::Created(Json(
user.try_into().map_err(WarpgateError::from)?,
)))
}
}
#[derive(ApiResponse)]
enum GetUserResponse {
#[oai(status = 200)]
Ok(Json<UserConfig>),
#[oai(status = 404)]
NotFound,
}
#[derive(ApiResponse)]
enum UpdateUserResponse {
#[oai(status = 200)]
Ok(Json<UserConfig>),
#[oai(status = 404)]
NotFound,
}
#[derive(ApiResponse)]
enum DeleteUserResponse {
#[oai(status = 204)]
Deleted,
#[oai(status = 404)]
NotFound,
}
pub struct DetailApi;
#[OpenApi]
impl DetailApi {
#[oai(path = "/users/:id", method = "get", operation_id = "get_user")]
async fn api_get_user(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
id: Path<Uuid>,
) -> poem::Result<GetUserResponse> {
let db = db.lock().await;
let Some(user) = User::Entity::find_by_id(id.0)
.one(&*db)
.await
.map_err(poem::error::InternalServerError)? else {
return Ok(GetUserResponse::NotFound);
};
Ok(GetUserResponse::Ok(Json(
user.try_into().map_err(poem::error::InternalServerError)?,
)))
}
#[oai(path = "/users/:id", method = "put", operation_id = "update_user")]
async fn api_update_user(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
body: Json<UserDataRequest>,
id: Path<Uuid>,
) -> poem::Result<UpdateUserResponse> {
let db = db.lock().await;
let Some(user) = User::Entity::find_by_id(id.0)
.one(&*db)
.await
.map_err(poem::error::InternalServerError)? else {
return Ok(UpdateUserResponse::NotFound);
};
let mut model: User::ActiveModel = user.into();
model.username = Set(body.username.clone());
model.credentials =
Set(serde_json::to_value(body.credentials.clone()).map_err(WarpgateError::from)?);
model.credential_policy =
Set(serde_json::to_value(body.credential_policy.clone())
.map_err(WarpgateError::from)?);
let user = model
.update(&*db)
.await
.map_err(poem::error::InternalServerError)?;
Ok(UpdateUserResponse::Ok(Json(
user.try_into().map_err(WarpgateError::from)?,
)))
}
#[oai(path = "/users/:id", method = "delete", operation_id = "delete_user")]
async fn api_delete_user(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
id: Path<Uuid>,
) -> poem::Result<DeleteUserResponse> {
let db = db.lock().await;
let Some(user) = User::Entity::find_by_id(id.0)
.one(&*db)
.await
.map_err(poem::error::InternalServerError)? else {
return Ok(DeleteUserResponse::NotFound);
};
user.delete(&*db)
.await
.map_err(poem::error::InternalServerError)?;
Ok(DeleteUserResponse::Deleted)
}
}
#[derive(ApiResponse)]
enum GetUserRolesResponse {
#[oai(status = 200)]
Ok(Json<Vec<RoleConfig>>),
#[oai(status = 404)]
NotFound,
}
#[derive(ApiResponse)]
enum AddUserRoleResponse {
#[oai(status = 201)]
Created,
#[oai(status = 409)]
AlreadyExists,
}
#[derive(ApiResponse)]
enum DeleteUserRoleResponse {
#[oai(status = 204)]
Deleted,
#[oai(status = 404)]
NotFound,
}
pub struct RolesApi;
#[OpenApi]
impl RolesApi {
#[oai(
path = "/users/:id/roles",
method = "get",
operation_id = "get_user_roles"
)]
async fn api_get_user_roles(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
id: Path<Uuid>,
) -> poem::Result<GetUserRolesResponse> {
let db = db.lock().await;
let Some((_, roles)) = User::Entity::find_by_id(*id)
.find_with_related(Role::Entity)
.all(&*db)
.await
.map(|x| x.into_iter().next())
.map_err(WarpgateError::from)? else {
return Ok(GetUserRolesResponse::NotFound)
};
Ok(GetUserRolesResponse::Ok(Json(
roles.into_iter().map(|x| x.into()).collect(),
)))
}
#[oai(
path = "/users/:id/roles/:role_id",
method = "post",
operation_id = "add_user_role"
)]
async fn api_add_user_role(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
id: Path<Uuid>,
role_id: Path<Uuid>,
) -> poem::Result<AddUserRoleResponse> {
let db = db.lock().await;
if !UserRoleAssignment::Entity::find()
.filter(UserRoleAssignment::Column::UserId.eq(id.0))
.filter(UserRoleAssignment::Column::RoleId.eq(role_id.0))
.all(&*db)
.await
.map_err(WarpgateError::from)?
.is_empty()
{
return Ok(AddUserRoleResponse::AlreadyExists);
}
let values = UserRoleAssignment::ActiveModel {
user_id: Set(id.0),
role_id: Set(role_id.0),
..Default::default()
};
values.insert(&*db).await.map_err(WarpgateError::from)?;
Ok(AddUserRoleResponse::Created)
}
#[oai(
path = "/users/:id/roles/:role_id",
method = "delete",
operation_id = "delete_user_role"
)]
async fn api_delete_user_role(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
id: Path<Uuid>,
role_id: Path<Uuid>,
) -> poem::Result<DeleteUserRoleResponse> {
let db = db.lock().await;
let Some(_user) = User::Entity::find_by_id(id.0)
.one(&*db)
.await
.map_err(poem::error::InternalServerError)? else {
return Ok(DeleteUserRoleResponse::NotFound);
};
let Some(_role) = Role::Entity::find_by_id(role_id.0)
.one(&*db)
.await
.map_err(poem::error::InternalServerError)? else {
return Ok(DeleteUserRoleResponse::NotFound);
};
let Some(model) = UserRoleAssignment::Entity::find()
.filter(UserRoleAssignment::Column::UserId.eq(id.0))
.filter(UserRoleAssignment::Column::RoleId.eq(role_id.0))
.one(&*db)
.await
.map_err(WarpgateError::from)? else {
return Ok(DeleteUserRoleResponse::NotFound);
};
model.delete(&*db).await.map_err(WarpgateError::from)?;
Ok(DeleteUserRoleResponse::Deleted)
}
}

View file

@ -1,28 +0,0 @@
use std::sync::Arc;
use poem::web::Data;
use poem_openapi::payload::Json;
use poem_openapi::{ApiResponse, OpenApi};
use tokio::sync::Mutex;
use warpgate_common::{ConfigProvider, UserSnapshot};
pub struct Api;
#[derive(ApiResponse)]
enum GetUsersResponse {
#[oai(status = 200)]
Ok(Json<Vec<UserSnapshot>>),
}
#[OpenApi]
impl Api {
#[oai(path = "/users", method = "get", operation_id = "get_users")]
async fn api_get_all_users(
&self,
config_provider: Data<&Arc<Mutex<dyn ConfigProvider + Send>>>,
) -> poem::Result<GetUsersResponse> {
let mut users = config_provider.lock().await.list_users().await?;
users.sort_by(|a, b| a.username.cmp(&b.username));
Ok(GetUsersResponse::Ok(Json(users)))
}
}

View file

@ -2,7 +2,7 @@
mod api;
use poem::{EndpointExt, IntoEndpoint, Route};
use poem_openapi::OpenApiService;
use warpgate_common::Services;
use warpgate_core::Services;
pub fn admin_api_app(services: &Services) -> impl IntoEndpoint {
let api_service = OpenApiService::new(

View file

@ -11,6 +11,7 @@ async-trait = "0.1"
bytes = "1.2"
chrono = { version = "0.4", features = ["serde"] }
data-encoding = "2.3"
delegate = "0.6"
humantime-serde = "1.1"
lazy_static = "1.4"
futures = "0.3"
@ -39,11 +40,8 @@ tokio = { version = "1.20", features = ["tracing"] }
totp-rs = { version = "3.0", features = ["otpauth"] }
tracing = "0.1"
tracing-core = "0.1"
tracing-subscriber = "0.3"
url = "2.2"
uuid = { version = "1.0", features = ["v4", "serde"] }
warpgate-db-entities = { version = "*", path = "../warpgate-db-entities" }
warpgate-db-migrations = { version = "*", path = "../warpgate-db-migrations" }
warpgate-sso = { version = "*", path = "../warpgate-sso" }
rustls = { version = "0.20", features = ["dangerous_configuration"] }
rustls-pemfile = "1.0"

View file

@ -1,16 +1,17 @@
use bytes::Bytes;
use poem_openapi::Enum;
use serde::{Deserialize, Serialize};
use crate::Secret;
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash, Enum)]
pub enum CredentialKind {
#[serde(rename = "password")]
Password,
#[serde(rename = "publickey")]
PublicKey,
#[serde(rename = "otp")]
Otp,
Totp,
#[serde(rename = "sso")]
Sso,
#[serde(rename = "web")]
@ -37,7 +38,7 @@ impl AuthCredential {
match self {
Self::Password { .. } => CredentialKind::Password,
Self::PublicKey { .. } => CredentialKind::PublicKey,
Self::Otp { .. } => CredentialKind::Otp,
Self::Otp { .. } => CredentialKind::Totp,
Self::Sso { .. } => CredentialKind::Sso,
Self::WebUserApproval => CredentialKind::WebUserApproval,
}

View file

@ -2,9 +2,7 @@ mod cred;
mod policy;
mod selector;
mod state;
mod store;
pub use cred::*;
pub use policy::*;
pub use selector::*;
pub use state::*;
pub use store::*;

View file

@ -1,7 +1,15 @@
use std::collections::HashSet;
use uuid::Uuid;
use super::{AuthCredential, CredentialPolicy, CredentialPolicyResponse};
use crate::AuthResult;
use super::{AuthCredential, CredentialKind, CredentialPolicy, CredentialPolicyResponse};
#[derive(Debug, Clone)]
pub enum AuthResult {
Accepted { username: String },
Need(HashSet<CredentialKind>),
Rejected,
}
pub struct AuthState {
id: Uuid,
@ -13,7 +21,7 @@ pub struct AuthState {
}
impl AuthState {
pub(crate) fn new(
pub fn new(
id: Uuid,
username: String,
protocol: String,

View file

@ -0,0 +1,71 @@
use std::net::ToSocketAddrs;
use std::time::Duration;
use crate::{ListenEndpoint, Secret};
pub(crate) const fn _default_true() -> bool {
true
}
pub(crate) const fn _default_false() -> bool {
false
}
pub(crate) const fn _default_ssh_port() -> u16 {
22
}
pub(crate) const fn _default_mysql_port() -> u16 {
3306
}
#[inline]
pub(crate) fn _default_username() -> String {
"root".to_owned()
}
#[inline]
pub(crate) fn _default_empty_string() -> String {
"".to_owned()
}
#[inline]
pub(crate) fn _default_recordings_path() -> String {
"./data/recordings".to_owned()
}
#[inline]
pub(crate) fn _default_database_url() -> Secret<String> {
Secret::new("sqlite:data/db".to_owned())
}
#[inline]
pub(crate) fn _default_http_listen() -> ListenEndpoint {
#[allow(clippy::unwrap_used)]
ListenEndpoint("0.0.0.0:8888".to_socket_addrs().unwrap().next().unwrap())
}
#[inline]
pub(crate) fn _default_mysql_listen() -> ListenEndpoint {
#[allow(clippy::unwrap_used)]
ListenEndpoint("0.0.0.0:33306".to_socket_addrs().unwrap().next().unwrap())
}
#[inline]
pub(crate) fn _default_retention() -> Duration {
Duration::SECOND * 60 * 60 * 24 * 7
}
#[inline]
pub(crate) fn _default_empty_vec<T>() -> Vec<T> {
vec![]
}
pub(crate) fn _default_ssh_listen() -> ListenEndpoint {
#[allow(clippy::unwrap_used)]
ListenEndpoint("0.0.0.0:2222".to_socket_addrs().unwrap().next().unwrap())
}
pub(crate) fn _default_ssh_keys_path() -> String {
"./data/keys".to_owned()
}

View file

@ -1,221 +1,66 @@
use std::collections::HashMap;
use std::net::ToSocketAddrs;
mod defaults;
mod target;
use std::path::PathBuf;
use std::time::Duration;
use poem_openapi::{Enum, Object, Union};
use defaults::*;
use poem_openapi::{Object, Union};
use serde::{Deserialize, Serialize};
pub use target::*;
use url::Url;
use uuid::Uuid;
use warpgate_sso::SsoProviderConfig;
use crate::auth::CredentialKind;
use crate::helpers::otp::OtpSecretKey;
use crate::{ListenEndpoint, Secret, WarpgateError};
const fn _default_true() -> bool {
true
}
const fn _default_false() -> bool {
false
}
const fn _default_ssh_port() -> u16 {
22
}
const fn _default_mysql_port() -> u16 {
3306
}
#[inline]
fn _default_username() -> String {
"root".to_owned()
}
#[inline]
fn _default_empty_string() -> String {
"".to_owned()
}
#[inline]
fn _default_recordings_path() -> String {
"./data/recordings".to_owned()
}
#[inline]
fn _default_database_url() -> Secret<String> {
Secret::new("sqlite:data/db".to_owned())
}
#[inline]
fn _default_http_listen() -> ListenEndpoint {
#[allow(clippy::unwrap_used)]
ListenEndpoint("0.0.0.0:8888".to_socket_addrs().unwrap().next().unwrap())
}
#[inline]
fn _default_mysql_listen() -> ListenEndpoint {
#[allow(clippy::unwrap_used)]
ListenEndpoint("0.0.0.0:33306".to_socket_addrs().unwrap().next().unwrap())
}
#[inline]
fn _default_retention() -> Duration {
Duration::SECOND * 60 * 60 * 24 * 7
}
#[inline]
fn _default_empty_vec<T>() -> Vec<T> {
vec![]
}
#[derive(Debug, Deserialize, Serialize, Clone, Object)]
pub struct TargetSSHOptions {
pub host: String,
#[serde(default = "_default_ssh_port")]
pub port: u16,
#[serde(default = "_default_username")]
pub username: String,
#[serde(default)]
#[oai(skip)]
pub auth: SSHTargetAuth,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum SSHTargetAuth {
#[serde(rename = "password")]
Password { password: Secret<String> },
#[serde(rename = "publickey")]
PublicKey,
}
impl Default for SSHTargetAuth {
fn default() -> Self {
SSHTargetAuth::PublicKey
}
}
#[derive(Debug, Deserialize, Serialize, Clone, Object)]
pub struct TargetHTTPOptions {
#[serde(default = "_default_empty_string")]
pub url: String,
#[serde(default)]
pub tls: Tls,
#[serde(default)]
pub headers: Option<HashMap<String, String>>,
#[serde(default)]
pub external_host: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone, Enum, PartialEq, Eq, Default)]
pub enum TlsMode {
#[serde(rename = "disabled")]
Disabled,
#[serde(rename = "preferred")]
#[default]
Preferred,
#[serde(rename = "required")]
Required,
}
#[derive(Debug, Deserialize, Serialize, Clone, Object)]
pub struct Tls {
#[serde(default)]
pub mode: TlsMode,
#[serde(default = "_default_true")]
pub verify: bool,
}
#[allow(clippy::derivable_impls)]
impl Default for Tls {
fn default() -> Self {
Self {
mode: TlsMode::default(),
verify: false,
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone, Object)]
pub struct TargetMySqlOptions {
#[serde(default = "_default_empty_string")]
pub host: String,
#[serde(default = "_default_mysql_port")]
pub port: u16,
#[serde(default = "_default_username")]
pub username: String,
#[serde(default)]
pub password: Option<String>,
#[serde(default)]
pub tls: Tls,
}
#[derive(Debug, Deserialize, Serialize, Clone, Object, Default)]
pub struct TargetWebAdminOptions {}
#[derive(Debug, Deserialize, Serialize, Clone, Object)]
pub struct Target {
pub name: String,
#[serde(default = "_default_empty_vec")]
pub allow_roles: Vec<String>,
#[serde(flatten)]
pub options: TargetOptions,
}
#[derive(Debug, Deserialize, Serialize, Clone, Union)]
#[oai(discriminator_name = "kind", one_of)]
pub enum TargetOptions {
#[serde(rename = "ssh")]
Ssh(TargetSSHOptions),
#[serde(rename = "http")]
Http(TargetHTTPOptions),
#[serde(rename = "mysql")]
MySql(TargetMySqlOptions),
#[serde(rename = "web_admin")]
WebAdmin(TargetWebAdminOptions),
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Union)]
#[serde(tag = "type")]
#[oai(discriminator_name = "kind", one_of)]
pub enum UserAuthCredential {
#[serde(rename = "password")]
Password { hash: Secret<String> },
Password(UserPasswordCredential),
#[serde(rename = "publickey")]
PublicKey { key: Secret<String> },
PublicKey(UserPublicKeyCredential),
#[serde(rename = "otp")]
Totp {
#[serde(with = "crate::helpers::serde_base64_secret")]
key: OtpSecretKey,
},
Totp(UserTotpCredential),
#[serde(rename = "sso")]
Sso {
provider: Option<String>,
email: String,
},
Sso(UserSsoCredential),
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Object)]
pub struct UserPasswordCredential {
pub hash: Secret<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Object)]
pub struct UserPublicKeyCredential {
pub key: Secret<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Object)]
pub struct UserTotpCredential {
#[serde(with = "crate::helpers::serde_base64_secret")]
pub key: OtpSecretKey,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Object)]
pub struct UserSsoCredential {
pub provider: Option<String>,
pub email: String,
}
impl UserAuthCredential {
pub fn kind(&self) -> CredentialKind {
match self {
Self::Password { .. } => CredentialKind::Password,
Self::PublicKey { .. } => CredentialKind::PublicKey,
Self::Totp { .. } => CredentialKind::Otp,
Self::Sso { .. } => CredentialKind::Sso,
Self::Password(_) => CredentialKind::Password,
Self::PublicKey(_) => CredentialKind::PublicKey,
Self::Totp(_) => CredentialKind::Totp,
Self::Sso(_) => CredentialKind::Sso,
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[derive(Debug, Deserialize, Serialize, Clone, Object)]
pub struct UserRequireCredentialsPolicy {
#[serde(skip_serializing_if = "Option::is_none")]
pub http: Option<Vec<CredentialKind>>,
@ -225,29 +70,24 @@ pub struct UserRequireCredentialsPolicy {
pub mysql: Option<Vec<CredentialKind>>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[derive(Debug, Deserialize, Serialize, Clone, Object)]
pub struct User {
#[serde(default)]
pub id: Uuid,
pub username: String,
pub credentials: Vec<UserAuthCredential>,
#[serde(skip_serializing_if = "Option::is_none")]
pub require: Option<UserRequireCredentialsPolicy>,
#[serde(skip_serializing_if = "Option::is_none", rename = "require")]
pub credential_policy: Option<UserRequireCredentialsPolicy>,
pub roles: Vec<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash, Object)]
pub struct Role {
#[serde(default)]
pub id: Uuid,
pub name: String,
}
fn _default_ssh_listen() -> ListenEndpoint {
#[allow(clippy::unwrap_used)]
ListenEndpoint("0.0.0.0:2222".to_socket_addrs().unwrap().next().unwrap())
}
fn _default_ssh_keys_path() -> String {
"./data/keys".to_owned()
}
#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, Copy)]
pub enum SshHostKeyVerificationMode {
#[serde(rename = "prompt")]
@ -330,7 +170,7 @@ impl Default for MySQLConfig {
fn default() -> Self {
MySQLConfig {
enable: false,
listen: _default_http_listen(),
listen: _default_mysql_listen(),
certificate: "".to_owned(),
key: "".to_owned(),
}
@ -373,10 +213,27 @@ impl Default for LogConfig {
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Default)]
pub enum ConfigProviderKind {
#[serde(rename = "file")]
File,
#[serde(rename = "database")]
#[default]
Database,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct WarpgateConfigStore {
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub targets: Vec<Target>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub users: Vec<User>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub roles: Vec<Role>,
#[serde(default)]
@ -402,6 +259,9 @@ pub struct WarpgateConfigStore {
#[serde(default)]
pub log: LogConfig,
#[serde(default)]
pub config_provider: ConfigProviderKind,
}
impl Default for WarpgateConfigStore {
@ -411,13 +271,14 @@ impl Default for WarpgateConfigStore {
users: vec![],
roles: vec![],
sso_providers: vec![],
recordings: RecordingsConfig::default(),
recordings: <_>::default(),
external_host: None,
database_url: _default_database_url(),
ssh: SSHConfig::default(),
http: HTTPConfig::default(),
mysql: MySQLConfig::default(),
log: LogConfig::default(),
ssh: <_>::default(),
http: <_>::default(),
mysql: <_>::default(),
log: <_>::default(),
config_provider: <_>::default(),
}
}
}

View file

@ -0,0 +1,133 @@
use std::collections::HashMap;
use poem_openapi::{Enum, Object, Union};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::defaults::*;
use crate::Secret;
#[derive(Debug, Deserialize, Serialize, Clone, Object)]
pub struct TargetSSHOptions {
pub host: String,
#[serde(default = "_default_ssh_port")]
pub port: u16,
#[serde(default = "_default_username")]
pub username: String,
#[serde(default)]
pub auth: SSHTargetAuth,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Union)]
#[serde(untagged)]
#[oai(discriminator_name = "kind", one_of)]
pub enum SSHTargetAuth {
#[serde(rename = "password")]
Password(SshTargetPasswordAuth),
#[serde(rename = "publickey")]
PublicKey(SshTargetPublicKeyAuth),
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Object)]
pub struct SshTargetPasswordAuth {
pub password: Secret<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Object, Default)]
pub struct SshTargetPublicKeyAuth {}
impl Default for SSHTargetAuth {
fn default() -> Self {
SSHTargetAuth::PublicKey(SshTargetPublicKeyAuth::default())
}
}
#[derive(Debug, Deserialize, Serialize, Clone, Object)]
pub struct TargetHTTPOptions {
#[serde(default = "_default_empty_string")]
pub url: String,
#[serde(default)]
pub tls: Tls,
#[serde(default)]
pub headers: Option<HashMap<String, String>>,
#[serde(default)]
pub external_host: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone, Enum, PartialEq, Eq, Default)]
pub enum TlsMode {
#[serde(rename = "disabled")]
Disabled,
#[serde(rename = "preferred")]
#[default]
Preferred,
#[serde(rename = "required")]
Required,
}
#[derive(Debug, Deserialize, Serialize, Clone, Object)]
pub struct Tls {
#[serde(default)]
pub mode: TlsMode,
#[serde(default = "_default_true")]
pub verify: bool,
}
#[allow(clippy::derivable_impls)]
impl Default for Tls {
fn default() -> Self {
Self {
mode: TlsMode::default(),
verify: false,
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone, Object)]
pub struct TargetMySqlOptions {
#[serde(default = "_default_empty_string")]
pub host: String,
#[serde(default = "_default_mysql_port")]
pub port: u16,
#[serde(default = "_default_username")]
pub username: String,
#[serde(default)]
pub password: Option<String>,
#[serde(default)]
pub tls: Tls,
}
#[derive(Debug, Deserialize, Serialize, Clone, Object, Default)]
pub struct TargetWebAdminOptions {}
#[derive(Debug, Deserialize, Serialize, Clone, Object)]
pub struct Target {
#[serde(default)]
pub id: Uuid,
pub name: String,
#[serde(default = "_default_empty_vec")]
pub allow_roles: Vec<String>,
#[serde(flatten)]
pub options: TargetOptions,
}
#[derive(Debug, Deserialize, Serialize, Clone, Union)]
#[oai(discriminator_name = "kind", one_of)]
pub enum TargetOptions {
#[serde(rename = "ssh")]
Ssh(TargetSSHOptions),
#[serde(rename = "http")]
Http(TargetHTTPOptions),
#[serde(rename = "mysql")]
MySql(TargetMySqlOptions),
#[serde(rename = "web_admin")]
WebAdmin(TargetWebAdminOptions),
}

View file

@ -1,102 +0,0 @@
use std::time::Duration;
use anyhow::Result;
use sea_orm::sea_query::Expr;
use sea_orm::{
ConnectOptions, Database, DatabaseConnection, EntityTrait, QueryFilter, TransactionTrait,
};
use warpgate_db_entities::LogEntry;
use warpgate_db_migrations::migrate_database;
use crate::helpers::fs::secure_file;
use crate::WarpgateConfig;
pub async fn connect_to_db(config: &WarpgateConfig) -> Result<DatabaseConnection> {
let mut url = url::Url::parse(&config.store.database_url.expose_secret()[..])?;
if url.scheme() == "sqlite" {
let path = url.path();
let mut abs_path = config.paths_relative_to.clone();
abs_path.push(path);
abs_path.push("db.sqlite3");
if let Some(parent) = abs_path.parent() {
std::fs::create_dir_all(parent)?
}
url.set_path(
abs_path
.to_str()
.ok_or_else(|| anyhow::anyhow!("Failed to convert database path to string"))?,
);
url.set_query(Some("mode=rwc"));
let db = Database::connect(ConnectOptions::new(url.to_string())).await?;
db.begin().await?.commit().await?;
drop(db);
secure_file(&abs_path)?;
}
let mut opt = ConnectOptions::new(url.to_string());
opt.max_connections(100)
.min_connections(5)
.connect_timeout(Duration::from_secs(8))
.idle_timeout(Duration::from_secs(8))
.max_lifetime(Duration::from_secs(8))
.sqlx_logging(true);
let connection = Database::connect(opt).await?;
migrate_database(&connection).await?;
Ok(connection)
}
pub async fn sanitize_db(db: &mut DatabaseConnection) -> Result<()> {
use sea_orm::ActiveValue::Set;
use warpgate_db_entities::{Recording, Session};
Recording::Entity::update_many()
.set(Recording::ActiveModel {
ended: Set(Some(chrono::Utc::now())),
..Default::default()
})
.filter(Expr::col(Recording::Column::Ended).is_null())
.exec(db)
.await?;
Session::Entity::update_many()
.set(Session::ActiveModel {
ended: Set(Some(chrono::Utc::now())),
..Default::default()
})
.filter(Expr::col(Session::Column::Ended).is_null())
.exec(db)
.await?;
Ok(())
}
pub async fn cleanup_db(db: &mut DatabaseConnection, retention: &Duration) -> Result<()> {
use warpgate_db_entities::{Recording, Session};
let cutoff = chrono::Utc::now() - chrono::Duration::from_std(*retention)?;
LogEntry::Entity::delete_many()
.filter(Expr::col(LogEntry::Column::Timestamp).lt(cutoff))
.exec(db)
.await?;
Recording::Entity::delete_many()
.filter(Expr::col(Session::Column::Ended).is_not_null())
.filter(Expr::col(Session::Column::Ended).lt(cutoff))
.exec(db)
.await?;
Session::Entity::delete_many()
.filter(Expr::col(Session::Column::Ended).is_not_null())
.filter(Expr::col(Session::Column::Ended).lt(cutoff))
.exec(db)
.await?;
Ok(())
}

View file

@ -1,45 +0,0 @@
use std::io::Write;
use crate::UUID;
impl<B: Backend> FromSql<Binary, B> for UUID
where
Vec<u8>: FromSql<Binary, B>,
{
fn from_sql(bytes: Option<&B::RawValue>) -> diesel::deserialize::Result<Self> {
let value = <Vec<u8>>::from_sql(bytes)?;
Ok(UUID::from_bytes(&value)?)
}
}
impl<B: Backend> ToSql<Binary, B> for UUID
where
[u8]: ToSql<Binary, B>,
{
fn to_sql<W: Write>(
&self,
out: &mut diesel::serialize::Output<W, B>,
) -> diesel::serialize::Result {
let bytes = self.0.as_bytes();
<[u8] as ToSql<Binary, B>>::to_sql(bytes, out)
}
}
impl AsExpression<Binary> for UUID {
type Expression = Bound<Binary, UUID>;
fn as_expression(self) -> Self::Expression {
Bound::new(self)
}
}
impl<'a> AsExpression<Binary> for &'a UUID {
type Expression = Bound<Binary, &'a UUID>;
fn as_expression(self) -> Self::Expression {
Bound::new(self)
}
}
// impl Expression for UUID {
// type SqlType = diesel::sql_types::Binary;
// }

View file

@ -17,10 +17,14 @@ pub enum WarpgateError {
UserNotFound,
#[error("failed to parse URL: {0}")]
UrlParse(#[from] url::ParseError),
#[error("deserialization failed: {0}")]
DeserializeJson(#[from] serde_json::Error),
#[error("external_url config option is not set")]
ExternalHostNotSet,
#[error("URL contains no host")]
NoHostInUrl,
#[error("Inconsistent state error")]
InconsistentState,
}
impl ResponseError for WarpgateError {

View file

@ -1,17 +1,16 @@
use std::time::SystemTime;
use bytes::Bytes;
use rand::Rng;
use totp_rs::{Algorithm, TOTP};
use super::rng::get_crypto_rng;
use crate::types::Secret;
pub type OtpExposedSecretKey = Bytes;
pub type OtpExposedSecretKey = Vec<u8>;
pub type OtpSecretKey = Secret<OtpExposedSecretKey>;
pub fn generate_key() -> OtpSecretKey {
Secret::new(Bytes::from_iter(get_crypto_rng().gen::<[u8; 32]>()))
Secret::new(get_crypto_rng().gen::<[u8; 32]>().into())
}
pub fn generate_setup_url(key: &OtpSecretKey, label: &str) -> Secret<String> {

View file

@ -1,16 +1,18 @@
use bytes::Bytes;
use serde::Serializer;
use super::serde_base64;
use crate::Secret;
pub fn serialize<S: Serializer>(secret: &Secret<Bytes>, serializer: S) -> Result<S::Ok, S::Error> {
serde_base64::serialize(secret.expose_secret().as_ref(), serializer)
pub fn serialize<S: Serializer>(
secret: &Secret<Vec<u8>>,
serializer: S,
) -> Result<S::Ok, S::Error> {
serde_base64::serialize(secret.expose_secret(), serializer)
}
pub fn deserialize<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Secret<Bytes>, D::Error> {
) -> Result<Secret<Vec<u8>>, D::Error> {
let inner = serde_base64::deserialize(deserializer)?;
Ok(Secret::new(inner))
}

View file

@ -1,29 +1,16 @@
#![feature(let_else, drain_filter, duration_constants)]
pub mod auth;
mod config;
mod config_providers;
pub mod consts;
mod data;
pub mod db;
mod error;
pub mod eventhub;
pub mod helpers;
pub mod logging;
mod protocols;
pub mod recordings;
mod services;
mod state;
mod tls;
mod try_macro;
mod types;
pub use config::*;
pub use config_providers::*;
pub use data::*;
pub use error::WarpgateError;
pub use protocols::*;
pub use services::*;
pub use state::{SessionState, SessionStateInit, State};
pub use tls::*;
pub use try_macro::*;
pub use types::*;

View file

@ -1,7 +1,11 @@
use std::borrow::Cow;
use std::fmt::Debug;
use bytes::Bytes;
use data_encoding::HEXLOWER;
use delegate::delegate;
use poem_openapi::registry::{MetaSchemaRef, Registry};
use poem_openapi::types::{ParseError, ParseFromJSON, ToJSON};
use rand::Rng;
use serde::{Deserialize, Serialize};
@ -62,3 +66,44 @@ impl<T> Debug for Secret<T> {
write!(f, "<secret>")
}
}
impl<T: poem_openapi::types::Type> poem_openapi::types::Type for Secret<T> {
const IS_REQUIRED: bool = T::IS_REQUIRED;
type RawValueType = T::RawValueType;
type RawElementValueType = T::RawElementValueType;
fn name() -> Cow<'static, str> {
T::name()
}
fn schema_ref() -> MetaSchemaRef {
T::schema_ref()
}
fn register(registry: &mut Registry) {
T::register(registry)
}
delegate! {
to self.0 {
fn as_raw_value(&self) -> Option<&Self::RawValueType>;
fn raw_element_iter<'a>(
&'a self,
) -> Box<dyn Iterator<Item = &'a Self::RawElementValueType> + 'a>;
fn is_empty(&self) -> bool;
fn is_none(&self) -> bool;
}
}
}
impl<T: ParseFromJSON> ParseFromJSON for Secret<T> {
fn parse_from_json(value: Option<serde_json::Value>) -> poem_openapi::types::ParseResult<Self> {
T::parse_from_json(value)
.map(Self::new)
.map_err(|e| ParseError::custom(e.into_message()))
}
}
impl<T: ToJSON> ToJSON for Secret<T> {
fn to_json(&self) -> Option<serde_json::Value> {
self.0.to_json()
}
}

52
warpgate-core/Cargo.toml Normal file
View file

@ -0,0 +1,52 @@
[package]
edition = "2021"
license = "Apache-2.0"
name = "warpgate-core"
version = "0.5.0"
[dependencies]
warpgate-common = { version = "*", path = "../warpgate-common" }
warpgate-db-entities = { version = "*", path = "../warpgate-db-entities" }
warpgate-db-migrations = { version = "*", path = "../warpgate-db-migrations" }
anyhow = "1.0"
argon2 = "0.4"
async-trait = "0.1"
bytes = "1.2"
chrono = { version = "0.4", features = ["serde"] }
data-encoding = "2.3"
humantime-serde = "1.1"
lazy_static = "1.4"
futures = "0.3"
once_cell = "1.10"
packet = "0.1"
password-hash = "0.4"
poem = { version = "^1.3.37", features = ["rustls"] }
poem-openapi = { version = "^2.0.6", features = [
"swagger-ui",
"chrono",
"uuid",
"static-files",
] }
rand = "0.8"
rand_chacha = "0.3"
rand_core = { version = "0.6", features = ["std"] }
sea-orm = { version = "^0.9", features = [
"sqlx-sqlite",
"runtime-tokio-native-tls",
"macros",
], default-features = false }
serde = "1.0"
serde_json = "1.0"
thiserror = "1.0"
tokio = { version = "1.20", features = ["tracing"] }
totp-rs = { version = "3.0", features = ["otpauth"] }
tracing = "0.1"
tracing-core = "0.1"
tracing-subscriber = "0.3"
url = "2.2"
uuid = { version = "1.0", features = ["v4", "serde"] }
warpgate-sso = { version = "*", path = "../warpgate-sso" }
rustls = { version = "0.20", features = ["dangerous_configuration"] }
rustls-pemfile = "1.0"
webpki = "0.22"

View file

@ -5,9 +5,10 @@ use std::time::{Duration, Instant};
use once_cell::sync::Lazy;
use tokio::sync::{broadcast, Mutex};
use uuid::Uuid;
use warpgate_common::auth::{AuthResult, AuthState};
use warpgate_common::WarpgateError;
use super::AuthState;
use crate::{AuthResult, ConfigProvider, WarpgateError};
use crate::ConfigProvider;
#[allow(clippy::unwrap_used)]
pub static TIMEOUT: Lazy<Duration> = Lazy::new(|| Duration::from_secs(60 * 10));

View file

@ -0,0 +1,288 @@
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use async_trait::async_trait;
use data_encoding::BASE64;
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter, QueryOrder};
use tokio::sync::Mutex;
use tracing::*;
use warpgate_common::auth::{
AllCredentialsPolicy, AnySingleCredentialPolicy, AuthCredential, CredentialKind,
CredentialPolicy, PerProtocolCredentialPolicy,
};
use warpgate_common::helpers::hash::verify_password_hash;
use warpgate_common::helpers::otp::verify_totp;
use warpgate_common::{
Role as RoleConfig, Target as TargetConfig, User as UserConfig, UserAuthCredential,
UserPasswordCredential, UserPublicKeyCredential, UserSsoCredential, UserTotpCredential,
WarpgateError,
};
use warpgate_db_entities::{Role, Target, User};
use super::ConfigProvider;
pub struct DatabaseConfigProvider {
db: Arc<Mutex<DatabaseConnection>>,
}
impl DatabaseConfigProvider {
pub async fn new(db: &Arc<Mutex<DatabaseConnection>>) -> Self {
Self { db: db.clone() }
}
}
#[async_trait]
impl ConfigProvider for DatabaseConfigProvider {
async fn list_users(&mut self) -> Result<Vec<UserConfig>, WarpgateError> {
let db = self.db.lock().await;
let users = User::Entity::find()
.order_by_asc(User::Column::Username)
.all(&*db)
.await?;
let users: Result<Vec<UserConfig>, _> = users.into_iter().map(|t| t.try_into()).collect();
Ok(users?)
}
async fn list_targets(&mut self) -> Result<Vec<TargetConfig>, WarpgateError> {
let db = self.db.lock().await;
let targets = Target::Entity::find()
.order_by_asc(Target::Column::Name)
.all(&*db)
.await?;
let targets: Result<Vec<TargetConfig>, _> =
targets.into_iter().map(|t| t.try_into()).collect();
Ok(targets?)
}
async fn get_credential_policy(
&mut self,
username: &str,
) -> Result<Option<Box<dyn CredentialPolicy + Sync + Send>>, WarpgateError> {
let db = self.db.lock().await;
let user_model = User::Entity::find()
.filter(User::Column::Username.eq(username))
.one(&*db)
.await?;
let Some(user_model) = user_model else {
error!("Selected user not found: {}", username);
return Ok(None);
};
let user: UserConfig = user_model.try_into()?;
let supported_credential_types: HashSet<CredentialKind> =
user.credentials.iter().map(|x| x.kind()).collect();
let default_policy = Box::new(AnySingleCredentialPolicy {
supported_credential_types: supported_credential_types.clone(),
}) as Box<dyn CredentialPolicy + Sync + Send>;
if let Some(req) = user.credential_policy {
let mut policy = PerProtocolCredentialPolicy {
default: default_policy,
protocols: HashMap::new(),
};
if let Some(p) = req.http {
policy.protocols.insert(
"HTTP",
Box::new(AllCredentialsPolicy {
supported_credential_types: supported_credential_types.clone(),
required_credential_types: p.into_iter().collect(),
}),
);
}
if let Some(p) = req.mysql {
policy.protocols.insert(
"MySQL",
Box::new(AllCredentialsPolicy {
supported_credential_types: supported_credential_types.clone(),
required_credential_types: p.into_iter().collect(),
}),
);
}
if let Some(p) = req.ssh {
policy.protocols.insert(
"SSH",
Box::new(AllCredentialsPolicy {
supported_credential_types,
required_credential_types: p.into_iter().collect(),
}),
);
}
Ok(Some(
Box::new(policy) as Box<dyn CredentialPolicy + Sync + Send>
))
} else {
Ok(Some(default_policy))
}
}
async fn username_for_sso_credential(
&mut self,
client_credential: &AuthCredential,
) -> Result<Option<String>, WarpgateError> {
let AuthCredential::Sso { provider: client_provider, email : client_email} = client_credential else {
return Ok(None);
};
Ok(self
.list_users()
.await?
.iter()
.find(|x| {
for cred in x.credentials.iter() {
if let UserAuthCredential::Sso(UserSsoCredential { provider, email }) = cred {
if provider.as_ref().unwrap_or(client_provider) == client_provider
&& email == client_email
{
return true;
}
}
}
false
})
.map(|x| x.username.clone()))
}
async fn validate_credential(
&mut self,
username: &str,
client_credential: &AuthCredential,
) -> Result<bool, WarpgateError> {
let db = self.db.lock().await;
let user_model = User::Entity::find()
.filter(User::Column::Username.eq(username))
.one(&*db)
.await?;
let Some(user_model) = user_model else {
error!("Selected user not found: {}", username);
return Ok(false);
};
let user: UserConfig = user_model.try_into()?;
match client_credential {
AuthCredential::PublicKey {
kind,
public_key_bytes,
} => {
let base64_bytes = BASE64.encode(public_key_bytes);
let client_key = format!("{} {}", kind, base64_bytes);
debug!(username = &user.username[..], "Client key: {}", client_key);
return Ok(user.credentials.iter().any(|credential| match credential {
UserAuthCredential::PublicKey(UserPublicKeyCredential {
key: ref user_key,
}) => &client_key == user_key.expose_secret(),
_ => false,
}));
}
AuthCredential::Password(client_password) => {
return Ok(user.credentials.iter().any(|credential| match credential {
UserAuthCredential::Password(UserPasswordCredential {
hash: ref user_password_hash,
}) => verify_password_hash(
client_password.expose_secret(),
user_password_hash.expose_secret(),
)
.unwrap_or_else(|e| {
error!(
username = &user.username[..],
"Error verifying password hash: {}", e
);
false
}),
_ => false,
}))
}
AuthCredential::Otp(client_otp) => {
return Ok(user.credentials.iter().any(|credential| match credential {
UserAuthCredential::Totp(UserTotpCredential {
key: ref user_otp_key,
}) => verify_totp(client_otp.expose_secret(), user_otp_key),
_ => false,
}))
}
AuthCredential::Sso {
provider: client_provider,
email: client_email,
} => {
for credential in user.credentials.iter() {
if let UserAuthCredential::Sso(UserSsoCredential {
ref provider,
ref email,
}) = credential
{
if provider.as_ref().unwrap_or(client_provider) == client_provider {
return Ok(email == client_email);
}
}
}
return Ok(false);
}
_ => return Err(WarpgateError::InvalidCredentialType),
}
}
async fn authorize_target(
&mut self,
username: &str,
target_name: &str,
) -> Result<bool, WarpgateError> {
let db = self.db.lock().await;
let target_model: Option<Target::Model> = Target::Entity::find()
.filter(Target::Column::Name.eq(target_name))
.one(&*db)
.await?;
let user_model = User::Entity::find()
.filter(User::Column::Username.eq(username))
.one(&*db)
.await?;
let Some(user_model) = user_model else {
error!("Selected user not found: {}", username);
return Ok(false);
};
let Some(target_model) = target_model else {
warn!("Selected target not found: {}", target_name);
return Ok(false);
};
let target_roles: HashSet<String> = target_model
.find_related(Role::Entity)
.all(&*db)
.await?
.into_iter()
.map(Into::<RoleConfig>::into)
.map(|x| x.name)
.collect();
let user_roles: HashSet<String> = user_model
.find_related(Role::Entity)
.all(&*db)
.await?
.into_iter()
.map(Into::<RoleConfig>::into)
.map(|x| x.name)
.collect();
let intersect = user_roles.intersection(&target_roles).count() > 0;
Ok(intersect)
}
}

View file

@ -3,34 +3,28 @@ use std::sync::Arc;
use async_trait::async_trait;
use data_encoding::BASE64;
use sea_orm::ActiveValue::Set;
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait};
use tokio::sync::Mutex;
use tracing::*;
use uuid::Uuid;
use warpgate_db_entities::Ticket;
use super::ConfigProvider;
use crate::auth::{
use warpgate_common::auth::{
AllCredentialsPolicy, AnySingleCredentialPolicy, AuthCredential, CredentialKind,
CredentialPolicy, PerProtocolCredentialPolicy,
};
use crate::helpers::hash::verify_password_hash;
use crate::helpers::otp::verify_totp;
use crate::{Target, User, UserAuthCredential, UserSnapshot, WarpgateConfig, WarpgateError};
use warpgate_common::helpers::hash::verify_password_hash;
use warpgate_common::helpers::otp::verify_totp;
use warpgate_common::{
Target, User, UserAuthCredential, UserPasswordCredential, UserPublicKeyCredential,
UserSsoCredential, UserTotpCredential, WarpgateConfig, WarpgateError,
};
use super::ConfigProvider;
pub struct FileConfigProvider {
db: Arc<Mutex<DatabaseConnection>>,
config: Arc<Mutex<WarpgateConfig>>,
}
impl FileConfigProvider {
pub async fn new(
db: &Arc<Mutex<DatabaseConnection>>,
config: &Arc<Mutex<WarpgateConfig>>,
) -> Self {
pub async fn new(config: &Arc<Mutex<WarpgateConfig>>) -> Self {
Self {
db: db.clone(),
config: config.clone(),
}
}
@ -38,7 +32,7 @@ impl FileConfigProvider {
#[async_trait]
impl ConfigProvider for FileConfigProvider {
async fn list_users(&mut self) -> Result<Vec<UserSnapshot>, WarpgateError> {
async fn list_users(&mut self) -> Result<Vec<User>, WarpgateError> {
Ok(self
.config
.lock()
@ -46,7 +40,7 @@ impl ConfigProvider for FileConfigProvider {
.store
.users
.iter()
.map(UserSnapshot::new)
.map(Clone::clone)
.collect::<Vec<_>>())
}
@ -87,7 +81,7 @@ impl ConfigProvider for FileConfigProvider {
supported_credential_types: supported_credential_types.clone(),
}) as Box<dyn CredentialPolicy + Sync + Send>;
if let Some(req) = user.require {
if let Some(req) = user.credential_policy {
let mut policy = PerProtocolCredentialPolicy {
default: default_policy,
protocols: HashMap::new(),
@ -146,7 +140,7 @@ impl ConfigProvider for FileConfigProvider {
.iter()
.find(|x| {
for cred in x.credentials.iter() {
if let UserAuthCredential::Sso { provider, email } = cred {
if let UserAuthCredential::Sso(UserSsoCredential { provider, email }) = cred {
if provider.as_ref().unwrap_or(client_provider) == client_provider
&& email == client_email
{
@ -190,17 +184,17 @@ impl ConfigProvider for FileConfigProvider {
debug!(username = &user.username[..], "Client key: {}", client_key);
return Ok(user.credentials.iter().any(|credential| match credential {
UserAuthCredential::PublicKey { key: ref user_key } => {
&client_key == user_key.expose_secret()
}
UserAuthCredential::PublicKey(UserPublicKeyCredential {
key: ref user_key,
}) => &client_key == user_key.expose_secret(),
_ => false,
}));
}
AuthCredential::Password(client_password) => {
return Ok(user.credentials.iter().any(|credential| match credential {
UserAuthCredential::Password {
UserAuthCredential::Password(UserPasswordCredential {
hash: ref user_password_hash,
} => verify_password_hash(
}) => verify_password_hash(
client_password.expose_secret(),
user_password_hash.expose_secret(),
)
@ -216,9 +210,9 @@ impl ConfigProvider for FileConfigProvider {
}
AuthCredential::Otp(client_otp) => {
return Ok(user.credentials.iter().any(|credential| match credential {
UserAuthCredential::Totp {
UserAuthCredential::Totp(UserTotpCredential {
key: ref user_otp_key,
} => verify_totp(client_otp.expose_secret(), user_otp_key),
}) => verify_totp(client_otp.expose_secret(), user_otp_key),
_ => false,
}))
}
@ -227,10 +221,10 @@ impl ConfigProvider for FileConfigProvider {
email: client_email,
} => {
for credential in user.credentials.iter() {
if let UserAuthCredential::Sso {
if let UserAuthCredential::Sso(UserSsoCredential {
ref provider,
ref email,
} = credential
}) = credential
{
if provider.as_ref().unwrap_or(client_provider) == client_provider {
return Ok(email == client_email);
@ -271,33 +265,17 @@ impl ConfigProvider for FileConfigProvider {
.roles
.iter()
.map(|x| config.store.roles.iter().find(|y| &y.name == x))
.filter_map(|x| x.to_owned())
.filter_map(|x| x.to_owned().map(|x| x.name.clone()))
.collect::<HashSet<_>>();
let target_roles = target
.allow_roles
.iter()
.map(|x| config.store.roles.iter().find(|y| &y.name == x))
.filter_map(|x| x.to_owned())
.filter_map(|x| x.to_owned().map(|x| x.name.clone()))
.collect::<HashSet<_>>();
let intersect = user_roles.intersection(&target_roles).count() > 0;
Ok(intersect)
}
async fn consume_ticket(&mut self, ticket_id: &Uuid) -> Result<(), WarpgateError> {
let db = self.db.lock().await;
let ticket = Ticket::Entity::find_by_id(*ticket_id).one(&*db).await?;
let Some(ticket) = ticket else {
return Err(WarpgateError::InvalidTicket(*ticket_id));
};
if let Some(uses_left) = ticket.uses_left {
let mut model: Ticket::ActiveModel = ticket.into();
model.uses_left = Set(Some(uses_left - 1));
model.update(&*db).await?;
}
Ok(())
}
}

View file

@ -1,28 +1,22 @@
mod db;
mod file;
use std::collections::HashSet;
use std::sync::Arc;
use async_trait::async_trait;
pub use db::DatabaseConfigProvider;
pub use file::FileConfigProvider;
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
use sea_orm::ActiveValue::Set;
use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
use tokio::sync::Mutex;
use tracing::*;
use uuid::Uuid;
use warpgate_common::auth::{AuthCredential, CredentialPolicy};
use warpgate_common::{Secret, Target, User, WarpgateError};
use warpgate_db_entities::Ticket;
use crate::auth::{AuthCredential, CredentialKind, CredentialPolicy};
use crate::{Secret, Target, UserSnapshot, WarpgateError};
#[derive(Debug, Clone)]
pub enum AuthResult {
Accepted { username: String },
Need(HashSet<CredentialKind>),
Rejected,
}
#[async_trait]
pub trait ConfigProvider {
async fn list_users(&mut self) -> Result<Vec<UserSnapshot>, WarpgateError>;
async fn list_users(&mut self) -> Result<Vec<User>, WarpgateError>;
async fn list_targets(&mut self) -> Result<Vec<Target>, WarpgateError>;
@ -47,8 +41,6 @@ pub trait ConfigProvider {
username: &str,
target: &str,
) -> Result<bool, WarpgateError>;
async fn consume_ticket(&mut self, ticket_id: &Uuid) -> Result<(), WarpgateError>;
}
//TODO: move this somewhere
@ -85,3 +77,22 @@ pub async fn authorize_ticket(
}
}
}
pub async fn consume_ticket(
db: &Arc<Mutex<DatabaseConnection>>,
ticket_id: &Uuid,
) -> Result<(), WarpgateError> {
let db = db.lock().await;
let ticket = Ticket::Entity::find_by_id(*ticket_id).one(&*db).await?;
let Some(ticket) = ticket else {
return Err(WarpgateError::InvalidTicket(*ticket_id));
};
if let Some(uses_left) = ticket.uses_left {
let mut model: Ticket::ActiveModel = ticket.into();
model.uses_left = Set(Some(uses_left - 1));
model.update(&*db).await?;
}
Ok(())
}

View file

@ -0,0 +1,3 @@
pub static BUILTIN_ADMIN_TARGET_NAME: &str = "warpgate:admin";
pub static BUILTIN_ADMIN_ROLE_NAME: &str = "warpgate:admin";
pub static BUILTIN_ADMIN_USERNAME: &str = "admin";

View file

@ -2,10 +2,9 @@ use chrono::{DateTime, Utc};
use poem_openapi::Object;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use warpgate_common::{SessionId, Target};
use warpgate_db_entities::Session;
use crate::{SessionId, Target, User};
#[derive(Serialize, Deserialize, Object)]
pub struct SessionSnapshot {
pub id: SessionId,
@ -32,16 +31,3 @@ impl From<Session::Model> for SessionSnapshot {
}
}
}
#[derive(Serialize, Deserialize, Object)]
pub struct UserSnapshot {
pub username: String,
}
impl UserSnapshot {
pub fn new(user: &User) -> Self {
Self {
username: user.username.clone(),
}
}
}

306
warpgate-core/src/db/mod.rs Normal file
View file

@ -0,0 +1,306 @@
use std::collections::HashMap;
use std::time::Duration;
use anyhow::Result;
use sea_orm::sea_query::Expr;
use sea_orm::{
ActiveModelTrait, ColumnTrait, ConnectOptions, Database, DatabaseConnection, EntityTrait,
QueryFilter, TransactionTrait,
};
use tracing::*;
use uuid::Uuid;
use warpgate_common::helpers::fs::secure_file;
use warpgate_common::{
ConfigProviderKind, TargetOptions, TargetWebAdminOptions, WarpgateConfig, WarpgateError,
};
use warpgate_db_entities::Target::TargetKind;
use warpgate_db_entities::{
LogEntry, Role, Target, TargetRoleAssignment, User, UserRoleAssignment,
};
use warpgate_db_migrations::migrate_database;
use crate::consts::{BUILTIN_ADMIN_ROLE_NAME, BUILTIN_ADMIN_TARGET_NAME};
pub async fn connect_to_db(config: &WarpgateConfig) -> Result<DatabaseConnection> {
let mut url = url::Url::parse(&config.store.database_url.expose_secret()[..])?;
if url.scheme() == "sqlite" {
let path = url.path();
let mut abs_path = config.paths_relative_to.clone();
abs_path.push(path);
abs_path.push("db.sqlite3");
if let Some(parent) = abs_path.parent() {
std::fs::create_dir_all(parent)?
}
url.set_path(
abs_path
.to_str()
.ok_or_else(|| anyhow::anyhow!("Failed to convert database path to string"))?,
);
url.set_query(Some("mode=rwc"));
let db = Database::connect(ConnectOptions::new(url.to_string())).await?;
db.begin().await?.commit().await?;
drop(db);
secure_file(&abs_path)?;
}
let mut opt = ConnectOptions::new(url.to_string());
opt.max_connections(100)
.min_connections(5)
.connect_timeout(Duration::from_secs(8))
.idle_timeout(Duration::from_secs(8))
.max_lifetime(Duration::from_secs(8))
.sqlx_logging(true);
let connection = Database::connect(opt).await?;
migrate_database(&connection).await?;
Ok(connection)
}
pub async fn populate_db(
db: &mut DatabaseConnection,
config: &mut WarpgateConfig,
) -> Result<(), WarpgateError> {
use sea_orm::ActiveValue::Set;
use warpgate_db_entities::{Recording, Session};
Recording::Entity::update_many()
.set(Recording::ActiveModel {
ended: Set(Some(chrono::Utc::now())),
..Default::default()
})
.filter(Expr::col(Recording::Column::Ended).is_null())
.exec(db)
.await
.map_err(WarpgateError::from)?;
Session::Entity::update_many()
.set(Session::ActiveModel {
ended: Set(Some(chrono::Utc::now())),
..Default::default()
})
.filter(Expr::col(Session::Column::Ended).is_null())
.exec(db)
.await
.map_err(WarpgateError::from)?;
let db_was_empty = Role::Entity::find().all(&*db).await?.is_empty();
let admin_role = match Role::Entity::find()
.filter(Role::Column::Name.eq(BUILTIN_ADMIN_ROLE_NAME))
.all(db)
.await?
.first()
{
Some(x) => x.to_owned(),
None => {
let values = Role::ActiveModel {
id: Set(Uuid::new_v4()),
name: Set(BUILTIN_ADMIN_ROLE_NAME.to_owned()),
};
values.insert(&*db).await.map_err(WarpgateError::from)?
}
};
let admin_target = match Target::Entity::find()
.filter(Target::Column::Kind.eq(TargetKind::WebAdmin))
.all(db)
.await?
.first()
{
Some(x) => x.to_owned(),
None => {
let values = Target::ActiveModel {
id: Set(Uuid::new_v4()),
name: Set(BUILTIN_ADMIN_TARGET_NAME.to_owned()),
kind: Set(TargetKind::WebAdmin),
options: Set(serde_json::to_value(TargetOptions::WebAdmin(
TargetWebAdminOptions {},
))
.map_err(WarpgateError::from)?),
};
values.insert(&*db).await.map_err(WarpgateError::from)?
}
};
if TargetRoleAssignment::Entity::find()
.filter(TargetRoleAssignment::Column::TargetId.eq(admin_target.id))
.filter(TargetRoleAssignment::Column::RoleId.eq(admin_role.id))
.all(db)
.await?
.is_empty()
{
let values = TargetRoleAssignment::ActiveModel {
target_id: Set(admin_target.id),
role_id: Set(admin_role.id),
..Default::default()
};
values.insert(&*db).await.map_err(WarpgateError::from)?;
}
if db_was_empty && config.store.config_provider == ConfigProviderKind::Database {
migrate_config_into_db(db, config).await?;
} else if !config.store.targets.is_empty() {
warn!("Warpgate is now using the database for its configuration, but you still have leftover configuration in the config file.");
warn!("Configuration changes in the config file will be ignored.");
warn!("Remove `targets` and `roles` keys from the config to disable this warning.");
}
Ok(())
}
async fn migrate_config_into_db(
db: &mut DatabaseConnection,
config: &mut WarpgateConfig,
) -> Result<(), WarpgateError> {
use sea_orm::ActiveValue::Set;
info!("Migrating config file into the database");
let mut role_lookup = HashMap::new();
for role_config in config.store.roles.iter() {
let role = match Role::Entity::find()
.filter(Role::Column::Name.eq(role_config.name.clone()))
.all(db)
.await?
.first()
{
Some(x) => x.to_owned(),
None => {
let values = Role::ActiveModel {
id: Set(Uuid::new_v4()),
name: Set(role_config.name.clone()),
};
info!("Migrating role {}", role_config.name);
values.insert(&*db).await.map_err(WarpgateError::from)?
}
};
role_lookup.insert(role_config.name.clone(), role.id);
}
config.store.roles = vec![];
for target_config in config.store.targets.iter() {
if TargetKind::WebAdmin == (&target_config.options).into() {
continue;
}
let target = match Target::Entity::find()
.filter(Target::Column::Kind.ne(TargetKind::WebAdmin))
.filter(Target::Column::Name.eq(target_config.name.clone()))
.all(db)
.await?
.first()
{
Some(x) => x.to_owned(),
None => {
let values = Target::ActiveModel {
id: Set(Uuid::new_v4()),
name: Set(target_config.name.clone()),
kind: Set((&target_config.options).into()),
options: Set(serde_json::to_value(target_config.options.clone())
.map_err(WarpgateError::from)?),
};
info!("Migrating target {}", target_config.name);
values.insert(&*db).await.map_err(WarpgateError::from)?
}
};
for role_name in target_config.allow_roles.iter() {
if let Some(role_id) = role_lookup.get(role_name) {
if TargetRoleAssignment::Entity::find()
.filter(TargetRoleAssignment::Column::TargetId.eq(target.id))
.filter(TargetRoleAssignment::Column::RoleId.eq(*role_id))
.all(db)
.await?
.is_empty()
{
let values = TargetRoleAssignment::ActiveModel {
target_id: Set(target.id),
role_id: Set(*role_id),
..Default::default()
};
values.insert(&*db).await.map_err(WarpgateError::from)?;
}
}
}
}
config.store.targets = vec![];
for user_config in config.store.users.iter() {
let user = match User::Entity::find()
.filter(User::Column::Username.eq(user_config.username.clone()))
.all(db)
.await?
.first()
{
Some(x) => x.to_owned(),
None => {
let values = User::ActiveModel {
id: Set(Uuid::new_v4()),
username: Set(user_config.username.clone()),
credentials: Set(serde_json::to_value(user_config.credentials.clone())
.map_err(WarpgateError::from)?),
credential_policy: Set(serde_json::to_value(
user_config.credential_policy.clone(),
)
.map_err(WarpgateError::from)?),
};
info!("Migrating user {}", user_config.username);
values.insert(&*db).await.map_err(WarpgateError::from)?
}
};
for role_name in user_config.roles.iter() {
if let Some(role_id) = role_lookup.get(role_name) {
if UserRoleAssignment::Entity::find()
.filter(UserRoleAssignment::Column::UserId.eq(user.id))
.filter(UserRoleAssignment::Column::RoleId.eq(*role_id))
.all(db)
.await?
.is_empty()
{
let values = UserRoleAssignment::ActiveModel {
user_id: Set(user.id),
role_id: Set(*role_id),
..Default::default()
};
values.insert(&*db).await.map_err(WarpgateError::from)?;
}
}
}
}
config.store.users = vec![];
Ok(())
}
pub async fn cleanup_db(db: &mut DatabaseConnection, retention: &Duration) -> Result<()> {
use warpgate_db_entities::{Recording, Session};
let cutoff = chrono::Utc::now() - chrono::Duration::from_std(*retention)?;
LogEntry::Entity::delete_many()
.filter(Expr::col(LogEntry::Column::Timestamp).lt(cutoff))
.exec(db)
.await?;
Recording::Entity::delete_many()
.filter(Expr::col(Session::Column::Ended).is_not_null())
.filter(Expr::col(Session::Column::Ended).lt(cutoff))
.exec(db)
.await?;
Session::Entity::delete_many()
.filter(Expr::col(Session::Column::Ended).is_not_null())
.filter(Expr::col(Session::Column::Ended).lt(cutoff))
.exec(db)
.await?;
Ok(())
}

17
warpgate-core/src/lib.rs Normal file
View file

@ -0,0 +1,17 @@
#![feature(let_else, drain_filter, duration_constants)]
pub mod consts;
mod data;
mod state;
pub use data::*;
pub use state::{SessionState, SessionStateInit, State};
mod config_providers;
pub use config_providers::*;
pub mod db;
mod protocols;
pub use protocols::*;
pub mod recordings;
mod services;
pub use services::*;
mod auth_state_store;
pub use auth_state_store::*;
pub mod logging;

View file

@ -1,7 +1,7 @@
mod database;
mod layer;
mod socket;
mod values;
pub use database::{install_database_logger, make_database_logger_layer};
pub use socket::make_socket_logger_layer;
mod database;
pub use database::{install_database_logger, make_database_logger_layer};

View file

@ -4,9 +4,9 @@ use tokio::net::UnixDatagram;
use tracing::*;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::Layer;
use warpgate_common::WarpgateConfig;
use super::layer::ValuesLogLayer;
use crate::WarpgateConfig;
static SKIP_KEY: &str = "is_socket_logging_error";

View file

@ -2,9 +2,10 @@ use std::sync::Arc;
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
use tokio::sync::Mutex;
use warpgate_common::{SessionId, Target, WarpgateError};
use warpgate_db_entities::Session;
use crate::{SessionId, SessionState, State, Target, WarpgateError};
use crate::{SessionState, State};
pub trait SessionHandle {
fn close(&mut self);

View file

@ -4,8 +4,7 @@ use std::net::SocketAddr;
use anyhow::Result;
use async_trait::async_trait;
pub use handle::{SessionHandle, WarpgateServerHandle};
use crate::Target;
use warpgate_common::Target;
#[derive(Debug, thiserror::Error)]
pub enum TargetTestError {

View file

@ -7,9 +7,9 @@ use sea_orm::{ActiveModelTrait, DatabaseConnection};
use tokio::sync::{broadcast, Mutex};
use tracing::*;
use uuid::Uuid;
use warpgate_common::helpers::fs::secure_directory;
use warpgate_common::{RecordingsConfig, SessionId, WarpgateConfig};
use warpgate_db_entities::Recording::{self, RecordingKind};
use crate::{RecordingsConfig, SessionId, WarpgateConfig};
mod terminal;
mod traffic;
mod writer;
@ -58,7 +58,7 @@ impl SessionRecordings {
path.push(&config.store.recordings.path);
if config.store.recordings.enable {
std::fs::create_dir_all(&path)?;
crate::helpers::fs::secure_directory(&path)?;
secure_directory(&path)?;
}
Ok(Self {
db,

View file

@ -1,4 +1,4 @@
use bytes::{Bytes, BytesMut};
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use tokio::time::Instant;
use warpgate_db_entities::Recording::RecordingKind;
@ -39,7 +39,7 @@ pub enum TerminalRecordingItem {
time: f32,
#[serde(default)]
stream: TerminalRecordingStreamId,
#[serde(with = "crate::helpers::serde_base64")]
#[serde(with = "warpgate_common::helpers::serde_base64")]
data: Bytes,
},
PtyResize {
@ -93,7 +93,7 @@ impl TerminalRecorder {
self.write_item(&TerminalRecordingItem::Data {
time: self.get_time(),
stream,
data: BytesMut::from(data).freeze(),
data: Bytes::from(data.to_vec()),
})
.await
}

View file

@ -3,18 +3,18 @@ use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use bytes::{Bytes, BytesMut};
use bytes::Bytes;
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait};
use tokio::fs::File;
use tokio::io::{AsyncWriteExt, BufWriter};
use tokio::sync::{broadcast, mpsc, Mutex};
use tracing::*;
use uuid::Uuid;
use warpgate_common::helpers::fs::secure_file;
use warpgate_common::try_block;
use warpgate_db_entities::Recording;
use super::{Error, Result};
use crate::helpers::fs::secure_file;
use crate::try_block;
#[derive(Clone)]
pub struct RecordingWriter {
@ -102,7 +102,7 @@ impl RecordingWriter {
}
pub async fn write(&mut self, data: &[u8]) -> Result<()> {
let data = BytesMut::from(data).freeze();
let data = Bytes::from(data.to_vec());
self.sender
.send(data.clone())
.await

View file

@ -4,11 +4,13 @@ use std::time::Duration;
use anyhow::Result;
use sea_orm::DatabaseConnection;
use tokio::sync::Mutex;
use warpgate_common::{ConfigProviderKind, WarpgateConfig};
use crate::auth::AuthStateStore;
use crate::db::{connect_to_db, sanitize_db};
use crate::db::{connect_to_db, populate_db};
use crate::recordings::SessionRecordings;
use crate::{ConfigProvider, FileConfigProvider, State, WarpgateConfig};
use crate::{AuthStateStore, ConfigProvider, DatabaseConfigProvider, FileConfigProvider, State};
type ConfigProviderArc = Arc<Mutex<dyn ConfigProvider + Send + 'static>>;
#[derive(Clone)]
pub struct Services {
@ -16,21 +18,30 @@ pub struct Services {
pub recordings: Arc<Mutex<SessionRecordings>>,
pub config: Arc<Mutex<WarpgateConfig>>,
pub state: Arc<Mutex<State>>,
pub config_provider: Arc<Mutex<dyn ConfigProvider + Send + 'static>>,
pub config_provider: ConfigProviderArc,
pub auth_state_store: Arc<Mutex<AuthStateStore>>,
}
impl Services {
pub async fn new(config: WarpgateConfig) -> Result<Self> {
pub async fn new(mut config: WarpgateConfig) -> Result<Self> {
let mut db = connect_to_db(&config).await?;
sanitize_db(&mut db).await?;
populate_db(&mut db, &mut config).await?;
let db = Arc::new(Mutex::new(db));
let recordings = SessionRecordings::new(db.clone(), &config)?;
let recordings = Arc::new(Mutex::new(recordings));
let provider = config.store.config_provider.clone();
let config = Arc::new(Mutex::new(config));
let config_provider = Arc::new(Mutex::new(FileConfigProvider::new(&db, &config).await));
let config_provider = match provider {
ConfigProviderKind::File => {
Arc::new(Mutex::new(FileConfigProvider::new(&config).await)) as ConfigProviderArc
}
ConfigProviderKind::Database => {
Arc::new(Mutex::new(DatabaseConfigProvider::new(&db).await)) as ConfigProviderArc
}
};
let auth_state_store = Arc::new(Mutex::new(AuthStateStore::new(config_provider.clone())));

View file

@ -7,9 +7,10 @@ use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait};
use tokio::sync::{broadcast, Mutex};
use tracing::*;
use uuid::Uuid;
use warpgate_common::{ProtocolName, SessionId, Target};
use warpgate_db_entities::Session;
use crate::{ProtocolName, SessionHandle, SessionId, Target, WarpgateServerHandle};
use crate::{SessionHandle, WarpgateServerHandle};
pub struct State {
pub sessions: HashMap<SessionId, Arc<Mutex<SessionState>>>,

View file

@ -11,3 +11,4 @@ sea-orm = {version = "^0.9", features = ["macros", "with-chrono", "with-uuid", "
serde = "1.0"
serde_json = "1.0"
uuid = {version = "1.0", features = ["v4", "serde"]}
warpgate-common = { version = "*", path = "../warpgate-common" }

View file

@ -0,0 +1,28 @@
use poem_openapi::Object;
use sea_orm::entity::prelude::*;
use serde::Serialize;
use uuid::Uuid;
use warpgate_common::Role;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)]
#[sea_orm(table_name = "roles")]
#[oai(rename = "Role")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub name: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
impl From<Model> for Role {
fn from(model: Model) -> Self {
Self {
id: model.id,
name: model.name,
}
}
}

View file

@ -0,0 +1,78 @@
use poem_openapi::{Enum, Object};
use sea_orm::entity::prelude::*;
use serde::Serialize;
use uuid::Uuid;
use warpgate_common::{Target, TargetOptions};
#[derive(Debug, PartialEq, Eq, Serialize, Clone, Enum, EnumIter, DeriveActiveEnum)]
#[sea_orm(rs_type = "String", db_type = "String(Some(16))")]
pub enum TargetKind {
#[sea_orm(string_value = "http")]
Http,
#[sea_orm(string_value = "mysql")]
MySql,
#[sea_orm(string_value = "ssh")]
Ssh,
#[sea_orm(string_value = "web_admin")]
WebAdmin,
}
impl From<&TargetOptions> for TargetKind {
fn from(options: &TargetOptions) -> Self {
match options {
TargetOptions::Http(_) => Self::Http,
TargetOptions::MySql(_) => Self::MySql,
TargetOptions::Ssh(_) => Self::Ssh,
TargetOptions::WebAdmin(_) => Self::WebAdmin,
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Clone, Enum, EnumIter, DeriveActiveEnum)]
#[sea_orm(rs_type = "String", db_type = "String(Some(16))")]
pub enum SshAuthKind {
#[sea_orm(string_value = "password")]
Password,
#[sea_orm(string_value = "publickey")]
PublicKey,
}
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)]
#[sea_orm(table_name = "targets")]
#[oai(rename = "Target")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub name: String,
pub kind: TargetKind,
pub options: serde_json::Value,
}
impl Related<super::Role::Entity> for Entity {
fn to() -> RelationDef {
super::TargetRoleAssignment::Relation::Role.def()
}
fn via() -> Option<RelationDef> {
Some(super::TargetRoleAssignment::Relation::Target.def().rev())
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
impl TryFrom<Model> for Target {
type Error = serde_json::Error;
fn try_from(model: Model) -> Result<Self, Self::Error> {
let options: TargetOptions = serde_json::from_value(model.options)?;
Ok(Self {
id: model.id,
name: model.name,
allow_roles: vec![],
options,
})
}
}

View file

@ -0,0 +1,37 @@
use poem_openapi::Object;
use sea_orm::entity::prelude::*;
use serde::Serialize;
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)]
#[sea_orm(table_name = "target_roles")]
#[oai(rename = "TargetRoleAssignment")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: u32,
pub target_id: Uuid,
pub role_id: Uuid,
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
Target,
Role,
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::Target => Entity::belongs_to(super::Target::Entity)
.from(Column::TargetId)
.to(super::Target::Column::Id)
.into(),
Self::Role => Entity::belongs_to(super::Role::Entity)
.from(Column::RoleId)
.to(super::Role::Column::Id)
.into(),
}
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -0,0 +1,48 @@
use poem_openapi::Object;
use sea_orm::entity::prelude::*;
use serde::Serialize;
use uuid::Uuid;
use warpgate_common::{User, UserAuthCredential, UserRequireCredentialsPolicy};
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)]
#[sea_orm(table_name = "users")]
#[oai(rename = "User")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub username: String,
pub credentials: serde_json::Value,
pub credential_policy: serde_json::Value,
}
impl Related<super::Role::Entity> for Entity {
fn to() -> RelationDef {
super::UserRoleAssignment::Relation::Role.def()
}
fn via() -> Option<RelationDef> {
Some(super::UserRoleAssignment::Relation::User.def().rev())
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
impl TryFrom<Model> for User {
type Error = serde_json::Error;
fn try_from(model: Model) -> Result<Self, Self::Error> {
let credentials: Vec<UserAuthCredential> = serde_json::from_value(model.credentials)?;
let credential_policy: Option<UserRequireCredentialsPolicy> =
serde_json::from_value(model.credential_policy)?;
Ok(Self {
id: model.id,
username: model.username,
roles: vec![],
credentials,
credential_policy,
})
}
}

View file

@ -0,0 +1,37 @@
use poem_openapi::Object;
use sea_orm::entity::prelude::*;
use serde::Serialize;
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)]
#[sea_orm(table_name = "user_roles")]
#[oai(rename = "UserRoleAssignment")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: u32,
pub user_id: Uuid,
pub role_id: Uuid,
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
User,
Role,
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::User => Entity::belongs_to(super::User::Entity)
.from(Column::UserId)
.to(super::User::Column::Id)
.into(),
Self::Role => Entity::belongs_to(super::Role::Entity)
.from(Column::RoleId)
.to(super::Role::Column::Id)
.into(),
}
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -3,5 +3,10 @@
pub mod KnownHost;
pub mod LogEntry;
pub mod Recording;
pub mod Role;
pub mod Session;
pub mod Target;
pub mod TargetRoleAssignment;
pub mod Ticket;
pub mod User;
pub mod UserRoleAssignment;

View file

@ -13,3 +13,4 @@ chrono = "0.4"
sea-orm = {version = "^0.9", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros", "with-chrono", "with-uuid", "with-json"], default-features = false}
sea-orm-migration = {version = "^0.9", default-features = false}
uuid = {version = "1.0", features = ["v4", "serde"]}
serde_json = "1.0"

View file

@ -8,6 +8,8 @@ mod m00003_create_recording;
mod m00004_create_known_host;
mod m00005_create_log_entry;
mod m00006_add_session_protocol;
mod m00007_targets_and_roles;
mod m00008_users;
pub struct Migrator;
@ -21,6 +23,8 @@ impl MigratorTrait for Migrator {
Box::new(m00004_create_known_host::Migration),
Box::new(m00005_create_log_entry::Migration),
Box::new(m00006_add_session_protocol::Migration),
Box::new(m00007_targets_and_roles::Migration),
Box::new(m00008_users::Migration),
]
}
}

View file

@ -0,0 +1,152 @@
use sea_orm::Schema;
use sea_orm_migration::prelude::*;
pub(crate) mod role {
use sea_orm::entity::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "roles")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub name: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
mod target {
use sea_orm::entity::prelude::*;
use uuid::Uuid;
#[derive(Debug, PartialEq, Eq, Clone, EnumIter, DeriveActiveEnum)]
#[sea_orm(rs_type = "String", db_type = "String(Some(16))")]
pub enum TargetKind {
#[sea_orm(string_value = "http")]
Http,
#[sea_orm(string_value = "mysql")]
MySql,
#[sea_orm(string_value = "ssh")]
Ssh,
#[sea_orm(string_value = "web_admin")]
WebAdmin,
}
#[derive(Debug, PartialEq, Eq, Clone, EnumIter, DeriveActiveEnum)]
#[sea_orm(rs_type = "String", db_type = "String(Some(16))")]
pub enum SshAuthKind {
#[sea_orm(string_value = "password")]
Password,
#[sea_orm(string_value = "publickey")]
PublicKey,
}
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "targets")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub name: String,
pub kind: TargetKind,
pub options: serde_json::Value,
}
impl Related<super::role::Entity> for Entity {
fn to() -> RelationDef {
super::target_role_assignment::Relation::Target.def()
}
fn via() -> Option<RelationDef> {
Some(super::target_role_assignment::Relation::Role.def().rev())
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
mod target_role_assignment {
use sea_orm::entity::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "target_roles")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: u32,
pub target_id: Uuid,
pub role_id: Uuid,
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
Target,
Role,
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::Target => Entity::belongs_to(super::target::Entity)
.from(Column::TargetId)
.to(super::target::Column::Id)
.into(),
Self::Role => Entity::belongs_to(super::role::Entity)
.from(Column::RoleId)
.to(super::role::Column::Id)
.into(),
}
}
}
impl ActiveModelBehavior for ActiveModel {}
}
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m00007_targets_and_roles"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let builder = manager.get_database_backend();
let schema = Schema::new(builder);
manager
.create_table(schema.create_table_from_entity(role::Entity))
.await?;
manager
.create_table(schema.create_table_from_entity(target::Entity))
.await?;
manager
.create_table(schema.create_table_from_entity(target_role_assignment::Entity))
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(
Table::drop()
.table(target_role_assignment::Entity)
.to_owned(),
)
.await?;
manager
.drop_table(Table::drop().table(target::Entity).to_owned())
.await?;
manager
.drop_table(Table::drop().table(role::Entity).to_owned())
.await?;
Ok(())
}
}

View file

@ -0,0 +1,102 @@
use sea_orm::Schema;
use sea_orm_migration::prelude::*;
mod user {
use sea_orm::entity::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "users")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
pub username: String,
pub credentials: serde_json::Value,
pub credential_policy: serde_json::Value,
}
impl Related<crate::m00007_targets_and_roles::role::Entity> for Entity {
fn to() -> RelationDef {
super::user_role_assignment::Relation::User.def()
}
fn via() -> Option<RelationDef> {
Some(super::user_role_assignment::Relation::Role.def().rev())
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
mod user_role_assignment {
use sea_orm::entity::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "user_roles")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: u32,
pub user_id: Uuid,
pub role_id: Uuid,
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
User,
Role,
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::User => Entity::belongs_to(super::user::Entity)
.from(Column::UserId)
.to(super::user::Column::Id)
.into(),
Self::Role => Entity::belongs_to(crate::m00007_targets_and_roles::role::Entity)
.from(Column::RoleId)
.to(crate::m00007_targets_and_roles::role::Column::Id)
.into(),
}
}
}
impl ActiveModelBehavior for ActiveModel {}
}
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m00008_users"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let builder = manager.get_database_backend();
let schema = Schema::new(builder);
manager
.create_table(schema.create_table_from_entity(user::Entity))
.await?;
manager
.create_table(schema.create_table_from_entity(user_role_assignment::Entity))
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(user_role_assignment::Entity).to_owned())
.await?;
manager
.drop_table(Table::drop().table(user::Entity).to_owned())
.await?;
Ok(())
}
}

View file

@ -24,6 +24,7 @@ tokio-tungstenite = {version = "0.17", features = ["rustls-tls-native-roots"]}
tracing = "0.1"
warpgate-admin = {version = "*", path = "../warpgate-admin"}
warpgate-common = {version = "*", path = "../warpgate-common"}
warpgate-core = {version = "*", path = "../warpgate-core"}
warpgate-db-entities = {version = "*", path = "../warpgate-db-entities"}
warpgate-web = {version = "*", path = "../warpgate-web"}
warpgate-sso = {version = "*", path = "../warpgate-sso"}

View file

@ -9,8 +9,9 @@ use poem_openapi::{ApiResponse, Enum, Object, OpenApi};
use tokio::sync::Mutex;
use tracing::*;
use uuid::Uuid;
use warpgate_common::auth::{AuthCredential, AuthState, CredentialKind};
use warpgate_common::{AuthResult, Secret, Services, WarpgateError};
use warpgate_common::auth::{AuthCredential, AuthResult, AuthState, CredentialKind};
use warpgate_common::{Secret, WarpgateError};
use warpgate_core::Services;
use crate::common::{
authorize_session, endpoint_auth, get_auth_state_for_request, SessionAuthorization, SessionExt,
@ -82,7 +83,7 @@ impl From<AuthResult> for ApiAuthState {
AuthResult::Rejected => ApiAuthState::Failed,
AuthResult::Need(kinds) => match kinds.iter().next() {
Some(CredentialKind::Password) => ApiAuthState::PasswordNeeded,
Some(CredentialKind::Otp) => ApiAuthState::OtpNeeded,
Some(CredentialKind::Totp) => ApiAuthState::OtpNeeded,
Some(CredentialKind::Sso) => ApiAuthState::SsoNeeded,
Some(CredentialKind::WebUserApproval) => ApiAuthState::WebUserApprovalNeeded,
Some(CredentialKind::PublicKey) => ApiAuthState::PublicKeyNeeded,
@ -228,7 +229,7 @@ impl Api {
auth: Option<Data<&SessionAuthorization>>,
id: Path<Uuid>,
) -> poem::Result<AuthStateResponse> {
let Some(state_arc) = get_auth_state(&*id, *services, auth.map(|x|x.0)).await else {
let Some(state_arc) = get_auth_state(&id, &services, auth.map(|x|x.0)).await else {
return Ok(AuthStateResponse::NotFound);
};
serialize_auth_state_inner(state_arc).await
@ -246,7 +247,7 @@ impl Api {
auth: Option<Data<&SessionAuthorization>>,
id: Path<Uuid>,
) -> poem::Result<AuthStateResponse> {
let Some(state_arc) = get_auth_state(&*id, *services, auth.map(|x|x.0)).await else {
let Some(state_arc) = get_auth_state(&id, &services, auth.map(|x|x.0)).await else {
return Ok(AuthStateResponse::NotFound);
};
@ -257,7 +258,7 @@ impl Api {
};
if let AuthResult::Accepted { .. } = auth_result {
services.auth_state_store.lock().await.complete(&*id).await;
services.auth_state_store.lock().await.complete(&id).await;
}
serialize_auth_state_inner(state_arc).await
}
@ -274,11 +275,11 @@ impl Api {
auth: Option<Data<&SessionAuthorization>>,
id: Path<Uuid>,
) -> poem::Result<AuthStateResponse> {
let Some(state_arc) = get_auth_state(&*id, *services, auth.map(|x|x.0)).await else {
let Some(state_arc) = get_auth_state(&id, &services, auth.map(|x|x.0)).await else {
return Ok(AuthStateResponse::NotFound);
};
state_arc.lock().await.reject();
services.auth_state_store.lock().await.complete(&*id).await;
services.auth_state_store.lock().await.complete(&id).await;
serialize_auth_state_inner(state_arc).await
}
}
@ -298,7 +299,7 @@ async fn get_auth_state(
return None;
};
let Some(state_arc) = store.get(&*id) else {
let Some(state_arc) = store.get(&id) else {
return None;
};

View file

@ -4,7 +4,7 @@ use poem::Request;
use poem_openapi::payload::Json;
use poem_openapi::{ApiResponse, Object, OpenApi};
use serde::Serialize;
use warpgate_common::Services;
use warpgate_core::Services;
use crate::common::{SessionAuthorization, SessionExt};

View file

@ -5,7 +5,7 @@ use poem_openapi::param::{Path, Query};
use poem_openapi::payload::Json;
use poem_openapi::{ApiResponse, Object, OpenApi};
use serde::{Deserialize, Serialize};
use warpgate_common::Services;
use warpgate_core::Services;
use warpgate_sso::{SsoClient, SsoLoginRequest};
pub struct Api;

View file

@ -5,8 +5,8 @@ use poem_openapi::param::Query;
use poem_openapi::payload::{Json, Response};
use poem_openapi::{ApiResponse, Enum, Object, OpenApi};
use tracing::*;
use warpgate_common::auth::AuthCredential;
use warpgate_common::{AuthResult, Services};
use warpgate_common::auth::{AuthCredential, AuthResult};
use warpgate_core::Services;
use warpgate_sso::SsoInternalProviderConfig;
use super::sso_provider_detail::{SsoContext, SSO_CONTEXT_SESSION_KEY};
@ -54,7 +54,7 @@ impl Api {
services: Data<&Services>,
) -> poem::Result<GetSsoProvidersResponse> {
let mut providers = services.config.lock().await.store.sso_providers.clone();
providers.sort_by(|a, b| a.label().cmp(&b.label()));
providers.sort_by(|a, b| a.label().cmp(b.label()));
Ok(GetSsoProvidersResponse::Ok(Json(
providers
.into_iter()
@ -130,12 +130,9 @@ impl Api {
state.add_valid_credential(cred);
}
match state.verify() {
AuthResult::Accepted { username } => {
auth_state_store.complete(state.id()).await;
authorize_session(req, username).await?;
}
_ => (),
if let AuthResult::Accepted { username } = state.verify() {
auth_state_store.complete(state.id()).await;
authorize_session(req, username).await?;
}
Ok(Response::new(ReturnToSsoResponse::Ok).header(

View file

@ -1,33 +1,27 @@
use futures::{stream, StreamExt};
use poem::web::Data;
use poem_openapi::payload::Json;
use poem_openapi::{ApiResponse, Enum, Object, OpenApi};
use poem_openapi::{ApiResponse, Object, OpenApi};
use serde::Serialize;
use warpgate_common::{Services, TargetOptions};
use warpgate_common::TargetOptions;
use warpgate_core::Services;
use warpgate_db_entities::Target;
use crate::common::{endpoint_auth, SessionAuthorization};
pub struct Api;
#[derive(Debug, Serialize, Clone, Enum)]
pub enum TargetKind {
Http,
MySql,
Ssh,
WebAdmin,
}
#[derive(Debug, Serialize, Clone, Object)]
pub struct Target {
pub struct TargetSnapshot {
pub name: String,
pub kind: TargetKind,
pub kind: Target::TargetKind,
pub external_host: Option<String>,
}
#[derive(ApiResponse)]
enum GetTargetsResponse {
#[oai(status = 200)]
Ok(Json<Vec<Target>>),
Ok(Json<Vec<TargetSnapshot>>),
}
#[OpenApi]
@ -57,13 +51,13 @@ impl Api {
SessionAuthorization::Ticket { target_name, .. } => target_name == name,
SessionAuthorization::User(_) => {
let mut config_provider = services.config_provider.lock().await;
match config_provider
.authorize_target(auth.username(), &name)
.await
{
Ok(true) => true,
_ => false,
}
matches!(
config_provider
.authorize_target(auth.username(), &name)
.await,
Ok(true)
)
}
}
}
@ -75,13 +69,13 @@ impl Api {
Ok(GetTargetsResponse::Ok(Json(
targets
.into_iter()
.map(|t| Target {
.map(|t| TargetSnapshot {
name: t.name.clone(),
kind: match t.options {
TargetOptions::Ssh(_) => TargetKind::Ssh,
TargetOptions::Http(_) => TargetKind::Http,
TargetOptions::MySql(_) => TargetKind::MySql,
TargetOptions::WebAdmin(_) => TargetKind::WebAdmin,
TargetOptions::Ssh(_) => Target::TargetKind::Ssh,
TargetOptions::Http(_) => Target::TargetKind::Http,
TargetOptions::MySql(_) => Target::TargetKind::MySql,
TargetOptions::WebAdmin(_) => Target::TargetKind::WebAdmin,
},
external_host: match t.options {
TargetOptions::Http(ref opt) => opt.external_host.clone(),

View file

@ -7,7 +7,8 @@ use poem::{handler, Body, IntoResponse, Request, Response};
use serde::Deserialize;
use tokio::sync::Mutex;
use tracing::*;
use warpgate_common::{Services, Target, TargetHTTPOptions, TargetOptions, WarpgateServerHandle};
use warpgate_common::{Target, TargetHTTPOptions, TargetOptions};
use warpgate_core::{Services, WarpgateServerHandle};
use crate::common::{SessionAuthorization, SessionExt};
use crate::proxy::{proxy_normal_request, proxy_websocket_request};
@ -68,18 +69,17 @@ async fn get_target_for_request(
let host_based_target_name = if let Some(host) = req.original_uri().host() {
services
.config
.config_provider
.lock()
.await
.store
.targets
.list_targets()
.await?
.iter()
.filter_map(|t| match t.options {
TargetOptions::Http(ref options) => Some((t, options)),
_ => None,
})
.filter(|(_, o)| o.external_host.as_deref() == Some(host))
.next()
.find(|(_, o)| o.external_host.as_deref() == Some(host))
.map(|(t, _)| t.name.clone())
} else {
None
@ -105,11 +105,11 @@ async fn get_target_for_request(
if let Some(target_name) = selected_target_name {
let target = {
services
.config
.config_provider
.lock()
.await
.store
.targets
.list_targets()
.await?
.iter()
.filter(|t| t.name == target_name)
.filter_map(|t| match t.options {
@ -126,7 +126,7 @@ async fn get_target_for_request(
.config_provider
.lock()
.await
.authorize_target(&auth.username(), &target.0.name)
.authorize_target(auth.username(), &target.0.name)
.await?
{
return Ok(None);
@ -136,5 +136,5 @@ async fn get_target_for_request(
}
}
return Ok(None);
Ok(None)
}

View file

@ -10,8 +10,9 @@ use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use tracing::*;
use uuid::Uuid;
use warpgate_common::auth::{AuthState, AuthStateStore};
use warpgate_common::{ProtocolName, Services, TargetOptions, WarpgateError};
use warpgate_common::auth::AuthState;
use warpgate_common::{ProtocolName, TargetOptions, WarpgateError};
use warpgate_core::{AuthStateStore, Services};
use crate::session::SessionStore;
@ -52,7 +53,7 @@ impl SessionExt for Session {
}
fn get_username(&self) -> Option<String> {
return self.get_auth().map(|x| x.username().to_owned());
self.get_auth().map(|x| x.username().to_owned())
}
fn get_auth(&self) -> Option<SessionAuthorization> {
@ -90,7 +91,7 @@ impl SessionAuthorization {
}
async fn is_user_admin(req: &Request, auth: &SessionAuthorization) -> poem::Result<bool> {
let services: Data<&Services> = <_>::from_request_without_body(&req).await?;
let services: Data<&Services> = <_>::from_request_without_body(req).await?;
let SessionAuthorization::User(username) = auth else {
return Ok(false)
@ -101,7 +102,7 @@ async fn is_user_admin(req: &Request, auth: &SessionAuthorization) -> poem::Resu
for target in targets {
if matches!(target.options, TargetOptions::WebAdmin(_))
&& config_provider
.authorize_target(&username, &target.name)
.authorize_target(username, &target.name)
.await?
{
drop(config_provider);
@ -168,7 +169,7 @@ pub fn gateway_redirect(req: &Request) -> Response {
.original_uri()
.path_and_query()
.map(|p| p.to_string())
.unwrap_or("".into());
.unwrap_or_else(|| "".into());
let path = format!(
"/@warpgate#/login?next={}",
@ -183,20 +184,17 @@ pub async fn get_auth_state_for_request(
session: &Session,
store: &mut AuthStateStore,
) -> Result<Arc<Mutex<AuthState>>, WarpgateError> {
match session.get_auth_state_id() {
Some(id) => {
if !store.contains_key(&id.0) {
session.remove(AUTH_STATE_ID_SESSION_KEY)
}
if let Some(id) = session.get_auth_state_id() {
if !store.contains_key(&id.0) {
session.remove(AUTH_STATE_ID_SESSION_KEY)
}
None => (),
};
}
match session.get_auth_state_id() {
Some(id) => Ok(store.get(&id.0).unwrap()),
Some(id) => Ok(store.get(&id.0).ok_or(WarpgateError::InconsistentState)?),
None => {
let (id, state) = store
.create(&username, crate::common::PROTOCOL_NAME)
.create(username, crate::common::PROTOCOL_NAME)
.await?;
session.set(AUTH_STATE_ID_SESSION_KEY, AuthStateId(id));
Ok(state)
@ -206,13 +204,13 @@ pub async fn get_auth_state_for_request(
pub async fn authorize_session(req: &Request, username: String) -> poem::Result<()> {
let session_middleware: Data<&Arc<Mutex<SessionStore>>> =
<_>::from_request_without_body(&req).await?;
let session: &Session = <_>::from_request_without_body(&req).await?;
<_>::from_request_without_body(req).await?;
let session: &Session = <_>::from_request_without_body(req).await?;
let server_handle = session_middleware
.lock()
.await
.create_handle_for(&req)
.create_handle_for(req)
.await?;
server_handle
.lock()

View file

@ -2,7 +2,7 @@ use http::StatusCode;
use poem::IntoResponse;
pub fn error_page(e: poem::Error) -> impl IntoResponse {
return poem::web::Html(format!(
poem::web::Html(format!(
r#"<!DOCTYPE html>
<style>
body {{
@ -24,5 +24,5 @@ pub fn error_page(e: poem::Error) -> impl IntoResponse {
<p>{e}</p>
</main>
"#
)).with_status(StatusCode::BAD_GATEWAY);
)).with_status(StatusCode::BAD_GATEWAY)
}

View file

@ -30,9 +30,9 @@ use tokio::sync::Mutex;
use tracing::*;
use warpgate_admin::admin_api_app;
use warpgate_common::{
ProtocolServer, Services, Target, TargetOptions, TargetTestError, TlsCertificateAndPrivateKey,
TlsCertificateBundle, TlsPrivateKey,
Target, TargetOptions, TlsCertificateAndPrivateKey, TlsCertificateBundle, TlsPrivateKey,
};
use warpgate_core::{ProtocolServer, Services, TargetTestError};
use warpgate_web::Assets;
use crate::common::{
@ -186,7 +186,7 @@ impl ProtocolServer for HTTPProtocolServer {
crate::proxy::proxy_normal_request(&request, poem::Body::empty(), &options)
.await
.map_err(|e| {
return TargetTestError::ConnectionError(format!("{e}"));
TargetTestError::ConnectionError(format!("{e}"))
})?;
Ok(())
}

View file

@ -3,7 +3,8 @@ use poem::session::Session;
use poem::web::{Data, FromRequest};
use poem::{Endpoint, Middleware, Request};
use serde::Deserialize;
use warpgate_common::{authorize_ticket, Secret, Services};
use warpgate_common::Secret;
use warpgate_core::{authorize_ticket, consume_ticket, Services};
use crate::common::SessionExt;
@ -64,9 +65,8 @@ impl<E: Endpoint> Endpoint for TicketMiddlewareEndpoint<E> {
if let Some(ticket_model) = {
let ticket = Secret::new(ticket);
let mut cp = services.config_provider.lock().await;
if let Some(res) = authorize_ticket(&services.db, &ticket).await? {
cp.consume_ticket(&res.id).await?;
consume_ticket(&services.db, &res.id).await?;
Some(res)
} else {
None

View file

@ -62,7 +62,9 @@ impl SomeRequestBuilder for http::request::Builder {
}
lazy_static::lazy_static! {
#[allow(clippy::mutable_key_type)]
static ref DONT_FORWARD_HEADERS: HashSet<HeaderName> = {
#[allow(clippy::mutable_key_type)]
let mut s = HashSet::new();
s.insert(http::header::ACCEPT_ENCODING);
s.insert(http::header::SEC_WEBSOCKET_EXTENSIONS);
@ -78,9 +80,9 @@ lazy_static::lazy_static! {
};
}
const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
const X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
const X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
static X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
static X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
static X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
fn construct_uri(req: &Request, options: &TargetHTTPOptions, websocket: bool) -> Result<Uri> {
let target_uri = Uri::try_from(options.url.clone())?;
@ -90,7 +92,7 @@ fn construct_uri(req: &Request, options: &TargetHTTPOptions, websocket: bool) ->
.authority()
.context("No authority in the URL")?
.to_string();
let authority = authority.split("@").last().context("Authority is empty")?;
let authority = authority.split('@').last().context("Authority is empty")?;
let authority: Authority = authority.try_into()?;
let mut uri = http::uri::Builder::new()
.authority(authority)
@ -101,7 +103,11 @@ fn construct_uri(req: &Request, options: &TargetHTTPOptions, websocket: bool) ->
.clone(),
);
let scheme = target_uri.scheme().context("No scheme in the URL")?;
let scheme = match options.tls.mode {
TlsMode::Disabled => &Scheme::HTTP,
TlsMode::Preferred => target_uri.scheme().context("No scheme in the URL")?,
TlsMode::Required => &Scheme::HTTPS,
};
uri = uri.scheme(scheme.clone());
#[allow(clippy::unwrap_used)]
@ -217,7 +223,7 @@ pub async fn proxy_normal_request(
body: Body,
options: &TargetHTTPOptions,
) -> poem::Result<Response> {
let uri = construct_uri(req, &options, false)?;
let uri = construct_uri(req, options, false)?;
tracing::debug!("URI: {:?}", uri);
@ -228,6 +234,23 @@ pub async fn proxy_normal_request(
if let TlsMode::Required = options.tls.mode {
client = client.https_only(true);
}
client = client.redirect(reqwest::redirect::Policy::custom({
let tls_mode = options.tls.mode.clone();
let uri = uri.clone();
move |attempt| {
if tls_mode == TlsMode::Preferred
&& uri.scheme() == Some(&Scheme::HTTP)
&& attempt.url().scheme() == "https"
{
debug!("Following HTTP->HTTPS redirect");
attempt.follow()
} else {
attempt.stop()
}
}
}));
if !options.tls.verify {
client = client.danger_accept_invalid_certs(true);
}
@ -236,8 +259,8 @@ pub async fn proxy_normal_request(
let mut client_request = client.request(req.method().into(), uri.to_string());
client_request = copy_server_request(&req, client_request);
client_request = inject_forwarding_headers(&req, client_request)?;
client_request = copy_server_request(req, client_request);
client_request = inject_forwarding_headers(req, client_request)?;
client_request = rewrite_request(client_request, options)?;
client_request = client_request.body(reqwest::Body::wrap_stream(body.into_bytes_stream()));
client_request = client_request.header(
@ -252,7 +275,7 @@ pub async fn proxy_normal_request(
.execute(client_request)
.await
.map_err(|e| anyhow::anyhow!("Could not execute request: {e}"))?;
let status = client_response.status().clone();
let status = client_response.status();
let mut response: Response = "".into();
@ -292,7 +315,7 @@ async fn copy_client_body_and_embed(
r#"<script type="module" src="/@warpgate/{}"></script>"#,
script_manifest.file
);
for css_file in script_manifest.css.unwrap_or(vec![]) {
for css_file in script_manifest.css.unwrap_or_default() {
inject += &format!(
r#"<link rel="stylesheet" href="/@warpgate/{}" />"#,
css_file
@ -323,7 +346,7 @@ pub async fn proxy_websocket_request(
ws: WebSocket,
options: &TargetHTTPOptions,
) -> poem::Result<impl IntoResponse> {
let uri = construct_uri(req, &options, true)?;
let uri = construct_uri(req, options, true)?;
proxy_ws_inner(req, ws, uri.clone(), options)
.await
.map_err(|error| {
@ -354,8 +377,8 @@ async fn proxy_ws_inner(
.to_string(),
);
client_request = copy_server_request(&req, client_request);
client_request = inject_forwarding_headers(&req, client_request)?;
client_request = copy_server_request(req, client_request);
client_request = inject_forwarding_headers(req, client_request)?;
client_request = rewrite_request(client_request, options)?;
let (client, client_response) = connect_async_with_config(

View file

@ -9,7 +9,8 @@ use poem::{FromRequest, Request};
use serde_json::Value;
use tokio::sync::Mutex;
use tracing::*;
use warpgate_common::{Services, SessionId, SessionStateInit, WarpgateServerHandle};
use warpgate_common::SessionId;
use warpgate_core::{Services, SessionStateInit, WarpgateServerHandle};
use crate::common::{PROTOCOL_NAME, SESSION_MAX_AGE};
use crate::session_handle::{
@ -96,16 +97,16 @@ impl SessionStore {
&mut self,
req: &Request,
) -> poem::Result<WarpgateServerHandleFromRequest> {
let session: &Session = <_>::from_request_without_body(&req).await?;
let session: &Session = <_>::from_request_without_body(req).await?;
if let Some(handle) = self.handle_for(session) {
return Ok(handle.into());
}
let services = Data::<&Services>::from_request_without_body(&req).await?;
let remote_address: &RemoteAddr = <_>::from_request_without_body(&req).await?;
let services = Data::<&Services>::from_request_without_body(req).await?;
let remote_address: &RemoteAddr = <_>::from_request_without_body(req).await?;
let session_storage =
Data::<&SharedSessionStorage>::from_request_without_body(&req).await?;
Data::<&SharedSessionStorage>::from_request_without_body(req).await?;
let (session_handle, mut session_handle_rx) = HttpSessionHandle::new();
@ -133,13 +134,12 @@ impl SessionStore {
tokio::spawn({
let session_storage = (*session_storage).clone();
let poem_session_id: Option<String> = session.get(POEM_SESSION_ID_SESSION_KEY);
let id = id.clone();
async move {
while let Some(command) = session_handle_rx.recv().await {
match command {
SessionHandleCommand::Close => {
if let Some(ref poem_session_id) = poem_session_id {
let _ = session_storage.remove_session(&poem_session_id).await;
let _ = session_storage.remove_session(poem_session_id).await;
}
info!(%id, "Removed HTTP session");
let mut that = this.lock().await;

View file

@ -6,11 +6,11 @@ use poem::session::Session;
use poem::web::Data;
use poem::{FromRequest, Request, RequestBody};
use tokio::sync::{mpsc, Mutex};
use warpgate_common::{SessionHandle, WarpgateServerHandle};
use warpgate_core::{SessionHandle, WarpgateServerHandle};
use crate::session::SessionStore;
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SessionHandleCommand {
Close,
}
@ -47,7 +47,7 @@ impl std::ops::Deref for WarpgateServerHandleFromRequest {
impl<'a> FromRequest<'a> for WarpgateServerHandleFromRequest {
async fn from_request(req: &'a Request, _: &mut RequestBody) -> poem::Result<Self> {
let sm = Data::<&Arc<Mutex<SessionStore>>>::from_request_without_body(req).await?;
let session: &Session = <_>::from_request_without_body(&req).await?;
let session: &Session = <_>::from_request_without_body(req).await?;
Ok(sm
.lock()
.await

View file

@ -6,6 +6,7 @@ version = "0.5.0"
[dependencies]
warpgate-common = { version = "*", path = "../warpgate-common" }
warpgate-core = { version = "*", path = "../warpgate-core" }
warpgate-db-entities = { version = "*", path = "../warpgate-db-entities" }
warpgate-database-protocols = { version = "*", path = "../warpgate-database-protocols" }
anyhow = { version = "1.0", features = ["std"] }

View file

@ -18,9 +18,9 @@ use rustls::ServerConfig;
use tokio::net::TcpListener;
use tracing::*;
use warpgate_common::{
ProtocolServer, Services, SessionStateInit, Target, TargetOptions, TargetTestError,
TlsCertificateAndPrivateKey, TlsCertificateBundle, TlsPrivateKey,
Target, TargetOptions, TlsCertificateAndPrivateKey, TlsCertificateBundle, TlsPrivateKey,
};
use warpgate_core::{ProtocolServer, Services, SessionStateInit, TargetTestError};
use crate::session::MySqlSession;
use crate::session_handle::MySqlSessionHandle;

View file

@ -7,12 +7,10 @@ use tokio::net::TcpStream;
use tokio::sync::Mutex;
use tracing::*;
use uuid::Uuid;
use warpgate_common::auth::{AuthCredential, AuthSelector};
use warpgate_common::auth::{AuthCredential, AuthResult, AuthSelector};
use warpgate_common::helpers::rng::get_crypto_rng;
use warpgate_common::{
authorize_ticket, AuthResult, Secret, Services, TargetMySqlOptions, TargetOptions,
WarpgateServerHandle,
};
use warpgate_common::{Secret, TargetMySqlOptions, TargetOptions};
use warpgate_core::{authorize_ticket, consume_ticket, Services, WarpgateServerHandle};
use warpgate_database_protocols::io::{BufExt, Decode};
use warpgate_database_protocols::mysql::protocol::auth::AuthPlugin;
use warpgate_database_protocols::mysql::protocol::connect::{
@ -237,11 +235,7 @@ impl MySqlSession {
{
Some(ticket) => {
info!("Authorized for {} with a ticket", ticket.target);
self.services
.config_provider
.lock()
.await
.consume_ticket(&ticket.id)
consume_ticket(&self.services.db, &ticket.id)
.await
.map_err(MySqlError::other)?;
@ -275,11 +269,11 @@ impl MySqlSession {
let target = {
self.services
.config
.config_provider
.lock()
.await
.store
.targets
.list_targets()
.await?
.iter()
.filter_map(|t| match t.options {
TargetOptions::MySql(ref options) => Some((t, options)),

View file

@ -1,5 +1,5 @@
use tokio::sync::mpsc;
use warpgate_common::SessionHandle;
use warpgate_core::SessionHandle;
pub struct MySqlSessionHandle {
abort_tx: mpsc::UnboundedSender<()>,

View file

@ -12,8 +12,12 @@ bimap = "0.6"
bytes = "1.2"
dialoguer = "0.10"
futures = "0.3"
russh = { version = "0.34.0-beta.12", features = ["vendored-openssl"], path = "../russh/russh" }
russh-keys = { version = "0.22.0-beta.5", features = ["vendored-openssl"], path = "../russh/russh-keys" }
russh = { version = "0.34.0-beta.12", features = [
"vendored-openssl",
], path = "../russh/russh" }
russh-keys = { version = "0.22.0-beta.5", features = [
"vendored-openssl",
], path = "../russh/russh-keys" }
sea-orm = { version = "^0.9", features = [
"runtime-tokio-native-tls",
], default-features = false }
@ -23,5 +27,6 @@ tokio = { version = "1.20", features = ["tracing", "signal"] }
tracing = "0.1"
uuid = { version = "1.0", features = ["v4"] }
warpgate-common = { version = "*", path = "../warpgate-common" }
warpgate-core = { version = "*", path = "../warpgate-core" }
warpgate-db-entities = { version = "*", path = "../warpgate-db-entities" }
zeroize = "^1.5"

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use bytes::{Bytes, BytesMut};
use bytes::Bytes;
use russh::client::Msg;
use russh::Channel;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
@ -59,7 +59,7 @@ impl DirectTCPIPChannel {
let bytes: &[u8] = &data;
self.events_tx.send(RCEvent::Output(
self.channel_id,
Bytes::from(BytesMut::from(bytes)),
Bytes::from(bytes.to_vec()),
)).map_err(|_| SshClientError::MpscError)?;
}
Some(russh::ChannelMsg::Close) => {

View file

@ -1,5 +1,5 @@
use anyhow::Result;
use bytes::{Bytes, BytesMut};
use bytes::Bytes;
use russh::client::Msg;
use russh::Channel;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
@ -106,7 +106,7 @@ impl SessionChannel {
debug!("channel data: {bytes:?}");
self.events_tx.send(RCEvent::Output(
self.channel_id,
Bytes::from(BytesMut::from(bytes)),
Bytes::from(bytes.to_vec()),
)).map_err(|_| SshClientError::MpscError)?;
}
Some(russh::ChannelMsg::Close) => {
@ -138,7 +138,7 @@ impl SessionChannel {
let data: &[u8] = &data;
self.events_tx.send(RCEvent::ExtendedData {
channel: self.channel_id,
data: Bytes::from(BytesMut::from(data)),
data: Bytes::from(data.to_vec()),
ext,
}).map_err(|_| SshClientError::MpscError)?;
}

View file

@ -7,7 +7,8 @@ use russh_keys::PublicKeyBase64;
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::oneshot;
use tracing::*;
use warpgate_common::{Services, SessionId, TargetSSHOptions};
use warpgate_common::{SessionId, TargetSSHOptions};
use warpgate_core::Services;
use crate::known_hosts::{KnownHostValidationResult, KnownHosts};
use crate::ConnectionError;

View file

@ -22,7 +22,8 @@ use tokio::sync::{oneshot, Mutex};
use tokio::task::JoinHandle;
use tracing::*;
use uuid::Uuid;
use warpgate_common::{SSHTargetAuth, Services, SessionId, TargetSSHOptions};
use warpgate_common::{SSHTargetAuth, SessionId, TargetSSHOptions};
use warpgate_core::Services;
use self::handler::ClientHandlerEvent;
use super::{ChannelOperation, DirectTCPIPParams};
@ -415,15 +416,15 @@ impl RemoteClient {
let mut auth_result = false;
match ssh_options.auth {
SSHTargetAuth::Password { password } => {
SSHTargetAuth::Password(auth) => {
auth_result = session
.authenticate_password(ssh_options.username.clone(), password.expose_secret())
.authenticate_password(ssh_options.username.clone(), auth.password.expose_secret())
.await?;
if auth_result {
debug!(username=&ssh_options.username[..], "Authenticated with password");
}
}
SSHTargetAuth::PublicKey => {
SSHTargetAuth::PublicKey(_) => {
#[allow(clippy::explicit_auto_deref)]
let keys = load_client_keys(&*self.services.config.lock().await)?;
for key in keys.into_iter() {

Some files were not shown because too many files have changed in this diff Show more