This commit is contained in:
Eugene Pankov 2022-08-28 22:54:28 +02:00
parent 3fdd33c96f
commit c8f92cda9f
No known key found for this signature in database
GPG key ID: 5896FCBBDD1CF4F4
27 changed files with 1338 additions and 321 deletions

View file

@ -12,19 +12,17 @@ mod ssh_keys;
mod targets;
mod tickets_detail;
mod tickets_list;
mod users_list;
mod users;
pub fn get() -> impl OpenApi {
(
sessions_list::Api,
sessions_detail::Api,
recordings_detail::Api,
users_list::Api,
roles::ListApi,
roles::DetailApi,
targets::ListApi,
targets::DetailApi,
targets::RolesApi,
(targets::ListApi, targets::DetailApi, targets::RolesApi),
(users::ListApi, users::DetailApi, users::RolesApi),
tickets_list::Api,
tickets_detail::Api,
known_hosts_list::Api,

View file

@ -0,0 +1,328 @@
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 = 400)]
BadRequest,
#[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.clone()))
.filter(UserRoleAssignment::Column::RoleId.eq(role_id.0.clone()))
.all(&*db)
.await
.map_err(WarpgateError::from)?
.is_empty()
{
return Ok(AddUserRoleResponse::AlreadyExists);
}
let values = UserRoleAssignment::ActiveModel {
user_id: Set(id.0),
role_id: Set(role_id.0),
..Default::default()
};
values.insert(&*db).await.map_err(WarpgateError::from)?;
Ok(AddUserRoleResponse::Created)
}
#[oai(
path = "/users/:id/roles/:role_id",
method = "delete",
operation_id = "delete_user_role"
)]
async fn api_delete_user_role(
&self,
db: Data<&Arc<Mutex<DatabaseConnection>>>,
id: Path<Uuid>,
role_id: Path<Uuid>,
) -> poem::Result<DeleteUserRoleResponse> {
let db = db.lock().await;
let Some(user) = User::Entity::find_by_id(id.0)
.one(&*db)
.await
.map_err(poem::error::InternalServerError)? else {
return Ok(DeleteUserRoleResponse::NotFound);
};
let Some(role) = Role::Entity::find_by_id(role_id.0)
.one(&*db)
.await
.map_err(poem::error::InternalServerError)? else {
return Ok(DeleteUserRoleResponse::NotFound);
};
let Some(model) = UserRoleAssignment::Entity::find()
.filter(UserRoleAssignment::Column::UserId.eq(id.0))
.filter(UserRoleAssignment::Column::RoleId.eq(role_id.0))
.one(&*db)
.await
.map_err(WarpgateError::from)? else {
return Ok(DeleteUserRoleResponse::NotFound);
};
model.delete(&*db).await.map_err(WarpgateError::from)?;
Ok(DeleteUserRoleResponse::Deleted)
}
}

View file

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

View file

@ -1,9 +1,10 @@
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,

View file

@ -5,7 +5,7 @@ use std::path::PathBuf;
use std::time::Duration;
use defaults::*;
use poem_openapi::Object;
use poem_openapi::{Object, Union};
use serde::{Deserialize, Serialize};
pub use target::*;
use url::Url;
@ -16,37 +16,51 @@ use crate::auth::CredentialKind;
use crate::helpers::otp::OtpSecretKey;
use crate::{ListenEndpoint, Secret, WarpgateError};
#[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::Otp,
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>>,
@ -56,8 +70,10 @@ 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")]

View file

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

View file

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

View file

@ -94,6 +94,7 @@ impl<T: poem_openapi::types::Type> poem_openapi::types::Type for Secret<T> {
}
}
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)

View file

