mirror of
https://github.com/warp-tech/warpgate.git
synced 2024-09-20 06:46:17 +08:00
database config provider
This commit is contained in:
parent
739df5a507
commit
51df7083de
|
@ -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}"
|
||||
|
|
9
.github/workflows/build.yml
vendored
9
.github/workflows/build.yml
vendored
|
@ -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: |
|
||||
|
|
12
.github/workflows/test.yml
vendored
12
.github/workflows/test.yml
vendored
|
@ -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
52
Cargo.lock
generated
|
@ -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]]
|
||||
|
|
|
@ -5,6 +5,7 @@ members = [
|
|||
"warpgate",
|
||||
"warpgate-admin",
|
||||
"warpgate-common",
|
||||
"warpgate-core",
|
||||
"warpgate-db-migrations",
|
||||
"warpgate-db-entities",
|
||||
"warpgate-database-protocols",
|
||||
|
|
2
justfile
2
justfile
|
@ -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
2
russh
|
@ -1 +1 @@
|
|||
Subproject commit 9ac839abf415817d5622e54d12943e2033a9e9f4
|
||||
Subproject commit 811680a3bf04c563fd1a4cfa39b05224d08e85c0
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
187
warpgate-admin/src/api/roles.rs
Normal file
187
warpgate-admin/src/api/roles.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
349
warpgate-admin/src/api/targets.rs
Normal file
349
warpgate-admin/src/api/targets.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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)))
|
||||
}
|
||||
}
|
326
warpgate-admin/src/api/users.rs
Normal file
326
warpgate-admin/src/api/users.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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)))
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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,
|
||||
|
|
71
warpgate-common/src/config/defaults.rs
Normal file
71
warpgate-common/src/config/defaults.rs
Normal 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()
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
133
warpgate-common/src/config/target.rs
Normal file
133
warpgate-common/src/config/target.rs
Normal 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),
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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;
|
||||
// }
|
|
@ -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 {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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
52
warpgate-core/Cargo.toml
Normal 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"
|
|
@ -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));
|
288
warpgate-core/src/config_providers/db.rs
Normal file
288
warpgate-core/src/config_providers/db.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
3
warpgate-core/src/consts.rs
Normal file
3
warpgate-core/src/consts.rs
Normal 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";
|
|
@ -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
306
warpgate-core/src/db/mod.rs
Normal 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
17
warpgate-core/src/lib.rs
Normal 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;
|
|
@ -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};
|
|
@ -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";
|
||||
|
|
@ -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);
|
|
@ -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 {
|
|
@ -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,
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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())));
|
||||
|
|
@ -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>>>,
|
|
@ -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" }
|
||||
|
|
28
warpgate-db-entities/src/Role.rs
Normal file
28
warpgate-db-entities/src/Role.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
78
warpgate-db-entities/src/Target.rs
Normal file
78
warpgate-db-entities/src/Target.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
37
warpgate-db-entities/src/TargetRoleAssignment.rs
Normal file
37
warpgate-db-entities/src/TargetRoleAssignment.rs
Normal 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 {}
|
48
warpgate-db-entities/src/User.rs
Normal file
48
warpgate-db-entities/src/User.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
37
warpgate-db-entities/src/UserRoleAssignment.rs
Normal file
37
warpgate-db-entities/src/UserRoleAssignment.rs
Normal 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 {}
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
152
warpgate-db-migrations/src/m00007_targets_and_roles.rs
Normal file
152
warpgate-db-migrations/src/m00007_targets_and_roles.rs
Normal 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(())
|
||||
}
|
||||
}
|
102
warpgate-db-migrations/src/m00008_users.rs
Normal file
102
warpgate-db-migrations/src/m00008_users.rs
Normal 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(())
|
||||
}
|
||||
}
|
|
@ -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"}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use tokio::sync::mpsc;
|
||||
use warpgate_common::SessionHandle;
|
||||
use warpgate_core::SessionHandle;
|
||||
|
||||
pub struct MySqlSessionHandle {
|
||||
abort_tx: mpsc::UnboundedSender<()>,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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)?;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue