bb-plane-fork/BINARYBEACHIO.md
binarybeach 69b499c9ec binarybeachio: trusted view — key User on bb_mailbox (four-layer identity model)
Identity-model rollout T2.4. Trusted view now derives `lookup_email = bb_mailbox or email`
and uses it for both User.objects.filter() and the new-User row's email field. WARN-log
fallback to federation email when the claim is absent (transitional safety; should never
fire once Zitadel `bb-claims` Action + bridge-side userinfo enrichment are live).

Decode-time required-claims unchanged (`bb_mailbox` stays optional) so partial deploys
aren't bricked. Pre-migration SQL rename of operator's existing User row required —
see binarybeachio docs/services/plane/migration-plan.md §9.

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

14 KiB
Raw Blame History

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 keyed on bb_mailbox (four-layer identity model — falls back to email when the claim is absent), 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, and bb_mailbox (when emitted by Zitadel's bb-claims Action — see binarybeachio/docs/architecture/multi-tenant-identity.md §4).
  • 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 keyed on bb_mailbox (preferred) or email (fallback), 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):

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

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 — 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:

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:

# 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 sourcegit.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.ymlpy infrastructure/_shared/bootstrap.py → verify on pm.binarybeach.binarybeach.io.