Skip to content

Commit

Permalink
Merge pull request #5629 from freedomofpress/4280-set-org-name
Browse files Browse the repository at this point in the history
Adds option to set and use an organization name
  • Loading branch information
rmol authored Dec 22, 2020
2 parents 405d0cf + 83bbe25 commit cf7d1ef
Show file tree
Hide file tree
Showing 23 changed files with 496 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,7 @@
/var/www/securedrop/journalist_templates/js-strings.html r,
/var/www/securedrop/journalist_templates/locales.html r,
/var/www/securedrop/journalist_templates/login.html r,
/var/www/securedrop/journalist_templates/logo_upload_flashed.html r,
/var/www/securedrop/journalist_templates/submission_preferences_saved_flash.html r,
/var/www/securedrop/journalist_templates/preferences_saved_flash.html r,
/var/www/securedrop/models.py r,
/var/www/securedrop/request_that_secures_file_uploads.py r,
/var/www/securedrop/rm.py r,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Added organization_name field in instance_config table
Revision ID: 92fba0be98e9
Revises: 48a75abc0121
Create Date: 2020-11-15 19:36:20.351993
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '92fba0be98e9'
down_revision = '48a75abc0121'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('instance_config', schema=None) as batch_op:
batch_op.add_column(sa.Column('organization_name', sa.String(length=255), nullable=True))

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('instance_config', schema=None) as batch_op:
batch_op.drop_column('organization_name')

# ### end Alembic commands ###
5 changes: 5 additions & 0 deletions securedrop/journalist_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ def setup_g() -> 'Optional[Response]':
g.html_lang = i18n.locale_to_rfc_5646(g.locale)
g.locales = i18n.get_locale2name()

if app.instance_config.organization_name:
g.organization_name = app.instance_config.organization_name
else:
g.organization_name = gettext('SecureDrop')

if not app.config['V3_ONION_ENABLED'] or app.config['V2_ONION_ENABLED']:
g.show_v2_onion_eol_warning = True

Expand Down
38 changes: 32 additions & 6 deletions securedrop/journalist_app/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@

import i18n
from db import db
from html import escape
from models import (InstanceConfig, Journalist, InvalidUsernameException,
FirstOrLastNameError, PasswordError)
from journalist_app.decorators import admin_required
from journalist_app.utils import (commit_account_changes, set_diceware_password,
validate_hotp_secret, revoke_token)
from journalist_app.forms import LogoForm, NewUserForm, SubmissionPreferencesForm
from journalist_app.forms import LogoForm, NewUserForm, SubmissionPreferencesForm, OrgNameForm
from sdconfig import SDConfig
from passphrases import PassphraseGenerator

Expand All @@ -38,6 +39,8 @@ def manage_config() -> Union[str, werkzeug.Response]:
# The UI prompt ("prevent") is the opposite of the setting ("allow"):
submission_preferences_form = SubmissionPreferencesForm(
prevent_document_uploads=not current_app.instance_config.allow_document_uploads)
organization_name_form = OrgNameForm(
organization_name=current_app.instance_config.organization_name)
logo_form = LogoForm()
if logo_form.validate_on_submit():
f = logo_form.logo.data
Expand All @@ -53,13 +56,14 @@ def manage_config() -> Union[str, werkzeug.Response]:
flash("Unable to process the image file."
" Try another one.", "logo-error")
finally:
return redirect(url_for("admin.manage_config"))
return redirect(url_for("admin.manage_config") + "#config-logoimage")
else:
for field, errors in list(logo_form.errors.items()):
for error in errors:
flash(error, "logo-error")
return render_template("config.html",
submission_preferences_form=submission_preferences_form,
organization_name_form=organization_name_form,
logo_form=logo_form)

