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 "safe deletion" functionality to journalist interface #5770

Merged
merged 5 commits into from
Feb 22, 2021
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 @@ -179,6 +179,8 @@
/var/www/securedrop/journalist_app/utils.py r,
/var/www/securedrop/journalist_templates/_confirmation_modal.html r,
/var/www/securedrop/journalist_templates/_source_row.html r,
/var/www/securedrop/journalist_templates/_sources_confirmation_final_modal.html r,
/var/www/securedrop/journalist_templates/_sources_confirmation_modal.html r,
/var/www/securedrop/journalist_templates/account_edit_hotp_secret.html r,
/var/www/securedrop/journalist_templates/account_new_two_factor.html r,
/var/www/securedrop/journalist_templates/admin.html r,
Expand Down Expand Up @@ -264,6 +266,10 @@
/var/www/securedrop/static/i/bang-stop.png r,
/var/www/securedrop/static/i/bang-circle.png r,
/var/www/securedrop/static/i/favicon.png r,
/var/www/securedrop/static/i/modal-x-white.png r,
zenmonkeykstop marked this conversation as resolved.
Show resolved Hide resolved
/var/www/securedrop/static/i/flash-success.png r,
/var/www/securedrop/static/i/flash-error.png r,
/var/www/securedrop/static/i/flash-notification.png r,
/var/www/securedrop/static/i/font-awesome/black/guard.svg r,
/var/www/securedrop/static/i/font-awesome/black/times.svg r,
/var/www/securedrop/static/i/font-awesome/cancel-blue.png r,
Expand Down
31 changes: 25 additions & 6 deletions securedrop/journalist_app/col.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
request,
send_file,
url_for,
Markup,
escape,
)
import werkzeug
from flask_babel import gettext
Expand All @@ -24,7 +26,7 @@
from journalist_app.utils import (make_star_true, make_star_false, get_source,
delete_collection, col_download_unread,
col_download_all, col_star, col_un_star,
col_delete, mark_seen)
col_delete, col_delete_data, mark_seen)
from sdconfig import SDConfig


Expand Down Expand Up @@ -61,18 +63,35 @@ def delete_single(filesystem_id: str) -> werkzeug.Response:
current_app.logger.error("error deleting collection: %s", e)
abort(500)

flash(gettext("{source_name}'s collection deleted.")
.format(source_name=source.journalist_designation),
"notification")
flash(
Markup(
"<b>{}</b> {}".format(
# Translators: Here, "Success!" appears before a message
# confirming the success of an operation.
escape(gettext("Success!")),
escape(gettext(
"The account and data for the source {} has been deleted.").format(
source.journalist_designation))
)
), 'success')

return redirect(url_for('main.index'))

@view.route('/process', methods=('POST',))
def process() -> werkzeug.Response:
actions = {'download-unread': col_download_unread,
'download-all': col_download_all, 'star': col_star,
'un-star': col_un_star, 'delete': col_delete}
'un-star': col_un_star, 'delete': col_delete,
'delete-data': col_delete_data}
if 'cols_selected' not in request.form:
flash(gettext('No collections selected.'), 'error')
flash(
Markup("<b>{}</b> {}".format(
# Translators: Here, "Nothing Selected" appears before a message
# asking the user to select one or more items.
escape(gettext('Nothing Selected')),
escape(gettext('You must select one or more items.'))
)
), 'error')
return redirect(url_for('main.index'))

# getlist is cgi.FieldStorage.getlist
Expand Down
37 changes: 30 additions & 7 deletions securedrop/journalist_app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import werkzeug
from flask import (Blueprint, request, current_app, session, url_for, redirect,
render_template, g, flash, abort)
render_template, g, flash, abort, Markup, escape)
from flask_babel import gettext

import store
Expand Down Expand Up @@ -138,8 +138,16 @@ def reply() -> werkzeug.Response:
g.user.id,
exc.__class__))
else:
flash(gettext("Thanks. Your reply has been stored."),
"notification")

flash(
Markup(
"<b>{}</b> {}".format(
# Translators: Here, "Success!" appears before a message
# confirming the success of an operation.
escape(gettext("Success!")),
escape(gettext("Your reply has been stored."))
)
), 'success')
finally:
return redirect(url_for('col.col', filesystem_id=g.filesystem_id))

