mirror of
https://github.com/GSA/notifications-api.git
synced 2025-12-14 17:22:17 -05:00
add template personalisation redaction
If passing in `redact_personalisation` to the template update endpoint, we should mark that template permanently as redacted - this means that we won't ever return the personalisation for any notifications for it. This is to be used with templates containing one time passwords, 2FA codes or other sensitive information that you may not want service workers to be able to see. This is implemented via a separate table, `template_redacted`, which just contains when the template was redacted.
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy.sql.expression import bindparam
|
||||
|
||||
from app import db
|
||||
from app.models import (Template, TemplateHistory)
|
||||
from app.models import (Template, TemplateHistory, TemplateRedacted)
|
||||
from app.dao.dao_utils import (
|
||||
transactional,
|
||||
version_class
|
||||
@@ -16,6 +17,13 @@ from app.dao.dao_utils import (
|
||||
def dao_create_template(template):
|
||||
template.id = uuid.uuid4() # must be set now so version history model can use same id
|
||||
template.archived = False
|
||||
|
||||
template.template_redacted = TemplateRedacted(
|
||||
template=template,
|
||||
redact_personalisation=False,
|
||||
updated_by=template.created_by
|
||||
)
|
||||
|
||||
db.session.add(template)
|
||||
|
||||
|
||||
@@ -25,6 +33,14 @@ def dao_update_template(template):
|
||||
db.session.add(template)
|
||||
|
||||
|
||||
@transactional
|
||||
def dao_redact_template(template, user_id):
|
||||
template.template_redacted.redact_personalisation = True
|
||||
template.template_redacted.updated_at = datetime.utcnow()
|
||||
template.template_redacted.updated_by_id = user_id
|
||||
db.session.add(template.template_redacted)
|
||||
|
||||
|
||||
def dao_get_template_by_id_and_service_id(template_id, service_id, version=None):
|
||||
if version is not None:
|
||||
return TemplateHistory.query.filter_by(
|
||||
|
||||
@@ -434,12 +434,14 @@ class Template(db.Model):
|
||||
subject = db.Column(db.Text, index=False, unique=False, nullable=True)
|
||||
created_by_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), index=True, nullable=False)
|
||||
created_by = db.relationship('User')
|
||||
version = db.Column(db.Integer, default=1, nullable=False)
|
||||
process_type = db.Column(db.String(255),
|
||||
db.ForeignKey('template_process_type.name'),
|
||||
index=True,
|
||||
nullable=False,
|
||||
default=NORMAL)
|
||||
version = db.Column(db.Integer, default=0, nullable=False)
|
||||
process_type = db.Column(
|
||||
db.String(255),
|
||||
db.ForeignKey('template_process_type.name'),
|
||||
index=True,
|
||||
nullable=False,
|
||||
default=NORMAL
|
||||
)
|
||||
|
||||
def get_link(self):
|
||||
# TODO: use "/v2/" route once available
|
||||
@@ -465,6 +467,19 @@ class Template(db.Model):
|
||||
return serialized
|
||||
|
||||
|
||||
class TemplateRedacted(db.Model):
|
||||
__tablename__ = 'template_redacted'
|
||||
|
||||
template_id = db.Column(UUID(as_uuid=True), db.ForeignKey('templates.id'), primary_key=True, nullable=False)
|
||||
redact_personalisation = db.Column(db.Boolean, nullable=False, default=False)
|
||||
updated_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
updated_by_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), nullable=False)
|
||||
updated_by = db.relationship('User')
|
||||
|
||||
# uselist=False as this is a one-to-one relationship
|
||||
template = db.relationship('Template', uselist=False, backref=db.backref('template_redacted', uselist=False))
|
||||
|
||||
|
||||
class TemplateHistory(db.Model):
|
||||
__tablename__ = 'templates_history'
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from flask import (
|
||||
from app.dao.templates_dao import (
|
||||
dao_update_template,
|
||||
dao_create_template,
|
||||
dao_redact_template,
|
||||
dao_get_template_by_id_and_service_id,
|
||||
dao_get_all_templates_for_service,
|
||||
dao_get_template_versions
|
||||
@@ -55,9 +56,17 @@ def create_template(service_id):
|
||||
def update_template(service_id, template_id):
|
||||
fetched_template = dao_get_template_by_id_and_service_id(template_id=template_id, service_id=service_id)
|
||||
|
||||
data = request.get_json()
|
||||
|
||||
# if redacting, don't update anything else
|
||||
if data.get('redact_personalisation') is True and 'updated_by_id' in data:
|
||||
# we also don't need to check what was passed in redact_personalisation - its presence in the dict is enough.
|
||||
dao_redact_template(fetched_template, data['updated_by_id'])
|
||||
return '', 200
|
||||
|
||||
current_data = dict(template_schema.dump(fetched_template).data.items())
|
||||
updated_template = dict(template_schema.dump(fetched_template).data.items())
|
||||
updated_template.update(request.get_json())
|
||||
updated_template.update(data)
|
||||
# Check if there is a change to make.
|
||||
if _template_has_not_changed(current_data, updated_template):
|
||||
return jsonify(data=updated_template), 200
|
||||
|
||||
31
migrations/versions/0102_template_redacted.py
Normal file
31
migrations/versions/0102_template_redacted.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: db6d9d9f06bc
|
||||
Revises: 0101_een_logo
|
||||
Create Date: 2017-06-27 15:37:28.878359
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'db6d9d9f06bc'
|
||||
down_revision = '0101_een_logo'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
def upgrade():
|
||||
op.create_table('template_redacted',
|
||||
sa.Column('template_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('redact_personalisation', sa.Boolean(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_by_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['template_id'], ['templates.id'], ),
|
||||
sa.ForeignKeyConstraint(['updated_by_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('template_id')
|
||||
)
|
||||
op.create_index(op.f('ix_template_redacted_updated_by_id'), 'template_redacted', ['updated_by_id'], unique=False)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('template_redacted')
|
||||
@@ -1,14 +1,21 @@
|
||||
from datetime import datetime
|
||||
|
||||
from freezegun import freeze_time
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
import pytest
|
||||
|
||||
from app.dao.templates_dao import (
|
||||
dao_create_template,
|
||||
dao_get_template_by_id_and_service_id,
|
||||
dao_get_all_templates_for_service,
|
||||
dao_update_template,
|
||||
dao_get_template_versions,
|
||||
dao_get_templates_for_cache)
|
||||
dao_get_templates_for_cache,
|
||||
dao_redact_template)
|
||||
from app.models import Template, TemplateHistory, TemplateRedacted
|
||||
|
||||
from tests.app.conftest import sample_template as create_sample_template
|
||||
from app.models import Template, TemplateHistory
|
||||
import pytest
|
||||
from tests.app.db import create_template
|
||||
|
||||
|
||||
@pytest.mark.parametrize('template_type, subject', [
|
||||
@@ -35,6 +42,17 @@ def test_create_template(sample_service, sample_user, template_type, subject):
|
||||
assert dao_get_all_templates_for_service(sample_service.id)[0].process_type == 'normal'
|
||||
|
||||
|
||||
def test_create_template_creates_redact_entry(sample_service):
|
||||
assert TemplateRedacted.query.count() == 0
|
||||
|
||||
template = create_template(sample_service)
|
||||
|
||||
redacted = TemplateRedacted.query.one()
|
||||
assert redacted.template_id == template.id
|
||||
assert redacted.redact_personalisation is False
|
||||
assert redacted.updated_by_id == sample_service.created_by_id
|
||||
|
||||
|
||||
def test_update_template(sample_service, sample_user):
|
||||
data = {
|
||||
'name': 'Sample Template',
|
||||
@@ -53,6 +71,20 @@ def test_update_template(sample_service, sample_user):
|
||||
assert dao_get_all_templates_for_service(sample_service.id)[0].name == 'new name'
|
||||
|
||||
|
||||
def test_redact_template(sample_template):
|
||||
redacted = TemplateRedacted.query.one()
|
||||
assert redacted.template_id == sample_template.id
|
||||
assert redacted.redact_personalisation is False
|
||||
|
||||
time = datetime.now()
|
||||
with freeze_time(time):
|
||||
dao_redact_template(sample_template, sample_template.created_by_id)
|
||||
|
||||
assert redacted.redact_personalisation is True
|
||||
assert redacted.updated_at == time
|
||||
assert redacted.updated_by_id == sample_template.created_by_id
|
||||
|
||||
|
||||
def test_get_all_templates_for_service(notify_db, notify_db_session, service_factory):
|
||||
service_1 = service_factory.get('service 1', email_from='service.1')
|
||||
service_2 = service_factory.get('service 2', email_from='service.2')
|
||||
|
||||
Reference in New Issue
Block a user