-
Notifications
You must be signed in to change notification settings - Fork 687
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2803 from freedomofpress/wip-dachary-1195-journal…
…ist-notification daily email alert to journalists about submissions received in the past 24h
- Loading branch information
Showing
23 changed files
with
712 additions
and
166 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,9 @@ | |
**/venv | ||
**/.venv | ||
|
||
**/*~ | ||
**/#* | ||
|
||
**/__pycache__ | ||
**/*.pyc | ||
securedrop/.sass-cache | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -43,6 +43,10 @@ class FingerprintException(Exception): | |
pass | ||
|
||
|
||
class JournalistAlertEmailException(Exception): | ||
pass | ||
|
||
|
||
class SiteConfig(object): | ||
|
||
class ValidateNotEmpty(Validator): | ||
|
@@ -129,6 +133,13 @@ def validate(self, document): | |
raise ValidationError( | ||
message=path + ' file does not exist') | ||
|
||
class ValidateOptionalPath(ValidatePath): | ||
def validate(self, document): | ||
if document.text == '': | ||
return True | ||
return super(SiteConfig.ValidateOptionalPath, self).validate( | ||
document) | ||
|
||
class ValidateYesNo(Validator): | ||
def validate(self, document): | ||
text = document.text.lower() | ||
|
@@ -150,6 +161,13 @@ def validate(self, document): | |
message='fingerprints must be 40 hexadecimal characters') | ||
return True | ||
|
||
class ValidateOptionalFingerprint(ValidateFingerprint): | ||
def validate(self, document): | ||
if document.text == '': | ||
return True | ||
return super(SiteConfig.ValidateOptionalFingerprint, | ||
self).validate(document) | ||
|
||
class ValidateInt(Validator): | ||
def validate(self, document): | ||
if re.match('\d+$', document.text): | ||
|
@@ -199,14 +217,33 @@ def validate(self, document): | |
raise ValidationError( | ||
message="Password for OSSEC email account must be strong") | ||
|
||
class ValidateOSSECEmail(Validator): | ||
class ValidateEmail(Validator): | ||
def validate(self, document): | ||
text = document.text | ||
if text and '@' in text and '[email protected]' != text: | ||
if text == '': | ||
raise ValidationError( | ||
message=("Must not be empty")) | ||
if '@' not in text: | ||
raise ValidationError( | ||
message=("Must contain a @")) | ||
return True | ||
|
||
class ValidateOSSECEmail(ValidateEmail): | ||
def validate(self, document): | ||
super(SiteConfig.ValidateOSSECEmail, self).validate(document) | ||
text = document.text | ||
if '[email protected]' != text: | ||
return True | ||
raise ValidationError( | ||
message=("Must contain a @ and be set to " | ||
"something other than [email protected]")) | ||
message=("Must be set to something other than " | ||
"[email protected]")) | ||
|
||
class ValidateOptionalEmail(ValidateEmail): | ||
def validate(self, document): | ||
if document.text == '': | ||
return True | ||
return super(SiteConfig.ValidateOptionalEmail, self).validate( | ||
document) | ||
|
||
def __init__(self, args): | ||
self.args = args | ||
|
@@ -268,6 +305,19 @@ def __init__(self, args): | |
u'Admin email address for receiving OSSEC alerts', | ||
SiteConfig.ValidateOSSECEmail(), | ||
None], | ||
['journalist_alert_gpg_public_key', '', str, | ||
u'Local filepath to journalist alerts GPG public key (optional)', | ||
SiteConfig.ValidateOptionalPath(self.args.ansible_path), | ||
None], | ||
['journalist_gpg_fpr', '', str, | ||
u'Full fingerprint for the journalist alerts ' | ||
u'GPG public key (optional)', | ||
SiteConfig.ValidateOptionalFingerprint(), | ||
self.sanitize_fingerprint], | ||
['journalist_alert_email', '', str, | ||
u'Email address for receiving journalist alerts (optional)', | ||
SiteConfig.ValidateOptionalEmail(), | ||
None], | ||
['smtp_relay', "smtp.gmail.com", str, | ||
u'SMTP relay for sending OSSEC alerts', | ||
SiteConfig.ValidateNotEmpty(), | ||
|
@@ -306,6 +356,7 @@ def update_config(self): | |
self.config.update(self.user_prompt_config()) | ||
self.save() | ||
self.validate_gpg_keys() | ||
self.validate_journalist_alert_email() | ||
return True | ||
|
||
def user_prompt_config(self): | ||
|
@@ -351,11 +402,17 @@ def validate_gpg_keys(self): | |
'securedrop_app_gpg_fingerprint'), | ||
|
||
('ossec_alert_gpg_public_key', | ||
'ossec_gpg_fpr')) | ||
'ossec_gpg_fpr'), | ||
|
||
('journalist_alert_gpg_public_key', | ||
'journalist_gpg_fpr')) | ||
validate = os.path.join( | ||
os.path.dirname(__file__), '..', 'bin', | ||
'validate-gpg-key.sh') | ||
for (public_key, fingerprint) in keys: | ||
validate = os.path.join( | ||
os.path.dirname(__file__), '..', 'bin', | ||
'validate-gpg-key.sh') | ||
if (self.config[public_key] == '' and | ||
self.config[fingerprint] == ''): | ||
continue | ||
public_key = os.path.join(self.args.ansible_path, | ||
self.config[public_key]) | ||
fingerprint = self.config[fingerprint] | ||
|
@@ -371,6 +428,23 @@ def validate_gpg_keys(self): | |
"the public key {}".format(public_key)) | ||
return True | ||
|
||
def validate_journalist_alert_email(self): | ||
if (self.config['journalist_alert_gpg_public_key'] == '' and | ||
self.config['journalist_gpg_fpr'] == ''): | ||
return True | ||
|
||
class Document(object): | ||
def __init__(self, text): | ||
self.text = text | ||
|
||
try: | ||
SiteConfig.ValidateEmail().validate(Document( | ||
self.config['journalist_alert_email'])) | ||
except ValidationError as e: | ||
raise JournalistAlertEmailException( | ||
"journalist alerts email: " + e.message) | ||
return True | ||
|
||
def exists(self): | ||
return os.path.exists(self.args.site_config) | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -193,18 +193,29 @@ def test_validate_ossec_password(self): | |
with pytest.raises(ValidationError): | ||
validator.validate(Document('short')) | ||
|
||
def test_validate_ossec_email(self): | ||
validator = securedrop_admin.SiteConfig.ValidateOSSECEmail() | ||
def test_validate_email(self): | ||
validator = securedrop_admin.SiteConfig.ValidateEmail() | ||
|
||
assert validator.validate(Document('[email protected]')) | ||
with pytest.raises(ValidationError): | ||
validator.validate(Document('badmail')) | ||
with pytest.raises(ValidationError): | ||
validator.validate(Document('')) | ||
|
||
def test_validate_ossec_email(self): | ||
validator = securedrop_admin.SiteConfig.ValidateOSSECEmail() | ||
|
||
assert validator.validate(Document('[email protected]')) | ||
with pytest.raises(ValidationError) as e: | ||
validator.validate(Document('[email protected]')) | ||
assert 'something other than [email protected]' in e.value.message | ||
|
||
def test_validate_optional_email(self): | ||
validator = securedrop_admin.SiteConfig.ValidateOptionalEmail() | ||
|
||
assert validator.validate(Document('[email protected]')) | ||
assert validator.validate(Document('')) | ||
|
||
def test_is_tails(self): | ||
validator = securedrop_admin.SiteConfig.ValidateDNS() | ||
with mock.patch('subprocess.check_output', return_value='Tails'): | ||
|
@@ -288,6 +299,13 @@ def test_validate_path(self): | |
with pytest.raises(ValidationError): | ||
validator.validate(Document("")) | ||
|
||
def test_validate_optional_path(self): | ||
mydir = dirname(__file__) | ||
myfile = basename(__file__) | ||
validator = securedrop_admin.SiteConfig.ValidateOptionalPath(mydir) | ||
assert validator.validate(Document(myfile)) | ||
assert validator.validate(Document("")) | ||
|
||
def test_validate_yes_no(self): | ||
validator = securedrop_admin.SiteConfig.ValidateYesNo() | ||
with pytest.raises(ValidationError): | ||
|
@@ -324,6 +342,12 @@ def test_validate_fingerprint(self): | |
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz")) | ||
assert '40 hexadecimal' in e.value.message | ||
|
||
def test_validate_optional_fingerprint(self): | ||
validator = securedrop_admin.SiteConfig.ValidateOptionalFingerprint() | ||
assert validator.validate(Document( | ||
"012345678901234567890123456789ABCDEFABCD")) | ||
assert validator.validate(Document("")) | ||
|
||
def test_sanitize_fingerprint(self): | ||
args = argparse.Namespace(site_config='DOES_NOT_EXIST', | ||
ansible_path='.', | ||
|
@@ -369,8 +393,7 @@ def test_validate_gpg_key(self, caplog): | |
args = argparse.Namespace(site_config='INVALID', | ||
ansible_path='tests/files', | ||
app_path=dirname(__file__)) | ||
site_config = securedrop_admin.SiteConfig(args) | ||
site_config.config = { | ||
good_config = { | ||
'securedrop_app_gpg_public_key': | ||
'test_journalist_key.pub', | ||
|
||
|
@@ -382,12 +405,61 @@ def test_validate_gpg_key(self, caplog): | |
|
||
'ossec_gpg_fpr': | ||
'65A1B5FF195B56353CC63DFFCC40EF1228271441', | ||
|
||
'journalist_alert_gpg_public_key': | ||
'test_journalist_key.pub', | ||
|
||
'journalist_gpg_fpr': | ||
'65A1B5FF195B56353CC63DFFCC40EF1228271441', | ||
} | ||
site_config = securedrop_admin.SiteConfig(args) | ||
site_config.config = good_config | ||
assert site_config.validate_gpg_keys() | ||
site_config.config['ossec_gpg_fpr'] = 'FAIL' | ||
with pytest.raises(securedrop_admin.FingerprintException) as e: | ||
site_config.validate_gpg_keys() | ||
assert 'FAIL does not match' in e.value.message | ||
|
||
for key in ('securedrop_app_gpg_fingerprint', | ||
'ossec_gpg_fpr', | ||
'journalist_gpg_fpr'): | ||
bad_config = good_config.copy() | ||
bad_config[key] = 'FAIL' | ||
site_config.config = bad_config | ||
with pytest.raises(securedrop_admin.FingerprintException) as e: | ||
site_config.validate_gpg_keys() | ||
assert 'FAIL does not match' in e.value.message | ||
|
||
def test_journalist_alert_email(self): | ||
args = argparse.Namespace(site_config='INVALID', | ||
ansible_path='tests/files', | ||
app_path=dirname(__file__)) | ||
site_config = securedrop_admin.SiteConfig(args) | ||
site_config.config = { | ||
'journalist_alert_gpg_public_key': | ||
'', | ||
|
||
'journalist_gpg_fpr': | ||
'', | ||
} | ||
assert site_config.validate_journalist_alert_email() | ||
site_config.config = { | ||
'journalist_alert_gpg_public_key': | ||
'test_journalist_key.pub', | ||
|
||
'journalist_gpg_fpr': | ||
'65A1B5FF195B56353CC63DFFCC40EF1228271441', | ||
} | ||
site_config.config['journalist_alert_email'] = '' | ||
with pytest.raises( | ||
securedrop_admin.JournalistAlertEmailException) as e: | ||
site_config.validate_journalist_alert_email() | ||
assert 'not be empty' in e.value.message | ||
|
||
site_config.config['journalist_alert_email'] = 'bademail' | ||
with pytest.raises( | ||
securedrop_admin.JournalistAlertEmailException) as e: | ||
site_config.validate_journalist_alert_email() | ||
assert 'Must contain a @' in e.value.message | ||
|
||
site_config.config['journalist_alert_email'] = '[email protected]' | ||
assert site_config.validate_journalist_alert_email() | ||
|
||
@mock.patch('securedrop_admin.SiteConfig.validated_input', | ||
side_effect=lambda p, d, v, t: d) | ||
|
@@ -452,11 +524,15 @@ def get_desc(self, site_config, var): | |
if desc[0] == var: | ||
return desc | ||
|
||
def verify_desc_consistency(self, site_config, desc): | ||
def verify_desc_consistency_optional(self, site_config, desc): | ||
(var, default, etype, prompt, validator, transform) = desc | ||
# verify the default passes validation | ||
assert site_config.user_prompt_config_one(desc, None) == default | ||
assert type(default) == etype | ||
|
||
def verify_desc_consistency(self, site_config, desc): | ||
self.verify_desc_consistency_optional(site_config, desc) | ||
(var, default, etype, prompt, validator, transform) = desc | ||
with pytest.raises(ValidationError): | ||
site_config.user_prompt_config_one(desc, '') | ||
|
||
|
@@ -482,8 +558,7 @@ def verify_prompt_not_empty(self, site_config, desc): | |
with pytest.raises(ValidationError): | ||
site_config.user_prompt_config_one(desc, '') | ||
|
||
def verify_prompt_fingerprint(self, site_config, desc): | ||
self.verify_prompt_not_empty(site_config, desc) | ||
def verify_prompt_fingerprint_optional(self, site_config, desc): | ||
fpr = "0123456 789012 34567890123456789ABCDEFABCD" | ||
clean_fpr = site_config.sanitize_fingerprint(fpr) | ||
assert site_config.user_prompt_config_one(desc, fpr) == clean_fpr | ||
|
@@ -494,10 +569,18 @@ def verify_desc_consistency_allow_empty(self, site_config, desc): | |
assert site_config.user_prompt_config_one(desc, None) == default | ||
assert type(default) == etype | ||
|
||
def verify_prompt_fingerprint(self, site_config, desc): | ||
self.verify_prompt_not_empty(site_config, desc) | ||
self.verify_prompt_fingerprint_optional(site_config, desc) | ||
|
||
verify_prompt_securedrop_app_gpg_fingerprint = verify_prompt_fingerprint | ||
verify_prompt_ossec_alert_gpg_public_key = verify_desc_consistency | ||
verify_prompt_ossec_gpg_fpr = verify_prompt_fingerprint | ||
verify_prompt_ossec_alert_email = verify_prompt_not_empty | ||
verify_prompt_journalist_alert_gpg_public_key = ( | ||
verify_desc_consistency_optional) | ||
verify_prompt_journalist_gpg_fpr = verify_prompt_fingerprint_optional | ||
verify_prompt_journalist_alert_email = verify_desc_consistency_optional | ||
verify_prompt_smtp_relay = verify_prompt_not_empty | ||
verify_prompt_smtp_relay_port = verify_desc_consistency | ||
verify_prompt_daily_reboot_time = verify_desc_consistency | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.