Skip to content

Commit 1530d0c

Browse files
committed
issue2551182 - ... allow loading values from external file. flake8 cleanups
Secrets (passwords, secrets) can specify a file using file:// or file:///. The first line of the file is used as the secret. This allows committing config.ini to a VCS. Following settings are changed: [tracker] secret_key [tracker] jwt_secret [rdbms] password [mail] password details: in roundup/configuration.py: Defined SecretMandatoryOptions and SecretNullableOptions. Converted all secret keys and password to one of the above. Also if [mail] username is defined but [mail] password is not it throws an error at load. Cleaned up a couple of methods whose call signature included: def ...(..., settings={}): settings=None and it is set to empty dict inside the method. Also replace exception.message with str(exception) for python3 compatibility. in test/test_config: changed munge_configini to support changing only within a section, replacing keyword text.
1 parent 5ef0bfa commit 1530d0c

File tree

3 files changed

+306
-28
lines changed

3 files changed

+306
-28
lines changed

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ Features:
112112
separated by # (John Rouillard).
113113
- added small utility script to dump dbm based tracker databases
114114
(e.g. db/sessions). (John Rouillard)
115+
- issue2551182 - Enhance configuration module to allow loading values
116+
from an external file. Secrets (passwords, secrets) can specify
117+
file using file:// or file:///. The first line of the file is used
118+
as the secret. This allows committing config.ini to a VCS.
115119

116120

117121
2021-07-13 2.1.0

roundup/configuration.py

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
# used here instead of try/except.
1010
import sys
1111
import getopt
12-
import logging, logging.config
12+
import errno
13+
import logging
14+
import logging.config
1315
import os
1416
import re
1517
import time
@@ -37,6 +39,7 @@
3739
class ConfigurationError(RoundupException):
3840
pass
3941

42+
4043
class ParsingOptionError(ConfigurationError):
4144
def __str__(self):
4245
return self.args[0]
@@ -268,12 +271,12 @@ def load_ini(self, config):
268271
self.set(config.get(self.section, self.setting))
269272
except configparser.InterpolationSyntaxError as e:
270273
raise ParsingOptionError(
271-
_("Error in %(filepath)s with section [%(section)s] at option %(option)s: %(message)s")%{
272-
"filepath": self.config.filepath,
273-
"section": e.section,
274-
"option": e.option,
275-
"message": e.message})
276-
274+
_("Error in %(filepath)s with section [%(section)s] at "
275+
"option %(option)s: %(message)s") % {
276+
"filepath": self.config.filepath,
277+
"section": e.section,
278+
"option": e.option,
279+
"message": str(e)})
277280

278281

279282
class BooleanOption(Option):
@@ -419,6 +422,7 @@ def str2value(self, value):
419422
return _val
420423
raise OptionValueError(self, value, self.class_description)
421424

425+
422426
class IndexerOption(Option):
423427
"""Valid options for indexer"""
424428

@@ -432,6 +436,7 @@ def str2value(self, value):
432436
return _val
433437
raise OptionValueError(self, value, self.class_description)
434438

439+
435440
class MailAddressOption(Option):
436441

