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:
Leo Hemsted
2017-06-28 10:26:25 +01:00
parent 73e0432a69
commit 29fc81090e
5 changed files with 114 additions and 11 deletions

View File

@@ -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(

View File

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

View File

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

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

View File

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