Skip to content

Commit

Permalink
storage: add new tombstone_repo method
Browse files Browse the repository at this point in the history
...and implement in `MemoryStorage` and `DatastoreStorage`. Used to delete accounts: bluesky-social/atproto#2503 (comment) . for snarfed/bridgy-fed#783
  • Loading branch information
snarfed committed May 21, 2024
1 parent bb7044c commit 14cd0ff
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 6 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ _Non-breaking changes:_

* Bug fix: base32-encode TIDs in record keys, `at://` URIs, commit `rev`s, etc. Before, we were using the integer UNIX timestamp directly, which happened to be the same 13 character length. Oops.
* Switch from `BGS_HOST` environment variable to `RELAY_HOST`. `BGS_HOST` is still supported for backward compatibility.
* `storage`:
* Add new `Storage.tombstone_repo` method, implement in `MemoryStorage` and `DatastoreStorage`. [Used to delete accounts.](https://github.com/bluesky-social/atproto/discussions/2503#discussioncomment-9502339) ([bridgy-fed#783](https://github.com/snarfed/bridgy-fed/issues/783))
* `datastore_storage`:
* Bug fix for `DatastoreStorage.last_seq`, handle new NSID.
* Add new `AtpRemoteBlob` class for storing "remote" blobs, available at public HTTP URLs, that we don't store ourselves.
Expand Down
21 changes: 19 additions & 2 deletions arroba/datastore_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from .repo import Repo
from . import storage
from .storage import Action, Block, Storage, SUBSCRIBE_REPOS_NSID
from .util import dag_cbor_cid, tid_to_int
from .util import dag_cbor_cid, tid_to_int, TOMBSTONED, TombstonedRepo

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -80,7 +80,7 @@ class CommitOp(ndb.Model):
https://googleapis.dev/python/python-ndb/latest/model.html#google.cloud.ndb.model.StructuredProperty
"""
action = ndb.StringProperty(required=True, choices=['create', 'update', 'delete'])
action = ndb.StringProperty(required=True, choices=('create', 'update', 'delete'))
path = ndb.StringProperty(required=True)
cid = ndb.StringProperty() # unset for deletes

Expand All @@ -96,6 +96,7 @@ class AtpRepo(ndb.Model):
* head (str): CID
* signing_key (str)
* rotation_key (str)
* status (str)
"""
handles = ndb.StringProperty(repeated=True)
head = ndb.StringProperty(required=True)
Expand All @@ -107,6 +108,7 @@ class AtpRepo(ndb.Model):
# TODO: rename this recovery_key_pem?
# https://discord.com/channels/1097580399187738645/1098725036917002302/1153447354003894372
rotation_key_pem = ndb.BlobProperty()
status = ndb.StringProperty(choices=(TOMBSTONED,))

created = ndb.DateTimeProperty(auto_now_add=True)
updated = ndb.DateTimeProperty(auto_now=True)
Expand Down Expand Up @@ -411,6 +413,8 @@ def load_repo(self, did_or_handle):
if not atp_repo:
logger.info(f"Couldn't find repo for {did_or_handle}")
return None
elif atp_repo.status == TOMBSTONED:
raise TombstonedRepo(f'{atp_repo.key} is tombstoned')

logger.info(f'Loading repo {atp_repo.key}')
self.head = CID.decode(atp_repo.head)
Expand All @@ -420,6 +424,19 @@ def load_repo(self, did_or_handle):
signing_key=atp_repo.signing_key,
rotation_key=atp_repo.rotation_key)

@ndb_context
def tombstone_repo(self, repo):
@ndb.transactional()
def update():
atp_repo = AtpRepo.get_by_id(repo.did)
atp_repo.status = TOMBSTONED
atp_repo.put()

update()

if repo.callback:
repo.callback(None) # TODO: format tombstone event

@ndb_context
def read(self, cid):
block = AtpBlock.get_by_id(cid.encode('base32'))
Expand Down
1 change: 1 addition & 0 deletions arroba/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class Repo:
callback = None
signing_key = None
rotation_key = None
status = None

def __init__(self, *, storage=None, mst=None, head=None, handle=None,
callback=None, signing_key=None, rotation_key=None):
Expand Down
21 changes: 20 additions & 1 deletion arroba/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from multiformats import CID, multicodec, multihash

