pray_core/client_trust/
commands.rs1use std::path::Path;
2
3use crate::{PrayError, PrayResult};
4
5use super::git::repository_signing_keys;
6use super::policy::{
7 append_missing_keys, best_rule, format_rule_block, load_policy_or_default,
8 mutable_rule_for_match_prefix, normalize_key, save_policy, ClientTrustPolicy, ClientTrustRule,
9};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum TrustListScope {
13 All,
14 Global,
15 Local,
16}
17
18pub fn show_policy_toml(home: &Path) -> PrayResult<String> {
19 let policy = load_policy_or_default(home)?;
20 let text =
21 toml::to_string_pretty(&policy).map_err(|error| PrayError::Manifest(error.to_string()))?;
22 Ok(text)
23}
24
25pub fn list_policy(
26 home: &Path,
27 scope: TrustListScope,
28 source_url: Option<&str>,
29) -> PrayResult<String> {
30 let policy = load_policy_or_default(home)?;
31 let mut output = String::new();
32
33 if let Some(source) = source_url {
34 output.push_str(&format!("source: {source}\n\n"));
35 if matches!(scope, TrustListScope::All | TrustListScope::Global) {
36 output.push_str(&format_rule_block("scope: global", &policy.default));
37 output.push('\n');
38 }
39 if matches!(scope, TrustListScope::All | TrustListScope::Local) {
40 let mut matched: Vec<&ClientTrustRule> = policy
41 .rules
42 .iter()
43 .filter(|rule| {
44 rule.match_prefix
45 .as_deref()
46 .is_some_and(|prefix| source.starts_with(prefix))
47 })
48 .collect();
49 matched.sort_by_key(|rule| {
50 std::cmp::Reverse(rule.match_prefix.as_deref().map(str::len).unwrap_or(0))
51 });
52 if matched.is_empty() {
53 output.push_str("scope: local\n (no matching rules)\n");
54 } else {
55 for rule in matched {
56 let prefix = rule.match_prefix.as_deref().unwrap_or("-");
57 output.push_str(&format_rule_block(
58 &format!("scope: local ({prefix})"),
59 rule,
60 ));
61 }
62 }
63 output.push('\n');
64 }
65 if matches!(scope, TrustListScope::All) {
66 let effective = best_rule(&policy, source);
67 if let Some(prefix) = effective.match_prefix.as_deref() {
68 output.push_str(&format!("effective_scope: local ({prefix})\n"));
69 } else {
70 output.push_str("effective_scope: global\n");
71 }
72 }
73 return Ok(output.trim_end().to_string());
74 }
75
76 if matches!(scope, TrustListScope::All | TrustListScope::Global) {
77 output.push_str(&format_rule_block("scope: global", &policy.default));
78 output.push('\n');
79 }
80 if matches!(scope, TrustListScope::All | TrustListScope::Local) {
81 if policy.rules.is_empty() {
82 output.push_str("scope: local\n (no rules)\n");
83 } else {
84 let mut rules: Vec<&ClientTrustRule> = policy.rules.iter().collect();
85 rules.sort_by(|left, right| left.match_prefix.cmp(&right.match_prefix));
86 for rule in rules {
87 let prefix = rule.match_prefix.as_deref().unwrap_or("-");
88 output.push_str(&format_rule_block(
89 &format!("scope: local ({prefix})"),
90 rule,
91 ));
92 }
93 }
94 }
95 Ok(output.trim_end().to_string())
96}
97
98pub fn add_allowed_signing_key(
99 home: &Path,
100 key: &str,
101 match_prefix: Option<&str>,
102) -> PrayResult<()> {
103 let normalized = normalize_key(key);
104 if normalized.is_empty() {
105 return Err(PrayError::Unsupported("signing key is empty".into()));
106 }
107
108 let mut policy = load_policy_or_default(home)?;
109 let rule = if let Some(prefix) = match_prefix {
110 mutable_rule_for_match_prefix(&mut policy, prefix)
111 } else {
112 &mut policy.default
113 };
114 if !rule
115 .allowed_signing_keys
116 .iter()
117 .any(|existing| normalize_key(existing) == normalized)
118 {
119 rule.allowed_signing_keys.push(normalized);
120 }
121 save_policy(home, &policy)
122}
123
124pub fn remove_allowed_signing_key(
125 home: &Path,
126 key: &str,
127 match_prefix: Option<&str>,
128) -> PrayResult<()> {
129 let normalized = normalize_key(key);
130 if normalized.is_empty() {
131 return Err(PrayError::Unsupported("signing key is empty".into()));
132 }
133
134 let mut policy = load_policy_or_default(home)?;
135 let rule = if let Some(prefix) = match_prefix {
136 mutable_rule_for_match_prefix(&mut policy, prefix)
137 } else {
138 &mut policy.default
139 };
140 let before = rule.allowed_signing_keys.len();
141 rule.allowed_signing_keys
142 .retain(|existing| normalize_key(existing) != normalized);
143 if rule.allowed_signing_keys.len() == before {
144 return Err(PrayError::Unsupported(format!(
145 "signing key not found in allowed_signing_keys for {}",
146 match_prefix.unwrap_or("<default>")
147 )));
148 }
149 save_policy(home, &policy)
150}
151
152pub fn set_require_signed_commit(home: &Path, match_prefix: &str, enabled: bool) -> PrayResult<()> {
153 if match_prefix.trim().is_empty() {
154 return Err(PrayError::Unsupported("match-prefix is empty".into()));
155 }
156 let mut policy = load_policy_or_default(home)?;
157 let rule = mutable_rule_for_match_prefix(&mut policy, match_prefix);
158 rule.require_signed_commit = enabled;
159 save_policy(home, &policy)
160}
161
162pub fn set_allow(home: &Path, match_prefix: &str, allow: bool) -> PrayResult<()> {
163 if match_prefix.trim().is_empty() {
164 return Err(PrayError::Unsupported("match-prefix is empty".into()));
165 }
166 let mut policy = load_policy_or_default(home)?;
167 let rule = mutable_rule_for_match_prefix(&mut policy, match_prefix);
168 rule.allow = allow;
169 save_policy(home, &policy)
170}
171
172pub fn import_signing_keys_from_repository(
173 home: &Path,
174 source_url: &str,
175 repository: &Path,
176 match_prefix: Option<&str>,
177) -> PrayResult<usize> {
178 let keys = repository_signing_keys(home, source_url, repository);
179 if keys.is_empty() {
180 return Err(PrayError::Unsupported(format!(
181 "no commit signing key/fingerprint found for HEAD in {}",
182 repository.display()
183 )));
184 }
185 let mut policy = load_policy_or_default(home)?;
186 let rule = if let Some(prefix) = match_prefix {
187 mutable_rule_for_match_prefix(&mut policy, prefix)
188 } else {
189 &mut policy.default
190 };
191 let added = append_missing_keys(rule, &keys);
192 save_policy(home, &policy)?;
193 Ok(added)
194}
195
196pub fn ensure_policy_file(home: &Path) -> PrayResult<ClientTrustPolicy> {
197 let policy = load_policy_or_default(home)?;
198 if super::policy::trust_policy_path(home).is_file() {
199 return Ok(policy);
200 }
201 save_policy(home, &policy)?;
202 Ok(policy)
203}