99# used here instead of try/except.
1010import sys
1111import getopt
12- import logging , logging .config
12+ import errno
13+ import logging
14+ import logging .config
1315import os
1416import re
1517import time
3739class ConfigurationError (RoundupException ):
3840 pass
3941
42+
4043class 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
279282class BooleanOption (Option ):
@@ -419,6 +422,7 @@ def str2value(self, value):
419422 return _val
420423 raise OptionValueError (self , value , self .class_description )
421424
425+
422426class 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+
435440class 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+
566611class 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+
628685class 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