@ -13,43 +13,37 @@ use warpgate_common::auth::{
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,
Role as RoleConfig, Target as TargetConfig, User as UserConfig, UserAuthCredential,
UserPasswordCredential, UserPublicKeyCredential, UserSsoCredential, UserTotpCredential,
WarpgateError,
};
use warpgate_db_entities::{Role, Target};
use warpgate_db_entities::{Role, Target, User};
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(),
}
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<UserSnapshot>, WarpgateError> {
Ok(self
.config
.lock()
.await
.store
.users
.iter()
.map(UserSnapshot::new)
.collect::<Vec<_>>())
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> {
@ -70,21 +64,20 @@ impl ConfigProvider for DatabaseConfigProvider {
&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 {
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 {
@ -142,15 +135,12 @@ impl ConfigProvider for DatabaseConfigProvider {
};
Ok(self
.config
.lock()
.await
.store
.users
.list_users()
.await?
.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
{
@ -168,20 +158,19 @@ impl ConfigProvider for DatabaseConfigProvider {
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);
};
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 {
@ -194,17 +183,17 @@ impl ConfigProvider for DatabaseConfigProvider {
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(),
)
@ -220,9 +209,9 @@ impl ConfigProvider for DatabaseConfigProvider {
}
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,
}))
}
@ -231,10 +220,10 @@ impl ConfigProvider for DatabaseConfigProvider {
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);
@ -252,23 +241,19 @@ impl ConfigProvider for DatabaseConfigProvider {
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 {
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);
};
@ -287,8 +272,8 @@ impl ConfigProvider for DatabaseConfigProvider {
.map(|x| x.name)
.collect();
let user_roles: HashSet<String> = Role::Entity::find()
.filter(Role::Column::Name.is_in(user.roles))
let user_roles: HashSet<String> = user_model
.find_related(Role::Entity)
.all(&*db)
.await?
.into_iter()

View file

@ -11,10 +11,12 @@ use warpgate_common::auth::{
};
use warpgate_common::helpers::hash::verify_password_hash;
use warpgate_common::helpers::otp::verify_totp;
use warpgate_common::{Target, User, UserAuthCredential, WarpgateConfig, WarpgateError};
use warpgate_common::{
Target, User, UserAuthCredential, UserPasswordCredential, UserPublicKeyCredential,
UserSsoCredential, UserTotpCredential, WarpgateConfig, WarpgateError,
};
use super::ConfigProvider;
use crate::UserSnapshot;
pub struct FileConfigProvider {
config: Arc<Mutex<WarpgateConfig>>,
@ -30,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()
@ -38,7 +40,7 @@ impl ConfigProvider for FileConfigProvider {
.store
.users
.iter()
.map(UserSnapshot::new)
.map(Clone::clone)
.collect::<Vec<_>>())
}
@ -138,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
{
@ -182,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(),
)
@ -208,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,
}))
}
@ -219,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);

View file

@ -11,14 +11,12 @@ use tokio::sync::Mutex;
use tracing::*;
use uuid::Uuid;
use warpgate_common::auth::{AuthCredential, CredentialPolicy};
use warpgate_common::{Secret, Target, WarpgateError};
use warpgate_common::{Secret, Target, WarpgateError, User};
use warpgate_db_entities::Ticket;
use crate::UserSnapshot;
#[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>;

View file

@ -2,7 +2,7 @@ use chrono::{DateTime, Utc};
use poem_openapi::Object;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use warpgate_common::{SessionId, Target, User};
use warpgate_common::{SessionId, Target};
use warpgate_db_entities::Session;
#[derive(Serialize, Deserialize, Object)]
@ -31,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(),
}
}
}

View file