Expand All @@ -159,11 +167,26 @@ def bulk() -> Union[str, werkzeug.Response]:
if doc.filename in doc_names_selected]
if selected_docs == []:
if action == 'download':
flash(gettext("No collections selected for download."),
"error")
flash(
Markup(
"<b>{}</b> {}".format(
# Translators: Here, "Nothing Selected" appears before a message
# asking the users to select one or more items
escape(gettext("Nothing Selected")),
escape(gettext("You must select one or more items for download"))
)
), 'error')
elif action in ('delete', 'confirm_delete'):
flash(gettext("No collections selected for deletion."),
"error")
flash(
Markup(
"<b>{}</b> {}".format(
# Translators: Here, "Nothing Selected" appears before a message
# asking the users to select one or more items
escape(gettext("Nothing Selected")),
escape(gettext("You must select one or more items for deletion"))
)
), 'error')

return redirect(error_redirect)

if action == 'download':
Expand Down
79 changes: 66 additions & 13 deletions securedrop/journalist_app/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import flask
import werkzeug
from flask import (g, flash, current_app, abort, send_file, redirect, url_for,
render_template, Markup, sessions, request)
render_template, Markup, sessions, request, escape)
from flask_babel import gettext, ngettext
from sqlalchemy.exc import IntegrityError

Expand Down Expand Up @@ -99,7 +99,7 @@ def validate_user(
InvalidPasswordLength) as e:
current_app.logger.error("Login for '{}' failed: {}".format(
username, e))
login_flashed_msg = error_message if error_message else gettext('Login failed.')
login_flashed_msg = error_message if error_message else gettext('<b>Login failed.</b>')