437442
"""Email address
@@ -563,6 +568,46 @@ def str2value(self, value):
563568
return value
564569

565570

571+
class SecretOption(Option):
572+
"""A string not beginning with file:// or a file starting with file://
573+
574+
Paths may be either absolute or relative to the HOME.
575+
Value for option is the first line in the file.
576+
It is mean to store secret information in the config file but
577+
allow the config file to be stored in version control without
578+
storing the secret there.
579+
580+
"""
581+
582+
class_description = \
583+
"A string that starts with 'file://' is interpreted as a file path \n" \
584+
"relative to the tracker home. Using 'file:///' defines an absolute \n" \
585+
"path. The first line of the file will be used as the value. Any \n" \
586+
"string that does not start with 'file://' is used as is. It \n" \
587+
"removes any whitespace at the end of the line, so a newline can \n" \
588+
"be put in the file.\n"
589+
590+
def get(self):
591+
_val = Option.get(self)
592+
if isinstance(_val, str) and _val.startswith('file://'):
593+
filepath = _val[7:]
594+
if filepath and not os.path.isabs(filepath):
595+
filepath = os.path.join(self.config["HOME"], filepath.strip())
596+
try:
597+
with open(filepath) as f:
598+
_val = f.readline().rstrip()
599+
# except FileNotFoundError: py2/py3
600+
# compatible version
601+
except EnvironmentError as e:
602+
if e.errno != errno.ENOENT:
603+
raise
604+
else:
605+
raise OptionValueError(self, _val,
606+
"Unable to read value for %s. Error opening "
607+
"%s: %s." % (self.name, e.filename, e.args[1]))
608+
return self.str2value(_val)
609+
610+
566611
class WebUrlOption(Option):
567612
"""URL MUST start with http/https scheme and end with '/'"""
568613

@@ -625,6 +670,18 @@ class NullableFilePathOption(NullableOption, FilePathOption):
625670
# everything else taken from NullableOption (inheritance order)
626671

627672

673+
class SecretMandatoryOption(MandatoryOption, SecretOption):
674+
# use get from SecretOption and rest from MandatoryOption
675+
get = SecretOption.get
676+
class_description = SecretOption.class_description
677+
678+
679+
class SecretNullableOption(NullableOption, SecretOption):
680+
# use get from SecretOption and rest from NullableOption
681+
get = SecretOption.get
682+
class_description = SecretOption.class_description
683+
684+
628685
class TimezoneOption(Option):
629686

630687
class_description = \
@@ -656,7 +713,8 @@ class HttpVersionOption(Option):
656713

657714
def str2value(self, value):
658715
if value not in ["HTTP/1.0", "HTTP/1.1"]:
659-
raise OptionValueError(self, value, "Valid vaues for -V or --http_version are: HTTP/1.0, HTTP/1.1")
716+
raise OptionValueError(self, value,
717+
"Valid vaues for -V or --http_version are: HTTP/1.0, HTTP/1.1")
660718
return value
661719

662720

@@ -1059,7 +1117,7 @@ def str2value(self, value):
10591117
"Setting this option makes Roundup migrate passwords with\n"
10601118
"an insecure password-scheme to a more secure scheme\n"
10611119
"when the user logs in via the web-interface."),
1062-
(MandatoryOption, "secret_key", create_token(),
1120+
(SecretMandatoryOption, "secret_key", create_token(),
10631121
"A per tracker secret used in etag calculations for\n"
10641122
"an object. It must not be empty.\n"
10651123
"It prevents reverse engineering hidden data in an object\n"
@@ -1071,7 +1129,7 @@ def str2value(self, value):
10711129
"(Note the default value changes every time\n"
10721130
" roundup-admin updateconfig\n"
10731131
"is run, so it must be explicitly set to a non-empty string.\n"),
1074-
(MandatoryOption, "jwt_secret", "disabled",
1132+
(SecretNullableOption, "jwt_secret", "disabled",
10751133
"This is used to generate/validate json web tokens (jwt).\n"
10761134
"Even if you don't use jwts it must not be empty.\n"
10771135
"If less than 256 bits (32 characters) in length it will\n"
@@ -1097,7 +1155,7 @@ def str2value(self, value):
10971155
(NullableOption, 'user', 'roundup',
10981156
"Database user name that Roundup should use.",
10991157
['MYSQL_DBUSER']),
1100-
(NullableOption, 'password', 'roundup',
1158+
(SecretNullableOption, 'password', 'roundup',
11011159
"Database user password.",
11021160
['MYSQL_DBPASSWORD']),
11031161
(NullableOption, 'read_default_file', '~/.my.cnf',
@@ -1178,7 +1236,7 @@ def str2value(self, value):
11781236
(Option, "username", "", "SMTP login name.\n"
11791237
"Set this if your mail host requires authenticated access.\n"
11801238
"If username is not empty, password (below) MUST be set!"),
1181-
(Option, "password", NODEFAULT, "SMTP login password.\n"
1239+
(SecretMandatoryOption, "password", NODEFAULT, "SMTP login password.\n"
11821240
"Set this if your mail host requires authenticated access."),
11831241
(IntegerNumberGeqZeroOption, "port", smtplib.SMTP_PORT,
11841242
"Default port to send SMTP on.\n"
@@ -1421,7 +1479,7 @@ class Config:
14211479
# actual name of the config file. set on load.
14221480
filepath = os.path.join(HOME, INI_FILE)
14231481

1424-
def __init__(self, config_path=None, layout=None, settings={}):
1482+
def __init__(self, config_path=None, layout=None, settings=None):
14251483
"""Initialize confing instance
14261484
14271485
Parameters:
@@ -1438,6 +1496,8 @@ def __init__(self, config_path=None, layout=None, settings={}):
14381496
The overrides are applied after loading config file.
14391497
14401498
"""
1499+
if settings is None:
1500+
settings = {}
14411501
# initialize option containers:
14421502
self.sections = []
14431503
self.section_descriptions = {}
@@ -1836,7 +1896,9 @@ class CoreConfig(Config):
18361896
ext = None
18371897
detectors = None
18381898

1839-
def __init__(self, home_dir=None, settings={}):
1899+
def __init__(self, home_dir=None, settings=None):
1900+
if settings is None:
1901+
settings = {}
18401902
Config.__init__(self, home_dir, layout=SETTINGS, settings=settings)
18411903
# load the config if home_dir given
18421904
if home_dir is None:
@@ -1920,9 +1982,16 @@ def validator(self, options):
19201982
languages = textwrap.fill(_("Valid languages: ") +
19211983
lang_avail, 75,
19221984
subsequent_indent=" ")
1923-
raise OptionValueError( options["INDEXER_LANGUAGE"],
1985+
raise OptionValueError(options["INDEXER_LANGUAGE"],
19241986
lang, languages)
19251987

1988+
if options['MAIL_USERNAME']._value != "":
1989+
# require password to be set
1990+
if options['MAIL_PASSWORD']._value is NODEFAULT:
1991+
raise OptionValueError(options["MAIL_PASSWORD"],
1992+
"not defined",
1993+
"mail username is set, so this must be defined.")
1994+
19261995
def load(self, home_dir):
19271996
"""Load configuration from path designated by home_dir argument"""
19281997
if os.path.isfile(os.path.join(home_dir, self.INI_FILE)):

0 commit comments

Comments
 (0)