New reference docs

This commit is contained in:
southerntofu 2020-04-28 11:14:02 +02:00
parent ce94fc8929
commit 4bfa999a1e
4 changed files with 255 additions and 150 deletions

266
README.md
View File

@ -1,196 +1,162 @@
# A KISS CI/CD system for the tildeverse?
# Simple and interoperable CI/CD system
So, for a while i've been thinking about how to deploy a simple and permissionless Continuous Integration/Delivery system for tilde servers, with a scriptable CLI. Here's where my thoughts have got me so far:
forgehook is a collection of scripts following a simple interface to build a full Continuous Integration/Delivery platform. forgehook provides first-class multi-user experience for [tilde](https://tildeverse.org)/pubnix servers, and easy integration with your custom tooling.
# General overview
These scripts are not intended to be used for generic webhooks, but for those produced indicating changes on a software repository. If you are looking for a more generic solution, please take a look at [webhook](https://github.com/adnanh/webhook) or [webhookd](https://github.com/ncarlier/webhookd) instead.
Webhooks are simple HTTP+JSON requests sent by a forge such as Gitea to a remote endpoint, to inform it some updates were performed on the repository. So, on a high-level a webhook lifetime would be:
**Note**: Some specific parts of forgehook do not yet correspond to what you will find here. That's because I'm taking some time to step back and think about what's implemented so far and how it should end up, before i deepdive into the code again. The inconsistencies are marked with TODO notes.
1. If the updated remote is not in the local database, exit
2. If the HTTP signature on the webhook doesn't match the local secret, exit
3. Run the `webhook-run` script (which is privileged), which for each user subscribing to the remote:
1. Finds the corresponding git-build.sh task(s)
2. Runs git-build.sh as the user, with the matching task(s) as arguments
4. Enjoy!
# Introduction to webhooks
In this document, you will find information about:
Webhooks are simple web pings performed by a client to inform a server that something happened (push model). In the context of a [forge](https://en.wikipedia.org/wiki/Forge_(software)) (such as Gitea), a webhook additionally contains information as JSON payload about what changed on the repository, as well as an [HTTP signature](https://datatracker.ietf.org/doc/draft-ietf-httpbis-message-signatures/) in order to authenticate the client who sent the webhook via a shared secret.
- `webhook`: a user-facing wrapper script
- `webhook-backend`: a user-friendly CLI to manage your remote subscriptions, which cannot be called outside of `webhook`
- `webhook-run`: a script the HTTP endpoint calls when a remote was found to have a legitimate update
- `webhook-endpoint`: a script that acts as an HTTP endpoint, and validates incoming webhooks
Examples of forge webhooks can be found for: [Gitea](https://docs.gitea.io/en-us/webhooks/) [Github](https://developer.github.com/webhooks/) [Gitlab](https://docs.gitlab.com/ee/user/project/integrations/webhooks.html) [Gogs](https://gogs.io/docs/features/webhook). Currently, no endpoint is implemented, however Gitea support will come soon (for [tildegit](https://tildegit.org) integration).
A simple CLI interface is presented in the next section. How the system works under the hood is explained in the [Architecture](#architecture) section. Current limitations and further ideas are described in the [Future](#future) section. Please note that this system is (at the moment) entirely **hypothetical** and was merely described in order to gather feedback before implementation.
**Note**: If you are not running a web-based forge such as those mentioned above, but run your own git server, you do not need webhooks to build a CI/CD system. All you need are server-side git hooks as explained [in the docs](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#_server_side_hooks).
# CLI interface
# Getting started
We need a user interface to subscribe to a remote, unsubscribe from a remote, update the corresponding secret:
In this section, you will find:
- an [overview](#general-overview) of the core concepts of forgehook
- a short intro to the `forgehook` [command-line interface](#cli-intro) (CLI)
- a [setup guide](#setup)
- a [configuration guide](#configuration)
## General overview
forgehook is a collection of scripts to let users manage their subscriptions to remote repositories (currently git only) and trigger something when a legitimate updated notification is received. Typically, forgehook is intended to be run alongside [git-build.sh](https://tildegit.org/southerntofu/git-build.sh) to automatically trigger build tasks when an update is perfomed on the repository.
**Simplicity** and **extensibility** are core concerns of forgehook, focusing on defining standard interfaces between different components so that you can reimplement each part of it to better suit your needs. Sharing of your tricks is highly encouraged! And remember, if the tool gets in your way, it's a bug so please report it.
The three components of forgehook are:
- **endpoints** which receive and validate webhooks of dubious authenticity (through a shared secret) (TODO: no endpoint is implemented yet!)
- **databases** which retrieve/store subscriptions and secrets (TODO: rename webhook-backend into databases/unix.sh in the repo)
- **triggers** which does stuff as a subscribed user in case of update (TODO: rename webhook-run-backend into triggers/git-build)
Although there can be many endpoints, there can only ever be one database and one trigger configured for the system. These components are interchangeable as they follow simple interfaces described in the [Endpoints](#endpoints), [Triggers](#triggers) and [Databases](#databases) sections.
forgehook aims to be secure through simplicity and auditability. Security concerns are addressed in a [dedicated section](#security).
## Setup
In most cases, you can just do ./setup.sh from the repository folder and it will setup everything just fine as long as you are a sudoer
TODO: introduce the setup.sh script and describe manual steps for installing
## Configuration
Configurating the forgehook user can only be done at setup time. Please refer to the setup docs.
TODO: Configuration for endpoint (none at the moment), databases (only unix support at the moment) and triggers (only git-build at the moment)
## CLI intro
So you want to subscribe to updates on a remote repository? This is a quick introduction to the `forgehook` CLI. The complete reference can be found in the [docs/cli.md](docs/cli.md) file.
### Subscribing to a repository
To subscribe to a remote repository, use the `forgehook add REPO` command, where `REPO` is the URL of this repository.
If there is already a secret shared with the repository, the command will simply subscribe you to updates. However, if no secret has been configured yet, it will either extract it from your first argument (`forgehook add REPO SECRET`) or generate one for you.
**Note**: If you just registered a secret, don't forget to give it to your forge.
```
user$ webhook add "https://tildegit.org/tilde-fr/infra"
$ webhook add "https://tildegit.org/tilde-fr/infra"
[webhook] Your secret for https://tildegit.org/tilde-fr/infra is now:
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX # <--- Hex-encoded /dev/urandom
```
user$ webhook list
https://tildegit.org/tilde-fr/infra\tXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
**TODO**: Currently, no ownership check is performed on the repo (see issue #6)
user$ webhook secret "https://tildegit.org/tilde-fr/infra" YYYYYYYYYYYYYYYY
[webhook] Your secret for https://tildegit.org/tilde-fr/infra is now:
YYYYYYYYYYYYYYYY
### Unsubscribing
user$ webhook unsubscribe "https://tildegit.org/tilde-fr/infra"
To unsubscribe from a repository, simply use the `forgehook remove` command:
```
$ webhook unsubscribe "https://tildegit.org/tilde-fr/infra"
[webhook] Unsubscribed from https://tildegit.org/tilde-fr/infra
[webhook] Users can still subscribe to this remote. To remove it entirely, run:
webhook remove "https://tildegit.org/tilde-fr/infra"
```
The opposite of `add` is `remove`. The opposite of `subscribe` is `unsubscribe`. Subscribe automatically implies Add, and vice-versa. Remove only means unsubscribe unless the `--force` flag is passed, but unsubscribe never implies Remove.
### Listing subscriptions
The commands are presented in greater detail in the [webhook-backend](#webhook-backend) section.
The `list` command lists your current subscriptions. If a URL is passed as argument, the command returns the list of users currently subscribed to the repository.
```
$ webhook list
https://tildegit.org/tilde-fr/infra\tXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```
**Note**: For repositories you do not own, the `list` command only returns one repository URLs per line. However, additional information separated by tabulations (`\t`) are output for repositories you own, as explained in the [CLI reference](docs/cli.md).
**TODO**: currently, only listing one's subscriptions is supported
### Viewing/changing a secret
For a repository you own, you can always view your secret with the `forgehook secret URL` command. If you give it an additional `SECRET`, it will replace the current secret with this value:
```
$ webhook secret https://tildegit.org/tilde-fr/infra
[webhook] Your secret for https://tildegit.org/tilde-fr/infra is now:
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
$
$ webhook secret "https://tildegit.org/tilde-fr/infra" YYYYYYYYYYYYYYYY
[webhook] Your secret for https://tildegit.org/tilde-fr/infra is now:
YYYYYYYYYYYYYYYY
```
# Architecture
## Introduction
There are two [entry points](https://en.wikipedia.org/wiki/Entry_point) for the forgehook system:
A very naive approach to subscriptions storage would have users manage their own database in `$HOME`. However, that would require to iterate over all homedirs on every webhook to figure out which are legitimate, which is a vector for DOS attacks, so we need another way.
- a user running the `forgehook` script to manage their subscriptions/secrets
I propose to introduce a `webhook` unprivileged user, which manages a central database of subscriptions for the server. This user would have a `~webhook/webhooks` folder. For each remote URL `$r` (where `$rhex` is the hex-encoded representation of it), there would be in this folder:
- an endpoint running `forgehook-notify` (TODO: rename forgehook-run to forgehook-notify) to announce a legitimate update was received for a remote
- `.$rhex.owner` is the local user owning the repository, and is therefore responsible for keeping the secret in sync with the remote
- `.$rhex.secret` contains the secret shared with the repo
- `$rhex.$u` for each `$u` local user subscribed to the repo
`forgehook-notify` takes the URL of the updated repository as argument, checks for current user subscription, and runs the trigger backend `/usr/local/bin/forgehook-trigger-backend` as each user currently subscribed. The forgehook CLI interface is further described [here](docs/cli.md).
Additionally, for each user `$u` owning one or more repositories, there would be a `.owned-by/$u` folder containing files named `$rhex` for each `$r` remote the user owns.
In the following sections, we'll explore how [endpoints](#endpoints), [triggers](#triggers) and [databases](#databases) are.
## webhook (wrapper script)
## Endpoints
Because of how the system is designed, the `webhook` script needs to run as user `webhook` (as this user owns the database) but the script needs to reliably know which user called it. To run as `webhook` user, we simply rely on sudo with the following `/etc/sudoers` rules:
Endpoints are simple services vouching for repository updates, usually by verifying a shared secret. Shortly, they are trusted 3rd party tools querying the local database in order to validate update notifications submitted by a remote forge (usually through a webhook). When a request is validated, an endpoint notifies the current trigger through the `forgehook-notify` command.
```
* ALL=(webhook:webhook) NOPASSWD: /usr/local/bin/webhook-backend
* webhook ALL=(root:root) NOPASSWD: /usr/local/bin/webhook-run
```
The steps performed by an endpoint are:
So, in fact, `webhook` would be a simple wrapper script for `webhook-backend`.
1. Extract repository URL and secret from the remote notification (eg. webhook)
1. Query the local database for a secret matching this URL
2. If nothing is found, the program stops
3. Compare the remote and local secrets
4. If they don't match, the program stops
5. Run `forgehook-notify` with the remote URL as argument
## webhook-backend
There can be as many endpoints as you like to suit your needs. This allows to receive updates from multiple sources, such as different web forges, or even from a local `post-receive` git hook. However, remember your system is only as secure as your endpoints, as explained in the [Security](#security) section.
`webhook-backend` has a first command argument, which can be one of the following: `list`, `add`, `remove`, `subscribe`, `unsubscribe`, `secret`. If no argument is supplied, the `list` command is implied.
## Triggers
Some commands take a remote argument, and some take secret arguments. The remote is abbreviated `$r`, and the secret `$s`. Additionally, `$rhex` designates `$r` hex-encoded. `$u` is the real user running the script.
Triggers are simple scripts running as a user who subscribed to a task, thanks to sudo magic performed by `forgehook-notify`. Trigger configuration is system-wide and there is only one trigger at any given moment (`/usr/local/bin/forgehook-trigger`).
### list
A trigger receives a repository URL as argument. This repository is assumed to have legitimately received an update, as bad-faith requests have been filtered by the endpoint who received the original request.
`list` command takes no further arguments. It lists the current subscriptions for the user, as well as unsubscribed remotes for which a secret is defined for when a user owns a remote (and its secret) but does not subscribe to it.
A trigger may:
For each remote found for the user, the `list` command prints with tab separation (`\t`) between those fields:
- send a mail or some sort of notification for the update
- trigger a test/deployment using a 3rd party tool such as [git-build.sh](https://tildegit.org/southerntofu/git-build.sh)
- the remote URL
- (for owners) the remote's secret
- (for owners who unsubscribed to the remote) a literal `x`
Currently, only `git-build` is supported as trigger. More may come in the future, and contributions are welcome! Notably, it should be possible to integrate Github/Gitlab CI scripts as a trigger.
`list` iterates over `~/.webhooks/*.$u`, and the current matching file with `.$u` stripped is called `$rhex`. Foreach of those:
**Note**: Triggers are by nature unprivileged and only runs code the user should consent to (having subscribed to a repository). Running tests or building websites should not require complex infrastructure, as long as you have an account on a machine somewhere.
1. Hex-decode `$rhex` into `$r`
2. Print `$r`
3. If `.$rhex.owner` does not contain `$u`, continue
4. Print `\t` plus the contents of `.$rhex.secret`
5. Print a newline
## Databases
Additionally, for each file `$f` in owned-by/$u:
Databases are simple programs storing information about repository ownership, secrets and subscriptions. They may operate over an SQL or LDAP database, as long as they respect the `forgehook` CLI interface described [here](docs/cli.md). Database configuration is systemwide and there is only one database at any given moment (`/usr/local/bin/forgehook-db`) (TODO: rename forgehook-backend to forgehook-db)
1. If `$rhex.$u` exists, do nothing because the user is still subscribed
2. Read the secret `$s` from `.$rhex.secret`
2. Print `$r\t\$s\tx` plus a newline
The provided reference implementation for a forgehook database is a flat-file database managed by bash scripts, located in `databases/unix.sh`, and documented [here](docs/unix.md).
### add
# Security
`add` command takes `$r` and optionally `$s`. As explained before, `$r` is hex-encoded as `$rhex`.
If `~/.webhooks/.$rhex.owner` already exists, runs the `subscribe` command instead. Otherwise, `add` takes the following steps:
1. Clone `$r` to `/tmp/` to ensure the repository can be reached
2. **TODO**: Here is where we introduce additional authentication steps to prevent accidentally claiming a repository you do not own
2. Create `.$rhex.owner` with `$u` for content
3. Create `owned-by/$u/$rhex`
4. If a secret `$s` was provided, write it to `.$rhex.secret`. Otherwise, generate a 32 hex characters secret `$s` and write it to the same file.
5. Print `[webhook] Your secret for $r is now:\n$s`
6. Run subscribe command
### remove
`remove` command takes a `$r` and an optional `-f|--force` flag. Unless the force flag is set, the user is warned that forcibly removing the remote they own (and the associated secret) would break stuff for other users, but that they can do it with `-f`. After this warning, the `unsubscribe` command is run instead.
If the force flag is indeed passed to `remove`, the command checks whether `$u` owns the repository (by looking up `.$rhex.owner`) and errors if that is not the case.
If the user is the legitimate owner of the remote, the following files are deleted:
- `.owned-by/$u/$rhex`
- `.$rhex.owner`
- `.$rhex.secret`
- `$rhex.*` (current subscriptions)
### subscribe
`subscribe` command takes `$r`. If `$rhex.$u` exists, the user is already subscribed and it does nothing, maybe print a warning?
If the file `.$rhex.secret` exists, then `$rhex.$u` is created and a confirmation message printed. Otherwise, the add command is run.
### unsubscribe
`unsubscribe` command takes `$r`. If `$rhex.$u` doesn't exist, the user isn't subscribed, so a warning is emitted and the command does nothing. Otherwise, the file `$rhex.$u` is removed.
### secret
`secret` command takes `$r` and an optional `$s`. If `.$rhex.secret` does not exist, the remote was not found, so an error is printed and the command does nothing. If `$u` doesn't match the content of `.$rhex.owner`, then an error is printed and the command does nothing.
If `$s` is specified, it's written to `.$rhex.secret`. Otherwise, the contents of `.$rhex.secret` are printed.
## webhook-run (privileged)
Once a HTTP webhook has been authenticated as legitimate by the local HTTP endpoint, it calls the `webhook-run` script (which executes as root) with the updated remote `$r` as argument.
For each subscribed file in `~webhook/webhooks/$rhex.*`:
1. Extract the username `$u` by removing the `~webhook/webhooks/$rhex.` prefix
2. For each file in `~$u/.git-build/*.source`:
1. If it does not match the updated repository, continue
2. Extact task name `$t` by removing the `.source` suffix
3. Runs `git-build.sh $t` as user `$u` (using sudo)
## webhook-endpoint (HTTP service)
`webhook-endpoint` is the internet-facing service of this system, and it ensures incoming webhooks are legitimate, by following these steps:
1. Decode the JSON into a native data structure `$w`, and register its `$w.repository.html_url` as `$r`, or exit silently if this fails
2. Hex-encode `$r` into `$rhex`
3. Check that `~webhook/webhooks/.$rhex.secret` exists, or exit silently
4. Verify that `$w.secret` matches `~webhook/webhooks/.$rhex.secret`, or exit silently
5. Verify that the HTTP signature in header `HTTP_X_GITEA_SIGNATURE` matches the secret, or exit silently
6. Run `webhook-run $r`
# Future
Currently, this software is not implemented. I was merely gathering ideas which turned into a complete manual, which is great for when i finally implement that. However, there are current limitations in the current design i would like to tackle, and suggestions are more than welcome.
## Validate remote repository ownership through a side-channel
Under the current proposal, it would be possible for a user to register any unclaimed repository and claim ownership for it on the local system. This could prevent the legitimate owner from registering it themselves, and therefore would block subscription to this remote because the local secret would be invalid (as the forge would be unaware of it).
In order to prevent illegitimate ownership claims, we could implement an additional verification by:
1. Checking a repository's website URL `$web` through the forge API
2. Generating a secret to challenge the user `$u`
3. Placing the secret in `~$u/public_html/.well-known/webhook-challenge`
4. Looking up `$web/.well-known/webhook-challenge`
5. If this fails, inform the user to complete the challenge manually
## Repository petnames
Currently, repositories are archived on disk as hex-encoded URLs. This is done to prevent the URL from colliding with unwanted characters on the filesystem (such as `/`). However, this approach is not really admin-friendly because you can't simply `ls` what's in `~webhook/webhooks` to get information about the system.
Maybe a petname system would be more appropriate.
## User ownership
Currently, the whole database is understood to be owned by `webhook` user. Maybe this can be reconsidered in the future so that users can script their subscriptions without using the `webhook` wrapper script. However, i do not believe this is important.
TODO: Explain sudo tricks and suggest everyone should read the code in its entirely because it's brief

121
docs/cli.md Normal file
View File

@ -0,0 +1,121 @@
# CLI reference
In this document, you will find all there is to know about the `forgehook` CLI interface. If you are looking to develop another database for forgehook, this is the reference documentation you're looking for!
**TODO**: Investigate return codes that would not collide with one another because of command nesting + investigate semantic codes (eg. 1=repo not found, 2=noperm 3=dberror etc..)
`forgehook` accepts a number of subcommands, which are detailed in this document. When no command is supplied, the `list` command is implied:
- [help](#help) provide help to the user
- [list](#list) list user's current subscriptions, or a repository's current subscribers
- [add](#add) register or subscribe to a remote
- [remove](#remove) (alias: `rm`) unsubscribe, or remove a repository you own
- [subscribe](#subscribe) (alias: `sub`) subscribe to a repository
- [unsubscribe](#unsubscribe) (alias `unsub`) unsubscribe from a repository
- [secret](#secret) view/update the secret for a repository you own
The opposite of `add` is `remove`. The opposite of `subscribe` is `unsubscribe`. Subscribe automatically implies Add, and vice-versa. Remove only means unsubscribe unless the `--force` flag is passed, but unsubscribe never implies Remove.
**Returns**: `forgehook` always returns the return code of its command run, unless the command was not found in which case `32` is returned (reserved code).
In the following reference, we use the following conventions:
- `$r` is a repository URL
- `$rhex` is the same URL, hex-encoded
- `$s` is a secret
- `$u` is the real user running `forgehook`
## help
`help` command takes no further argument, and prints a friendly help message.
**Returns**: `0`, always
## list
`list` lists the current user's subscriptions, or the list of users subscribed to a repository.
When no argument is passed, `forgehook list` prints the user's current subscriptions, with one line per repository. Each line contains, separated by tabulations (`\t`):
- the remote URL
- (for owners) the remote's secret
- (for owners who unsubscribed to the remote) a literal `x`
Example:
```
$ forgehook list
https://tildegit.org/southerntofu/git-build.sh 20cd982ebc2d8dbe37259e6d860dc4a83105510f80342083a4bf407cacc66283
```
When `$r` is passed as argument, `forgehook list REPO` prints one line per subscriber, each containing the subscriber's name.
```
$ forgehook list https://tildegit.org/southerntofu/git-build.sh
southerntofu
```
**Returns**: `0` on success, `1` when the URL passed as argument does not match a known repository
## add
`add` command takes `$r` and optionally `$s`. The command does the following:
1. If the repository is already claimed by someone on the machine, exit with code `1` (TODO)
2. Ensure the repository $r is clonable, otherwise exit with code `2` (TODO)
2. **TODO**: Here is where we introduce additional authentication steps to prevent accidentally claiming a repository you do not own
3. Register `$u` as owner for `$r`
4. If a secret `$s` was provided, save it. Otherwise, generate a random hexadecimal secret
5. Print the current secret
6. Subscribe `$u` to `$r` (usually through the `subscribe` command)
7. Exit with `0` (subscription should not fail because we ensure its conditions)
**Returns**: `0` on success, `1` when the repository is already claimed, `2` when the repository was not understood to be such
## remove
`remove` command takes a `$r` and an optional `-f` or `--force` flag. When the forge flag is set, the command removes all subscriptions to a given repository `$u` owns, and deletes associated secret and ownership information. Otherwise, it is equivalent to `unsubscribe`:
1. Ensure repository `$r` is known, otherwise exit with `1`
2. If the force flag is not set
1. If `$u` owns `$r`, inform the user about the `--force` flag
2. Unsubscribe `$u` from `$r` and exit with `0` when this is successful, `2` otherwise
3. Otherwise, if the force flag is set
1. If `$u` does not own `$r`, print an error an exit with `3`
2. Unsubscribe all subscribers from `$r` (cannot fail)
3. Delete secret and ownership info for `$r` and exit with `0` on success, `4` otherwise
Unless the force flag is set, the user is warned that removing the remote they own (and the associated secret) would break stuff for other users, but they are still informed on how to proceed.
**Returns**: `0` on success, `1` when the repository is unknown, `2` when unsubscription was unsuccessful, `3` when the user attempted to forcibly remove a repository they do not own, `4` when removing associated information was unsuccessful (should not happen)
## subscribe
`subscribe` command takes `$r` and makes `$u` susbcribe to `$r`:
1. If `$r` is unknown, the `add` command is run with the same arguments, and `subscribe` exits with the exit code of `add` command
2. Subscribe `$u` to `$r`
**Retuns**: `0` on success, the exit code from `add` on failure
## unsubscribe
`unsubscribe` command takes `$r`, and makes `$u` unsubscribe to `$r`:
1. If `$r` is unknown, exit with `1`
2. Unsubscribe `$u` from `$r`
**Returns**: `0` on success, or `1` when the repository was not found
If `$rhex.$u` doesn't exist, the user isn't subscribed, so a warning is emitted and the command does nothing. Otherwise, the file `$rhex.$u` is removed.
## secret
`secret` command takes `$r` and an optional `$s`. It either displays the current secret for a repository `$u` owns, or replaces it with `$s`:
1. If `$r` is unknown, exit with `1`
2. If `$u` is not owner of `$r`, exit with `2`
3. If `$s` was supplied, replace the current secret for `$r`
3. Print the curent secret for `$r`
**Returns**: `0` on success, `1` on unknown repository, and `2` on wrong ownership for the repository

3
docs/database.md Normal file
View File

@ -0,0 +1,3 @@
# Databases
A forgehook database is a simple data store implementing the forgehook [CLI interface](cli.md). Currently, the only database implemented is a flat-file database for [UNIX derivatives](unix.md) (GNU/Linux & BSDs) implemented in bash.

15
docs/unix.md Normal file
View File

@ -0,0 +1,15 @@
# unix database
The `unix` forgehook database is the reference implementation. Here, you will find information about its architecture.
A naive approach to subscriptions storage would have users manage their own database in `$HOME`. However, that would require to iterate over all homedirs on every webhook to figure out which are legitimate, which is a vector for DOS attacks, so we need another way.
Instead, we let the configured forgehook user manage a central database. This is done in its home directory, in a `database` folder (TODO: update code). For each known repository URL `$r` (where `$rhex` is the hex-encoded representation of it), there is in this folder:
- `$rhex.owner` is the local user owning the repository, and is therefore responsible for keeping the secret in sync with the remote
- `.$rhex.secret` contains the secret shared with the repo
- `$rhex.$u` for each `$u` local user subscribed to the repo
TODO: update code which currently does the exact opposite, see https://tildegit.org/southerntofu/webhook/issues/4
Additionally, for each user `$u` owning one or more repositories, there is a `.owned-by/$u` folder containing files named after the `$rhex` for each repository `$r` remote the user owns.