from . import util
from .util import dag_cbor_cid, tid_to_int
from .util import dag_cbor_cid, tid_to_int, TOMBSTONED, TombstonedRepo

SUBSCRIBE_REPOS_NSID = 'com.atproto.sync.subscribeRepos'

Expand Down Expand Up @@ -151,6 +151,20 @@ def load_repo(self, did_or_handle):
Returns:
Repo, or None if the did or handle wasn't found:
Raises:
TombstonedRepo: if the repo is tombstoned
"""
raise NotImplementedError()

def tombstone_repo(self, repo):
"""Marks a repo as tombstoned.
After this, :meth:`load_repo` will raise :class:`TombstonedRepo` for
this repo.
Args:
repo (Repo)
"""
raise NotImplementedError()

Expand Down Expand Up @@ -329,8 +343,13 @@ def load_repo(self, did_or_handle):

for repo in self.repos:
if did_or_handle in (repo.did, repo.handle):
if repo.status == TOMBSTONED:
raise TombstonedRepo(f'{repo.did} is tombstoned')
return repo

def tombstone_repo(self, repo):
repo.status = TOMBSTONED

def read(self, cid):
return self.blocks.get(cid)

Expand Down
13 changes: 11 additions & 2 deletions arroba/tests/test_datastore_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
)
from ..repo import Action, Repo, Write
from ..storage import Block, CommitData, MemoryStorage, SUBSCRIBE_REPOS_NSID
from ..util import dag_cbor_cid, new_key, next_tid
from ..util import dag_cbor_cid, new_key, next_tid, TOMBSTONED, TombstonedRepo

from . import test_repo
from .testutil import DatastoreTest, requests_response
Expand Down Expand Up @@ -80,10 +80,19 @@ def test_create_load_repo(self):
def test_create_load_repo_no_handle(self):
repo = Repo.create(self.storage, 'did:web:user.com', signing_key=self.key,
rotation_key=self.key)
# self.storage.create_repo(repo)
self.assertEqual([], AtpRepo.get_by_id('did:web:user.com').handles)
self.assertIsNone(self.storage.load_repo('han.dull'))

def test_tombstone_repo(self):
repo = Repo.create(self.storage, 'did:user', signing_key=self.key)
self.assertIsNone(AtpRepo.get_by_id('did:user').status)

self.storage.tombstone_repo(repo)
self.assertEqual(TOMBSTONED, AtpRepo.get_by_id('did:user').status)

with self.assertRaises(TombstonedRepo):
self.storage.load_repo('did:user')

def test_atp_block_create(self):
data = {'foo': 'bar'}
AtpBlock.create(repo_did='did:web:user.com', data=data, seq=1)
Expand Down
12 changes: 11 additions & 1 deletion arroba/tests/test_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import dag_cbor
from multiformats import CID

from ..util import next_tid
from ..repo import Repo, Write
from ..storage import Action, Block, MemoryStorage
from ..util import next_tid, TOMBSTONED, TombstonedRepo

from .testutil import TestCase

Expand Down Expand Up @@ -76,3 +76,13 @@ def test_read_commits_by_seq_include_record_block_even_if_preexisting(self):

record = Block(decoded={'foo': 'bar'})
self.assertEqual(record, commits[0].blocks[record.cid])

def test_tombstone_repo(self):
storage = MemoryStorage()
repo = Repo.create(storage, 'did:user', signing_key=self.key)
storage.tombstone_repo(repo)

self.assertEqual(TOMBSTONED, repo.status)

with self.assertRaises(TombstonedRepo):
storage.load_repo('did:user')
5 changes: 5 additions & 0 deletions arroba/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
ec.SECP256K1: 0xFFFFFFFF_FFFFFFFF_FFFFFFFF_FFFFFFFE_BAAEDCE6_AF48A03B_BFD25E8C_D0364141
}

TOMBSTONED = 'tombstoned'

class TombstonedRepo(Exception):
pass


def now(tz=timezone.utc, **kwargs):
"""Wrapper for :meth:`datetime.datetime.now` that lets us mock it out in tests."""
Expand Down

0 comments on commit 14cd0ff

Please sign in to comment.