mirror of
https://github.com/warp-tech/warpgate.git
synced 2024-09-20 06:46:17 +08:00
wip
This commit is contained in:
parent
c179585742
commit
3fdd33c96f
|
@ -208,8 +208,13 @@ pub enum ConfigProviderKind {
|
|||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct WarpgateConfigStore {
|
||||
#[serde(default)]
|
||||
pub targets: Vec<Target>,
|
||||
|
||||
#[serde(default)]
|
||||
pub users: Vec<User>,
|
||||
|
||||
#[serde(default)]
|
||||
pub roles: Vec<Role>,
|
||||
|
||||
#[serde(default)]
|
||||
|
|
|
@ -287,12 +287,14 @@ impl ConfigProvider for DatabaseConfigProvider {
|
|||
.map(|x| x.name)
|
||||
.collect();
|
||||
|
||||
let user_roles = user
|
||||
.roles
|
||||
.iter()
|
||||
.map(|x| config.store.roles.iter().find(|y| &y.name == x))
|
||||
.filter_map(|x| x.to_owned().map(|x| x.name.clone()))
|
||||
.collect::<HashSet<_>>();
|
||||
let user_roles: HashSet<String> = Role::Entity::find()
|
||||
.filter(Role::Column::Name.is_in(user.roles))
|
||||
.all(&*db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::<RoleConfig>::into)
|
||||
.map(|x| x.name)
|
||||
.collect();
|
||||
|
||||
let intersect = user_roles.intersection(&target_roles).count() > 0;
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use tracing::*;
|
||||
use anyhow::Result;
|
||||
use sea_orm::sea_query::Expr;
|
||||
use sea_orm::{
|
||||
|
@ -56,7 +57,10 @@ pub async fn connect_to_db(config: &WarpgateConfig) -> Result<DatabaseConnection
|
|||
Ok(connection)
|
||||
}
|
||||
|
||||
pub async fn sanitize_db(db: &mut DatabaseConnection) -> Result<(), WarpgateError> {
|
||||
pub async fn populate_db(
|
||||
db: &mut DatabaseConnection,
|
||||
config: &mut WarpgateConfig,
|
||||
) -> Result<(), WarpgateError> {
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use warpgate_db_entities::{Recording, Session};
|
||||
|
||||
|
@ -80,6 +84,8 @@ pub async fn sanitize_db(db: &mut DatabaseConnection) -> Result<(), WarpgateErro
|
|||
.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)
|
||||
|
@ -133,6 +139,96 @@ pub async fn sanitize_db(db: &mut DatabaseConnection) -> Result<(), WarpgateErro
|
|||
values.insert(&*db).await.map_err(WarpgateError::from)?;
|
||||
}
|
||||
|
||||
if db_was_empty {
|
||||
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();
|
||||
let mut target_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)?
|
||||
}
|
||||
};
|
||||
target_lookup.insert(target_config.name.clone(), target.id);
|
||||
|
||||
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![];
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ use sea_orm::DatabaseConnection;
|
|||
use tokio::sync::Mutex;
|
||||
use warpgate_common::{ConfigProviderKind, WarpgateConfig};
|
||||
|
||||
use crate::db::{connect_to_db, sanitize_db};
|
||||
use crate::db::{connect_to_db, populate_db};
|
||||
use crate::recordings::SessionRecordings;
|
||||
use crate::{AuthStateStore, ConfigProvider, DatabaseConfigProvider, FileConfigProvider, State};
|
||||
|
||||
|
@ -23,9 +23,9 @@ pub struct Services {
|
|||
}
|
||||
|
||||
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)?;
|
||||
|
|
|
@ -69,11 +69,11 @@ 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)),
|
||||
|
@ -106,11 +106,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 {
|
||||
|
|
|
@ -269,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)),
|
||||
|
|
|
@ -24,6 +24,7 @@ use warpgate_common::auth::{AuthCredential, AuthResult, AuthSelector, AuthState,
|
|||
use warpgate_common::eventhub::{EventHub, EventSender};
|
||||
use warpgate_common::{
|
||||
Secret, SessionId, SshHostKeyVerificationMode, Target, TargetOptions, TargetSSHOptions,
|
||||
WarpgateError,
|
||||
};
|
||||
use warpgate_core::recordings::{
|
||||
self, ConnectionRecorder, TerminalRecorder, TerminalRecordingStreamId, TrafficConnectionParams,
|
||||
|
@ -1246,7 +1247,7 @@ impl ServerSession {
|
|||
);
|
||||
return Ok(AuthResult::Rejected);
|
||||
}
|
||||
self._auth_accept(&username, target_name).await;
|
||||
self._auth_accept(&username, target_name).await?;
|
||||
Ok(AuthResult::Accepted { username })
|
||||
}
|
||||
x => Ok(x),
|
||||
|
@ -1257,7 +1258,7 @@ impl ServerSession {
|
|||
Some(ticket) => {
|
||||
info!("Authorized for {} with a ticket", ticket.target);
|
||||
consume_ticket(&self.services.db, &ticket.id).await?;
|
||||
self._auth_accept(&ticket.username, &ticket.target).await;
|
||||
self._auth_accept(&ticket.username, &ticket.target).await?;
|
||||
Ok(AuthResult::Accepted {
|
||||
username: ticket.username.clone(),
|
||||
})
|
||||
|
@ -1268,7 +1269,11 @@ impl ServerSession {
|
|||
}
|
||||
}
|
||||
|
||||
async fn _auth_accept(&mut self, username: &str, target_name: &str) {
|
||||
async fn _auth_accept(
|
||||
&mut self,
|
||||
username: &str,
|
||||
target_name: &str,
|
||||
) -> Result<(), WarpgateError> {
|
||||
info!(%username, "Authenticated");
|
||||
|
||||
let _ = self
|
||||
|
@ -1281,11 +1286,11 @@ impl ServerSession {
|
|||
|
||||
let target = {
|
||||
self.services
|
||||
.config
|
||||
.config_provider
|
||||
.lock()
|
||||
.await
|
||||
.store
|
||||
.targets
|
||||
.list_targets()
|
||||
.await?
|
||||
.iter()
|
||||
.filter_map(|t| match t.options {
|
||||
TargetOptions::Ssh(ref options) => Some((t, options)),
|
||||
|
@ -1298,11 +1303,12 @@ impl ServerSession {
|
|||
let Some((target, ssh_options)) = target else {
|
||||
self.target = TargetSelection::NotFound(target_name.to_string());
|
||||
warn!("Selected target not found");
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let _ = self.server_handle.lock().await.set_target(&target).await;
|
||||
self.target = TargetSelection::Found(target, ssh_options);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn _channel_close(&mut self, server_channel_id: ServerChannelId) -> Result<()> {
|
||||
|
|
|
@ -10,22 +10,27 @@ import { serverInfo } from './lib/store'
|
|||
const dispatch = createEventDispatcher()
|
||||
|
||||
let targets: TargetSnapshot[]|undefined
|
||||
let haveAdminTarget = false
|
||||
let selectedTarget: TargetSnapshot|undefined
|
||||
|
||||
async function init () {
|
||||
targets = await api.getTargets()
|
||||
haveAdminTarget = targets.some(t => t.kind === TargetKind.WebAdmin)
|
||||
targets = targets.filter(t => t.kind !== TargetKind.WebAdmin)
|
||||
}
|
||||
|
||||
function selectTarget (target: TargetSnapshot) {
|
||||
if (target.kind === TargetKind.Http) {
|
||||
loadURL(`/?warpgate-target=${target.name}`)
|
||||
} else if (target.kind === TargetKind.WebAdmin) {
|
||||
loadURL('/@warpgate/admin')
|
||||
} else {
|
||||
selectedTarget = target
|
||||
}
|
||||
}
|
||||
|
||||
function selectAdminTarget () {
|
||||
loadURL('/@warpgate/admin')
|
||||
}
|
||||
|
||||
function loadURL (url: string) {
|
||||
dispatch('navigation')
|
||||
location.href = url
|
||||
|
@ -37,6 +42,23 @@ init()
|
|||
|
||||
{#if targets}
|
||||
<div class="list-group list-group-flush">
|
||||
{#if haveAdminTarget}
|
||||
<a
|
||||
class="list-group-item list-group-item-action target-item"
|
||||
href="/@warpgate/admin"
|
||||
on:click|preventDefault={e => {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
return
|
||||
}
|
||||
selectAdminTarget()
|
||||
}}
|
||||
>
|
||||
<span class="me-auto">
|
||||
Manage Warpgate
|
||||
</span>
|
||||
<Fa icon={faArrowRight} fw />
|
||||
</a>
|
||||
{/if}
|
||||
{#each targets as target}
|
||||
<a
|
||||
class="list-group-item list-group-item-action target-item"
|
||||
|
@ -45,15 +67,16 @@ init()
|
|||
? `/?warpgate-target=${target.name}`
|
||||
: '/@warpgate/admin'
|
||||
}
|
||||
on:click={e => {
|
||||
on:click|preventDefault={e => {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
return
|
||||
}
|
||||
selectTarget(target)
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<span class="me-auto">{target.name}</span>
|
||||
<span class="me-auto">
|
||||
{target.name}
|
||||
</span>
|
||||
<small class="protocol text-muted ms-auto">
|
||||
{#if target.kind === TargetKind.Ssh}
|
||||
SSH
|
||||
|
@ -62,7 +85,7 @@ init()
|
|||
MySQL
|
||||
{/if}
|
||||
</small>
|
||||
{#if target.kind === TargetKind.Http || target.kind === TargetKind.WebAdmin}
|
||||
{#if target.kind === TargetKind.Http}
|
||||
<Fa icon={faArrowRight} fw />
|
||||
{/if}
|
||||
</a>
|
||||
|
|
|
@ -7,10 +7,14 @@ use crate::config::load_config;
|
|||
|
||||
pub(crate) async fn command(cli: &crate::Cli, target_name: &String) -> Result<()> {
|
||||
let config = load_config(&cli.config, true)?;
|
||||
let services = Services::new(config.clone()).await?;
|
||||
|
||||
let Some(target) = config
|
||||
.store
|
||||
.targets
|
||||
let Some(target) = services
|
||||
.config_provider
|
||||
.lock()
|
||||
.await
|
||||
.list_targets()
|
||||
.await?
|
||||
.iter()
|
||||
.find(|x| &x.name == target_name)
|
||||
.map(Target::clone) else {
|
||||
|
@ -18,8 +22,6 @@ pub(crate) async fn command(cli: &crate::Cli, target_name: &String) -> Result<()
|
|||
return Ok(());
|
||||
};
|
||||
|
||||
let services = Services::new(config.clone()).await?;
|
||||
|
||||
let s: Box<dyn ProtocolServer> = match target.options {
|
||||
TargetOptions::Ssh(_) => {
|
||||
Box::new(warpgate_protocol_ssh::SSHProtocolServer::new(&services).await?)
|
||||
|
|
Loading…
Reference in a new issue