bb-plane-fork/BINARYBEACHIO.md
binarybeach 712612865d binarybeachio: Bucket-4 trusted-JWT auth — replaces in-place github.py patch
Migrates this fork to the binarybeachio platform-architecture pivot:
oauth2-proxy at the edge enforces a Zitadel session, the auth-bridge
mints a short-lived RS256 JWT, and a NEW additive endpoint at
/auth/sign-in-trusted/ verifies the JWT, claims its jti against
shared-redis (single-use replay protection, fail-closed), find-or-creates
the User, and starts a Django session via user_login().

Net surface vs. upstream-clean: 1 new view file + 1 url path + 1
exports __init__ entry + 7 reserved error codes (6000-6099 range).
github.py and the GitHub-button rebrand patch are reverted to upstream
— sign-in entry-point UX is now driven by Traefik redirectregex on
/sign-in* in infrastructure/plane/docker-compose.yml.

Replay protection contract: jti claim minted by bridge, consumed via
Redis SETNX with ttl = exp - now + 30s. Documented at
binarybeachio/docs/architecture/bridge-jwt-replay-protection.md.

Public-key transport: BB_BRIDGE_PUBLIC_KEY_URL env points at the
in-cluster bridge's /.well-known/bb-bridge.pub.pem (avoids the
env-PEM corruption issue Coolify has with backslash-escaped keys).
Endpoint is implicitly disabled (404) when env unset — vanilla
upstream behavior preserved.

Storage patches (Patch 2) unchanged. Brand asset preserved (dormant).
Pre-migration source state preserved on branch pre-migration-2026-05-04.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:24:48 -10:00

