This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git
The following commit(s) were added to refs/heads/main by this push:
new 77551b5 Add documentation about the storage interface
77551b5 is described below
commit 77551b5961b6fd13e5442cb8b6315f31f95997fa
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Jul 21 11:24:13 2025 +0100
Add documentation about the storage interface
---
docs/storage-interface.html | 60 +++++++++++++++++++++++++++++++
docs/storage-interface.md | 86 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 146 insertions(+)
diff --git a/docs/storage-interface.html b/docs/storage-interface.html
new file mode 100644
index 0000000..9c9165d
--- /dev/null
+++ b/docs/storage-interface.html
@@ -0,0 +1,60 @@
+<h1>Storage interface</h1>
+<p>All writes to the database and filesystem are to be mediated through the
storage interface in <code>atr.storage</code>. The storage interface
<strong>enforces permissions</strong>, <strong>centralises audit
logging</strong>, and <strong>exposes misuse resistant methods</strong>.</p>
+<h2>How do we use the storage interface?</h2>
+<p>Open a storage interface session with a context manager. Then:</p>
+<ol>
+<li>Request permissions from the session depending on the role of the
user.</li>
+<li>Use the exposed functionality.</li>
+<li>Handle the outcome or outcomes.</li>
+</ol>
+<p>Here is an actual example from our API code:</p>
+<pre><code class="language-python">async with storage.write(asf_uid) as write:
+ wafm = write.as_foundation_member().writer_or_raise()
+ ocr: types.Outcome[types.Key] = await wafm.keys.ensure_stored_one(data.key)
+ key = ocr.result_or_raise()
+
+ for selected_committee_name in selected_committee_names:
+ wacm =
write.as_committee_member(selected_committee_name).writer_or_raise()
+ outcome: types.Outcome[types.LinkedCommittee] = await
wacm.keys.associate_fingerprint(
+ key.key_model.fingerprint
+ )
+ outcome.result_or_raise()
+</code></pre>
+<p>The <code>wafm</code> (<strong>w</strong>rite <strong>a</strong>s
<strong>f</strong>oundation <strong>m</strong>ember) object exposes
functionality which is only available to foundation members. The
<code>wafm.keys.ensure_stored_one</code> method is an example of such
functionality. The <code>wacm</code> object goes further and exposes
functionality only available to committee members.</p>
+<p>In this case we decide to raise as soon as there is any error. We could
also choose instead to display a warning, ignore the error, etc.</p>
+<p>The first few lines in the context session show the classic three step
approach. Here they are again with comments:</p>
+<pre><code> # 1. Request permissions
+ wafm = write.as_foundation_member().writer_or_raise()
+
+ # 2. Use the exposed functionality
+ ocr: types.Outcome[types.Key] = await wafm.keys.ensure_stored_one(data.key)
+
+ # 3. Handle the outcome
+ key = ocr.result_or_raise()
+</code></pre>
+<h2>How do we add functionality to the storage interface?</h2>
+<p>Add all the functionality to classes in modules in the
<code>atr/storage/writers</code> directory. Code to write public keys to
storage, for example, goes in <code>atr/storage/writers/keys.py</code>.</p>
+<p>Classes in modules in the <code>atr/storage/writers</code> directory must
be named as follows:</p>
+<pre><code>class FoundationParticipant:
+ ...
+
+class FoundationMember(FoundationParticipant):
+ ...
+
+class CommitteeParticipant(FoundationMember):
+ ...
+
+class CommitteeMember(CommitteeParticipant):
+ ...
+</code></pre>
+<p>This creates a hierarchy, <code>FoundationParticipant</code> →
<code>FoundationMember</code> → <code>CommitteeParticipant</code> →
<code>CommitteeMember</code>. We can add other permissions levels if
necessary.</p>
+<p>Use <code>__private_methods</code> for code specific to one permission
level which is not exposed in the interface, e.g. helpers. Use
<code>public_methods</code> for code appropriate to expose when users meet the
appropriate permission level. Consider returning outcomes, as explained in the
next section.</p>
+<h2>Returning outcomes</h2>
+<p>Consider using the <strong>outcome types</strong> in
<code>atr.storage.types</code> when returning results from writer module
methods. The outcome types <em>solve many problems</em>, but here is an
example:</p>
+<p>Imagine the user is submitting a <code>KEYS</code> file containing several
keys. Some of the keys are already in the database, some are not in the
database, and some are broken keys that do not parse. After processing, each
key is associated with a different state: the key was parsed but not added, the
key was parsed and added, or the key wasn't even parsed. We consider some of
these success states, some warning states, and others error states.</p>
+<p>How do we represent this?</p>
+<p>Outcomes are one possibility. For each key we can return
<code>OutcomeResult</code> for a success, and <code>OutcomeException</code>
when there was a Python error. The caller can then decide what to do with this
information. It might ignore the exception, raise it, or print an error message
to the user. Better yet, we can aggregate these into an <code>Outcomes</code>
list, which provides many useful methods for processing all of the outcomes
together. It can count how many exceptions [...]
+<p>We do not have to return outcomes from public storage interface methods,
but these classes were designed to make the storage interface easy to use.</p>
+<h2>What makes this safe?</h2>
+<p>We can always open a database session or write to the filesystem, so there
is no way to make storage access truly safe. But abstracting these operations
to a well known interface makes it more likely that we use only this way of
doing things, which we can then concentrate on getting right. This is in
contrast to writing storage access in <em>ad hoc</em> ways, some of which may
be correct and some of which may not.</p>
+<p>Code relative to a permissions level is only ever exposed in the storage
interface when it is proven, at the type level and during runtime, that the
user has credentials for those permissions. Helper code remains private due to
the use of <code>__private_methods</code>, which undergo name mangling in
Python. As mentioned in the introduction, the storage interface is also the
suitable place to add audit logging, currently planned and not yet
implemented.</p>
diff --git a/docs/storage-interface.md b/docs/storage-interface.md
new file mode 100644
index 0000000..ef7d04c
--- /dev/null
+++ b/docs/storage-interface.md
@@ -0,0 +1,86 @@
+# Storage interface
+
+All writes to the database and filesystem are to be mediated through the
storage interface in `atr.storage`. The storage interface **enforces
permissions**, **centralises audit logging**, and **exposes misuse resistant
methods**.
+
+## How do we use the storage interface?
+
+Open a storage interface session with a context manager. Then:
+
+1. Request permissions from the session depending on the role of the user.
+2. Use the exposed functionality.
+3. Handle the outcome or outcomes.
+
+Here is an actual example from our API code:
+
+```python
+async with storage.write(asf_uid) as write:
+ wafm = write.as_foundation_member().writer_or_raise()
+ ocr: types.Outcome[types.Key] = await wafm.keys.ensure_stored_one(data.key)
+ key = ocr.result_or_raise()
+
+ for selected_committee_name in selected_committee_names:
+ wacm =
write.as_committee_member(selected_committee_name).writer_or_raise()
+ outcome: types.Outcome[types.LinkedCommittee] = await
wacm.keys.associate_fingerprint(
+ key.key_model.fingerprint
+ )
+ outcome.result_or_raise()
+```
+
+The `wafm` (**w**rite **a**s **f**oundation **m**ember) object exposes
functionality which is only available to foundation members. The
`wafm.keys.ensure_stored_one` method is an example of such functionality. The
`wacm` object goes further and exposes functionality only available to
committee members.
+
+In this case we decide to raise as soon as there is any error. We could also
choose instead to display a warning, ignore the error, etc.
+
+The first few lines in the context session show the classic three step
approach. Here they are again with comments:
+
+```
+ # 1. Request permissions
+ wafm = write.as_foundation_member().writer_or_raise()
+
+ # 2. Use the exposed functionality
+ ocr: types.Outcome[types.Key] = await wafm.keys.ensure_stored_one(data.key)
+
+ # 3. Handle the outcome
+ key = ocr.result_or_raise()
+```
+
+## How do we add functionality to the storage interface?
+
+Add all the functionality to classes in modules in the `atr/storage/writers`
directory. Code to write public keys to storage, for example, goes in
`atr/storage/writers/keys.py`.
+
+Classes in modules in the `atr/storage/writers` directory must be named as
follows:
+
+```
+class FoundationParticipant:
+ ...
+
+class FoundationMember(FoundationParticipant):
+ ...
+
+class CommitteeParticipant(FoundationMember):
+ ...
+
+class CommitteeMember(CommitteeParticipant):
+ ...
+```
+
+This creates a hierarchy, `FoundationParticipant` → `FoundationMember` →
`CommitteeParticipant` → `CommitteeMember`. We can add other permissions levels
if necessary.
+
+Use `__private_methods` for code specific to one permission level which is not
exposed in the interface, e.g. helpers. Use `public_methods` for code
appropriate to expose when users meet the appropriate permission level.
Consider returning outcomes, as explained in the next section.
+
+## Returning outcomes
+
+Consider using the **outcome types** in `atr.storage.types` when returning
results from writer module methods. The outcome types _solve many problems_,
but here is an example:
+
+Imagine the user is submitting a `KEYS` file containing several keys. Some of
the keys are already in the database, some are not in the database, and some
are broken keys that do not parse. After processing, each key is associated
with a different state: the key was parsed but not added, the key was parsed
and added, or the key wasn't even parsed. We consider some of these success
states, some warning states, and others error states.
+
+How do we represent this?
+
+Outcomes are one possibility. For each key we can return `OutcomeResult` for a
success, and `OutcomeException` when there was a Python error. The caller can
then decide what to do with this information. It might ignore the exception,
raise it, or print an error message to the user. Better yet, we can aggregate
these into an `Outcomes` list, which provides many useful methods for
processing all of the outcomes together. It can count how many exceptions there
were, for example, or apply a [...]
+
+We do not have to return outcomes from public storage interface methods, but
these classes were designed to make the storage interface easy to use.
+
+## What makes this safe?
+
+We can always open a database session or write to the filesystem, so there is
no way to make storage access truly safe. But abstracting these operations to a
well known interface makes it more likely that we use only this way of doing
things, which we can then concentrate on getting right. This is in contrast to
writing storage access in _ad hoc_ ways, some of which may be correct and some
of which may not.
+
+Code relative to a permissions level is only ever exposed in the storage
interface when it is proven, at the type level and during runtime, that the
user has credentials for those permissions. Helper code remains private due to
the use of `__private_methods`, which undergo name mangling in Python. As
mentioned in the introduction, the storage interface is also the suitable place
to add audit logging, currently planned and not yet implemented.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]