@ -1,17 +1,20 @@
use std::collections::HashMap;
use std::time::Duration;
use tracing::*;
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::{TargetOptions, TargetWebAdminOptions, WarpgateConfig, WarpgateError};
use warpgate_db_entities::Target::TargetKind;
use warpgate_db_entities::{LogEntry, Role, Target, TargetRoleAssignment};
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};
@ -158,7 +161,6 @@ async fn migrate_config_into_db(
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()
@ -206,7 +208,6 @@ async fn migrate_config_into_db(
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) {
@ -229,6 +230,50 @@ async fn migrate_config_into_db(
}
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.require.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(())
}

View file

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

View file

@ -39,8 +39,7 @@ impl Services {
Arc::new(Mutex::new(FileConfigProvider::new(&config).await)) as ConfigProviderArc
}
ConfigProviderKind::Database => {
Arc::new(Mutex::new(DatabaseConfigProvider::new(&db, &config).await))
as ConfigProviderArc
Arc::new(Mutex::new(DatabaseConfigProvider::new(&db).await)) as ConfigProviderArc
}
};

View file

@ -5,7 +5,7 @@ use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)]
#[sea_orm(table_name = "target_roles")]
#[oai(rename = "Target")]
#[oai(rename = "TargetRoleAssignment")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: u32,

View file

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

View file

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

View file

@ -8,3 +8,5 @@ pub mod Session;
pub mod Target;
pub mod TargetRoleAssignment;
pub mod Ticket;
pub mod User;
pub mod UserRoleAssignment;

View file

@ -9,6 +9,7 @@ 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;
@ -23,6 +24,7 @@ impl MigratorTrait for Migrator {
Box::new(m00005_create_log_entry::Migration),
Box::new(m00006_add_session_protocol::Migration),
Box::new(m00007_targets_and_roles::Migration),
Box::new(m00008_users::Migration),
]
}
}

View file