if isinstance(e, LoginThrottledException):
login_flashed_msg += " "
Expand All @@ -123,7 +123,7 @@ def validate_user(
except Exception:
pass

flash(login_flashed_msg, "error")
flash(Markup(login_flashed_msg), "error")
return None


Expand Down Expand Up @@ -253,14 +253,17 @@ def bulk_delete(
deletion_errors += 1

num_selected = len(items_selected)
success_message = ngettext(
"The item has been deleted.", "{num} items have been deleted.",
num_selected).format(num=num_selected)

flash(
ngettext(
"Submission deleted.",
"{num} submissions deleted.",
num_selected
).format(num=num_selected),
"notification"
)
Markup(
"<b>{}</b> {}".format(
# Translators: Here, "Success!" appears before a message
# indicating a successful deletion
escape(gettext("Success!")), escape(success_message))), 'success')

if deletion_errors > 0:
current_app.logger.error("Disconnected submission entries (%d) were detected",
deletion_errors)
Expand Down Expand Up @@ -319,14 +322,64 @@ def col_delete(cols_selected: List[str]) -> werkzeug.Response:
db.session.commit()

num = len(cols_selected)
flash(ngettext('{num} collection deleted', '{num} collections deleted',
num).format(num=num),
"notification")

success_message = ngettext(
"The account and all data for {n} source have been deleted.",
"The accounts and all data for {n} sources have been deleted.",
num).format(n=num)

flash(
Markup(
"<b>{}</b> {}".format(
# Translators: Here, "Success!" appears before a message
# indicating a successful deletion
escape(gettext("Success!")), escape(success_message))), 'success')

return redirect(url_for('main.index'))


def delete_source_files(filesystem_id: str) -> None:
"""deletes submissions and replies for specified source"""
source = get_source(filesystem_id, include_deleted=True)
if source is not None:
# queue all files for deletion and remove them from the database
for f in source.collection:
try:
delete_file_object(f)
except Exception:
zenmonkeykstop marked this conversation as resolved.
Show resolved Hide resolved
pass


def col_delete_data(cols_selected: List[str]) -> werkzeug.Response:
"""deletes store data for selected sources"""
if len(cols_selected) < 1:
flash(
Markup(
"<b>{}</b> {}".format(
# Translators: Here, "Nothing Selected" appears before a message
# asking the user to select one or more items
escape(gettext("Nothing Selected")),
escape(gettext("You must select one or more items for deletion.")))
), 'error')
else:

for filesystem_id in cols_selected:
delete_source_files(filesystem_id)

flash(
Markup(
"<b>{}</b> {}".format(
# Translators: Here, "Success" appears before a message
# indicating a successful deletion
escape(gettext("Success!")),
escape(gettext("The files and messages have been deleted.")))
), 'success')

return redirect(url_for('main.index'))


def delete_collection(filesystem_id: str) -> None:
"""deletes source account including files and reply key"""
# Delete the source's collection of submissions
path = current_app.storage.path(filesystem_id)
if os.path.exists(path):
Expand Down
10 changes: 7 additions & 3 deletions securedrop/journalist_templates/_confirmation_modal.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
<div id="{{ modal_data.modal_id }}" class="modal-dialog">
<a href="#close" class="external"></a>
<div>
<a href="#close" title="{{ gettext('Close') }}" class="close">X</a>
<a href="#close" title="{{ gettext('Close') }}" class="close"> <img src="{{ url_for('static', filename='i/modal-x-white.png') }}" height="24" width="24"></a>
<h2>{{ modal_data.modal_header }}</h2>
<p>{{ modal_data.modal_body }}</p>
{% if modal_data.modal_warning is defined %}
<p><em>{{ modal_data.modal_warning }}</em></p>
{% endif %}
<a href="#close" id="{{ modal_data.cancel_id }}" title="{{ gettext('Cancel') }}" class="btn upper">{{ gettext('Cancel') }}</a>
<button type="submit" id="{{ modal_data.submit_id }}" name="action" value="delete" class="{{ modal_data.submit_btn_type }} upper">{{ modal_data.submit_btn_text }}</button>
<center>
<div class="btn-row">
<a href="#close" id="{{ modal_data.cancel_id }}" title="{{ gettext('Cancel') }}" class="btn cancel small">{{ gettext('Cancel') }}</a>
<button type="submit" id="{{ modal_data.submit_id }}" name="action" value="delete" class="btn small {{ modal_data.submit_btn_type }}">{{ modal_data.submit_btn_text }}</button>
</div>
</center>
</div>
</div>
1 change: 1 addition & 0 deletions securedrop/journalist_templates/_source_row.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
{% endif %}
</div>
<div class="submission-count">
<input type="hidden" name="count-{{ source.journalist_designation|lower|replace(" ", "_") }}" class="submission-count-element" value="{{ docs + msgs }}">
<span>
<img src="{{ url_for('static', filename='icons/files.png') }}" class="icon-drop" width="14" height="16" alt="">
{{ ngettext('1 doc', '{num} docs', docs).format(num=docs) }}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<div id="{{ modal_data.modal_id }}" class="menu-modal-dialog">
<a href="#close" class="external"></a>
<div id="delete-confirm-menu-dialog">
<p>
{{ gettext('When the account for a source is deleted:') }}
<ul>
<li>{{ gettext('The source will not be able to log in with their codename again.') }}</li>
<li>{{ gettext('You will not be able to send them replies.') }}</li>
<li>{{ gettext('All files and messages from that source will also be destroyed.') }}</li>
</ul>
</p>
<p>
<span class="modal-danger-text">{{ gettext('Are you sure this is what you want?') }}</span</p>
<div class="btn-row">
<a href="#close" id="cancel-collections-deletions" title="{{ gettext('Cancel') }}" class="btn cancel small">{{ gettext('Cancel') }}</a>
<button type="submit" id="delete-collections-confirm" name="action" value="delete" class="btn small danger">{{ gettext('Yes, Delete Selected Source Accounts') }}</button>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<div id="{{ modal_data.modal_id }}" class="menu-modal-dialog">
<a href="#close" class="external"></a>
<div id="delete-menu-dialog">
<div id="delete-menu-no-select">
<p class="modal-danger-text">
<span class="modal-danger-text">{{ gettext("Nothing Selected") }}</span>
</p>
<p>
{{ gettext("You must select one or more items for deletion.") }}
</p>
</div>
<div id="delete-menu-cta">
<p>
<span id="delete-menu-summary"></span>
</p>
<p>
{{ gettext("What would you like to delete?") }}
zenmonkeykstop marked this conversation as resolved.
Show resolved Hide resolved
</p>
<p>
<button id="delete-files-and-messages" type="submit" name="action" value="delete-data" class="small btn danger modal-stacked">{{ gettext('Files and Messages') }}</button>
</p>
<p>
<a href="#delete-sources-confirm-modal" id="delete-collections">
<button type="button" class="small danger btn modal-stacked">{{ gettext('Source Accounts') }}</button>
</a>
</p>
</div>
<p>
<a href="#close" id="delete-menu-dialog-cancel">{{ gettext('Cancel') }}</a>
</p>
</div>
</div>
11 changes: 7 additions & 4 deletions securedrop/journalist_templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,14 @@
{% include 'locales.html' %}
</div>
{% endblock %}
<div class="panel-container column">
<div class="flash-panel">
{% include 'flashed.html' %}
</div>
<div class="panel selected">

<div class="panel selected">
{% include 'flashed.html' %}

{% block body %}{% endblock %}
{% block body %}{% endblock %}
</div>
</div>
</div>

Expand Down
Loading