Skip to content

Commit c19a902

Browse files
committed
Replace 'configparser' with 'rust-ini' for quoting and stable section order
1 parent 32cd4c9 commit c19a902

File tree

5 files changed

+120
-69
lines changed

5 files changed

+120
-69
lines changed

Cargo.lock

Lines changed: 52 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ path = "src/main.rs"
4242
[dependencies]
4343
anyhow = "1.0.68"
4444
async-recursion = "1.0.2"
45-
configparser = "3.0.2"
4645
docopt = "1.1.1"
4746
hyper = { version = "0.14.23", features = ["full"] }
4847
ldap3 = "0.11.1"
4948
lru_time_cache = "0.11.11"
5049
regex = "1.7.1"
50+
rust-ini = "0.18.0"
5151
sha2 = "0.10.6"
5252
tokio = { version = "1.24.2", features = ["full"] }
5353
tracing = "0.1.37"

README.md

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,18 @@ The server is configured with an INI file, such as:
2323

2424
```ini
2525
[default]
26-
; Which HTTP header to read the %USERNAME% from
27-
username_http_header = X-Ldap-Authz-Username
26+
; Which HTTP header to read %USERNAME% from
27+
username_http_header = "X-Ldap-Authz-Username"
2828

2929
; Example LDAP server configuration. This is for Active Directory,
3030
; and makes a recursive membership query to given group.
31-
ldap_server_url = ldap://dc1.example.test:389
31+
ldap_server_url = "ldap://dc1.example.test:389"
3232
ldap_conn_timeout = 10.0
33-
ldap_bind_dn = CN=service,CN=Users,DC=example,DC=test
34-
ldap_bind_password = password123
35-
ldap_search_base = DC=example,DC=test
36-
ldap_query = (&(objectCategory=Person)(sAMAccountName=%USERNAME%)(memberOf:1.2.840.113556.1.4.1941:=CN=%MY_CUSTOM_VAR%,CN=Users,DC=example,DC=test))
37-
ldap_attribs = displayName, givenName, sn, mail
33+
ldap_bind_dn = "CN=service,CN=Users,DC=example,DC=test"
34+
ldap_bind_password = "password123"
35+
ldap_search_base = "DC=example,DC=test"
36+
ldap_query = "(&(objectCategory=Person)(sAMAccountName=%USERNAME%)(memberOf:1.2.840.113556.1.4.1941:=CN=%MY_CUSTOM_VAR%,CN=Users,DC=example,DC=test))"
37+
ldap_attribs = "displayName, givenName, sn, mail"
3838

3939
; Cache size (these are defaults)
4040
cache_time = 30
@@ -47,31 +47,34 @@ sub_query_join = Main
4747

4848
[users]
4949
; Regular expression to match against the request URI
50-
http_path = /users$
50+
http_path = "/users$"
5151
; Ldap query references variable MY_CUSTOM_VAR above. Set it for this query:
52-
query_vars = MY_CUSTOM_VAR=ACL_Users
52+
query_vars = "MY_CUSTOM_VAR = ACL_Users"
5353
; Fetch additional attributes from LDAP my performing additional queries
5454
; if this one succeeds. See below for their definitions.
55-
sub_queries = is_beta_tester, is_bug_reporter
55+
sub_queries = "is_beta_tester, is_bug_reporter"
5656

5757

5858
[admins]
59-
http_path = /admins$
60-
query_vars = MY_CUSTOM_VAR=ACL_Admins
59+
http_path = "/admins$"
60+
query_vars = "MY_CUSTOM_VAR = ACL_Admins"
6161
; Fictional example: instruct backend app to show debug info for admins
62-
set_attribs_on_success = extraGroups=show_debug_info
62+
set_attribs_on_success = "extraGroups = show_debug_info"
63+
6364

6465
; Internal sub-queries (not matched agains URI as http_path is not defined)
6566
; These examples set additional attributes ("extraGroups") if the user is a
6667
; member of specified groups.
6768

6869
[is_beta_tester]
69-
query_vars = MY_CUSTOM_VAR=ACL_Beta_Testers
70-
set_attribs_on_success = extraGroups=beta_tester
70+
query_vars = "MY_CUSTOM_VAR = Role_Beta_Testers"
71+
set_attribs_on_success = "extraGroups = beta_tester"
7172

7273
[is_bug_reporter]
73-
query_vars = MY_CUSTOM_VAR=ACL_Bug_Reporters
74-
set_attribs_on_success = extraGroups=bug_reporter, extraGroups=show_debug_info
74+
query_vars = "MY_CUSTOM_VAR = Role_Bug_Reporters"
75+
set_attribs_on_success = "extraGroups = bug_reporter, extraGroups = show_debug_info"
76+
; Circular references are pruned, so this nonsense won't crash - it's just useless:
77+
sub_queries = "is_bug_reporter, users"
7578
```
7679