@ -1,7 +1,7 @@
use sea_orm::Schema;
use sea_orm_migration::prelude::*;
mod role {
pub(crate) mod role {
use sea_orm::entity::prelude::*;
use uuid::Uuid;

View file

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

View file

@ -54,6 +54,12 @@ const routes = {
'/roles/:id': wrap({
asyncComponent: () => import('./Role.svelte'),
}),
'/users/create': wrap({
asyncComponent: () => import('./CreateUser.svelte'),
}),
// '/users/:id': wrap({
// asyncComponent: () => import('./User.svelte'),
// }),
'/ssh': wrap({
asyncComponent: () => import('./SSH.svelte'),
}),

View file

@ -4,79 +4,115 @@ import { link } from 'svelte-spa-router'
import { Alert, Spinner } from 'sveltestrap'
</script>
<div class="page-summary-bar">
<h1>Targets</h1>
<a
class="btn btn-outline-secondary ms-auto"
href="/targets/create"
use:link>
Add a target
</a>
</div>
{#await api.getTargets()}
<Spinner />
{:then targets}
<div class="list-group list-group-flush">
{#each targets as target}
<!-- svelte-ignore a11y-missing-attribute -->
<div class="row">
<div class="col-12 col-lg-6 mb-4 pe-4">
<div class="page-summary-bar">
<h1>Targets</h1>
<a
class="list-group-item list-group-item-action"
href="/targets/{target.id}"
class="btn btn-outline-secondary ms-auto"
href="/targets/create"
use:link>
<strong class="me-auto">
{target.name}
</strong>
<small class="text-muted ms-auto">
{#if target.options.kind === 'Http'}
HTTP
{/if}
{#if target.options.kind === 'MySql'}
MySQL
{/if}
{#if target.options.kind === 'Ssh'}
SSH
{/if}
{#if target.options.kind === 'WebAdmin'}
This web admin interface
{/if}
</small>
Add a target
</a>
{/each}
</div>
{#await api.getTargets()}
<Spinner />
{:then targets}
<div class="list-group list-group-flush">
{#each targets as target}
<!-- svelte-ignore a11y-missing-attribute -->
<a
class="list-group-item list-group-item-action"
href="/targets/{target.id}"
use:link>
<strong class="me-auto">
{target.name}
</strong>
<small class="text-muted ms-auto">
{#if target.options.kind === 'Http'}
HTTP
{/if}
{#if target.options.kind === 'MySql'}
MySQL
{/if}
{#if target.options.kind === 'Ssh'}
SSH
{/if}
{#if target.options.kind === 'WebAdmin'}
This web admin interface
{/if}
</small>
</a>
{/each}
</div>
{:catch error}
<Alert color="danger">{error}</Alert>
{/await}
</div>
{:catch error}
<Alert color="danger">{error}</Alert>
{/await}
<div class="page-summary-bar mt-4">
<h1>Roles</h1>
<a
class="btn btn-outline-secondary ms-auto"
href="/roles/create"
use:link>
Add a role
</a>
</div>
{#await api.getRoles()}
<Spinner />
{:then roles}
<div class="list-group list-group-flush">
{#each roles as role}
<!-- svelte-ignore a11y-missing-attribute -->
<div class="col-12 col-lg-6 pe-4">
<div class="page-summary-bar">
<h1>Users</h1>
<a
class="list-group-item list-group-item-action"
href="/roles/{role.id}"
class="btn btn-outline-secondary ms-auto"
href="/users/create"
use:link>
<strong class="me-auto">
{role.name}
</strong>
Add a user
</a>
{/each}
</div>
{#await api.getUsers()}
<Spinner />
{:then users}
<div class="list-group list-group-flush">
{#each users as user}
<!-- svelte-ignore a11y-missing-attribute -->
<a
class="list-group-item list-group-item-action"
href="/users/{user.id}"
use:link>
<strong class="me-auto">
{user.username}
</strong>
</a>
{/each}
</div>
{:catch error}
<Alert color="danger">{error}</Alert>
{/await}
<div class="page-summary-bar mt-4">
<h1>Roles</h1>
<a
class="btn btn-outline-secondary ms-auto"
href="/roles/create"
use:link>
Add a role
</a>
</div>
{#await api.getRoles()}
<Spinner />
{:then roles}
<div class="list-group list-group-flush">
{#each roles as role}
<!-- svelte-ignore a11y-missing-attribute -->
<a
class="list-group-item list-group-item-action"
href="/roles/{role.id}"
use:link>
<strong class="me-auto">
{role.name}
</strong>
</a>
{/each}
</div>
{:catch error}
<Alert color="danger">{error}</Alert>
{/await}
</div>
{:catch error}
<Alert color="danger">{error}</Alert>
{/await}
</div>
<style lang="scss">
.list-group-item {

View file

@ -0,0 +1,46 @@
<script lang="ts">
import { api } from 'admin/lib/api'
import AsyncButton from 'common/AsyncButton.svelte'
import { replace } from 'svelte-spa-router'
import { Alert, FormGroup } from 'sveltestrap'
let error: Error|null = null
let username = ''
async function create () {
if (!username) {
return
}
try {
const user = await api.createUser({
userDataRequest: {
username,
credentials: [],
credentialPolicy: {},
},
})
replace(`/users/${user.id}`)
} catch (err) {
error = err
}
}
</script>
{#if error}
<Alert color="danger">{error}</Alert>
{/if}
<div class="page-summary-bar">
<h1>Add a user</h1>
</div>
<FormGroup floating label="Username">
<input class="form-control" bind:value={username} />
</FormGroup>
<AsyncButton
outline
click={create}
>Create user</AsyncButton>

View file

@ -197,26 +197,6 @@
"operationId": "get_recording"
}
},
"/users": {
"get": {
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/UserSnapshot"
}
}
}
}
}
},
"operationId": "get_users"
}
},
"/roles": {
"get": {
"responses": {
@ -635,6 +615,262 @@
"operationId": "delete_target_role"
}
},
"/users": {
"get": {
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/User"
}
}
}
}
}
},
"operationId": "get_users"
},
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserDataRequest"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
},
"operationId": "create_user"
}
},
"/users/{id}": {
"get": {
"parameters": [
{
"name": "id",
"schema": {
"type": "string",
"format": "uuid"
},
"in": "path",
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"404": {
"description": ""
}
},
"operationId": "get_user"
},
"put": {
"parameters": [
{
"name": "id",
"schema": {
"type": "string",
"format": "uuid"
},
"in": "path",
"required": true,
"deprecated": false
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserDataRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"400": {
"description": ""
},
"404": {
"description": ""
}
},
"operationId": "update_user"
},
"delete": {
"parameters": [
{
"name": "id",
"schema": {
"type": "string",
"format": "uuid"
},
"in": "path",
"required": true,
"deprecated": false
}
],
"responses": {
"204": {
"description": ""
},
"404": {
"description": ""
}
},
"operationId": "delete_user"
}
},
"/users/{id}/roles": {
"get": {
"parameters": [
{
"name": "id",
"schema": {
"type": "string",
"format": "uuid"
},
"in": "path",
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Role"
}
}
}
}
},
"404": {
"description": ""
}
},
"operationId": "get_user_roles"
}
},
"/users/{id}/roles/{role_id}": {
"post": {
"parameters": [
{
"name": "id",
"schema": {
"type": "string",
"format": "uuid"
},
"in": "path",
"required": true,
"deprecated": false
},
{
"name": "role_id",
"schema": {
"type": "string",
"format": "uuid"
},
"in": "path",
"required": true,
"deprecated": false
}
],
"responses": {
"201": {
"description": ""
},
"409": {
"description": ""
}
},
"operationId": "add_user_role"
},
"delete": {
"parameters": [
{
"name": "id",
"schema": {
"type": "string",
"format": "uuid"
},
"in": "path",
"required": true,
"deprecated": false
},
{
"name": "role_id",
"schema": {
"type": "string",
"format": "uuid"
},
"in": "path",
"required": true,
"deprecated": false
}
],
"responses": {
"204": {
"description": ""
},
"404": {
"description": ""
}
},
"operationId": "delete_user_role"
}
},
"/tickets": {
"get": {
"responses": {
@ -828,6 +1064,16 @@
}
}
},
"CredentialKind": {
"type": "string",
"enum": [
"Password",
"PublicKey",
"Otp",
"Sso",
"WebUserApproval"
]
},
"GetLogsRequest": {
"type": "object",
"properties": {
@ -1420,14 +1666,233 @@
"Required"
]
},
"UserSnapshot": {
"User": {
"type": "object",
"required": [
"username"
"id",
"username",
"credentials",
"roles"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"username": {
"type": "string"
},
"credentials": {
"type": "array",
"items": {
"$ref": "#/components/schemas/UserAuthCredential"
}
},
"require": {
"$ref": "#/components/schemas/UserRequireCredentialsPolicy"
},
"roles": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"UserAuthCredential": {
"type": "object",
"oneOf": [
{
"$ref": "#/components/schemas/UserAuthCredential_UserPasswordCredential"
},
{
"$ref": "#/components/schemas/UserAuthCredential_UserPublicKeyCredential"
},
{
"$ref": "#/components/schemas/UserAuthCredential_UserTotpCredential"
},
{
"$ref": "#/components/schemas/UserAuthCredential_UserSsoCredential"
}
],
"discriminator": {
"propertyName": "kind",
"mapping": {
"Password": "#/components/schemas/UserAuthCredential_UserPasswordCredential",
"PublicKey": "#/components/schemas/UserAuthCredential_UserPublicKeyCredential",
"Totp": "#/components/schemas/UserAuthCredential_UserTotpCredential",
"Sso": "#/components/schemas/UserAuthCredential_UserSsoCredential"
}
}
},
"UserAuthCredential_UserPasswordCredential": {
"allOf": [
{
"type": "object",
"required": [
"kind"
],
"properties": {
"kind": {
"type": "string",
"example": "Password"
}
}
},
{
"$ref": "#/components/schemas/UserPasswordCredential"
}
]
},
"UserAuthCredential_UserPublicKeyCredential": {
"allOf": [
{
"type": "object",
"required": [
"kind"
],
"properties": {
"kind": {
"type": "string",
"example": "PublicKey"
}
}
},
{
"$ref": "#/components/schemas/UserPublicKeyCredential"
}
]
},
"UserAuthCredential_UserSsoCredential": {
"allOf": [
{
"type": "object",
"required": [
"kind"
],
"properties": {
"kind": {
"type": "string",
"example": "Sso"
}
}
},
{
"$ref": "#/components/schemas/UserSsoCredential"
}
]
},
"UserAuthCredential_UserTotpCredential": {
"allOf": [
{
"type": "object",
"required": [
"kind"
],
"properties": {
"kind": {
"type": "string",
"example": "Totp"
}
}
},
{
"$ref": "#/components/schemas/UserTotpCredential"
}
]
},
"UserDataRequest": {
"type": "object",
"required": [
"username",
"credentials"
],
"properties": {
"username": {
"type": "string"
},
"credentials": {
"type": "array",
"items": {
"$ref": "#/components/schemas/UserAuthCredential"
}
},
"credential_policy": {
"$ref": "#/components/schemas/UserRequireCredentialsPolicy"
}
}
},
"UserPasswordCredential": {
"type": "object",
"required": [
"hash"
],
"properties": {
"hash": {
"type": "string"
}
}
},
"UserPublicKeyCredential": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "string"
}
}
},
"UserRequireCredentialsPolicy": {
"type": "object",
"properties": {
"http": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CredentialKind"
}
},
"ssh": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CredentialKind"
}
},
"mysql": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CredentialKind"
}
}
}
},
"UserSsoCredential": {
"type": "object",
"required": [
"email"
],
"properties": {
"provider": {
"type": "string"
},
"email": {
"type": "string"
}
}
},
"UserTotpCredential": {
"type": "object",
"required": [
"key"
],
"properties": {
"key": {
"type": "array",
"items": {
"type": "integer",
"format": "uint8"
}
}
}
}

