diff --git a/src/_secureaccesstokenauthorizer/model.py b/src/_secureaccesstokenauthorizer/model.py index 2e852c42f1f1e365eba39e4ff945b3774e9fb0c0..bcdc4d636432856fc03015ff2d03a1152dfbfb50 100644 --- a/src/_secureaccesstokenauthorizer/model.py +++ b/src/_secureaccesstokenauthorizer/model.py @@ -17,26 +17,25 @@ This module implements models (in the MVC sense) for the client side of the storage plugin. """ -from os import ( - makedirs, - listdir, -) -from errno import ( - EEXIST, - ENOENT, +from functools import ( + wraps, ) from json import ( loads, dumps, ) + +from sqlite3 import ( + connect, +) + import attr -# XXX -from allmydata.node import ( - _Config, - MissingConfigEntry, +from twisted.python.filepath import ( + FilePath, ) + class StoreAddError(Exception): def __init__(self, reason): self.reason = reason @@ -47,73 +46,126 @@ class StoreDirectoryError(Exception): self.reason = reason +class SchemaError(TypeError): + pass + + +CONFIG_DB_NAME = u"privatestorageio-satauthz-v1.sqlite3" + +def open_and_initialize(path): + try: + path.parent().makedirs(ignoreExistingDirectory=True) + except OSError as e: + raise StoreDirectoryError(e) + + conn = connect( + path.asBytesMode().path, + isolation_level="IMMEDIATE", + ) + with conn: + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS [version] AS SELECT 1 AS [version] + """ + ) + cursor.execute( + """ + SELECT [version] FROM [version] + """ + ) + expected = [(1,)] + version = cursor.fetchall() + if version != expected: + raise SchemaError( + "Unexpected database schema version. Expected {}. Got {}.".format( + expected, + version, + ), + ) + + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS [payment-references] ( + number text, + + PRIMARY KEY(number) + ) + """, + ) + return conn + + +def with_cursor(f): + @wraps(f) + def with_cursor(self, *a, **kw): + with self._connection: + return f(self, self._connection.cursor(), *a, **kw) + return with_cursor + + @attr.s(frozen=True) class PaymentReferenceStore(object): """ This class implements persistence for payment references. - :ivar _Config node_config: The Tahoe-LAFS node configuration object for + :ivar allmydata.node._Config node_config: The Tahoe-LAFS node configuration object for the node that owns the persisted payment preferences. """ - _CONFIG_DIR = u"privatestorageio-satauthz-v1" - node_config = attr.ib(type=_Config) - - def _config_key(self, prn): - return u"{}/{}.prn+json".format(self._CONFIG_DIR, prn) - - def _prn(self, config_key): - if config_key.endswith(u".prn+json"): - return config_key[:-len(u".prn+json")] - raise ValueError("{} does not look like a config key".format(config_key)) - - def _read_pr_json(self, prn): - private_config_item = self._config_key(prn) - try: - return self.node_config.get_private_config(private_config_item) - except MissingConfigEntry: + database_path = attr.ib(type=FilePath) + _connection = attr.ib() + + @classmethod + def from_node_config(cls, node_config): + db_path = FilePath(node_config.get_private_path(CONFIG_DB_NAME)) + conn = open_and_initialize( + db_path, + ) + return cls( + db_path, + conn, + ) + + @with_cursor + def get(self, cursor, prn): + cursor.execute( + """ + SELECT + ([number]) + FROM + [payment-references] + WHERE + [number] = ? + """, + (prn,), + ) + refs = cursor.fetchall() + if len(refs) == 0: raise KeyError(prn) + return PaymentReference(refs[0][0]) + + @with_cursor + def add(self, cursor, prn): + cursor.execute( + """ + INSERT OR IGNORE INTO [payment-references] VALUES (?) + """, + (prn,) + ) + + @with_cursor + def list(self, cursor): + cursor.execute( + """ + SELECT ([number]) FROM [payment-references] + """, + ) + refs = cursor.fetchall() - def _write_pr_json(self, prn, pr_json): - private_config_item = self._config_key(prn) - # XXX Need an API to be able to avoid touching the filesystem directly - # here. - container = self.node_config.get_private_path(self._CONFIG_DIR) - try: - makedirs(container) - except EnvironmentError as e: - if EEXIST != e.errno: - raise StoreDirectoryError(e) - try: - self.node_config.write_private_config(private_config_item, pr_json) - except Exception as e: - raise StoreAddError(e) - - def get(self, prn): - payment_reference_json = self._read_pr_json(prn) - return PaymentReference.from_json(payment_reference_json) - - def add(self, prn): - # XXX Not *exactly* atomic is it? Probably want a - # write_private_config_if_not_exists or something. - try: - self._read_pr_json(prn) - except KeyError: - self._write_pr_json(prn, PaymentReference(prn).to_json()) - - def list(self): - # XXX Need an API to be able to avoid touching the filesystem directly - # here. - container = self.node_config.get_private_path(self._CONFIG_DIR) - try: - children = listdir(container) - except EnvironmentError as e: - if ENOENT != e.errno: - raise - children = [] return list( - PaymentReference(self._prn(config_key)) - for config_key - in children + PaymentReference(number) + for (number,) + in refs ) diff --git a/src/_secureaccesstokenauthorizer/resource.py b/src/_secureaccesstokenauthorizer/resource.py index 49b37f5b8b9038d352ac469749d289da75389523..cd9b06b9eef7e8fb771d88a9c794f6f15aa39a8a 100644 --- a/src/_secureaccesstokenauthorizer/resource.py +++ b/src/_secureaccesstokenauthorizer/resource.py @@ -60,7 +60,7 @@ def from_configuration(node_config): :return IResource: The root of the resource hierarchy presented by the client side of the plugin. """ - store = PaymentReferenceStore(node_config) + store = PaymentReferenceStore.from_node_config(node_config) controller = PaymentController(store) root = Resource() root.putChild( diff --git a/src/_secureaccesstokenauthorizer/tests/test_model.py b/src/_secureaccesstokenauthorizer/tests/test_model.py index 1159133d7a6fe8d0fd8834ad423af7e7b0e94551..0d3772afe2e2873baa520bf0998fb79df28b183c 100644 --- a/src/_secureaccesstokenauthorizer/tests/test_model.py +++ b/src/_secureaccesstokenauthorizer/tests/test_model.py @@ -17,7 +17,6 @@ Tests for ``_secureaccesstokenauthorizer.model``. """ from os import ( - chmod, mkdir, ) from errno import ( @@ -73,7 +72,7 @@ class PaymentReferenceStoreTests(TestCase): """ tempdir = self.useFixture(TempDir()) config = get_config(tempdir.join(b"node"), b"tub.port") - store = PaymentReferenceStore(config) + store = PaymentReferenceStore.from_node_config(config) self.assertThat( lambda: store.get(prn), raises(KeyError), @@ -88,7 +87,7 @@ class PaymentReferenceStoreTests(TestCase): """ tempdir = self.useFixture(TempDir()) config = get_config(tempdir.join(b"node"), b"tub.port") - store = PaymentReferenceStore(config) + store = PaymentReferenceStore.from_node_config(config) store.add(prn) payment_reference = store.get(prn) self.assertThat( @@ -106,7 +105,7 @@ class PaymentReferenceStoreTests(TestCase): """ tempdir = self.useFixture(TempDir()) config = get_config(tempdir.join(b"node"), b"tub.port") - store = PaymentReferenceStore(config) + store = PaymentReferenceStore.from_node_config(config) store.add(prn) store.add(prn) payment_reference = store.get(prn) @@ -128,7 +127,7 @@ class PaymentReferenceStoreTests(TestCase): tempdir = self.useFixture(TempDir()) nodedir = tempdir.join(b"node") config = get_config(nodedir, b"tub.port") - store = PaymentReferenceStore(config) + store = PaymentReferenceStore.from_node_config(config) for prn in prns: store.add(prn) @@ -142,59 +141,24 @@ class PaymentReferenceStoreTests(TestCase): ) - @given(tahoe_configs(), payment_reference_numbers(), payment_reference_numbers()) - def test_unwriteable_store_directory(self, get_config, prn_a, prn_b): - """ - If the underlying directory in the node configuration is not writeable - then ``PaymentReferenceStore.add`` raises ``StoreAddError``. - """ - assume(prn_a != prn_b) - tempdir = self.useFixture(TempDir()) - nodedir = tempdir.join(b"node") - config = get_config(nodedir, b"tub.port") - store = PaymentReferenceStore(config) - # Initialize the underlying directory. - store.add(prn_a) - # Mess it up - chmod(config.get_private_path(store._CONFIG_DIR), 0o500) - - self.assertThat( - lambda: store.add(prn_b), - Raises( - AfterPreprocessing( - lambda (type, exc, tb): exc, - MatchesAll( - IsInstance(StoreAddError), - MatchesStructure( - reason=MatchesAll( - IsInstance(IOError), - MatchesStructure( - errno=Equals(EACCES), - ), - ), - ), - ), - ), - ), - ) - @given(tahoe_configs(), payment_reference_numbers()) def test_uncreateable_store_directory(self, get_config, prn): """ If the underlying directory in the node configuration cannot be created - then ``PaymentReferenceStore.add`` raises ``StoreDirectoryError``. + then ``PaymentReferenceStore.from_node_config`` raises + ``StoreDirectoryError``. """ tempdir = self.useFixture(TempDir()) nodedir = tempdir.join(b"node") - config = get_config(nodedir, b"tub.port") - store = PaymentReferenceStore(config) # Create the node directory without permission to create the # underlying directory. mkdir(nodedir, 0o500) + config = get_config(nodedir, b"tub.port") + self.assertThat( - lambda: store.add(prn), + lambda: PaymentReferenceStore.from_node_config(config), Raises( AfterPreprocessing( lambda (type, exc, tb): exc, diff --git a/src/_secureaccesstokenauthorizer/tests/test_plugin.py b/src/_secureaccesstokenauthorizer/tests/test_plugin.py index 99d1db405ba341394313ba4a72f268c92259c9af..a7b9dceca6ac9f768d8d9b0f7722f4b25dc7a643 100644 --- a/src/_secureaccesstokenauthorizer/tests/test_plugin.py +++ b/src/_secureaccesstokenauthorizer/tests/test_plugin.py @@ -20,6 +20,10 @@ from zope.interface import ( implementer, ) +from fixtures import ( + TempDir, +) + from testtools import ( TestCase, ) @@ -234,11 +238,14 @@ class ClientResourceTests(TestCase): ``IFoolscapStoragePlugin.get_client_resource``. """ @given(tahoe_configs()) - def test_interface(self, tahoe_config): + def test_interface(self, get_config): """ ``get_client_resource`` returns an object that provides ``IResource``. """ + tempdir = self.useFixture(TempDir()) + nodedir = tempdir.join(b"node") + config = get_config(nodedir, b"tub.port") self.assertThat( - storage_server.get_client_resource(tahoe_config), + storage_server.get_client_resource(config), Provides([IResource]), )