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 Watari UI screenshot

+## 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`), From 7debd122d7c9e9a745ec9ba105c77168fa35383c Mon Sep 17 00:00:00 2001 From: Coko <91132775+Coko7@users.noreply.github.com> Date: Tue, 18 Aug 2026 07:49:36 +0200 Subject: [PATCH 4/4] docs: update README --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0da87ae..6fd376e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # โ˜• Watari > [!NOTE] -> ๐Ÿค– This project has been vibe-coded with Claude. Read bottom section to learn more. +> ๐Ÿค– 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). @@ -27,8 +27,6 @@ On top of providing a GUI, it comes with some additional features: 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) -

Watari UI screenshot

@@ -141,7 +139,7 @@ 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. +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.