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) -

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`), @@ -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