@view.route('/update-submission-preferences', methods=['POST'])
Expand All @@ -71,9 +75,31 @@ def update_submission_preferences() -> Optional[werkzeug.Response]:
flash(gettext("Preferences saved."), "submission-preferences-success")
value = not bool(request.form.get('prevent_document_uploads'))
InstanceConfig.set_allow_document_uploads(value)
return redirect(url_for('admin.manage_config'))
return redirect(url_for('admin.manage_config') + "#config-preventuploads")
else:
return None
for field, errors in list(form.errors.items()):
for error in errors:
flash(gettext("Preferences not updated.") + " " + error,
"submission-preferences-error")
return redirect(url_for('admin.manage_config') + "#config-preventuploads")

@view.route('/update-org-name', methods=['POST'])
@admin_required
def update_org_name() -> Union[str, werkzeug.Response]:
form = OrgNameForm()
if form.validate_on_submit():
try:
value = request.form['organization_name']
InstanceConfig.set_organization_name(escape(value, quote=True))
flash(gettext("Preferences saved."), "org-name-success")
except Exception:
flash(gettext('Failed to update organization name.'), 'org-name-error')
return redirect(url_for('admin.manage_config') + "#config-orgname")
else:
for field, errors in list(form.errors.items()):
for error in errors:
flash(error, "org-name-error")
return redirect(url_for('admin.manage_config') + "#config-orgname")

@view.route('/add', methods=('GET', 'POST'))
@admin_required
Expand Down Expand Up @@ -276,7 +302,7 @@ def new_password(user_id: int) -> werkzeug.Response:
def ossec_test() -> werkzeug.Response:
current_app.logger.error('This is a test OSSEC alert')
flash(gettext('Test alert sent. Please check your email.'),
'notification')
return redirect(url_for('admin.manage_config'))
'testalert-notification')
return redirect(url_for('admin.manage_config') + "#config-testalert")

return view
20 changes: 19 additions & 1 deletion securedrop/journalist_app/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
ValidationError)
from wtforms.validators import InputRequired, Optional

from models import Journalist
from models import Journalist, InstanceConfig


def otp_secret_validation(form: FlaskForm, field: Field) -> None:
Expand Down Expand Up @@ -46,6 +46,17 @@ def name_length_validation(form: FlaskForm, field: Field) -> None:
)


def check_orgname(form: FlaskForm, field: Field) -> None:
if len(field.data) > InstanceConfig.MAX_ORG_NAME_LEN:
raise ValidationError(
ngettext(
'Cannot be longer than {num} characters.',
'Cannot be longer than {num} characters.',
InstanceConfig.MAX_ORG_NAME_LEN
).format(num=InstanceConfig.MAX_ORG_NAME_LEN)
)


