CH-J Toolspamamahala ng server sa pamamagitan ng SSH
Menu
Nailathalang source

CH-J Tools

I-browse ang mga direktoryo at file ng isang partikular na release ng application.

I-download ang source ZIP
CH-J / Server Manager 1704 file
0.1.2-beta Idinagdag: · Binuo:
policy.rs Idinagdag: Hindi naitala ang petsa 24.1 KB · 576 linya
Code I-download ang file
1//! Per-user SSH policy in one CH-J managed sshd drop-in (port of `users/sshPolicy.js`).
2//! The `# chj-policy` lines are the source of truth; the directives are regenerated from them,
3//! so any manual edit is detected. `sshd -T` output is evaluated like sshd's `allowed_user()`.
5use crate::core::error::CoreError;
6use serde::Serialize;
7use serde_json::{json, Map, Value};
8use std::collections::BTreeMap;
9use std::net::IpAddr;
11pub const MANAGED_PATH: &str = "/etc/ssh/sshd_config.d/01-chj-users.conf";
12const HEADER: [&str; 2] = [
13 "# CH-J Server Manager - chj.users managed SSH access policy.",
14 "# Generated file. CH-J refuses to overwrite it after manual edits; keep custom rules in other files.",
15];
16pub const IPV4_ALL: &str = "0.0.0.0/0";
17pub const IPV6_ALL: &str = "::/0";
18/// Windows OpenSSH rewrites "/" to "\" in AllowUsers/DenyUsers, so address-family wildcards are used there.
19pub const WINDOWS_IPV4_PATTERN: &str = "*.*.*.*";
20pub const WINDOWS_IPV6_PATTERN: &str = "*:*";
21const MANAGED_HOST_PATTERNS: [&str; 4] = [IPV4_ALL, IPV6_ALL, WINDOWS_IPV4_PATTERN, WINDOWS_IPV6_PATTERN];
22const MULTI_VALUE_KEYS: [&str; 5] = ["denyusers", "allowusers", "denygroups", "allowgroups", "listenaddress"];
24#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub struct Policy {
27 pub access: String,
28 pub password: String,
29 pub pubkey: String,
30 pub ipv4: String,
31 pub ipv6: String,
32 pub permit_root_login: String,
35impl Default for Policy {
36 fn default() -> Self {
37 Policy { access: "allow".into(), password: "inherit".into(), pubkey: "inherit".into(), ipv4: "allow".into(), ipv6: "allow".into(), permit_root_login: "inherit".into() }
38 }
41impl Policy {
42 pub fn to_value(&self) -> Value {
43 serde_json::to_value(self).unwrap_or_default()
44 }
46 fn get(&self, key: &str) -> &str {
47 match key {
48 "access" => &self.access,
49 "password" => &self.password,
50 "pubkey" => &self.pubkey,
51 "ipv4" => &self.ipv4,
52 "ipv6" => &self.ipv6,
53 _ => &self.permit_root_login,
54 }
55 }
57 pub fn is_default(&self) -> bool {
58 *self == Policy::default()
59 }
62const POLICY_VALUES: [(&str, &[&str]); 6] = [
63 ("access", &["allow", "deny"]),
64 ("password", &["inherit", "yes", "no"]),
65 ("pubkey", &["inherit", "yes", "no"]),
66 ("ipv4", &["allow", "deny"]),
67 ("ipv6", &["allow", "deny"]),
68 ("permitRootLogin", &["inherit", "no", "prohibit-password", "forced-commands-only", "yes"]),
69];
70const BASE_POLICY_KEYS: [&str; 5] = ["access", "password", "pubkey", "ipv4", "ipv6"];
72fn invalid(message: String) -> CoreError {
73 CoreError::new("USERS_SSH_POLICY_INVALID", message)
76/// `username`: `None` skips the root-only check (Windows uses a placeholder name).
77pub fn normalize_policy(input: &Value, username: Option<&str>) -> Result<Policy, CoreError> {
78 let mut values: BTreeMap<&str, String> = BTreeMap::new();
79 for (key, allowed) in POLICY_VALUES {
80 let default = Policy::default().get(key).to_string();
81 let value = match input.get(key) {
82 None | Some(Value::Null) => default,
83 Some(Value::String(s)) => s.clone(),
84 Some(other) => other.to_string(),
85 };
86 if !allowed.contains(&value.as_str()) {
87 return Err(invalid(format!("Invalid SSH policy value for {key}.")));
88 }
89 values.insert(key, value);
90 }
91 let policy = Policy {
92 access: values["access"].clone(),
93 password: values["password"].clone(),
94 pubkey: values["pubkey"].clone(),
95 ipv4: values["ipv4"].clone(),
96 ipv6: values["ipv6"].clone(),
97 permit_root_login: values["permitRootLogin"].clone(),
98 };
99 if let Some(name) = username {
100 if name != "root" && policy.permit_root_login != "inherit" {
101 return Err(invalid("PermitRootLogin can only be set for the root account.".into()));
102 }
103 }
104 Ok(policy)
107pub fn policy_comment(username: &str, policy: &Policy) -> String {
108 let mut keys: Vec<&str> = BASE_POLICY_KEYS.to_vec();
109 if policy.permit_root_login != "inherit" {
110 keys.push("permitRootLogin");
111 }
112 format!("# chj-policy {username} {}", keys.iter().map(|k| format!("{k}={}", policy.get(k))).collect::<Vec<_>>().join(" "))
115pub type Policies = BTreeMap<String, Policy>;
117/// Sorted like JavaScript `localeCompare` for the ASCII names allowed here.
118fn sorted(policies: &Policies) -> Vec<(&String, &Policy)> {
119 let mut entries: Vec<(&String, &Policy)> = policies.iter().filter(|(_, p)| !p.is_default()).collect();
120 entries.sort_by(|a, b| locale_cmp(a.0, b.0));
121 entries
124/// ICU-like ordering for `[a-z0-9_.-]` names: punctuation before digits before letters.
125pub fn locale_cmp(left: &str, right: &str) -> std::cmp::Ordering {
126 let weight = |c: char| -> (u8, char) {
127 match c {
128 '_' => (0, c),
129 '-' => (1, c),
130 '.' => (2, c),
131 ' ' => (0, c),
132 '0'..='9' => (3, c),
133 _ => (4, c.to_ascii_lowercase()),
134 }
135 };
136 let a: Vec<(u8, char)> = left.chars().map(weight).collect();
137 let b: Vec<(u8, char)> = right.chars().map(weight).collect();
138 a.cmp(&b).then_with(|| left.cmp(right))
141pub fn generate_managed_config(policies: &Policies) -> String {
142 let entries = sorted(policies);
143 if entries.is_empty() {
144 return String::new();
145 }
146 let mut lines: Vec<String> = HEADER.iter().map(|s| s.to_string()).collect();
147 for (name, policy) in &entries {
148 lines.push(policy_comment(name, policy));
149 }
150 for (name, policy) in &entries {
151 if policy.access == "deny" {
152 lines.push(format!("DenyUsers {name}"));
153 }
154 if policy.ipv4 == "deny" {
155 lines.push(format!("DenyUsers {name}@{IPV4_ALL}"));
156 }
157 if policy.ipv6 == "deny" {
158 lines.push(format!("DenyUsers {name}@{IPV6_ALL}"));
159 }
160 }
161 for (name, policy) in &entries {
162 if policy.password == "inherit" && policy.pubkey == "inherit" && policy.permit_root_login == "inherit" {
163 continue;
164 }
165 lines.push(format!("Match User {name}"));
166 if policy.permit_root_login != "inherit" {
167 lines.push(format!("\tPermitRootLogin {}", policy.permit_root_login));
168 }
169 if policy.password != "inherit" {
170 lines.push(format!("\tPasswordAuthentication {}", policy.password));
171 if policy.password == "no" {
172 lines.push("\tKbdInteractiveAuthentication no".into());
173 }
174 }
175 if policy.pubkey != "inherit" {
176 lines.push(format!("\tPubkeyAuthentication {}", policy.pubkey));
177 }
178 }
179 // Ends any Match context for files that sshd reads after this one.
180 lines.push("Match all".into());
181 format!("{}\n", lines.join("\n"))
184pub fn is_policy_name(name: &str) -> bool {
185 let mut chars = name.chars();
186 matches!(chars.next(), Some(c) if c.is_ascii_lowercase() || c == '_') && name.len() <= 32 && chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-')
189/// Parses `key=value` pairs of a policy comment.
190pub fn policy_pairs(text: &str) -> Value {
191 let mut map = Map::new();
192 for pair in text.split(' ') {
193 let mut parts = pair.split('=');
194 let key = parts.next().unwrap_or("").to_string();
195 let value = parts.next().map(|v| json!(v)).unwrap_or(Value::Null);
196 map.insert(key, value);
197 }
198 Value::Object(map)
201pub struct ManagedConfig {
202 pub policies: Policies,
203 pub consistent: bool,
206pub fn parse_managed_config(text: &str) -> ManagedConfig {
207 let mut policies = Policies::new();
208 for line in text.split('\n') {
209 let line = line.strip_suffix('\r').unwrap_or(line);
210 let Some(rest) = line.strip_prefix("# chj-policy ") else { continue };
211 let Some((name, values)) = rest.split_once(' ') else { continue };
212 if !is_policy_name(name) || values.is_empty() {
213 continue;
214 }
215 match normalize_policy(&policy_pairs(values), Some(name)) {
216 Ok(policy) => {
217 policies.insert(name.to_string(), policy);
218 }
219 Err(_) => return ManagedConfig { policies: Policies::new(), consistent: false },
220 }
221 }
222 let consistent = text.is_empty() || generate_managed_config(&policies) == text;
223 ManagedConfig { policies, consistent }
226#[derive(Debug, Clone, Default, PartialEq)]
227pub struct Effective {
228 /// Single-valued keys hold a string, multi-valued keys an array (as 0.1.0).
229 pub values: BTreeMap<String, Value>,
230 pub errors: Vec<String>,
233impl Effective {
234 pub fn get(&self, key: &str) -> Option<&str> {
235 self.values.get(key).and_then(Value::as_str)
236 }
237 pub fn list(&self, key: &str) -> Vec<String> {
238 self.values.get(key).and_then(Value::as_array).map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).unwrap_or_default()
239 }
242pub fn parse_effective(text: &str) -> Effective {
243 let mut result = Effective::default();
244 for raw in text.split('\n') {
245 let line = raw.strip_suffix('\r').unwrap_or(raw).trim();
246 if line.is_empty() {
247 continue;
248 }
249 let parsed = line.split_once(' ').filter(|(k, _)| !k.is_empty() && k.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()));
250 let Some((key, value)) = parsed else {
251 result.errors.push(line.chars().take(300).collect());
252 continue;
253 };
254 if MULTI_VALUE_KEYS.contains(&key) {
255 let entry = result.values.entry(key.to_string()).or_insert_with(|| json!([]));
256 if let Some(list) = entry.as_array_mut() {
257 list.push(json!(value.trim()));
258 }
259 } else if !result.values.contains_key(key) {
260 result.values.insert(key.to_string(), json!(value));
261 }
262 }
263 result
266/// sshd-style wildcard match (`*`, `?`), case-sensitive.
267pub fn wildcard_match(pattern: &str, value: &str) -> bool {
268 fn go(p: &[char], v: &[char]) -> bool {
269 match (p.first(), v.first()) {
270 (None, None) => true,
271 (Some('*'), _) => go(&p[1..], v) || (!v.is_empty() && go(p, &v[1..])),
272 (Some('?'), Some(_)) => go(&p[1..], &v[1..]),
273 (Some(a), Some(b)) if a == b => go(&p[1..], &v[1..]),
274 _ => false,
275 }
276 }
277 let p: Vec<char> = pattern.chars().collect();
278 let v: Vec<char> = value.chars().collect();
279 go(&p, &v)
282fn ip_family(value: &str) -> u8 {
283 match value.parse::<IpAddr>() {
284 Ok(IpAddr::V4(_)) => 4,
285 Ok(IpAddr::V6(_)) => 6,
286 Err(_) => 0,
287 }
290fn in_subnet(network: IpAddr, bits: u32, address: IpAddr) -> Option<bool> {
291 match (network, address) {
292 (IpAddr::V4(n), IpAddr::V4(a)) => {
293 if bits > 32 {
294 return None;
295 }
296 let mask = if bits == 0 { 0 } else { u32::MAX << (32 - bits) };
297 Some(u32::from(n) & mask == u32::from(a) & mask)
298 }
299 (IpAddr::V6(n), IpAddr::V6(a)) => {
300 if bits > 128 {
301 return None;
302 }
303 let mask = if bits == 0 { 0 } else { u128::MAX << (128 - bits) };
304 Some(u128::from(n) & mask == u128::from(a) & mask)
305 }
306 _ => Some(false),
307 }
310/// `Some(bool)`, or `None` when the host part needs DNS names CH-J cannot evaluate.
311pub fn host_matches(pattern: &str, address: &str) -> Option<bool> {
312 if pattern.is_empty() {
313 return Some(true);
314 }
315 let family = ip_family(address);
316 if let Some((network, bits)) = pattern.split_once('/') {
317 let network_ip = network.parse::<IpAddr>().ok()?;
318 let bits: u32 = bits.parse().ok().filter(|_| bits.chars().all(|c| c.is_ascii_digit()))?;
319 let network_family = ip_family(network);
320 if network_family != family {
321 return Some(false);
322 }
323 return in_subnet(network_ip, bits, address.parse().ok()?);
324 }
325 if ip_family(pattern) != 0 {
326 return Some(ip_family(pattern) == family && pattern.to_lowercase() == address.to_lowercase());
327 }
328 if pattern.chars().all(|c| c.is_ascii_hexdigit() || ":.*?".contains(c)) {
329 return Some(wildcard_match(&pattern.to_lowercase(), &address.to_lowercase()));
330 }
331 None
334fn user_entry_matches(entry: &str, username: &str, address: &str) -> Option<bool> {
335 if entry.starts_with('!') {
336 return None;
337 }
338 let (user_pattern, host) = match entry.find('@') {
339 Some(at) => (&entry[..at], Some(&entry[at + 1..])),
340 None => (entry, None),
341 };
342 if !wildcard_match(user_pattern, username) {
343 return Some(false);
344 }
345 match host {
346 Some(host) => host_matches(host, address),
347 None => Some(true),
348 }
351fn method_satisfiable(authentication_methods: Option<&str>, method: &str) -> bool {
352 let value = authentication_methods.unwrap_or("any").trim();
353 if value.is_empty() || value == "any" {
354 return true;
355 }
356 let accepted = |item: &str| {
357 if method == "password" {
358 item == "password" || item == "keyboard-interactive" || item.strip_prefix("keyboard-interactive:").is_some_and(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_lowercase()))
359 } else {
360 item == "publickey"
361 }
362 };
363 value.split_whitespace().any(|list| list.split(',').all(accepted))
366pub struct AccessContext<'a> {
367 pub username: &'a str,
368 pub groups: &'a [String],
369 pub address: &'a str,
370 pub method: Option<&'a str>,
371 pub keyboard_interactive: bool,
374#[derive(Debug, Clone)]
375pub struct Evaluation {
376 /// `Some(true/false)` or `None` (uncertain).
377 pub allowed: Option<bool>,
378 pub reasons: Vec<Value>,
379 pub methods: Value,
380 pub method_allowed: Option<bool>,
383pub fn evaluate_access(effective: &Effective, context: &AccessContext) -> Evaluation {
384 let username = context.username;
385 let mut reasons: Vec<Value> = Vec::new();
386 let mut allowed: Option<bool> = Some(true);
387 let consider = |result: Option<bool>, reason: Value, allowed: &mut Option<bool>, reasons: &mut Vec<Value>| match result {
388 Some(true) => {
389 *allowed = Some(false);
390 reasons.push(reason);
391 }
392 None if *allowed != Some(false) => {
393 *allowed = None;
394 let mut uncertain = reason;
395 uncertain["uncertain"] = json!(true);
396 reasons.push(uncertain);
397 }
398 _ => {}
399 };
400 for entry in effective.list("denyusers") {
401 let managed = entry == username || MANAGED_HOST_PATTERNS.iter().any(|p| entry == format!("{username}@{p}"));
402 consider(user_entry_matches(&entry, username, context.address), json!({ "rule": "DenyUsers", "entry": entry, "managed": managed }), &mut allowed, &mut reasons);
403 }
404 let allow_users = effective.list("allowusers");
405 if !allow_users.is_empty() {
406 let results: Vec<Option<bool>> = allow_users.iter().map(|e| user_entry_matches(e, username, context.address)).collect();
407 if !results.contains(&Some(true)) {
408 consider(if results.contains(&None) { None } else { Some(true) }, json!({ "rule": "AllowUsers", "entry": "not listed", "managed": false }), &mut allowed, &mut reasons);
409 }
410 }
411 for pattern in effective.list("denygroups") {
412 if context.groups.iter().any(|g| wildcard_match(&pattern, g)) {
413 consider(Some(true), json!({ "rule": "DenyGroups", "entry": pattern, "managed": false }), &mut allowed, &mut reasons);
414 }
415 }
416 let allow_groups = effective.list("allowgroups");
417 if !allow_groups.is_empty() && !context.groups.iter().any(|g| allow_groups.iter().any(|p| wildcard_match(p, g))) {
418 consider(Some(true), json!({ "rule": "AllowGroups", "entry": "not a member", "managed": false }), &mut allowed, &mut reasons);
419 }
420 let root_login = match effective.get("permitrootlogin") {
421 Some("without-password") => "prohibit-password",
422 Some(v) => v,
423 None => "yes",
424 };
425 let password = effective.get("passwordauthentication") == Some("yes")
426 || (context.keyboard_interactive && (effective.get("kbdinteractiveauthentication") == Some("yes") || effective.get("challengeresponseauthentication") == Some("yes")));
427 let pubkey = effective.get("pubkeyauthentication") != Some("no");
428 let methods_password = password && (username != "root" || root_login == "yes") && method_satisfiable(effective.get("authenticationmethods"), "password");
429 // forced-commands-only allows keys only with command="..." options; reported as key login with restriction.
430 let methods_pubkey = pubkey && (username != "root" || ["yes", "prohibit-password", "forced-commands-only"].contains(&root_login)) && method_satisfiable(effective.get("authenticationmethods"), "publickey");
431 if username == "root" && root_login == "no" {
432 consider(Some(true), json!({ "rule": "PermitRootLogin", "entry": "no", "managed": false }), &mut allowed, &mut reasons);
433 }
434 let method_allowed = context.method.map(|m| {
435 let value = if m == "password" { methods_password } else { methods_pubkey };
436 match allowed {
437 Some(true) => Some(value),
438 Some(false) => Some(false),
439 None => None,
440 }
441 });
442 Evaluation { allowed, reasons, methods: json!({ "password": methods_password, "publickey": methods_pubkey }), method_allowed: method_allowed.flatten() }
445impl Evaluation {
446 pub fn allowed_value(&self) -> Value {
447 self.allowed.map(Value::from).unwrap_or(Value::Null)
448 }
451pub fn listener_families(effective: &Effective) -> Value {
452 let family = effective.get("addressfamily").unwrap_or("any");
453 let addresses = effective.list("listenaddress");
454 let has = |version: u8| {
455 addresses.iter().any(|entry| {
456 let host = if let Some(rest) = entry.strip_prefix('[') { rest.split(']').next().unwrap_or("").to_string() } else { strip_port(entry) };
457 ip_family(&host) == version
458 })
459 };
460 json!({
461 "ipv4": if family == "inet6" { false } else if addresses.is_empty() { true } else { has(4) },
462 "ipv6": if family == "inet" { false } else if addresses.is_empty() { true } else { has(6) }
463 })
466/// `entry.replace(/:\d+$/, "")`.
467fn strip_port(entry: &str) -> String {
468 if let Some(index) = entry.rfind(':') {
469 let tail = &entry[index + 1..];
470 if !tail.is_empty() && tail.chars().all(|c| c.is_ascii_digit()) {
471 return entry[..index].to_string();
472 }
473 }
474 entry.to_string()
477pub struct VerifyOptions<'a> {
478 pub keyboard_interactive: bool,
479 pub ipv4_pattern: &'a str,
480 pub ipv6_pattern: &'a str,
483impl Default for VerifyOptions<'_> {
484 fn default() -> Self {
485 VerifyOptions { keyboard_interactive: true, ipv4_pattern: IPV4_ALL, ipv6_pattern: IPV6_ALL }
486 }
489/// Checks that the regenerated configuration really produced the requested effective settings.
490pub fn verify_policy_effect(policy: &Policy, username: &str, effective: &Effective, label: &str, options: &VerifyOptions) -> Vec<String> {
491 let mut problems = Vec::new();
492 let deny = effective.list("denyusers");
493 let unknown = |key: &str| effective.get(key).unwrap_or("unknown").to_string();
494 if policy.access == "deny" && !deny.contains(&username.to_string()) {
495 problems.push(format!("{label}: DenyUsers {username}"));
496 }
497 if policy.ipv4 == "deny" && !deny.contains(&format!("{username}@{}", options.ipv4_pattern)) {
498 problems.push(format!("{label}: DenyUsers {username}@{}", options.ipv4_pattern));
499 }
500 if policy.ipv6 == "deny" && !deny.contains(&format!("{username}@{}", options.ipv6_pattern)) {
501 problems.push(format!("{label}: DenyUsers {username}@{}", options.ipv6_pattern));
502 }
503 if policy.password == "no" {
504 if effective.get("passwordauthentication") != Some("no") {
505 problems.push(format!("{label}: PasswordAuthentication is {}", unknown("passwordauthentication")));
506 }
507 let keyboard = effective.get("kbdinteractiveauthentication").or(effective.get("challengeresponseauthentication"));
508 if options.keyboard_interactive && keyboard == Some("yes") {
509 problems.push(format!("{label}: KbdInteractiveAuthentication is yes"));
510 }
511 }
512 if policy.password == "yes" && effective.get("passwordauthentication") != Some("yes") {
513 problems.push(format!("{label}: PasswordAuthentication is {}", unknown("passwordauthentication")));
514 }
515 if policy.pubkey != "inherit" && effective.get("pubkeyauthentication").unwrap_or("unknown") != policy.pubkey {
516 problems.push(format!("{label}: PubkeyAuthentication is {}", unknown("pubkeyauthentication")));
517 }
518 if policy.permit_root_login != "inherit" {
519 // sshd -T prints the historic alias "without-password" for prohibit-password.
520 let current = match effective.get("permitrootlogin") {
521 Some("without-password") => Some("prohibit-password"),
522 other => other,
523 };
524 if current != Some(policy.permit_root_login.as_str()) {
525 problems.push(format!("{label}: PermitRootLogin is {}", unknown("permitrootlogin")));
526 }
527 }
528 problems
531#[cfg(test)]
532mod tests {
533 use super::*;
535 #[test]
536 fn generate_and_parse_roundtrip() {
537 let mut policies = Policies::new();
538 policies.insert("bob".into(), normalize_policy(&json!({ "access": "deny" }), Some("bob")).unwrap());
539 policies.insert("alice".into(), normalize_policy(&json!({ "password": "no", "ipv6": "deny" }), Some("alice")).unwrap());
540 policies.insert("root".into(), normalize_policy(&json!({ "permitRootLogin": "prohibit-password" }), Some("root")).unwrap());
541 policies.insert("carol".into(), Policy::default());
542 let text = generate_managed_config(&policies);
543 assert_eq!(
544 text,
545 "# CH-J Server Manager - chj.users managed SSH access policy.\n# Generated file. CH-J refuses to overwrite it after manual edits; keep custom rules in other files.\n\
546# chj-policy alice access=allow password=no pubkey=inherit ipv4=allow ipv6=deny\n# chj-policy bob access=deny password=inherit pubkey=inherit ipv4=allow ipv6=allow\n\
547# chj-policy root access=allow password=inherit pubkey=inherit ipv4=allow ipv6=allow permitRootLogin=prohibit-password\n\
548DenyUsers alice@::/0\nDenyUsers bob\nMatch User alice\n\tPasswordAuthentication no\n\tKbdInteractiveAuthentication no\nMatch User root\n\tPermitRootLogin prohibit-password\nMatch all\n"
549 );
550 let parsed = parse_managed_config(&text);
551 assert!(parsed.consistent);
552 assert_eq!(parsed.policies.len(), 3);
553 assert!(!parse_managed_config(&text.replace("Match all", "Match all\n# edited")).consistent);
554 assert!(normalize_policy(&json!({ "permitRootLogin": "yes" }), Some("bob")).is_err());
555 }
557 #[test]
558 fn evaluates_like_sshd() {
559 let effective = parse_effective("passwordauthentication yes\npubkeyauthentication yes\ndenyusers bob@0.0.0.0/0\nallowgroups ssh users\npermitrootlogin without-password\nbroken\n");
560 assert_eq!(effective.errors, vec!["broken".to_string()]);
561 let groups = vec!["ssh users".to_string()];
562 let bob4 = evaluate_access(&effective, &AccessContext { username: "bob", groups: &groups, address: "10.0.0.1", method: Some("password"), keyboard_interactive: true });
563 assert_eq!(bob4.allowed, Some(false));
564 let bob6 = evaluate_access(&effective, &AccessContext { username: "bob", groups: &groups, address: "2001:db8::1", method: Some("password"), keyboard_interactive: true });
565 assert_eq!(bob6.allowed, Some(true));
566 assert_eq!(bob6.method_allowed, Some(true));
567 let root = evaluate_access(&effective, &AccessContext { username: "root", groups: &groups, address: "10.0.0.1", method: Some("password"), keyboard_interactive: true });
568 assert_eq!(root.methods, json!({ "password": false, "publickey": true }));
569 assert_eq!(host_matches("10.0.0.0/8", "10.1.2.3"), Some(true));
570 assert_eq!(host_matches("example.com", "10.1.2.3"), None);
571 assert_eq!(host_matches("*:*", "2001:db8::1"), Some(true));
572 assert_eq!(host_matches("*.*.*.*", "2001:db8::1"), Some(false));
573 assert_eq!(listener_families(&parse_effective("listenaddress 0.0.0.0:22\n")), json!({ "ipv4": true, "ipv6": false }));
574 assert!(locale_cmp("bob", "Carl").is_lt());
575 }
SHA-256dd8957a3923a07868db7a479cd25f35516485570a0de07ab4bd46d85565f7ef9

CH-J Proprietary Software License 1.14 · Ibinibigay ang source sa ilalim ng CH-J Proprietary Software License 1.14. Hindi binabago ng pagiging available nito ang mga tuntunin ng lisensya at hindi ito nagbibigay ng karagdagang karapatan.

SHA-256 ng archiveac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Mga file