प्रकाशित सोर्स
सोर्स ZIP डाउनलोड करें
CH-J Tools
किसी विशेष ऐप्लिकेशन रिलीज़ की डायरेक्टरी और फ़ाइलें देखें।
CH-J / Server Manager
1704 फ़ाइलें
0.1.2-beta
जोड़ा गया: · बिल्ड किया गया:
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()`.5
use crate::core::error::CoreError;6
use serde::Serialize;7
use serde_json::{json, Map, Value};8
use std::collections::BTreeMap;9
use std::net::IpAddr;11
pub const MANAGED_PATH: &str = "/etc/ssh/sshd_config.d/01-chj-users.conf";12
const 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
];16
pub const IPV4_ALL: &str = "0.0.0.0/0";17
pub const IPV6_ALL: &str = "::/0";18
/// Windows OpenSSH rewrites "/" to "\" in AllowUsers/DenyUsers, so address-family wildcards are used there.19
pub const WINDOWS_IPV4_PATTERN: &str = "*.*.*.*";20
pub const WINDOWS_IPV6_PATTERN: &str = "*:*";21
const MANAGED_HOST_PATTERNS: [&str; 4] = [IPV4_ALL, IPV6_ALL, WINDOWS_IPV4_PATTERN, WINDOWS_IPV6_PATTERN];22
const MULTI_VALUE_KEYS: [&str; 5] = ["denyusers", "allowusers", "denygroups", "allowgroups", "listenaddress"];24
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]25
#[serde(rename_all = "camelCase")]26
pub 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,33
}35
impl 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
}39
}41
impl 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
}60
}62
const 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
];70
const BASE_POLICY_KEYS: [&str; 5] = ["access", "password", "pubkey", "ipv4", "ipv6"];72
fn invalid(message: String) -> CoreError {73
CoreError::new("USERS_SSH_POLICY_INVALID", message)74
}76
/// `username`: `None` skips the root-only check (Windows uses a placeholder name).77
pub 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)105
}107
pub 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(" "))113
}115
pub type Policies = BTreeMap<String, Policy>;117
/// Sorted like JavaScript `localeCompare` for the ASCII names allowed here.118
fn 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
entries122
}124
/// ICU-like ordering for `[a-z0-9_.-]` names: punctuation before digits before letters.125
pub 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))139
}141
pub 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"))182
}184
pub 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 == '-')187
}189
/// Parses `key=value` pairs of a policy comment.190
pub 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)199
}201
pub struct ManagedConfig {202
pub policies: Policies,203
pub consistent: bool,204
}206
pub 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 }224
}226
#[derive(Debug, Clone, Default, PartialEq)]227
pub 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>,231
}233
impl 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
}240
}242
pub 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
result264
}266
/// sshd-style wildcard match (`*`, `?`), case-sensitive.267
pub 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)280
}282
fn ip_family(value: &str) -> u8 {283
match value.parse::<IpAddr>() {284
Ok(IpAddr::V4(_)) => 4,285
Ok(IpAddr::V6(_)) => 6,286
Err(_) => 0,287
}288
}290
fn 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
}308
}310
/// `Some(bool)`, or `None` when the host part needs DNS names CH-J cannot evaluate.311
pub 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
None332
}334
fn 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
}349
}351
fn 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))364
}366
pub 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,372
}374
#[derive(Debug, Clone)]375
pub 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>,381
}383
pub 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() }443
}445
impl Evaluation {446
pub fn allowed_value(&self) -> Value {447
self.allowed.map(Value::from).unwrap_or(Value::Null)448
}449
}451
pub 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) == version458
})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
})464
}466
/// `entry.replace(/:\d+$/, "")`.467
fn 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()475
}477
pub struct VerifyOptions<'a> {478
pub keyboard_interactive: bool,479
pub ipv4_pattern: &'a str,480
pub ipv6_pattern: &'a str,481
}483
impl Default for VerifyOptions<'_> {484
fn default() -> Self {485
VerifyOptions { keyboard_interactive: true, ipv4_pattern: IPV4_ALL, ipv6_pattern: IPV6_ALL }486
}487
}489
/// Checks that the regenerated configuration really produced the requested effective settings.490
pub 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
problems529
}531
#[cfg(test)]532
mod 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\548
DenyUsers 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
}576
}SHA-256
dd8957a3923a07868db7a479cd25f35516485570a0de07ab4bd46d85565f7ef9CH-J Proprietary Software License 1.14 · सोर्स CH-J Proprietary Software License 1.14 के तहत उपलब्ध कराया जाता है। इसकी उपलब्धता लाइसेंस की शर्तें नहीं बदलती और न ही अतिरिक्त अधिकार देती है।
आर्काइव SHA-256
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f