Skip to content

Commit

Permalink
Adds option to set and use an organization name in the web interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
zenmonkeykstop committed Nov 16, 2020
1 parent caa505a commit a9d57f2
Show file tree
Hide file tree
Showing 22 changed files with 335 additions and 40 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 @@ -157,6 +157,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
30 changes: 28 additions & 2 deletions securedrop/journalist_app/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
from sqlalchemy.orm.exc import NoResultFound

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 (make_password, 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


Expand All @@ -36,6 +37,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 @@ -58,6 +61,7 @@ def manage_config() -> Union[str, werkzeug.Response]:
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,7 +75,29 @@ def update_submission_preferences() -> Optional[werkzeug.Response]:
InstanceConfig.set_allow_document_uploads(value)
return redirect(url_for('admin.manage_config'))
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'))

@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'))
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'))

@view.route('/add', methods=('GET', 'POST'))
@admin_required
Expand Down
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 }}" width="250"></a>
{% include 'locales.html' %}
</div>
{% endblock %}
Expand Down
26 changes: 23 additions & 3 deletions securedrop/journalist_templates/config.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ <h2>{{ 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 @@ -40,7 +40,8 @@ <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">
Expand All @@ -57,7 +58,26 @@ <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>{{ gettext('Organization Name') }}</h2>

<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 your organization name:') }}</label>
{{ 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>

{% 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"] %}
<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 @@ -829,11 +829,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 @@ -872,6 +876,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 @@ -169,6 +169,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
1 change: 1 addition & 0 deletions securedrop/source_app/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def make_blueprint(config: SDConfig) -> Blueprint:
@view.route('/metadata')
def metadata() -> flask.Response:
meta = {
'organization_name': current_app.instance_config.organization_name,
'allow_document_uploads': current_app.instance_config.allow_document_uploads,
'gpg_fpr': config.JOURNALIST_KEY,
'sd_version': version.__version__,
Expand Down
4 changes: 2 additions & 2 deletions securedrop/source_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 | {{ gettext('Protecting Journalists and Sources') }}</title>
<title>{{ g.organization_name }} | {{ gettext('Protecting Journalists and Sources') }}</title>

<link rel="stylesheet" href="/static/css/source.css">
<link rel="icon" type="image/png" href="/static/i/favicon.png">
Expand All @@ -17,7 +17,7 @@
{% block header %}
<div id="header">
<a href="{% if 'logged_in' in session %}{{ url_for('main.lookup') }}{% else %}{{ url_for('main.index') }}{% endif %}" class="no-bottom-border">
<img src="{{ url_for('main.select_logo') }}" class="logo small" alt="{{ gettext('Logo Image') }}" width="250">
<img src="{{ url_for('main.select_logo') }}" class="logo small" alt="{{ g.organization_name }} | {{ gettext('Logo Image') }}" width="250">
</a>
{% include 'locales.html' %}
</div>
Expand Down
4 changes: 2 additions & 2 deletions securedrop/source_templates/index.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="{{ g.html_lang }}" dir="{{ g.text_direction }}">
<head>
<title>SecureDrop | {{ gettext('Protecting Journalists and Sources') }}</title>
<title> {{ g.organization_name }} | {{ gettext('Protecting Journalists and Sources') }}</title>

<link rel="stylesheet" href="/static/css/source.css">
<link rel="icon" type="image/png" href="/static/i/favicon.png">
Expand All @@ -26,7 +26,7 @@
See _source_index.sass for a more full understanding. #}
<div class="index-wrap">
<div class="header">
<img src="{{ url_for('main.select_logo') }}" alt="{{ gettext('Logo Image') }}" class="logo-image">
<img src="{{ url_for('main.select_logo') }}" alt="{{ g.organization_name }} | {{ gettext('Logo Image') }}" class="logo-image">
<div id="index-locales">
{% include 'locales.html' %}
</div>
Expand Down
3 changes: 3 additions & 0 deletions securedrop/tests/functional/functional_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ class FunctionalTest(object):
timeout = 10
poll_frequency = 0.1

orgname_default = "SecureDrop"
orgname_new = "Walden Inquirer"

accept_languages = None
default_driver_name = TORBROWSER
driver = None
Expand Down
Loading

0 comments on commit a9d57f2

Please sign in to comment.