View file

@ -11,8 +11,8 @@ use uuid::Uuid;
use warpgate_common::helpers::fs::{secure_directory, secure_file};
use warpgate_common::helpers::hash::hash_password;
use warpgate_common::{
HTTPConfig, ListenEndpoint, MySQLConfig, Role, SSHConfig, Secret, Target, TargetOptions,
TargetWebAdminOptions, User, UserAuthCredential, WarpgateConfigStore,
HTTPConfig, ListenEndpoint, MySQLConfig, SSHConfig, Secret, User, UserAuthCredential,
WarpgateConfigStore, UserPasswordCredential,
};
use warpgate_core::consts::BUILTIN_ADMIN_ROLE_NAME;
use warpgate_core::Services;
@ -74,10 +74,6 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> {
let theme = ColorfulTheme::default();
let mut store = WarpgateConfigStore {
roles: vec![Role {
id: Uuid::new_v4(),
name: BUILTIN_ADMIN_ROLE_NAME.to_owned(),
}],
http: HTTPConfig {
enable: true,
..Default::default()
@ -158,15 +154,6 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> {
}
}
if store.http.enable {
store.targets.push(Target {
id: Uuid::new_v4(),
name: "Web admin".to_owned(),
allow_roles: vec![BUILTIN_ADMIN_ROLE_NAME.to_owned()],
options: TargetOptions::WebAdmin(TargetWebAdminOptions {}),
});
}
store.http.certificate = PathBuf::from(&data_path)
.join("tls.certificate.pem")
.to_string_lossy()
@ -205,10 +192,11 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> {
.interact()?;
store.users.push(User {
id: Uuid::new_v4(),
username: "admin".into(),
credentials: vec![UserAuthCredential::Password {
credentials: vec![UserAuthCredential::Password(UserPasswordCredential {
hash: Secret::new(hash_password(&password)),
}],
})],
require: None,
roles: vec![BUILTIN_ADMIN_ROLE_NAME.into()],
});