199 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# bb-plane-fork — binarybeachio customizations of Plane
This file is the canonical contract between this fork and the binarybeachio platform repo. It exists so anyone (or any agent) on a fresh session can answer "what's customized, why, and how do I refresh from upstream" without reading code.
**Fork repo convention** (template — same shape for every Path B fork in binarybeachio):
```
upstream remote → original project on github.com (read-only, merge-source)
origin remote → git.binarybeach.io/binarybeach/bb-<name>-fork (where we push)
github mirror → github.com/binarybeachllc/bb-<name>-fork (push-mirror, off-site backup)
upstream branch — clean mirror of upstream's default branch, never modified
main branch — our customizations on top of latest upstream tag we've integrated
update/<v> — short-lived integration branch when pulling a new upstream version
```
`git log main..upstream` = "upstream changes I haven't pulled in"
`git log upstream..main` = "binarybeachio's customizations"
---
## Upstream
| Field | Value |
|---|---|
| Project | Plane (open-source project management) |
| Upstream repo | https://github.com/makeplane/plane |
| Upstream default branch | `preview` |
| Currently integrated upstream version | **v1.3.0** (release commit `cf696d2`) |
| License | AGPL-3.0-only (we MUST publish source of any deployed customizations — public Forgejo + push-mirror to GitHub satisfies this) |
## Why we forked (post-2026-05-04 platform-architecture pivot)
Plane's first-party OIDC support is gated behind the **Pro/Business commercial edition** (Pro tier minimum 25 users = $338+/mo). The community edition's `/god-mode/authentication/oidc` page is a frontend stub — the backend handler returns 404. Plane CE has GitHub/GitLab/Gitea/Google OAuth providers but no native OIDC, no SAML, and no trusted-proxy-header auth.
We integrate Plane into the binarybeachio platform via the **architecture's Bucket 4 pattern**: a single additive trusted-JWT endpoint that the platform's auth-bridge calls after oauth2-proxy validates a Zitadel session at the edge. See:
- `binarybeachio/docs/architecture/01-platform-architecture.md` for the bucket taxonomy and bridge contract
- `binarybeachio/docs/architecture/bridge-jwt-replay-protection.md` for the JWT replay-protection contract
- `binarybeachio/docs/services/plane/migration-plan.md` for the full per-service migration write-up
The previous shape of this fork (in-place patching of `provider/oauth/github.py` to repurpose GitHub OAuth as Zitadel OIDC) was reverted on 2026-05-04 in favor of the Bucket-4 additive endpoint, which has a smaller fork surface, fewer hot files to track on upstream merges, and centralizes OIDC handling in the auth-bridge instead of duplicating it per-app. The pre-revert source state is preserved on the `pre-migration-2026-05-04` branch for reference.
## What's customized (the inventory — keep current)
Three logical patch groups across the repo. Touch surface is intentionally minimal.
### Patch 1: Bucket-4 trusted-JWT entry-point (additive — 1 new file + 2 line additions)
| File | Change | Risk on upgrade |
|---|---|---|
| `apps/api/plane/authentication/views/app/trusted.py` | **New file**. Django `View` that validates a bridge-issued RS256 JWT, atomically claims its `jti` in shared-redis (replay protection), find-or-creates the User, and calls `user_login(request, user, is_app=True)` to set the Django session cookie. PEM is fetched at runtime from `BB_BRIDGE_PUBLIC_KEY_URL` (avoids the env-PEM corruption issue Coolify has with backslash-escaped keys). Endpoint is implicitly disabled (returns 404) when the env is unset. | **Low.** Depends only on `User` model, `user_login`, `post_user_auth_workflow`, and `get_safe_redirect_url` — all stable upstream APIs. PyJWT and `requests` are existing deps. |
| `apps/api/plane/authentication/urls.py` | 1-line addition appending `path("sign-in-trusted/", TrustedSignInEndpoint.as_view(), name="sign-in-trusted")` to the urlpatterns list. | **Low.** Pure append; no existing routes modified. |
| `apps/api/plane/authentication/views/__init__.py` | 1-line addition exporting `TrustedSignInEndpoint`. | **Low.** Pure append. |
| `apps/api/plane/authentication/adapter/error.py` | Adds 7 error codes in the 60006099 range (reserved for fork additions). Pure dict-additions; no existing entries renumbered. | **None.** |
The full bridge ↔ Plane contract:
- Bridge mints `RS256` JWT signed with `BRIDGE_SIGNING_KEY` (private). Claims: `iss=bb-bridge`, `aud=plane`, `iat`, `exp` (now+60s), `jti` (UUIDv4), `sub`, `email`, `first_name`, `last_name`, `tenant`.
- Bridge 302s the user's browser to `https://pm.<tenant>.binarybeach.io/auth/sign-in-trusted/?token=<jwt>&next_path=<rd>`.
- Plane's view: fetches public key from `BB_BRIDGE_PUBLIC_KEY_URL` (cached 5 min), verifies signature + claims, atomically `SETNX bb_bridge_jti:<jti>` in shared-redis with TTL = `exp - now + 30s`, find-or-creates User by email, calls `user_login()`, 302s to `next_path`.
- Replay protection is **fail closed**: if shared-redis is unavailable, the request is rejected. Operator break-glass uses the email+password sign-in (vanilla upstream code) which doesn't depend on either Redis or the bridge.
### Patch 2: Presigned PUT for uploads (R2/B2 don't implement PostObject)
| File | Change | Risk on upgrade |
|---|---|---|
| `apps/api/plane/settings/storage.py` | `S3Storage.generate_presigned_post(...)` rewritten to mint a presigned PUT URL via `generate_presigned_url(HttpMethod="PUT")`. Method name preserved for caller compat. | **Medium.** If Plane's upload flow changes upstream, conflict surface grows. Candidate for upstream PR. |
| `apps/api/plane/utils/openapi/responses.py` | OpenAPI example response updated to PUT shape. | **Low.** |
| `apps/api/plane/tests/unit/settings/test_storage.py` | 2 tests retargeted to assert `generate_presigned_url` boto3 call. | **Low.** |
| `packages/types/src/file.ts` | `TFileSignedURLResponse.upload_data` adds `method?: "PUT" \| "POST"`, drops AWS POST-form-data fields. | **Low.** |
| `packages/services/src/file/helper.ts` | `generateFileUploadPayload(...)` returns a `TFileUploadRequest` descriptor; dispatches PUT/POST. | **Medium.** |
| `packages/services/src/file/file-upload.service.ts` + `apps/web/core/services/file-upload.service.ts` | `uploadFile(...)` signature changed to `(payload, progress?)`. Uses `axios.request({method, url, data, headers})`. | **Medium.** |
| `apps/web/core/services/file.service.ts`, `apps/web/core/services/issue/issue_attachment.service.ts`, `packages/services/src/file/sites-file.service.ts` | 5 caller sites updated to pass `TFileUploadRequest` to `uploadFile`. | **Low.** |
Decision record at `binarybeachio/docs/features/storage-upload-flow.md`. Patch 2 is independent of Patch 1 — `git revert <storage-PUT sha>` undoes it cleanly.
### Patch 3: Brand asset (kept as dormant; entry-point UX is Traefik-driven)
| File | Change | Risk on upgrade |
|---|---|---|
| `apps/web/app/assets/logos/binarybeach-logo.png` | New asset. Currently unreferenced; preserved for future AGPL §13 footer-link addition or other branding work. | **None.** |
The previous fork's GitHub-button rebrand patch (`apps/web/core/hooks/oauth/core.tsx`) was reverted on 2026-05-04. Sign-in entry-point UX is now driven by a Traefik `redirectregex` middleware applied to the per-tenant Plane router that 302s `/sign-in*`, `/sign-up*`, `/accounts/sign-in*` to `https://bridge.binarybeach.io/handoff?app=plane&tenant=<slug>&...`. Pure infrastructure config; no source modification needed for the redirect.
Files **not** changed (deliberately):
- `apps/api/plane/authentication/provider/oauth/github.py` — upstream-clean. Vanilla GitHub OAuth still works if configured via god-mode UI.
- `apps/api/plane/authentication/views/app/github.py` and the gitlab/gitea/google equivalents — all upstream-clean.
- `apps/admin/...` — god-mode UI unchanged.
- `apps/space/...` — public-share OAuth unchanged. Authenticated public boards continue to use email+password sign-in (vanilla upstream). When a tenant needs SSO for shared boards, add a sibling `views/space/trusted.py` (estimated ~80 LOC, mirrors the app/ view).
## Required runtime config
Set on the patched `plane-backend` container (binarybeachio sets these in `infrastructure/plane/.env`):
```bash
# Activates the trusted-JWT endpoint. URL points at the in-cluster bridge
# service's public-key endpoint. Unset → endpoint returns 404 (regression-safe).
BB_BRIDGE_PUBLIC_KEY_URL=http://auth-bridge-<uuid>:3000/.well-known/bb-bridge.pub.pem
```
Bridge-side configuration (in `binarybeachio/infrastructure/auth-bridge/.env`):
```bash
ADAPTER_PLANE_BINARYBEACH_BASE_URL=https://pm.binarybeach.binarybeach.io
# BRIDGE_SIGNING_KEY is loaded centrally by bridge-key.ts; the matching
# public key is served at /.well-known/bb-bridge.pub.pem and consumed by
# Plane via BB_BRIDGE_PUBLIC_KEY_URL.
```
Plane god-mode admin UI (`/god-mode/authentication/...`):
- All four upstream OAuth providers (GitHub/GitLab/Gitea/Google) can be left disabled. The trusted-JWT entry-point is the SSO front door.
- Email+password sign-in remains available as the break-glass admin path. Per-tenant `bb-admin` user is seeded with a permanent password from `_shared/.env.bb-admin`.
## Cross-fork conventions adopted
This fork pulls in binarybeachio's [session lifecycle convention](https://git.binarybeach.io/binarybeach/binarybeachio-platform/src/branch/main/docs/features/session-lifecycle.md) — 15-min idle timeout, slide-on-activity. Applied automatically by `bootstrap.py` at deploy. To override for this fork specifically, set `SESSION_COOKIE_AGE` / `ADMIN_SESSION_COOKIE_AGE` / `SESSION_SAVE_EVERY_REQUEST` in `infrastructure/plane/.env` (per-app .env beats convention).
## Refresh from upstream — the procedure
When a new Plane release lands and we want to integrate:
```bash
git fetch upstream
# Sync the upstream mirror branch (never touched by us)
git switch upstream
git reset --hard upstream/preview # or @v1.4.0 if we track tags
git push origin upstream
# Integration branch
git switch main
git switch -c update/v1.4.0
git merge upstream # likely conflict-free since fork is additive
# Hand-test:
# 1. Local stack via docker-compose.bb-local.yml — confirm sign-in works.
# 2. Trusted endpoint with a hand-minted JWT (helper script TBD; for now,
# mint via a node REPL using bridge-key.ts:signBridgeJwt).
# 3. Vanilla email+password regression test.
# Once happy:
git switch main
git merge --ff-only update/v1.4.0
git branch -d update/v1.4.0
git push origin main
# Then on laptop: rebuild + tag + push images (see "Build" below)
# Then in binarybeachio repo: bump tag in infrastructure/plane/docker-compose.yml
# Then: py infrastructure/_shared/bootstrap.py to trigger the Coolify deploy
```
## Build — which images to rebuild and how
Per binarybeachio architecture doc §7.4 ("only rebuild what we touched"), this fork only requires rebuilding **two of the six** Plane images:
| Image | Customized? | Source | Build target |
|---|---|---|---|
| `plane-backend` | YES (Patch 1 + Patch 2) | `apps/api/Dockerfile.api` | `git.binarybeach.io/binarybeach/plane-backend:v1.3.0-mine.<n>` |
| `plane-frontend` (aka web) | YES (Patch 2 frontend bits only) | `apps/web/Dockerfile.web` | `git.binarybeach.io/binarybeach/plane-frontend:v1.3.0-mine.<n>` |
| `plane-space` | no | upstream `makeplane/plane-space:v1.3.0` | (no rebuild) |
| `plane-admin` | no | upstream `makeplane/plane-admin:v1.3.0` | (no rebuild) |
| `plane-live` | no | upstream `makeplane/plane-live:v1.3.0` | (no rebuild) |
| `plane-proxy` | no | upstream `makeplane/plane-proxy:v1.3.0` | (no rebuild) |
Tag scheme per architecture §6 #7: `<upstream-version>-mine.<n>`. Push immutable tag + `:latest`:
```bash
# from C:\Users\maxwe\GitHubRepos\bb-plane-fork
docker build -t git.binarybeach.io/binarybeach/plane-backend:v1.3.0-mine.2 \
-t git.binarybeach.io/binarybeach/plane-backend:latest \
-f apps/api/Dockerfile.api .
docker push git.binarybeach.io/binarybeach/plane-backend:v1.3.0-mine.2
docker push git.binarybeach.io/binarybeach/plane-backend:latest
docker build -t git.binarybeach.io/binarybeach/plane-frontend:v1.3.0-mine.2 \
-t git.binarybeach.io/binarybeach/plane-frontend:latest \
-f apps/web/Dockerfile.web .
docker push git.binarybeach.io/binarybeach/plane-frontend:v1.3.0-mine.2
docker push git.binarybeach.io/binarybeach/plane-frontend:latest
```
`mine.<n>` resets to `mine.1` on every upstream version bump; increments per local rebuild within the same upstream version.
## License compliance
Plane is AGPL-3.0-only. The license requires us to provide the source of any modified version we deploy or offer over a network. Our compliance:
1. **Forgejo source**`git.binarybeach.io/binarybeach/bb-plane-fork` is publicly readable.
2. **GitHub mirror** — push-mirror to `github.com/binarybeachllc/bb-plane-fork`.
3. **In-product source link** — TODO. AGPL §13 requires "prominent" notice to network users; a footer suffices. Tracked separately.
## Test plan (manual, until we have CI)
1. **Local build smoke**: both images build cleanly.
2. **Local stack**: `docker compose -f docker-compose.bb-local.yml --env-file .env.bb-local up -d` (with `BB_BRIDGE_PUBLIC_KEY_URL` unset) → vanilla email+password sign-in works (regression check).
3. **Trusted-JWT happy path**: with `BB_BRIDGE_PUBLIC_KEY_URL` pointing at production bridge, hand-mint a JWT (claims: `iss=bb-bridge`, `aud=plane`, valid `exp`, fresh `jti`, valid email), `GET /auth/sign-in-trusted/?token=<jwt>&next_path=/`, expect 302 to `/` with sessionid cookie set.
4. **Trusted-JWT replay rejection**: hit the same URL with the same token twice. First → 302 + sessionid. Second → 302 to error redirect with `TRUSTED_JWT_TOKEN_REPLAYED`.
5. **Trusted-JWT disabled regression**: unset `BB_BRIDGE_PUBLIC_KEY_URL`, hit `/auth/sign-in-trusted/`, expect 404.
6. **Production deploy**: bump tag in `binarybeachio/infrastructure/plane/docker-compose.yml``py infrastructure/_shared/bootstrap.py` → verify on `pm.binarybeach.binarybeach.io`.