7780
The `[default]` section contains defaults that can be overridden in other sections.
@@ -181,7 +184,7 @@ world-readable. The server itself doesn't need to be able to write
181184
to the configuration file.
182185

183186
Usernames are quoted before being used in LDAP queries, so they (hopefully)
184-
can't be used to inject arbitrary LDAP queries. In any case, it's recommended
187+
can't be used to inject arbitrary LDAP queries. It's recommended
185188
to use a read-only LDAP bind user just in case.
186189

187190
LDAPS is supported (even though the test scripts use plain ldap://), and is

example.ini

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
[default]
2-
; Which HTTP header to read the %USERNAME% from
3-
username_http_header = X-Ldap-Authz-Username
2+
; Which HTTP header to read %USERNAME% from
3+
username_http_header = "X-Ldap-Authz-Username"
44

55
; Example LDAP server configuration. This is for Active Directory,
66
; and makes a recursive membership query to given group.
7-
ldap_server_url = ldap://dc1.example.test:389
7+
ldap_server_url = "ldap://dc1.example.test:389"
88
ldap_conn_timeout = 10.0
9-
ldap_bind_dn = CN=service,CN=Users,DC=example,DC=test
10-
ldap_bind_password = password123
11-
ldap_search_base = DC=example,DC=test
12-
ldap_query = (&(objectCategory=Person)(sAMAccountName=%USERNAME%)(memberOf:1.2.840.113556.1.4.1941:=CN=%MY_CUSTOM_VAR%,CN=Users,DC=example,DC=test))
13-
ldap_attribs = displayName, givenName, sn, mail
9+
ldap_bind_dn = "CN=service,CN=Users,DC=example,DC=test"
10+
ldap_bind_password = "password123"
11+
ldap_search_base = "DC=example,DC=test"
12+
ldap_query = "(&(objectCategory=Person)(sAMAccountName=%USERNAME%)(memberOf:1.2.840.113556.1.4.1941:=CN=%MY_CUSTOM_VAR%,CN=Users,DC=example,DC=test))"
13+
ldap_attribs = "displayName, givenName, sn, mail"
1414

1515
; Cache size (these are defaults)
1616
cache_time = 30
@@ -23,31 +23,31 @@ sub_query_join = Main
2323

2424
[users]
2525
; Regular expression to match against the request URI
26-
http_path = /users$
26+
http_path = "/users$"
2727
; Ldap query references variable MY_CUSTOM_VAR above. Set it for this query:
28-
query_vars = MY_CUSTOM_VAR=ACL_Users
28+
query_vars = "MY_CUSTOM_VAR = ACL_Users"
2929
; Fetch additional attributes from LDAP my performing additional queries
3030
; if this one succeeds. See below for their definitions.
31-
sub_queries = is_beta_tester, is_bug_reporter
31+
sub_queries = "is_beta_tester, is_bug_reporter"
3232

3333

3434
[admins]
35-
http_path = /admins$
36-
query_vars = MY_CUSTOM_VAR=ACL_Admins
35+
http_path = "/admins$"
36+
query_vars = "MY_CUSTOM_VAR = ACL_Admins"
3737
; Fictional example: instruct backend app to show debug info for admins
38-
set_attribs_on_success = extraGroups=show_debug_info
38+
set_attribs_on_success = "extraGroups = show_debug_info"
3939

4040

4141
; Internal sub-queries (not matched agains URI as http_path is not defined)
4242
; These examples set additional attributes ("extraGroups") if the user is a
4343
; member of specified groups.
4444

4545
[is_beta_tester]
46-
query_vars = MY_CUSTOM_VAR=Role_Beta_Testers
47-
set_attribs_on_success = extraGroups=beta_tester
46+
query_vars = "MY_CUSTOM_VAR = Role_Beta_Testers"
47+
set_attribs_on_success = "extraGroups = beta_tester"
4848

4949
[is_bug_reporter]
50-
query_vars = MY_CUSTOM_VAR=Role_Bug_Reporters
51-
set_attribs_on_success = extraGroups=bug_reporter, extraGroups=show_debug_info
50+
query_vars = "MY_CUSTOM_VAR = Role_Bug_Reporters"
51+
set_attribs_on_success = "extraGroups = bug_reporter, extraGroups = show_debug_info"
5252
; Circular references are pruned, so this nonsense won't crash - it's just useless:
53-
sub_queries = is_bug_reporter, users
53+
sub_queries = "is_bug_reporter, users"

src/config.rs

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::collections::HashSet;
33

44
use anyhow::bail;
55
use anyhow::anyhow;
6-
use configparser::ini::Ini;
6+
use ini::Ini;
77
use anyhow::Error;
88
use anyhow::Result;
99
use regex::Regex;
@@ -140,21 +140,19 @@ config_options! {
140140

141141
/// Parse the configuration file
142142
///
143-
/// Returns a vector of ConfigSections, excluding the DEFAULT section, which
143+
/// Returns a vector of ConfigSections, excluding the [default] section, which
144144
/// is used to fill in missing values in the other sections.
145145
pub(crate) fn parse_config(config_file: &str) -> Result<Vec<ConfigSection>, Error>
146146
{
147-
let mut config = Ini::new();
148-
config.load(config_file).map_err(|e| anyhow!("Error loading config file: {}", e))?;
149-
let map = config.get_map_ref();
147+
let mut ini = Ini::load_from_file(config_file)?;
150148

151-
// Get the DEFAULT section
152-
let mut defaults = match map.get("default") {
153-
Some(d) => d,
154-
None => bail!("No 'default' section in config file"),
155-
}.clone();
156-
157-
// Fill in missing values from built-in defaults
149+
// Collect defaults
150+
let mut defaults = HashMap::new();
151+
// ...from the [default] section
152+
if let Some(default_sect) = ini.section(Some("default")) {
153+
defaults.extend(default_sect.iter().map(|(k, v)| (k.to_string(), Some(v.to_string()))));
154+
}
155+
// ..from built-in defaults
158156
for (key, _, def) in CONFIG_OPTIONS.iter() {
159157
if !defaults.contains_key(*key) {
160158
if let Some(def) = def {
@@ -167,18 +165,22 @@ pub(crate) fn parse_config(config_file: &str) -> Result<Vec<ConfigSection>, Erro
167165
let mut res = Vec::new();
168166

169167
// Walk through the sections
170-
for section_name in config.sections() {
171-
let mut sect_map = map.get(section_name.as_str()).unwrap().clone();
168+
for (section_name, sect_props) in ini.iter_mut() {
169+
let section_name = match section_name {
170+
Some(name) => name,
171+
None => { bail!("Options outside of a section are not allowed"); }
172+
};
173+
172174
if seen_sections.contains(&section_name) {
173175
bail!("Duplicate section [{}]", section_name);
174176
} else {
175177
seen_sections.insert(section_name.clone());
176178
}
177179

178180
// Check that no unknown keys are set
179-
let unknown_keys = sect_map.keys()
181+
let unknown_keys = sect_props.iter()
182+
.map(|(key, _)| key)
180183
.filter(|key| !CONFIG_OPTIONS.iter().any(|(k, _, _)| k == key))
181-
.map(|key| key.clone())
182184
.collect::<Vec<_>>();
183185
if !unknown_keys.is_empty() {
184186
bail!("Unknown key(s) in section [{}]: {}", &section_name, unknown_keys.join(", "));
@@ -190,25 +192,26 @@ pub(crate) fn parse_config(config_file: &str) -> Result<Vec<ConfigSection>, Erro
190192

191193
// Apply defaults
192194
for (key, value) in &defaults {
193-
if sect_map.get(key).is_none() {
194-
sect_map.insert(key.clone(), value.clone());
195+
if sect_props.get(key).is_none() {
196+
if let Some(value) = value {
197+
sect_props.insert(key.clone(), value.clone());
198+
}
195199
}
196200
}
197201

198202
// Check that all required keys are set
199203
let missing_keys = CONFIG_OPTIONS.iter()
200-
.filter(|(key, _, _)| !sect_map.contains_key(*key))
204+
.filter(|(key, _, _)| !sect_props.contains_key(*key))
201205
.map(|(key, _, _)| *key)
202206
.filter(|key| key != &"section")
203207
.collect::<Vec<_>>();
204208
if !missing_keys.is_empty() {
205209
bail!("Config option(s) not set in section [{}]: {}", section_name, missing_keys.join(", "));
206210
}
207211

208-
let get = |key: &str| sect_map.get(key)
212+
let get = |key: &str| sect_props.get(key)
209213
.unwrap_or_else(|| panic!("BUG: missing key '{key}' after checking that it's not missing?!"))
210-
.clone()
211-
.expect("BUG? None value for an config value?");
214+
.to_string();
212215

213216
// Compile regex
214217
let http_path = get("http_path");
@@ -240,7 +243,7 @@ pub(crate) fn parse_config(config_file: &str) -> Result<Vec<ConfigSection>, Erro
240243

241244
// Store result
242245
res.push(ConfigSection {
243-
section: section_name.clone(),
246+
section: section_name.to_string(),
244247
http_path: http_path_re,
245248
username_http_header: get("username_http_header"),
246249

0 commit comments

Comments
 (0)