Skip to main content

pray_core/client_trust/
commands.rs

1use 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_require_signed_packages(
163    home: &Path,
164    match_prefix: &str,
165    enabled: bool,
166) -> PrayResult<()> {
167    if match_prefix.trim().is_empty() {
168        return Err(PrayError::Unsupported("match-prefix is empty".into()));
169    }
170    let mut policy = load_policy_or_default(home)?;
171    let rule = mutable_rule_for_match_prefix(&mut policy, match_prefix);
172    rule.require_signed_packages = enabled;
173    save_policy(home, &policy)
174}
175
176pub fn set_allow(home: &Path, match_prefix: &str, allow: bool) -> PrayResult<()> {
177    if match_prefix.trim().is_empty() {
178        return Err(PrayError::Unsupported("match-prefix is empty".into()));
179    }
180    let mut policy = load_policy_or_default(home)?;
181    let rule = mutable_rule_for_match_prefix(&mut policy, match_prefix);
182    rule.allow = allow;
183    save_policy(home, &policy)
184}
185
186pub fn import_signing_keys_from_repository(
187    home: &Path,
188    source_url: &str,
189    repository: &Path,
190    match_prefix: Option<&str>,
191) -> PrayResult<usize> {
192    let keys = repository_signing_keys(home, source_url, repository);
193    if keys.is_empty() {
194        return Err(PrayError::Unsupported(format!(
195            "no commit signing key/fingerprint found for HEAD in {}",
196            repository.display()
197        )));
198    }
199    let mut policy = load_policy_or_default(home)?;
200    let rule = if let Some(prefix) = match_prefix {
201        mutable_rule_for_match_prefix(&mut policy, prefix)
202    } else {
203        &mut policy.default
204    };
205    let added = append_missing_keys(rule, &keys);
206    save_policy(home, &policy)?;
207    Ok(added)
208}
209
210pub fn ensure_policy_file(home: &Path) -> PrayResult<ClientTrustPolicy> {
211    let policy = load_policy_or_default(home)?;
212    if super::policy::trust_policy_path(home).is_file() {
213        return Ok(policy);
214    }
215    save_policy(home, &policy)?;
216    Ok(policy)
217}