Repository: Kinto/kinto

Issue:
PUT on a tombstone keeps delete field
Put on tombstones has an interesting behavior. When you PUT something with a deleted field, the object is recreated, but the `deleted: true` field is maintained. 

```json
gsurita-30820:kinto gsurita$ http delete localhost:8888/v1/buckets/b -a a:a
HTTP/1.1 200 OK
Access-Control-Expose-Headers: Alert, Retry-After, Content-Length, Backoff
Content-Length: 64
Content-Type: application/json
Date: Sat, 25 Feb 2017 16:19:11 GMT
Etag: "1488039551067"
Last-Modified: Sat, 25 Feb 2017 16:19:11 GMT
Server: waitress
X-Content-Type-Options: nosniff

{
    "data": {
        "deleted": true,
        "id": "b",
        "last_modified": 1488039551067
    }
}

gsurita-30820:kinto gsurita$ http put localhost:8888/v1/buckets/b -a a:a
HTTP/1.1 201 Created
Access-Control-Expose-Headers: Alert, Retry-After, Content-Length, Backoff
Content-Length: 167
Content-Type: application/json
Date: Sat, 25 Feb 2017 16:19:15 GMT
Etag: "1488039555986"
Last-Modified: Sat, 25 Feb 2017 16:19:15 GMT
Server: waitress
X-Content-Type-Options: nosniff

{
    "data": {
        "deleted": true,
        "id": "b",
        "last_modified": 1488039555986
    },
    "permissions": {
        "write": [
            "basicauth:007f513f3cb1622ac923571e17ea0b4512f41d62c51092acec6e2666e6f5d65b"
        ]
    }
}
```


Discussion:
Indeed the `delete: true` shouldn't be kept.
Now that we are here I think we have a few problems with the current way we handle preconditions and tombstones:

- We use `deleted` for precondition checks, but if a record exists we can change the `deleted` field to true and it will behave as a deleted record.
- Before #1102 we used to allow checking tombstones with `If-Match` checks but we also allowed creation with `If-None-Match: *`. Now tombstones behave like non-existing records. In my opinion tombstones should always have the same behavior of records for precondition checks. 
> if a record exists we can change the deleted field to true and it will behave as a deleted record.

What makes you say that? I don't think this is accurate.
> Now tombstones behave like non-existing records.

Actually tombstones should works as non existing record, the record has been deleted so it doesn't exists anymore. The tombstones is just an indication that the record once existed but if you create it again it is a totally brand new record.
>What makes you say that? I don't think this is accurate.

Take a look at this example, the second query should have failed, but precondition checks look directly for a `deleted` field.

```json
gsurita-30820:kinto gsurita$ echo '{"data": {"deleted": true}}' | http PUT localhost:8888/v1/buckets/b If-None-Match:* -a a:a

{
    "data": {
        "deleted": true,
        "id": "b",
        "last_modified": 1488202807022
    },
    "permissions": {
        "write": [
            "basicauth:007f513f3cb1622ac923571e17ea0b4512f41d62c51092acec6e2666e6f5d65b"
        ]
    }
}

gsurita-30820:kinto gsurita$ echo '{"data": {"deleted": true}}' | http PUT localhost:8888/v1/buckets/b If-None-Match:* -a a:a

{
    "data": {
        "deleted": true,
        "id": "b",
        "last_modified": 1488202809191
    },
    "permissions": {
        "write": [
            "basicauth:007f513f3cb1622ac923571e17ea0b4512f41d62c51092acec6e2666e6f5d65b"
        ]
    }
}


```
> Actually tombstones should works as non existing record, the record has been deleted so it doesn't exists anymore. The tombstones is just an indication that the record once existed but if you create it again it is a totally brand new record.

Actually thinking through it with the changes made on #1102 I agree with that. We can follow this approach and simplify a bit some parts of the code.

 My main concern for this was what happens if:
1. At timestamp 1, user A creates a record R.
2. At timestamp 2, user B fetches and deletes the record R.
3. At timestamp 3, user A uses put again over the record record R `If-Match: "1"`.

From my understanding the expected messages should be.
1. Create
2. Delete
3. Precondition failed 

Both before and after #1102 we would get the same messages, but from different sources. Before we would raise because we checked tombstones for `If-Match`, now it will raise because `If-Match` will always fail on non-existing records.

Resolving patch:
diff --git a/kinto/core/resource/__init__.py b/kinto/core/resource/__init__.py
--- a/kinto/core/resource/__init__.py
+++ b/kinto/core/resource/__init__.py
@@ -441,30 +441,22 @@ def put(self):
             :meth:`kinto.core.resource.UserResource.process_record`.
         """
         self._raise_400_if_invalid_id(self.record_id)
-        id_field = self.model.id_field
-        existing = None
-        tombstones = None
         try:
             existing = self._get_record_or_404(self.record_id)
         except HTTPNotFound:
-            # Look if this record used to exist (for preconditions check).
-            filter_by_id = Filter(id_field, self.record_id, COMPARISON.EQ)
-            tombstones, _ = self.model.get_records(filters=[filter_by_id],
-                                                   include_deleted=True)
-            if len(tombstones) > 0:
-                existing = tombstones[0]
-        finally:
-            self._raise_412_if_modified(record=existing)
+            existing = None
+
+        self._raise_412_if_modified(record=existing)
 
         # If `data` is not provided, use existing record (or empty if creation)
         post_record = self.request.validated['body'].get('data', existing) or {}
 
-        record_id = post_record.setdefault(id_field, self.record_id)
+        record_id = post_record.setdefault(self.model.id_field, self.record_id)
         self._raise_400_if_id_mismatch(record_id, self.record_id)
 
         new_record = self.process_record(post_record, old=existing)
 
-        if existing and not tombstones:
+        if existing:
             record = self.model.update_record(new_record)
         else:
             record = self.model.create_record(new_record)
@@ -830,8 +822,8 @@ def _raise_412_if_modified(self, record=None):
         if_match = self.request.validated['header'].get('If-Match')
         if_none_match = self.request.validated['header'].get('If-None-Match')
 
-        # Check if record exists and it's not a tombstone
-        record_exists = record is not None and not record.get(self.model.deleted_field)
+        # Check if record exists
+        record_exists = record is not None
 
         # If no precondition headers, just ignore
         if not if_match and not if_none_match:


Test patch:
diff --git a/tests/core/resource/test_record.py b/tests/core/resource/test_record.py
--- a/tests/core/resource/test_record.py
+++ b/tests/core/resource/test_record.py
@@ -126,6 +126,12 @@ def test_storage_is_not_used_if_context_provides_current_record(self):
             self.resource.put()
             self.assertFalse(get.called)
 
+    def test_put_over_tombstone_create_new_record(self):
+        self.resource.delete()
+        result = self.resource.put()
+        self.assertNotIn("old", result)
+        self.assertNotIn("deleted", result)
+
 
 class DeleteTest(BaseTest):
     def test_delete_record_returns_last_timestamp(self):