Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds option to set and use an organization name #5629

Merged
merged 4 commits into from
Dec 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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