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..6fd376e 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,8 @@
# โ Watari
+> [!NOTE]
+> ๐ค This project has been **vibe-coded** with [Claude](https://claude.ai). 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**.
@@ -18,18 +21,25 @@ 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
All built with a ***based*** technical stack: [axum](https://github.com/tokio-rs/axum) + [Askama](https://github.com/askama-rs/askama) + [HTMX](https://htmx.org/) + [SQLite](https://sqlite.org)
-This project has been **vibe-scaffolded** with [Claude](https://claude.ai), you can find the full design here: [`watari.md`](./watari.md)
-
+## Table of contents
+
+- [Running with Docker Compose (recommended)](#running-with-docker-compose-recommended)
+ - [OIDC groups setup](#oidc-groups-setup)
+- [Running locally for development](#running-locally-for-development)
+- [Project layout](#project-layout)
+- [License](#license)
+- [AI usage / vibe coding](#ai-usage--vibe-coding)
+
## Running with Docker Compose (recommended)
1. `cp env.example .env` and fill in `SESSION_SECRET` (`openssl rand -hex 32`),
@@ -37,11 +47,60 @@ 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
@@ -77,3 +136,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](./watari.md) 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.
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