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>
199 lines
14 KiB
Markdown
199 lines
14 KiB
Markdown
# 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 6000–6099 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`.
|