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
6b805e686f
commit
a2bbd336c4
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4746,6 +4746,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"sea-orm",
|
||||
"sea-orm-migration",
|
||||
"serde_json",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
|
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,10 +1,12 @@
|
|||
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 serde::{Deserialize, Serialize};
|
||||
pub use target::*;
|
||||
use url::Url;
|
||||
use warpgate_sso::SsoProviderConfig;
|
||||
|
||||
|
@ -12,179 +14,6 @@ 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)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum UserAuthCredential {
|
||||
|
@ -239,15 +68,6 @@ pub struct Role {
|
|||
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")]
|
||||
|
@ -373,6 +193,15 @@ impl Default for LogConfig {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Default)]
|
||||
pub enum ConfigProviderKind {
|
||||
#[serde(rename = "file")]
|
||||
#[default]
|
||||
File,
|
||||
#[serde(rename = "database")]
|
||||
Database,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct WarpgateConfigStore {
|
||||
pub targets: Vec<Target>,
|
||||
|
@ -402,6 +231,9 @@ pub struct WarpgateConfigStore {
|
|||
|
||||
#[serde(default)]
|
||||
pub log: LogConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub config_provider: ConfigProviderKind,
|
||||
}
|
||||
|
||||
impl Default for WarpgateConfigStore {
|
||||
|
@ -411,13 +243,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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
122
warpgate-common/src/config/target.rs
Normal file
122
warpgate-common/src/config/target.rs
Normal file
|
@ -0,0 +1,122 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use poem_openapi::{Enum, Object, Union};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
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)]
|
||||
#[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),
|
||||
}
|
|
@ -17,6 +17,8 @@ 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,
|
||||
}
|
||||
|
|
311
warpgate-core/src/config_providers/db.rs
Normal file
311
warpgate-core/src/config_providers/db.rs
Normal file
|
@ -0,0 +1,311 @@
|
|||
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, UserAuthCredential, WarpgateConfig,
|
||||
WarpgateError,
|
||||
};
|
||||
use warpgate_db_entities::{Role, Target};
|
||||
|
||||
use super::ConfigProvider;
|
||||
use crate::UserSnapshot;
|
||||
|
||||
pub struct DatabaseConfigProvider {
|
||||
db: Arc<Mutex<DatabaseConnection>>,
|
||||
config: Arc<Mutex<WarpgateConfig>>,
|
||||
}
|
||||
|
||||
impl DatabaseConfigProvider {
|
||||
pub async fn new(
|
||||
db: &Arc<Mutex<DatabaseConnection>>,
|
||||
config: &Arc<Mutex<WarpgateConfig>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
db: db.clone(),
|
||||
config: config.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ConfigProvider for DatabaseConfigProvider {
|
||||
async fn list_users(&mut self) -> Result<Vec<UserSnapshot>, WarpgateError> {
|
||||
Ok(self
|
||||
.config
|
||||
.lock()
|
||||
.await
|
||||
.store
|
||||
.users
|
||||
.iter()
|
||||
.map(UserSnapshot::new)
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
async fn list_targets(&mut self) -> Result<Vec<TargetConfig>, WarpgateError> {
|
||||
let db = self.db.lock().await;
|
||||
|
||||
let targets_and_roles = Target::Entity::find()
|
||||
.order_by_desc(Target::Column::Name)
|
||||
.find_with_related(Role::Entity)
|
||||
.all(&*db)
|
||||
.await?;
|
||||
|
||||
let targets: Result<Vec<TargetConfig>, _> = targets_and_roles
|
||||
.into_iter()
|
||||
.map(|(t, r)| {
|
||||
t.try_into().map(|mut x: TargetConfig| {
|
||||
x.allow_roles = r.into_iter().map(|x| x.name.clone()).collect();
|
||||
x
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(targets?)
|
||||
}
|
||||
|
||||
async fn get_credential_policy(
|
||||
&mut self,
|
||||
username: &str,
|
||||
) -> Result<Option<Box<dyn CredentialPolicy + Sync + Send>>, WarpgateError> {
|
||||
let user = {
|
||||
self.config
|
||||
.lock()
|
||||
.await
|
||||
.store
|
||||
.users
|
||||
.iter()
|
||||
.find(|x| x.username == username)
|
||||
.map(User::to_owned)
|
||||
};
|
||||
let Some(user) = user else {
|
||||
error!("Selected user not found: {}", username);
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
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.require {
|
||||
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
|
||||
.config
|
||||
.lock()
|
||||
.await
|
||||
.store
|
||||
.users
|
||||
.iter()
|
||||
.find(|x| {
|
||||
for cred in x.credentials.iter() {
|
||||
if let UserAuthCredential::Sso { 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 user = {
|
||||
self.config
|
||||
.lock()
|
||||
.await
|
||||
.store
|
||||
.users
|
||||
.iter()
|
||||
.find(|x| x.username == username)
|
||||
.map(User::to_owned)
|
||||
};
|
||||
let Some(user) = user else {
|
||||
error!("Selected user not found: {}", username);
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
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 { 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 {
|
||||
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 {
|
||||
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 {
|
||||
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 config = self.config.lock().await;
|
||||
let user = config
|
||||
.store
|
||||
.users
|
||||
.iter()
|
||||
.find(|x| x.username == username)
|
||||
.map(User::to_owned);
|
||||
|
||||
let db = self.db.lock().await;
|
||||
|
||||
let target_model: Option<Target::Model> = Target::Entity::find()
|
||||
.order_by_desc(Target::Column::Name)
|
||||
.filter(Target::Column::Name.eq(target_name))
|
||||
.one(&*db)
|
||||
.await?;
|
||||
|
||||
let Some(user) = user 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<RoleConfig> = target_model
|
||||
.find_related(Role::Entity)
|
||||
.all(&*db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
let user_roles = user
|
||||
.roles
|
||||
.iter()
|
||||
.map(|x| config.store.roles.iter().find(|y| &y.name == x))
|
||||
.filter_map(|x| x.to_owned())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let intersect = user_roles
|
||||
.intersection(&target_roles.iter().collect())
|
||||
.count()
|
||||
> 0;
|
||||
|
||||
Ok(intersect)
|
||||
}
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
mod db;
|
||||
mod file;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
pub use db::DatabaseConfigProvider;
|
||||
pub use file::FileConfigProvider;
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
||||
|
|
|
@ -4,11 +4,13 @@ use std::time::Duration;
|
|||
use anyhow::Result;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use tokio::sync::Mutex;
|
||||
use warpgate_common::WarpgateConfig;
|
||||
use warpgate_common::{ConfigProviderKind, WarpgateConfig};
|
||||
|
||||
use crate::db::{connect_to_db, sanitize_db};
|
||||
use crate::recordings::SessionRecordings;
|
||||
use crate::{AuthStateStore, ConfigProvider, FileConfigProvider, State};
|
||||
use crate::{AuthStateStore, ConfigProvider, FileConfigProvider, State, DatabaseConfigProvider};
|
||||
|
||||
type ConfigProviderArc = Arc<Mutex<dyn ConfigProvider + Send + 'static>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Services {
|
||||
|
@ -16,7 +18,7 @@ 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>>,
|
||||
}
|
||||
|
||||
|
@ -29,8 +31,17 @@ impl Services {
|
|||
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(&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, &config).await)) as ConfigProviderArc
|
||||
}
|
||||
};
|
||||
|
||||
let auth_state_store = Arc::new(Mutex::new(AuthStateStore::new(config_provider.clone())));
|
||||
|
||||
|
|
27
warpgate-db-entities/src/Role.rs
Normal file
27
warpgate-db-entities/src/Role.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
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 {
|
||||
name: model.name,
|
||||
}
|
||||
}
|
||||
}
|
66
warpgate-db-entities/src/Target.rs
Normal file
66
warpgate-db-entities/src/Target.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
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,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
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 = "Target")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: u64,
|
||||
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 {}
|
|
@ -3,5 +3,8 @@
|
|||
pub mod KnownHost;
|
||||
pub mod LogEntry;
|
||||
pub mod Recording;
|
||||
pub mod Role;
|
||||
pub mod Session;
|
||||
pub mod Target;
|
||||
pub mod TargetRoleAssignment;
|
||||
pub mod Ticket;
|
||||
|
|
|
@ -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,7 @@ mod m00003_create_recording;
|
|||
mod m00004_create_known_host;
|
||||
mod m00005_create_log_entry;
|
||||
mod m00006_add_session_protocol;
|
||||
mod m00007_targets_and_roles;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
|
@ -21,6 +22,7 @@ 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),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
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::*;
|
||||
|
||||
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: u64,
|
||||
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(())
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue