This commit is contained in:
Eugene Pankov 2022-08-28 19:11:38 +02:00
parent c179585742
commit 3fdd33c96f
No known key found for this signature in database
GPG key ID: 5896FCBBDD1CF4F4
9 changed files with 172 additions and 38 deletions

View file

@ -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)]

View file

@ -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;

View file

@ -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(())
}

View file

@ -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)?;

View file

@ -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 {

View file

@ -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)),

View file

@ -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<()> {

View file

@ -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>

View file

@ -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?)