use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::Level;
use tracing::span;
#[allow(unused)]
use tracing::{debug, error, info, trace, warn};
use crate::LocalHostHandler;
use crate::Ssh2AuthMethod;
use crate::Ssh2HostHandler;
use crate::WhichUser;
use crate::error::RegentError;
use crate::hosts::handlers::ConnectionMethod;
use crate::hosts::handlers::Handler;
use crate::hosts::handlers::HostHandler;
use crate::hosts::handlers::TargetUserKind;
use crate::hosts::handlers::ssh2::Ssh2AuthReference;
use crate::hosts::privilege::Credentials;
use crate::hosts::privilege::LoginKey;
use crate::hosts::privilege::Privilege;
use crate::hosts::properties::HostProperties;
use crate::secrets::SecretProvider;
use crate::secrets::SecretProvidersPool;
use crate::state::ExpectedState;
use crate::state::attribute::Remediation;
use crate::state::compliance::Action;
use crate::state::compliance::AttributeComplianceAssessment;
use crate::state::compliance::HostStatus;
use crate::state::compliance::ManagedHostStatus;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
#[serde(deny_unknown_fields)]
pub struct ManagedHostBuilder {
pub id: String,
endpoint: String,
pub host_connection_method: Option<ConnectionMethod>,
host_properties: Option<HostProperties>,
pub host_vars: Option<HashMap<String, String>>,
}
impl ManagedHostBuilder {
pub fn new(id: &str, endpoint: &str, connection_method: Option<ConnectionMethod>) -> Self {
Self {
id: id.to_string(),
endpoint: endpoint.to_string(),
host_connection_method: connection_method,
host_properties: None,
host_vars: None,
}
}
pub fn set_connection_method(&mut self, connection_method: ConnectionMethod) {
self.host_connection_method = Some(connection_method);
}
pub fn set_host_vars(&mut self, host_vars: Option<HashMap<String, String>>) {
self.host_vars = host_vars;
}
pub fn from_raw_yaml(raw_yaml: &str) -> Result<Self, RegentError> {
match yaml_serde::from_str::<Self>(raw_yaml) {
Ok(managed_host_builder) => Ok(managed_host_builder),
Err(details) => Err(RegentError::FailureToParseContent(format!("{:?}", details))),
}
}
pub fn from_raw_json(raw_json: &str) -> Result<Self, RegentError> {
match serde_json::from_str::<Self>(raw_json) {
Ok(managed_host_builder) => Ok(managed_host_builder),
Err(details) => Err(RegentError::FailureToParseContent(format!("{:?}", details))),
}
}
pub async fn build(
self,
optional_secret_provider: Option<SecretProvidersPool>,
) -> Result<ManagedHost, RegentError> {
if let None = self.host_connection_method {
return Err(RegentError::WrongInitialization(format!(
"connection method unset"
)));
}
match self.host_connection_method {
Some(connection) => {
match connection {
ConnectionMethod::Localhost(target_user) => {
match target_user.user_kind {
TargetUserKind::CurrentUser => {
Ok(ManagedHost::new(
self.id,
&self.endpoint,
Handler::localhost(LocalHostHandler::from(
WhichUser::CurrentUser,
)),
self.host_vars,
self.host_properties,
optional_secret_provider,
))
}
TargetUserKind::User(secret_reference) => {
match &optional_secret_provider {
Some(secret_provider) => {
match secret_provider
.get_secret_typed::<Credentials>(&secret_reference)
.await
{
Ok(secret) => Ok(ManagedHost::new(
self.id,
&self.endpoint,
Handler::localhost(LocalHostHandler::from(
WhichUser::UsernamePassword(secret.inner()),
)),
self.host_vars,
self.host_properties,
optional_secret_provider,
)),
Err(details) => Err(details),
}
}
None => Err(RegentError::WrongInitialization(format!(
"secret required to connect to host but secret_provider unset"
))),
}
}
}
}
ConnectionMethod::Ssh2(ssh2_auth_reference) => {
match ssh2_auth_reference.auth_method {
Ssh2AuthReference::UsernamePassword(secret_reference) => {
match &optional_secret_provider {
Some(secret_provider) => {
match secret_provider
.get_secret_typed::<Credentials>(&secret_reference)
.await
{
Ok(secret) => Ok(ManagedHost::new(
self.id,
&self.endpoint,
Handler::ss2(Ssh2HostHandler::from(
Ssh2AuthMethod::UsernamePassword(
secret.inner(),
),
)?),
self.host_vars,
self.host_properties,
optional_secret_provider,
)),
Err(details) => Err(details),
}
}
None => Err(RegentError::WrongInitialization(format!(
"secret required to connect to host but secret_provider unset"
))),
}
}
Ssh2AuthReference::Key(login_key_ref) => {
match &optional_secret_provider {
Some(secret_provider) => {
match secret_provider
.get_secret_raw(login_key_ref.key_ref())
.await
{
Ok(secret) => Ok(ManagedHost::new(
self.id,
&self.endpoint,
Handler::ss2(Ssh2HostHandler::from(
Ssh2AuthMethod::Key(LoginKey::from(
login_key_ref.username().to_string(),
secret.inner(),
)),
)?),
self.host_vars,
self.host_properties,
optional_secret_provider,
)),
Err(details) => Err(details),
}
}
None => Err(RegentError::WrongInitialization(format!(
"secret required to connect to host but secret_provider unset"
))),
}
}
Ssh2AuthReference::Agent(agent_name) => {
Ok(ManagedHost::new(
self.id,
&self.endpoint,
Handler::ss2(Ssh2HostHandler::from(
crate::Ssh2AuthMethod::Agent(agent_name),
)?),
self.host_vars,
self.host_properties,
optional_secret_provider,
))
}
}
}
}
}
None => Err(RegentError::WrongInitialization(format!(
"connection_method unset"
))),
}
}
}
#[derive(Clone)]
pub struct ManagedHost {
id: String,
endpoint: String,
pub handler: Handler,
context: tera::Context,
host_properties: Option<HostProperties>,
secret_providers: Option<SecretProvidersPool>,
}
impl ManagedHost {
pub fn new(
id: String,
endpoint: &str,
handler: Handler,
host_vars: Option<HashMap<String, String>>,
host_properties: Option<HostProperties>,
secret_providers: Option<SecretProvidersPool>,
) -> ManagedHost {
let context = match host_vars {
Some(content) => match tera::Context::from_serialize(content) {
Ok(context) => context,
Err(details) => {
error!("Failed to create Tera context : {:?}", details);
tera::Context::new()
}
},
None => tera::Context::new(),
};
ManagedHost {
id,
endpoint: endpoint.to_string(),
handler,
context,
host_properties,
secret_providers: secret_providers.clone(),
}
}
pub fn from(
id: String,
endpoint: &str,
handler: Handler,
vars: Option<impl IntoIterator<Item = (String, String)>>,
host_properties: Option<HostProperties>,
secret_providers: Option<SecretProvidersPool>,
) -> ManagedHost {
let final_vars = match vars {
Some(vars_list) => {
let mut final_vars: HashMap<String, String> = HashMap::new();
for (key, value) in vars_list.into_iter() {
final_vars.insert(key, value);
}
Some(final_vars)
}
None => None,
};
ManagedHost {
id,
endpoint: endpoint.to_string(),
handler,
context: tera::Context::from_serialize(final_vars).unwrap(),
host_properties,
secret_providers: secret_providers.clone(),
}
}
pub fn id(&self) -> &str {
&self.id
}
pub fn add_var(&mut self, key: String, value: String) {
self.context.insert(key, &value);
}
pub fn set_host_properties(&mut self, host_properties: Option<HostProperties>) {
self.host_properties = host_properties;
}
pub fn collect_properties(&mut self) -> Result<(), RegentError> {
match HostProperties::collect_dynamically(&mut self.handler) {
Ok(host_properties) => {
self.host_properties = Some(host_properties);
Ok(())
}
Err(details) => Err(details),
}
}
pub fn get_host_properties(&self) -> &Option<HostProperties> {
&self.host_properties
}
pub fn connect(&mut self) -> Result<(), RegentError> {
self.handler.connect(&self.endpoint)
}
pub fn is_connected(&mut self) -> bool {
self.handler.is_connected()
}
pub fn disconnect(&mut self) -> Result<(), RegentError> {
self.handler.disconnect()
}
pub async fn assess_compliance(
&mut self,
expected_state: &ExpectedState,
) -> Result<ManagedHostStatus, RegentError> {
if !self.is_connected() {
return Err(RegentError::NotConnectedToHost);
}
let mut already_compliant = true;
let mut final_remediations_list: Vec<Remediation> = Vec::new();
for attribute in expected_state.attributes.clone().iter_mut() {
let span = span!(Level::INFO, "attribute", name = attribute.name());
let _enter = span.enter();
match attribute.consider_context(&self.context) {
Ok(context_aware_attribute) => {
match context_aware_attribute
.assess(
&mut self.handler,
&self.host_properties,
&self.secret_providers,
)
.await
{
Ok(attribute_compliance) => {
if let AttributeComplianceAssessment::NonCompliant(remediations) =
attribute_compliance
{
already_compliant = false;
final_remediations_list.extend(remediations);
}
}
Err(details) => {
return Err(details);
}
}
}
Err(details) => {
let content = match &details {
RegentError::FailureToConsiderContext(content) => content,
_ => &format!("{:?}", details),
};
error!("{}", content);
return Err(details);
}
}
}
if already_compliant {
Ok(ManagedHostStatus::already_compliant())
} else {
Ok(ManagedHostStatus::not_compliant(final_remediations_list))
}
}
pub async fn reach_compliance(
&mut self,
expected_state: &ExpectedState,
) -> Result<ManagedHostStatus, RegentError> {
if !self.is_connected() {
return Err(RegentError::NotConnectedToHost);
}
let mut final_host_status = HostStatus::AlreadyCompliant;
let mut reaching_compliance_failed = false;
let mut actions_taken: Vec<Action> = Vec::new();
for attribute in &expected_state.attributes {
let span = span!(Level::INFO, "attribute", name = attribute.name());
let _enter = span.enter();
match attribute.consider_context(&self.context) {
Ok(context_aware_attribute) => {
match context_aware_attribute
.assess(
&mut self.handler,
&self.host_properties,
&self.secret_providers,
)
.await
{
Ok(attribute_compliance) => {
let outcome = attribute_compliance.clone();
match attribute_compliance {
AttributeComplianceAssessment::Compliant => {
info!(target: "run",assesment_outcome = ?outcome, "Attribute already met");
}
AttributeComplianceAssessment::NonCompliant(remediations) => {
warn!(target: "run",assesment_outcome = ?outcome, "Not compliant. Trying to remedy.");
final_host_status = HostStatus::ReachComplianceSuccess;
for remediation in remediations {
match remediation
.reach_compliance(
&mut self.handler,
&self.host_properties,
&self.secret_providers,
)
.await
{
Ok(internal_api_call_outcome) => {
actions_taken.push(Action::from(
remediation.clone(),
Some(internal_api_call_outcome.clone()),
));
match &internal_api_call_outcome {
InternalApiCallOutcome::Success(details) => {
info!(target: "run",remediation_outcome = "Success", "{:?} : {}", remediation, details.clone().unwrap_or("no details".to_string()));
}
InternalApiCallOutcome::AllowedFailure(
details,
) => {
info!(target: "run",remediation_outcome = "AllowedFailure", "Allowed failure occured : {}", details);
}
InternalApiCallOutcome::Failure(details) => {
reaching_compliance_failed = true;
final_host_status =
HostStatus::ReachComplianceFailed;
warn!(
remediation_outcome = "Failure",
"Attribute not met : {}", details
);
break;
}
}
}
Err(details) => {
warn!("Failed to apply remediation");
return Err(details);
}
}
}
if reaching_compliance_failed {
break;
}
}
}
}
Err(details) => {
warn!(reason = ?details, "Failed assessment");
return Err(details);
}
}
}
Err(details) => {
let content = match &details {
RegentError::FailureToConsiderContext(content) => content,
_ => &format!("{:?}", details),
};
error!("{}", content);
return Err(details);
}
}
}
match final_host_status {
HostStatus::AlreadyCompliant => Ok(ManagedHostStatus::already_compliant()),
HostStatus::ReachComplianceFailed => {
Ok(ManagedHostStatus::reach_compliance_failed(actions_taken))
}
_ => Ok(ManagedHostStatus::reach_compliance_success(actions_taken)),
}
}
}
pub trait AssessCompliance<Handler: HostHandler> {
async fn assess_compliance(
&self,
host_handler: &mut Handler,
host_properties: &Option<HostProperties>,
privilege: &Privilege,
optional_secret_provider: &Option<SecretProvidersPool>,
) -> Result<AttributeComplianceAssessment, RegentError>;
}
pub trait ReachCompliance<Handler: HostHandler> {
async fn call(
&self,
host_handler: &mut Handler,
host_properties: &Option<HostProperties>,
optional_secret_provider: &Option<SecretProvidersPool>,
) -> Result<InternalApiCallOutcome, RegentError>;
}
#[derive(Serialize, Deserialize)]
pub enum AttributeLevelOperationOutcome {
AlreadyCompliant,
NotCompliant(Vec<Remediation>),
ReachComplianceFailed(InternalApiCallOutcome),
ComplianceReachedWithAllowedFailure(InternalApiCallOutcome),
ComplianceReached(Vec<(Remediation, InternalApiCallOutcome)>),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum InternalApiCallOutcome {
Success(Option<String>),
Failure(String),
AllowedFailure(String),
}