use super::Invocation;
use super::dlp::{BodyWrite, check_body};
use super::redteam::{read, value};
use super::redteam_write::{self as rw, Contexts};
use crate::dlp::body::{self, Input, Write};
use crate::dlp::{self, Kind, views};
use crate::error::{CliError, InterruptContext};
use crate::json::OValue;
use crate::output::{Format, terminal_safe};
use crate::writes::{self, Change};
use scm::dlp::DlpClient;
pub async fn connect(
invocation: &mut Invocation,
format: Option<Format>,
) -> Result<(DlpClient, Format), CliError> {
let connected = invocation
.connect_dlp(None)
.await
.map_err(super::nothing_changed)?;
let format = connected.format(format);
Ok((connected.client, format))
}
pub async fn fetch(
invocation: &Invocation,
dlp: &DlpClient,
kind: Kind,
id: &str,
) -> Result<OValue, CliError> {
match kind {
Kind::FilteringProfile => {
value(&read(invocation, dlp.data_filtering_profiles().get(id, None)).await?)
}
Kind::Pattern => value(&read(invocation, dlp.data_patterns().get(id, None)).await?),
Kind::Profile => value(&read(invocation, dlp.data_profiles().get(id, None)).await?),
Kind::Dictionary => value(&read(invocation, dlp.dictionaries().get(id, None, None)).await?),
}
}
pub async fn current(
invocation: &Invocation,
dlp: &DlpClient,
kind: Kind,
id: &str,
) -> Result<OValue, CliError> {
let record = fetch(invocation, dlp, kind, id)
.await
.map_err(super::nothing_changed)?;
dlp::guard(kind, id, &record)?;
Ok(record)
}
fn name_part(invocation: &Invocation, record: &OValue) -> String {
let mut shown = record.clone();
views::withhold(&mut shown, &invocation.secrets);
match shown.get("name").and_then(OValue::as_str) {
Some(name) if !name.is_empty() => format!(" ({name})"),
_ => String::new(),
}
}
pub fn confirm_replace(
invocation: &mut Invocation,
kind: Kind,
id: &str,
current: &OValue,
body: &serde_json::Value,
force: bool,
) -> Result<bool, CliError> {
let omitted = body::omitted_fields(kind, current, body);
let resource = kind.resource();
let fields = omitted.join(", ");
if force {
if !omitted.is_empty() {
invocation.output.warning(&format!(
"⚠ The replacement omits {} field(s) the {resource} has now: {fields}",
omitted.len()
));
}
return Ok(true);
}
let mut message = format!("Replace {resource} {id}{}?", name_part(invocation, current));
if !omitted.is_empty() {
message.push_str(&format!(
" The replacement omits {} field(s) it has now: {fields}.",
omitted.len()
));
}
rw::confirm(invocation, &message)
}
pub const REGION_HINT: &str = "Dictionary upload rejected. Verify the SCM region display name (for example, --region \"United States\"); region codes such as GLOBAL or us-west-2 are not equivalent.";
fn region_rejected(error: &scm::Error) -> bool {
error.kind() == scm::ErrorKind::Http
&& error.status() == Some(400)
&& error.problem().is_none_or(|problem| {
problem.detail().is_none() && problem.errors().is_none_or(<[_]>::is_empty)
})
}
pub async fn send<T>(
invocation: &mut Invocation,
change: Change,
hint: Option<&str>,
upload: bool,
call: impl AsyncFnMut() -> scm::Result<T>,
) -> Result<T, CliError> {
let contexts = Contexts::plain();
let error = match writes::attempt(call).await {
Ok(answer) => return Ok(answer),
Err(error) => error,
};
if upload && region_rejected(&error) {
invocation.output.write_stderr(&format!("{REGION_HINT}\n"));
}
let mut failure = writes::describe(&error, change, &invocation.secrets);
if let (CliError::Operational(failure), Some(hint)) = (&mut failure, hint)
&& failure
.details
.iter()
.any(|line| *line == change.unknown_line() || *line == change.unreadable_line())
{
failure.details.push(hint.to_owned());
}
Err(match failure {
CliError::Interrupted(InterruptContext::NothingChanged) => {
CliError::Interrupted(contexts.not_sent)
}
CliError::Interrupted(InterruptContext::ChangeMayBeApplied) => {
CliError::Interrupted(contexts.sent)
}
other => other,
})
}
pub fn ack(
invocation: &mut Invocation,
format: Format,
action: &str,
mut record: OValue,
) -> Result<(), CliError> {
views::withhold(&mut record, &invocation.secrets);
views::render_ack(&mut invocation.output, format, action, &record)
}
pub fn done(write: Write) -> &'static str {
match write {
Write::Create => "created",
Write::Replace => "replaced",
Write::Patch => "patched",
}
}
pub async fn body_write(invocation: &mut Invocation, plan: &BodyWrite) -> Result<(), CliError> {
let mut request = body::read_input(&plan.input, plan.texts, &invocation.output)?;
if !matches!(plan.input, Input::Ready(_)) {
check_body(plan, &request)?;
}
let (dlp, format) = connect(invocation, plan.format).await?;
let id = plan.id.as_deref();
let record = match id {
Some(id) => Some(current(invocation, &dlp, plan.kind, id).await?),
None => None,
};
if (plan.fill_type || plan.fill_technique)
&& let Some(record) = &record
{
body::complete_pattern(&mut request, record, plan.fill_type, plan.fill_technique);
body::check(plan.kind, plan.write, id, &request, false)?;
}
if let Some(rules) = &plan.rules {
let patterns = super::dlp_profiles::resolve(invocation, &dlp, &rules.pattern_ids).await?;
request["detection_rules"] =
body::profile_rules(&patterns, &rules.combinator, &rules.confidence)?;
body::check(plan.kind, plan.write, id, &request, false)?;
}
if plan.write == Write::Replace
&& let (Some(id), Some(record)) = (id, &record)
&& !confirm_replace(invocation, plan.kind, id, record, &request, plan.force)?
{
return Ok(());
}
let answer = match plan.kind {
Kind::Pattern => {
super::dlp_patterns::send(invocation, &dlp, plan.write, id, &request).await?
}
Kind::Profile => {
super::dlp_profiles::send(invocation, &dlp, plan.write, id, &request).await?
}
Kind::FilteringProfile => {
super::dlp_filtering::send(invocation, &dlp, id.unwrap_or_default(), &request).await?
}
Kind::Dictionary => {
super::dlp_dictionaries::patch(invocation, &dlp, id.unwrap_or_default(), &request)
.await?
}
};
invocation.committed = true;
ack(invocation, format, done(plan.write), answer)
}
pub async fn delete(
invocation: &mut Invocation,
kind: Kind,
id: &str,
force: bool,
) -> Result<(), CliError> {
let (dlp, _) = connect(invocation, None).await?;
let record = current(invocation, &dlp, kind, id).await?;
if !force {
let message = format!(
"Delete {} {id}{}?",
kind.resource(),
name_part(invocation, &record)
);
if !rw::confirm(invocation, &message)? {
return Ok(());
}
}
let change = kind.change("deleted");
match kind {
Kind::Pattern => {
let patterns = dlp.data_patterns();
send(invocation, change, None, false, async || {
patterns.delete(id, None).await
})
.await?;
}
_ => {
let dictionaries = dlp.dictionaries();
send(invocation, change, None, false, async || {
dictionaries.delete(id, None).await
})
.await?;
}
}
invocation.committed = true;
let verb = if kind == Kind::Pattern {
"archived"
} else {
"deleted"
};
invocation
.output
.success(&format!("{verb} {}", terminal_safe(id)));
Ok(())
}