Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion rust/agama-software/src/callbacks.rs
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
pub mod commit_download;
mod commit_download;
use agama_utils::{
actor::Handler,
api::question::{Answer, QuestionSpec},
question::{self, ask_question, AskError},
};
pub use commit_download::CommitDownload;
mod security;
pub use security::Security;
use tokio::runtime::Handle;

fn ask_software_question(
handler: &Handler<question::Service>,
question: QuestionSpec,
) -> Result<Answer, AskError> {
Handle::current().block_on(async move { ask_question(handler, question).await })
}
42 changes: 22 additions & 20 deletions rust/agama-software/src/callbacks/commit_download.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
use agama_utils::{actor::Handler, api::question::QuestionSpec, progress, question};
use agama_utils::{
actor::Handler,
api::question::QuestionSpec,
progress,
question::{self, ask_question},
};
use gettextrs::gettext;
use tokio::runtime::Handle;
use zypp_agama::callbacks::pkg_download::{Callback, DownloadError};

use crate::callbacks::ask_software_question;

#[derive(Clone)]
pub struct CommitDownload {
progress: Handler<progress::Service>,
Expand All @@ -29,37 +36,32 @@ impl Callback for CommitDownload {

fn problem(
&self,
name: &str,
name: String,
error: DownloadError,
description: &str,
description: String,
) -> zypp_agama::callbacks::ProblemResponse {
// TODO: make it generic for any problemResponse questions
// TODO: we need support for abort and make it default action
let labels = [gettext("Retry"), gettext("Ignore")];
let actions = [
("Retry", labels[0].as_str()),
("Ignore", labels[1].as_str()),
];
let error_str = error.to_string();
let data = [("package", name), ("error_code", error_str.as_str())];
let question = QuestionSpec::new(description, "software.package_error.provide_error")
.with_actions(&actions)
.with_data(&data);
let result = Handle::current().block_on(async move {
self.questions
.call(question::message::Ask::new(question))
.await
});
let question =
QuestionSpec::new(description.as_str(), "software.package_error.provide_error")
.with_actions(&actions)
.with_data(&[
("package", name.as_str()),
("error_code", error_str.as_str()),
]);
let result = ask_software_question(&self.questions, question);
let Ok(answer) = result else {
tracing::warn!("Failed to ask question {:?}", result);
return zypp_agama::callbacks::ProblemResponse::ABORT;
};

let Some(answer_str) = answer.answer else {
tracing::warn!("No answer provided");
return zypp_agama::callbacks::ProblemResponse::ABORT;
};

answer_str
answer
.action
.as_str()
.parse::<zypp_agama::callbacks::ProblemResponse>()
Expand All @@ -68,8 +70,8 @@ impl Callback for CommitDownload {

fn gpg_check(
&self,
resolvable_name: &str,
_repo_url: &str,
resolvable_name: String,
_repo_url: String,
check_result: zypp_agama::callbacks::pkg_download::GPGCheckResult,
) -> Option<zypp_agama::callbacks::ProblemResponse> {
if check_result == zypp_agama::callbacks::pkg_download::GPGCheckResult::Ok {
Expand Down
202 changes: 202 additions & 0 deletions rust/agama-software/src/callbacks/security.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
use agama_utils::{
actor::Handler,
api::question::QuestionSpec,
question::{self, ask_question},
};
use gettextrs::gettext;
use tokio::runtime::Handle;
use zypp_agama::callbacks::security;

#[derive(Clone)]
pub struct Security {
questions: Handler<question::Service>,
}

impl Security {
pub fn new(questions: Handler<question::Service>) -> Self {
Self { questions }
}
}

impl security::Callback for Security {
fn unsigned_file(&self, file: String, repository_alias: String) -> bool {
// TODO: support for extra_repositories with allow_unsigned config
// TODO: localization for text when parameters in gextext will be solved
let text = if repository_alias.is_empty() {
format!(
"The file {file} is not digitally signed. The origin \
and integrity of the file cannot be verified. Use it anyway?"
)
} else {
format!(
"The file {file} from {repository_alias} is not digitally signed. The origin \
and integrity of the file cannot be verified. Use it anyway?"
)
};
let question = QuestionSpec::new(&text, "software.unsigned_file")
.with_yes_no_actions()
.with_data(&[("filename", file.as_str())]);
let result = Handle::current()
.block_on(async move { ask_question(&self.questions, question).await });
let Ok(answer) = result else {
tracing::warn!("Failed to ask question {:?}", result);
return false;
};

answer.action == "Yes"
}

fn accept_key(
&self,
key_id: String,
key_name: String,
key_fingerprint: String,
_repository_alias: String,
) -> security::GpgKeyTrust {
// TODO: support for extra_repositories with specified gpg key checksum
// TODO: localization with params
let text = format!(
"The key {key_id} ({key_name}) with fingerprint {key_fingerprint} is unknown. \
Do you want to trust this key?"
);
let labels = [gettext("Trust"), gettext("Skip")];
let actions = [("Trust", labels[0].as_str()), ("Skip", labels[1].as_str())];
let question = QuestionSpec::new(&text, "software.import_gpg")
.with_actions(&actions)
.with_data(&[
("id", key_id.as_str()),
("name", key_name.as_str()),
("fingerprint", key_fingerprint.as_str()),
])
.with_default_action("Skip");
let result = Handle::current()
.block_on(async move { ask_question(&self.questions, question).await });
let Ok(answer) = result else {
tracing::warn!("Failed to ask question {:?}", result);
return security::GpgKeyTrust::Reject;
};

answer
.action
.as_str()
.parse::<security::GpgKeyTrust>()
.unwrap_or(security::GpgKeyTrust::Reject)
}

fn unknown_key(&self, file: String, key_id: String, repository_alias: String) -> bool {
// TODO: localization for text when parameters in gextext will be solved
let text = if repository_alias.is_empty() {
format!(
"The file {file} is digitally signed with \
the following unknown GnuPG key: {key_id}. Use it anyway?"
)
} else {
format!(
"The file {file} from {repository_alias} is digitally signed with \
the following unknown GnuPG key: {key_id}. Use it anyway?"
)
};
let question = QuestionSpec::new(&text, "software.unknown_gpg")
.with_yes_no_actions()
.with_data(&[("filename", file.as_str()), ("id", key_id.as_str())]);
let result = Handle::current()
.block_on(async move { ask_question(&self.questions, question).await });
let Ok(answer) = result else {
tracing::warn!("Failed to ask question {:?}", result);
return false;
};

answer.action == "Yes"
}

fn verification_failed(
&self,
file: String,
key_id: String,
key_name: String,
_key_fingerprint: String,
repository_alias: String,
) -> bool {
// TODO: localization for text when parameters in gextext will be solved
let text = if repository_alias.is_empty() {
format!(
"The file {file} is digitally signed with the \
following GnuPG key, but the integrity check failed: {key_id} ({key_name}). \
Use it anyway?"
)
} else {
// TODO: Originally it uses repository url and not alias. Does it matter?
format!(
"The file {file} from {repository_alias} is digitally signed with the \
following GnuPG key, but the integrity check failed: {key_id} ({key_name}). \
Use it anyway?"
)
};
let question = QuestionSpec::new(&text, "software.verification_failed")
.with_yes_no_actions()
.with_data(&[("filename", file.as_str())]);
let result = Handle::current()
.block_on(async move { ask_question(&self.questions, question).await });
let Ok(answer) = result else {
tracing::warn!("Failed to ask question {:?}", result);
return false;
};

answer.action == "Yes"
}

fn checksum_missing(&self, file: String) -> bool {
// TODO: localization for text when parameters in gextext will be solved
let text = format!(
"No checksum for the file {file} was found in the repository. This means that \
although the file is part of the signed repository, the list of checksums \
does not mention this file. Use it anyway?"
);
let question = QuestionSpec::new(&text, "software.digest.no_digest").with_yes_no_actions();
let result = Handle::current()
.block_on(async move { ask_question(&self.questions, question).await });
let Ok(answer) = result else {
tracing::warn!("Failed to ask question {:?}", result);
return false;
};

answer.action == "Yes"
}

fn checksum_unknown(&self, file: String, checksum: String) -> bool {
let text = format!(
"The checksum of the file {file} is \"{checksum}\" but the expected checksum is \
unknown. This means that the origin and integrity of the file cannot be verified. \
Use it anyway?"
);
let question =
QuestionSpec::new(&text, "software.digest.unknown_digest").with_yes_no_actions();
let result = Handle::current()
.block_on(async move { ask_question(&self.questions, question).await });
let Ok(answer) = result else {
tracing::warn!("Failed to ask question {:?}", result);
return false;
};

answer.action == "Yes"
}

fn checksum_wrong(&self, file: String, expected: String, actual: String) -> bool {
let text = format!(
"The expected checksum of file %{file} is \"%{actual}\" but it was expected to be \
\"%{expected}\". The file has changed by accident or by an attacker since the \
creater signed it. Use it anyway?"
);

let question =
QuestionSpec::new(&text, "software.digest.unknown_digest").with_yes_no_actions();
let result = Handle::current()
.block_on(async move { ask_question(&self.questions, question).await });
let Ok(answer) = result else {
tracing::warn!("Failed to ask question {:?}", result);
return false;
};

answer.action == "Yes"
}
}
1 change: 1 addition & 0 deletions rust/agama-software/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ impl ModelAdapter for Model {
self.zypp_sender.send(SoftwareAction::Write {
state: software,
progress,
question: self.question.clone(),
tx,
})?;
Ok(rx.await??)
Expand Down
Loading
Loading