Skip to content

Commit

Permalink
Merge pull request #39 from Qabel/m/2
Browse files Browse the repository at this point in the history
Identify identities only by public key
  • Loading branch information
audax authored Sep 26, 2016
2 parents 2188165 + e792812 commit bfc4519
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 42 deletions.
9 changes: 7 additions & 2 deletions index_service/logic.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.db import transaction

import index_service
from .models import Identity


class UpdateItem:
Expand Down Expand Up @@ -39,8 +40,12 @@ def start_verification(self, pending_verification_factory, url_filter=None):

def execute(self):
with transaction.atomic():
identity, _ = Identity.objects.get_or_create(defaults=self.identity._asdict(), public_key=self.identity.public_key)
identity.alias = self.identity.alias
identity.drop_url = self.identity.drop_url
identity.save()
for item in self.items:
item.execute(self.identity)
item.execute(identity)
if any(item.action == 'delete' for item in self.items):
# If an entry was deleted the identity may be garbage
self.identity.delete_if_garbage()
identity.delete_if_garbage()
33 changes: 33 additions & 0 deletions index_service/migrations/0004_auto_20160926_1618.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.6 on 2016-09-26 16:18
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('index_service', '0003_singular_data_model'),
]

operations = [
migrations.AlterField(
model_name='entry',
name='field',
field=models.CharField(choices=[('email', 'E-Mail address'), ('phone', 'Phone number')], db_index=True, max_length=30),
),
migrations.AlterField(
model_name='identity',
name='public_key',
field=models.CharField(db_index=True, max_length=64, unique=True),
),
migrations.AlterUniqueTogether(
name='identity',
unique_together=set([]),
),
migrations.AlterIndexTogether(
name='identity',
index_together=set([]),
),
]
5 changes: 1 addition & 4 deletions index_service/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,11 @@ class Identity(ExportModelOperationsMixin('Identity'), CreationTimestampModel):
This is the only kind of data the register server is allowed to return to clients.
"""

public_key = models.CharField(max_length=64)
public_key = models.CharField(max_length=64, db_index=True, unique=True)
alias = models.CharField(max_length=255)
drop_url = models.URLField()

class Meta:
# Index over the whole triplet; we'll access this way when processing update requests.
index_together = ('public_key', 'alias', 'drop_url')
unique_together = index_together
verbose_name_plural = 'Identities'

def delete_if_garbage(self):
Expand Down
30 changes: 12 additions & 18 deletions index_service/serializers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from collections import namedtuple

from django.conf import settings

from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.validators import UniqueTogetherValidator

from index_service.logic import UpdateRequest, UpdateItem
from index_service.models import Identity, Entry
from index_service.models import Entry
from index_service.crypto import decode_key
from index_service.utils import normalize_phone_number_localised, parse_phone_number, get_current_cc

Expand All @@ -23,10 +25,12 @@ def scrub_field(field, value):
return value


class IdentitySerializer(serializers.ModelSerializer):
class Meta:
model = Identity
fields = ('alias', 'public_key', 'drop_url')
class IdentitySerializer(serializers.Serializer):
Identity = namedtuple('Identity', 'alias, public_key, drop_url')

alias = serializers.CharField()
public_key = serializers.CharField(min_length=64, max_length=64)
drop_url = serializers.URLField()

def run_validators(self, value):
for validator in self.validators:
Expand All @@ -37,15 +41,10 @@ def run_validators(self, value):
super().run_validators(value)

def update(self, instance, validated_data):
instance.alias = validated_data.get('alias', instance.alias)
instance.public_key = validated_data.get('public_key', instance.public_key)
instance.drop_url = validated_data.get('drop_url', instance.drop_url)
instance.save()
return instance
return instance._replace(**validated_data)

def create(self, validated_data):
identity, _ = Identity.objects.get_or_create(defaults=validated_data, **validated_data)
return identity
return self.Identity(**validated_data)

def validate_public_key(self, value):
try:
Expand All @@ -66,9 +65,6 @@ def create(self, validated_data):
class SearchResultSerializer(IdentitySerializer):
matches = FieldSerializer(many=True)

class Meta(IdentitySerializer.Meta):
fields = IdentitySerializer.Meta.fields + ('matches',)


class SearchSerializer(serializers.Serializer):
query = FieldSerializer(many=True, required=True)
Expand Down Expand Up @@ -103,7 +99,7 @@ def create(self, validated_data):
class UpdateRequestSerializer(serializers.Serializer):
identity = IdentitySerializer(required=True)
public_key_verified = serializers.BooleanField(default=False)
items = UpdateItemSerializer(many=True, required=True)
items = UpdateItemSerializer(many=True, required=False, default=tuple())

def create(self, validated_data):
items = []
Expand All @@ -119,8 +115,6 @@ def create(self, validated_data):
return request

def validate_items(self, value):
if not value:
raise ValidationError('At least one update item is required.')
items = []
fieldspecs = set()
for item in value:
Expand Down
44 changes: 26 additions & 18 deletions tests/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from django.core import mail
from django.core.cache import cache
from django.forms.models import model_to_dict

from rest_framework import status

Expand Down Expand Up @@ -88,7 +89,8 @@ def test_match_is_exact(self, search_client, email_entry):
assert not response.data['identities']

def test_cross_identity(self, search_client, email_entry, identity):
identity2 = Identity(alias='1234', drop_url='http://127.0.0.1:6000/qabel_1234', public_key=identity.public_key)
pk2 = identity.public_key.replace('8520', '1234')
identity2 = Identity(alias='1234', drop_url='http://127.0.0.1:6000/qabel_1234', public_key=pk2)
identity2.save()
phone1, phone2 = '+491234', '+491235'
email = '[email protected]'
Expand All @@ -103,17 +105,6 @@ def test_cross_identity(self, search_client, email_entry, identity):
identities = response.data['identities']
assert len(identities) == 2

expected1 = {
'alias': '1234',
'drop_url': 'http://127.0.0.1:6000/qabel_1234',
'public_key': identity.public_key,
'matches': [
{'field': 'email', 'value': email},
{'field': 'phone', 'value': phone1},
]
}
assert expected1 in identities

def test_unknown_field(self, search_client):
response = search_client({'no such field': '...'})
assert response.status_code == status.HTTP_400_BAD_REQUEST, response.json()
Expand All @@ -140,15 +131,17 @@ def _update_request_with_no_verification(self, api_client, mocker, simple_identi
# Short-cut verification to execution
mocker.patch.object(UpdateRequest, 'start_verification', lambda self, *_: self.execute())
response = api_client.put(self.path, request, content_type='application/json', **kwargs)
assert response.status_code == status.HTTP_204_NO_CONTENT
assert response.status_code == status.HTTP_204_NO_CONTENT, response.json()

def _search(self, api_client, what):
response = api_client.get(SearchTest.path, what)
assert response.status_code == status.HTTP_200_OK, response.json()
result = response.data['identities']
assert len(result) == 1
assert result[0]['alias'] == 'public alias'
assert result[0]['drop_url'] == 'http://example.com'
identity = result[0]
assert identity['alias'] == 'public alias'
assert identity['drop_url'] == 'http://example.com'
return identity

def test_create(self, api_client, mocker, simple_identity):
email = 'onlypeople_who_knew_this_address_already_can_find_the_entry@example.com'
Expand All @@ -159,6 +152,23 @@ def test_create(self, api_client, mocker, simple_identity):
}])
self._search(api_client, {'email': email})

def test_change_alias(self, api_client, mocker, simple_identity):
email = 'onlypeople_who_knew_this_address_already_can_find_the_entry@example.com'
self._update_request_with_no_verification(api_client, mocker, simple_identity, [{
'action': 'create',
'field': 'email',
'value': email,
}])
identity = self._search(api_client, {'email': email})
simple_identity['alias'] = 'foo the bar'
self._update_request_with_no_verification(api_client, mocker, simple_identity, [])
response = api_client.get(SearchTest.path, {'email': email})
assert response.status_code == status.HTTP_200_OK, response.json()
result = response.data['identities']
assert len(result) == 1
identity = result[0]
assert identity['alias'] == 'foo the bar'

@pytest.mark.parametrize('accept_language', (
'de-de', # an enabled language, also the default
'ko-kr', # best korea
Expand Down Expand Up @@ -207,7 +217,7 @@ def delete_prerequisite(self, api_client, email_entry):
message = mail.outbox.pop()
assert message.to == [email_entry.value]
message_context = message.context
assert message_context['identity'] == email_entry.identity
assert message_context['identity']._asdict() == model_to_dict(email_entry.identity, exclude=['id'])

return message_context

Expand All @@ -233,9 +243,7 @@ def test_delete_deny(self, api_client, delete_prerequisite, email_entry):
assert Entry.objects.filter(value=email_entry.value).count() == 1

@pytest.mark.parametrize('invalid_request', [
{},
{'items': "a string?"},
{'items': []},
{
'items': [
{
Expand Down

0 comments on commit bfc4519

Please sign in to comment.