From dca69565bc7562c08431663419c04b7cb2c339cc Mon Sep 17 00:00:00 2001
From: Coko <91132775+Coko7@users.noreply.github.com>
Date: Tue, 18 Aug 2026 07:35:18 +0200
Subject: [PATCH 1/4] ci(docker): add publish workflow, refactor to traefik
Add Docker image publishing on release. Remove embedded rustypaste
(external now). Switch to traefik routing (clean deployment, TLS
termination). Externalize configuration via environment variables.
Document OIDC groups for Zitadel, Microsoft Entra ID.
---
.github/workflows/docker-publish.yml | 51 ++++++++++++++++++++++++++++
README.md | 49 ++++++++++++++++++++++++--
docker-compose.yml | 43 ++++++++++-------------
env.example | 26 +++++++++++++-
4 files changed, 141 insertions(+), 28 deletions(-)
create mode 100644 .github/workflows/docker-publish.yml
diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
new file mode 100644
index 0000000..0809ccd
--- /dev/null
+++ b/.github/workflows/docker-publish.yml
@@ -0,0 +1,51 @@
+name: Docker Publish
+
+on:
+ release:
+ types: [published]
+
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: ${{ github.repository }}
+
+jobs:
+ build-and-push:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to GHCR
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ tags: |
+ type=semver,pattern={{version}},value=${{ github.event.release.tag_name }}
+ type=semver,pattern={{major}}.{{minor}},value=${{ github.event.release.tag_name }}
+ type=raw,value=latest
+
+ - name: Build and push
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
diff --git a/README.md b/README.md
index ba8baca..b890ae6 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ The project name comes from the japanese word **ๆธกใ** (`watari`, also written
On top of providing a GUI, it comes with some additional features:
-- ๐ [OpenID Connect](https://openid.net/developers/how-connect-works/) Single sign-on (tested against [Zitadel](https://zitadel.com/))
+- ๐ [OpenID Connect](https://openid.net/developers/how-connect-works/) Single sign-on (tested against [Zitadel](https://zitadel.com/) and [Microsoft Entra ID)](https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id))
- ๐๏ธ Per-group token mapping
- ๐ Optional client-side ([WebCrypto](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API)) password encryption
@@ -37,11 +37,56 @@ This project has been **vibe-scaffolded** with [Claude](https://claude.ai), you
2. `cp rustypaste-config.example.toml rustypaste-config.toml` and paste the
same two rustypaste token values into `auth_tokens`/`delete_tokens`.
3. `cp token-bindings.example.yaml token-bindings.yaml` and adjust the
- `groups` to match your IdP.
+ `groups` to match your IdP (see "OIDC groups setup" below).
4. Edit `docker-compose.yml`'s `OIDC_ISSUER_URL`, `OIDC_CLIENT_ID`,
`APP_BASE_URL`/`RUSTYPASTE_PUBLIC_URL` for your deployment.
5. `docker compose up -d --build`
+### OIDC groups setup
+
+Watari maps OIDC groups to rustypaste tokens via `token-bindings.yaml`
+(ยง7 in `watari.md`), so the groups claim must actually be present in the
+ID token. Two env vars control this: `OIDC_GROUPS_CLAIM` (which claim to
+read groups from) and `OIDC_GROUPS_SCOPE` (an extra scope some IdPs need
+requested before they'll populate that claim).
+
+**Zitadel** โ groups come from [project
+roles](https://zitadel.com/docs/guides/integrate/service-users/authenticate-service-users),
+not org groups:
+
+1. In your Zitadel project, define roles under **Roles**, then grant them
+ to users (directly or via an org role grant).
+2. Either enable **"Assert Roles on Authentication"** in the project's
+ general settings, or request the roles scope explicitly โ Watari does
+ the latter:
+ ```
+ OIDC_GROUPS_CLAIM=urn:zitadel:iam:org:project:roles
+ OIDC_GROUPS_SCOPE=urn:zitadel:iam:org:project:roles
+ ```
+3. `token-bindings.yaml`'s `groups` entries should match the role keys
+ you defined (e.g. `admin`, `user`).
+
+**Microsoft Entra ID** โ groups come from the token configuration, no
+extra scope needed:
+
+1. In the app registration, go to **Token configuration** โ **Add
+ groups claim**, and pick Security groups (or All groups). Add it to
+ the ID token.
+2. Leave `OIDC_GROUPS_SCOPE` unset; set:
+ ```
+ OIDC_GROUPS_CLAIM=groups
+ ```
+3. By default Entra ID emits group **object IDs** (GUIDs), not names โ
+ `token-bindings.yaml`'s `groups` entries need to be those GUIDs.
+ Alternatively, use **App roles** instead of security groups (assign
+ users/groups to roles in the app registration) and set
+ `OIDC_GROUPS_CLAIM=roles`, which emits the human-readable role value
+ strings instead.
+4. Entra ID caps the groups claim at 200 groups per token ("[overage](https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference#groups-overage-claim)")
+ โ above that it omits `groups` entirely and expects a Graph API call
+ instead, which Watari does not do. Prefer App roles if a user could
+ belong to that many groups.
+
## Running locally for development
Requires Rust (edition 2024, so a recent stable toolchain) and no external
diff --git a/docker-compose.yml b/docker-compose.yml
index 2f0fd31..7642942 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,44 +1,37 @@
services:
- rustypaste:
- image: ghcr.io/orhun/rustypaste:latest
- restart: unless-stopped
- volumes:
- - rustypaste-data:/app/upload
- - ./rustypaste-config.toml:/app/config.toml:ro
- networks: [internal]
- # No published ports โ only reachable from watari over the internal network.
-
watari:
build: .
restart: unless-stopped
environment:
- OIDC_ISSUER_URL: "https://your-idp.example.com"
- OIDC_CLIENT_ID: "watari"
+ OIDC_ISSUER_URL: "${OIDC_ISSUER_URL:?set in .env}"
+ OIDC_CLIENT_ID: "${OIDC_CLIENT_ID:?set in .env}"
OIDC_CLIENT_SECRET: "${OIDC_CLIENT_SECRET:?set in .env}"
- OIDC_REDIRECT_URI: "https://paste.example.com/auth/callback"
- OIDC_GROUPS_CLAIM: "groups"
+ OIDC_REDIRECT_URI: "${OIDC_REDIRECT_URI:?set in .env}"
+ OIDC_GROUPS_CLAIM: "${OIDC_GROUPS_CLAIM:?set in .env}"
+ OIDC_GROUPS_SCOPE: "${OIDC_GROUPS_SCOPE:-}"
SESSION_SECRET: "${SESSION_SECRET:?set in .env}"
- RUSTYPASTE_INTERNAL_URL: "http://rustypaste:8000"
- RUSTYPASTE_PUBLIC_URL: "https://paste.example.com"
- RUSTYPASTE_TOKEN_READONLY: "${RUSTYPASTE_TOKEN_READONLY:?set in .env}"
+ RUSTYPASTE_INTERNAL_URL: "${RUSTYPASTE_INTERNAL_URL:?set in .env}"
+ RUSTYPASTE_PUBLIC_URL: "${RUSTYPASTE_PUBLIC_URL:?set in .env}"
+ RUSTYPASTE_TOKEN_USER: "${RUSTYPASTE_TOKEN_USER:?set in .env}"
RUSTYPASTE_TOKEN_ADMIN: "${RUSTYPASTE_TOKEN_ADMIN:?set in .env}"
- APP_BASE_URL: "https://paste.example.com"
- APP_PORT: "3000"
+ APP_BASE_URL: "${APP_BASE_URL:?set in .env}"
DATABASE_PATH: "/data/app.db"
TOKEN_BINDINGS_PATH: "/app/token-bindings.yaml"
PBKDF2_ITERATIONS: "310000"
volumes:
- watari-data:/data
- ./token-bindings.yaml:/app/token-bindings.yaml:ro
- depends_on: [rustypaste]
- networks: [internal, public]
- ports:
- - "3000:3000"
+ networks: [traefik-network]
+ labels:
+ traefik.enable: true
+ traefik.http.routers.watari.rule: Host(`${DOMAIN}`)
+ traefik.http.routers.watari.entrypoints: websecure
+ traefik.http.routers.watari.tls.certresolver: letsencrypt
+ traefik.http.services.watari.loadbalancer.server.port: 3000
networks:
- internal:
- public:
+ traefik-network:
+ external: true
volumes:
- rustypaste-data:
watari-data:
diff --git a/env.example b/env.example
index 606be5d..81fa512 100644
--- a/env.example
+++ b/env.example
@@ -3,9 +3,33 @@
# 32+ bytes of hex-encoded random data, e.g. `openssl rand -hex 32`.
SESSION_SECRET=
+OIDC_ISSUER_URL=https://auth.example.com
+OIDC_REDIRECT_URI=https://watari.example.com/auth/callback
+OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
+# Claim (and, if the IdP needs it, scope) that carries group membership.
+# See README.md "OIDC groups setup" for Zitadel / Microsoft Entra ID specifics.
+#
+# Zitadel:
+# When using Zitadel as OIDC provider:
+# OIDC_GROUPS_CLAIM=urn:zitadel:iam:org:project:roles
+# OIDC_GROUPS_SCOPE=urn:zitadel:iam:org:project:roles
+#
+# Microsoft Entra ID (leave OIDC_GROUPS_SCOPE unset):
+# OIDC_GROUPS_CLAIM=groups
+OIDC_GROUPS_CLAIM=
+OIDC_GROUPS_SCOPE=
+
# Must exactly match the values in rustypaste-config.toml's auth_tokens
# (and, for the admin one, delete_tokens) โ see rustypaste-config.example.toml.
-RUSTYPASTE_TOKEN_READONLY=
+RUSTYPASTE_INTERNAL_URL=https://rustypaste.example.com/
+RUSTYPASTE_PUBLIC_URL=https://rustypaste.example.com/
+RUSTYPASTE_TOKEN_USER=
RUSTYPASTE_TOKEN_ADMIN=
+
+DOMAIN=watari.example.com
+APP_BASE_URL=https://watari.example.com
+APP_PORT=3000
+
+DATABASE_PATH=./dev.db
From 3a1b22c5628b91325778b88cfea4df616ab5c1e6 Mon Sep 17 00:00:00 2001
From: Coko <91132775+Coko7@users.noreply.github.com>
Date: Tue, 18 Aug 2026 07:42:51 +0200
Subject: [PATCH 2/4] docs: add vibe-coding mention in README
---
README.md | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/README.md b/README.md
index b890ae6..41f6b8d 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,8 @@
# โ Watari
+> [!NOTE]
+> ๐ค This project has been vibe-coded with Claude. Read bottom section to learn more.
+
**Watari** is a web GUI frontend for [rustypaste](https://github.com/orhun/rustypaste).
The project name comes from the japanese word **ๆธกใ** (`watari`, also written as **ใฏใฟใช** in [Katakana](https://en.wikipedia.org/wiki/Katakana)) which means ["crossing, passage, transit"](https://jisho.org/search/%22watari%22) and symbolizes the relationship with **rustypaste**.
@@ -59,10 +62,12 @@ not org groups:
2. Either enable **"Assert Roles on Authentication"** in the project's
general settings, or request the roles scope explicitly โ Watari does
the latter:
+
```
OIDC_GROUPS_CLAIM=urn:zitadel:iam:org:project:roles
OIDC_GROUPS_SCOPE=urn:zitadel:iam:org:project:roles
```
+
3. `token-bindings.yaml`'s `groups` entries should match the role keys
you defined (e.g. `admin`, `user`).
@@ -73,9 +78,11 @@ extra scope needed:
groups claim**, and pick Security groups (or All groups). Add it to
the ID token.
2. Leave `OIDC_GROUPS_SCOPE` unset; set:
+
```
OIDC_GROUPS_CLAIM=groups
```
+
3. By default Entra ID emits group **object IDs** (GUIDs), not names โ
`token-bindings.yaml`'s `groups` entries need to be those GUIDs.
Alternatively, use **App roles** instead of security groups (assign
@@ -122,3 +129,13 @@ Database migrations run automatically at startup (`DATABASE_PATH`, default
## License
AGPLv3 โ see [`LICENSE`](./LICENSE).
+
+## AI usage / vibe coding
+
+The initial project scaffolding was done via a technical spec generated by Claude and further features/fixes have been done with Claude as well.
+
+I have spent some time reading through the code, testing and troubleshooting things myself but far less
+than I should have given the current project size.
+
+I would like to reduce my AI usage and take back ownership of the code but I have honestly haven't had the time
+to do so.
From 826af3cd33c6b4ec7c1146805bb970710532eeff Mon Sep 17 00:00:00 2001
From: Coko <91132775+Coko7@users.noreply.github.com>
Date: Tue, 18 Aug 2026 07:47:10 +0200
Subject: [PATCH 3/4] docs(README): add table of content
---
README.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/README.md b/README.md
index 41f6b8d..0da87ae 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,15 @@ This project has been **vibe-scaffolded** with [Claude](https://claude.ai), you