def check_invalid_usernames(form: FlaskForm, field: Field) -> None:
if field.data in Journalist.INVALID_USERNAMES:
raise ValidationError(gettext(
Expand Down Expand Up @@ -83,6 +94,13 @@ class SubmissionPreferencesForm(FlaskForm):
prevent_document_uploads = BooleanField('prevent_document_uploads')


class OrgNameForm(FlaskForm):
organization_name = StringField('organization_name', validators=[
InputRequired(message=gettext('This field is required.')),
check_orgname
])


class LogoForm(FlaskForm):
logo = FileField(validators=[
FileRequired(message=gettext('File required.')),
Expand Down
4 changes: 2 additions & 2 deletions securedrop/journalist_templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SecureDrop</title>
<title>{{ g.organization_name }}</title>

<link rel="stylesheet" href="/static/css/journalist.css">

Expand Down Expand Up @@ -39,7 +39,7 @@
<div class="container">
{% block header %}
<div id="header">
<a href="{{ url_for('main.index') }}" class="no-bottom-border"><img src="{{ url_for('main.select_logo') }}" class="logo small" alt="SecureDrop" width="250"></a>
<a href="{{ url_for('main.index') }}" class="no-bottom-border"><img src="{{ url_for('main.select_logo') }}" class="logo small" alt="{{ g.organization_name }} | Home" width="250"></a>
{% include 'locales.html' %}
</div>
{% endblock %}
Expand Down
50 changes: 36 additions & 14 deletions securedrop/journalist_templates/config.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,30 @@

<h1>{{ gettext('Instance Configuration') }}</h1>

<h2>{{ gettext('Alerts') }}</h2>
<h2 id="config-orgname">{{ gettext('Organization Name') }}</h2>

<p>{{ gettext('Send an encrypted email to verify if OSSEC alerts work correctly:') }}</p>

<p>
<a class="btn" href="{{ url_for('admin.ossec_test') }}" id="test-ossec-alert">
<img src="{{ url_for('static', filename='icons/bell.png') }}" class="icon" width="13" height="15" alt="">
{{ gettext('SEND TEST OSSEC ALERT') }}
</a>
</p>
<form action="{{ url_for('admin.update_org_name') }}" method="post">
<input name="csrf_token" type="hidden" value="{{ csrf_token() }}">
<p>
<label for="organization_name">{{ gettext('Set the organization name used on the SecureDrop web interfaces:') }}</label><br>
{{ organization_name_form.organization_name() }}
</p>
<button type="submit" id="submit-update-org-name">
<img src="{{ url_for('static', filename='icons/pencil-alt.png') }}" class="icon" width="15" height="15" alt="">
{{ gettext('SET ORGANIZATION NAME') }}
</button>
{% set prefs_filter = ["org-name-error","org-name-success"] %}
{% include 'preferences_saved_flash.html' %}
</form>

<hr class="no-line">

<h2>{{ gettext('Logo Image') }}</h2>
<h2 id="config-logoimage">{{ gettext('Logo Image') }}</h2>

<p>{{ gettext('Here you can update the image displayed on the SecureDrop web interfaces:') }}</p>

<p>
<img src="{{ url_for('main.select_logo') }}" class="logo small" alt="SecureDrop" width="250">
<img src="{{ url_for('main.select_logo') }}" class="logo small" alt="{{ g.organization_name }}" width="250">
</p>

<form method="post" enctype="multipart/form-data">
Expand All @@ -41,12 +46,13 @@ <h5>
<img src="{{ url_for('static', filename='icons/pencil-alt.png') }}" class="icon" width="15" height="15" alt="">
{{ gettext('UPDATE LOGO') }}
</button>
{% include 'logo_upload_flashed.html' %}
{% set prefs_filter = ["logo-success","logo-error"] %}
{% include 'preferences_saved_flash.html' %}
</form>

<hr class="no-line">

<h2>{{ gettext('Submission Preferences') }}</h2>
<h2 id="config-preventuploads">{{ gettext('Submission Preferences') }}</h2>

<form action="{{ url_for('admin.update_submission_preferences') }}" method="post">
<input name="csrf_token" type="hidden" value="{{ csrf_token() }}">
Expand All @@ -58,7 +64,23 @@ <h2>{{ gettext('Submission Preferences') }}</h2>
<img src="{{ url_for('static', filename='icons/pencil-alt.png') }}" class="icon" width="15" height="15" alt="">
{{ gettext('UPDATE SUBMISSION PREFERENCES') }}
</button>
{% include 'submission_preferences_saved_flash.html' %}
{% set prefs_filter = ["submission-preferences-success","submission-preferences-error"] %}
{% include 'preferences_saved_flash.html' %}
</form>

<hr class="no-line">

<h2 id="config-testalert">{{ gettext('Alerts') }}</h2>

<p>{{ gettext('Send an encrypted email to verify if OSSEC alerts work correctly:') }}</p>

<p>
<a class="btn" href="{{ url_for('admin.ossec_test') }}" id="test-ossec-alert">
<img src="{{ url_for('static', filename='icons/bell.png') }}" class="icon" width="13" height="15" alt="">
{{ gettext('SEND TEST OSSEC ALERT') }}
</a>
{% set prefs_filter = ["testalert-success","testalert-error","testalert-notification"] %}
{% include 'preferences_saved_flash.html' %}
</p>

{% endblock %}
2 changes: 1 addition & 1 deletion securedrop/journalist_templates/flashed.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
{% if category != "banner-warning" and category != "logo-success" and category != "logo-error" and category != "submission-preferences-success" %}
{% if category not in ["banner-warning","logo-success","logo-error","submission-preferences-success","submission-preferences-error","org-name-success","org-name-error", "testalert-success", "testalert-error", "testalert-notification"] %}
<div class="flash {{ category }}">
{% if category == "notification" %}
<img src="{{ url_for('static', filename='i/font-awesome/info-circle-black.png') }}" height="16" width="20">
Expand Down
14 changes: 0 additions & 14 deletions securedrop/journalist_templates/logo_upload_flashed.html

This file was deleted.

19 changes: 19 additions & 0 deletions securedrop/journalist_templates/preferences_saved_flash.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% if prefs_filter is defined and prefs_filter|length %}
{% with messages = get_flashed_messages(with_categories=True, category_filter=prefs_filter) %}
{% for category, message in messages %}
{# Get the end of the of the category message which
contains the category status.(success/error/notification)#}
{% set category_status = category.split('-')|last %}
<div class="flash {{ category_status }}">
{% if category_status == "success" %}
<img src="{{ url_for('static', filename='i/success_checkmark.png') }}" height="17" width="20">
{% elif category_status == "error" %}
<img src="{{ url_for('static', filename='i/font-awesome/exclamation-triangle-black.png') }}" height="17" width="20">
{% elif category_status == "notification" %}
<img src="{{ url_for('static', filename='i/font-awesome/info-circle-black.png') }}" height="16" width="20">
{% endif %}
{{ message }}
</div>
{% endfor %}
{% endwith %}
{% endif %}

This file was deleted.

29 changes: 29 additions & 0 deletions securedrop/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -825,11 +825,15 @@ class InstanceConfig(db.Model):
interface. The current version has valid_until=None.
'''

# Limits length of org name used in SI and JI titles, image alt texts etc.
MAX_ORG_NAME_LEN = 64

__tablename__ = 'instance_config'
version = Column(Integer, primary_key=True)
valid_until = Column(DateTime, default=None, unique=True)

allow_document_uploads = Column(Boolean, default=True)
organization_name = Column(String(255), nullable=True, default="SecureDrop")

# Columns not listed here will be included by InstanceConfig.copy() when
# updating the configuration.
Expand Down Expand Up @@ -868,6 +872,31 @@ def get_current(cls) -> "InstanceConfig":
db.session.commit()
return current

@classmethod
def check_name_acceptable(cls, name: str) -> None:
# Enforce a reasonable maximum length for names
if name is None or len(name) == 0:
raise InvalidNameLength(name)
if len(name) > cls.MAX_ORG_NAME_LEN:
raise InvalidNameLength(name)

@classmethod
def set_organization_name(cls, name: str) -> None:
'''Invalidate the current configuration and append a new one with the
new organization name.
'''

old = cls.get_current()
old.valid_until = datetime.datetime.utcnow()
db.session.add(old)

new = old.copy()
cls.check_name_acceptable(name)
new.organization_name = name
db.session.add(new)

db.session.commit()

@classmethod
def set_allow_document_uploads(cls, value: bool) -> None:
'''Invalidate the current configuration and append a new one with the
Expand Down
6 changes: 6 additions & 0 deletions securedrop/source_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ def setup_g() -> Optional[werkzeug.Response]:
del session['codename']
return redirect(url_for('main.index'))
g.loc = app.storage.path(g.filesystem_id)

if app.instance_config.organization_name:
g.organization_name = app.instance_config.organization_name
else:
g.organization_name = gettext('SecureDrop')

return None

@app.errorhandler(404)
Expand Down
Loading

0 comments on commit cf7d1ef

Please sign in to comment.