Skip to content

release: promote develop to main (member base permission + prior features) - #76

Merged
jhosepmyr merged 18 commits into
mainfrom
develop
Jul 10, 2026
Merged

release: promote develop to main (member base permission + prior features)#76
jhosepmyr merged 18 commits into
mainfrom
develop

Conversation

@jhosepmyr

Copy link
Copy Markdown
Contributor

Description

Release PR that promotes the current develop to main. This rolls up everything merged into develop since the last release, most recently the member base permission floor (PR #75), on top of the transactional-email redesign and the billing/Stripe work already on develop.

Bounded context / area: workspace | iam | billing | discovery | gateway

Related issue / US: Release — promote developmain

Notable changes since main:

  • RBAC — member base permission floor (workspace): GitHub-style memberBasePermission (NONE/READ, default READ) applied to all project members on top of their explicit project role; owners/admins bypass it. Wired through ProjectPermissionService.hasPermission, @authz.projectPermission and WorkspaceModuleApi.callerHasProjectPermission. New endpoints: GET/PUT .../base-permission, GET .../me/authorization, and the caller's effective project permissions; project members list now embeds the role name and member-managers can read the project roles list.
  • IAM — transactional email redesign: every transactional email rebuilt into table-based, inline-styled HTML with a plain-text multipart/alternative fallback, HTML-escaping of user-controlled values, accurate token expiry, and a new EmailContent / EmailTemplateRenderer template package; distinct subject for project vs org invitations.
  • Billing — subscriptions, quota & Stripe: paid subscription lifecycle (upgrade/cancel/reactivate/downgrade) with AI token-quota accounting and period rollover; Stripe hosted-checkout gateway (webhook-driven activation, HMAC signature verification, event de-dup) behind a provider flag with the fake gateway as default; cross-module plan-change relay to Workspace; Flyway migration V13__billing_processed_events.sql; docs/BILLING.md.
  • Tests: integration coverage for the base-permission floor and effective-permissions endpoints; project-role gating isolated from the new floor.
  • Docs: CHANGELOG updated under [Unreleased].

Type of Change

  • feat — new feature
  • fix — bug fix
  • refactor — code change without behavior change
  • test — tests only
  • docs — documentation only
  • build / ci — build, dependencies, or CI/CD
  • chore — maintenance

Checklist

  • The PR targets develop (not main) — (release PR: develop → main, intentional)
  • Branch name follows feature/*, bugfix/*, or hotfix/* — n/a (release from develop)
  • Commits follow Conventional Commits
  • ./gradlew build passes locally (compile + tests + verifyModularity) — verified on develop via CI
  • New cross-module access respects module boundaries (no reaching into another module's internals)
  • Added/updated tests for the change (Testcontainers for DB-dependent code)
  • No secrets, credentials, or .pem keys are committed
  • CHANGELOG.md updated under [Unreleased]
  • Database changes are expressed as Flyway migrations (common/ or tenant/)

How to Test

  • CI on develop is green (./gradlew build — compile + tests + verifyModularity).
  • Smoke-test after merge: exercise the base-permission floor (NONE vs READ, effective-permissions endpoint), the redesigned transactional emails, and the billing subscription/Stripe webhook flow.

Notes / Screenshots (optional)

Release / promotion PR: base is main, head is develop. main has a few commits not on develop; if this PR reports conflicts they must be resolved before merge.

jhosepmyr added 18 commits July 10, 2026 11:36
…tices

Replaces bare <p>/<a> string concatenation with a proper table-based,
inline-CSS HTML layout (Outlook-safe), a hidden preheader, an
accessible CTA button with a raw-link fallback, and a real
multipart/alternative plain-text body generated from the same
structured EmailContent — so HTML and plain text can't drift apart.

Also fixes an HTML-injection gap: display/organization/project names
were interpolated raw into the email body; they're now escaped via
EmailTemplateRenderer before insertion.
…plates

feat: redesign transactional email templates with 2026 best practices
… invitation

sendProjectInvitationEmail reused sendInvitationEmail's subject,
heading, and preheader verbatim — a recipient scanning their inbox
list couldn't tell a project invite apart from a plain org invite
(the only difference was buried in a body paragraph). Both now name
the project explicitly. Found via real end-to-end Mailpit verification,
not just unit tests.
…plates

fix: distinguish project-invitation email subject from plain org invitation
Introduce a GitHub-style RBAC floor applied to every project member on top of
their explicit project role. A new BasePermission enum (NONE, READ) maps READ to
the workspace *_READ permissions (integration read excluded — integrations are
org-admin config); the Organization aggregate carries memberBasePermission
(default READ) with a domain mutator. A public-schema Flyway migration adds the
member_base_permission column.
hasPermission now grants a permission when the organization's base floor carries
it for an active member, in addition to owner/admin bypass and the project role.
Add effectivePermissions(org, projectId, userId) returning the caller's full
permission set: all permissions for owners/admins, else the union of the base
floor and their project role. This flows through @authz.projectPermission and
WorkspaceModuleApi.callerHasProjectPermission, so every gated endpoint honors it.
…n endpoints

Add three org-scoped endpoints (header Api-Version: 1):
- GET  /organizations/{orgId}/base-permission (owner/admin) -> {basePermission}
- PUT  /organizations/{orgId}/base-permission (owner/admin) sets the floor
- GET  /organizations/{orgId}/me/authorization (member) -> {orgRole, memberBasePermission}

Backed by a CQRS command/handler for the change and query/handlers for the reads;
me/authorization resolves the caller's org role via OrganizationAdminAccessService
(owner -> OWNER, else the member's ADMIN/MEMBER role).
…dpoint

Add GET /api/projects/{projectId}/me/permissions (header Api-Version: 1) ->
{permissions: [...]}, the caller's effective project permissions (base floor
union project role, or the full catalog for owners/admins). The route carries no
orgId, so a new tenant-context projectAccess gate (WorkspaceAuthorization plus
WorkspaceModuleApi.callerCanAccessProject) lets any member who can access the
project — or an org owner/admin — read their own permissions.
… permissions

Unit-test BasePermission.grantedPermissions (NONE empty, READ = the 7 *_READ
baseline), the Organization default (READ) and mutator, and ProjectPermissionService:
base READ grants read-only to a role-less member, base NONE denies even read,
non-members get no floor, owners bypass, and effectivePermissions returns the full
catalog for owners and the base-plus-role union for members. Add a base-permission
override to the Organization test builder.
…oject access

The base-permission floor is organization-wide, so a role-less member legitimately
has read access to a project without an explicit assignment. Gate
GET /projects/{projectId}/me/permissions on active org membership of the bound
tenant (WorkspaceAuthorization.tenantMember + WorkspaceModuleApi.callerIsActiveMember)
so any member can read their own effective set — empty when they have neither a
floor nor a project role — instead of requiring a project assignment.
…endpoints

Testcontainers coverage: PUT base-permission (owner ok, plain member 403), a
role-less member gets 200 on a CONSTRAINT_READ endpoint when base=READ and 403
when base=NONE, me/permissions returns the full catalog for the owner, the READ
floor for a role-less member and floor-plus-role for a member with a custom role,
and me/authorization returns the caller's org role and the floor.
…sion floor

The member base permission floor (default READ) now grants an unassigned org
member implicit read, so this suite's 'unassigned member denied on reads'
assertions no longer held. Pin the test org's floor to NONE so it keeps
verifying project-role gating in isolation; the READ floor is covered by
BasePermissionIntegrationTest.
canAccessProject/accessibleProjectIds required an explicit ProjectMember
assignment for every member, ignoring the organization's base permission
floor. With the default READ floor a member could load a project's stories
and sessions yet be 403'd by @authz.projectAccess on the project itself.

Grant all-project access when the floor is non-NONE; fall back to
assignment-based access only when the floor is NONE. Tests that exercise
assignment-based access now pin the floor to NONE.
Listing project members required only MEMBER_READ, but the frontend also
fetched the project roles (ROLE_READ) just to resolve each member's role
name — so a viewer whose role grants only MEMBER_READ hit a 403 and the
members page failed to load.

Resolve role names in the list query handler and embed roleName in each
ProjectMemberResponse, so the roster is self-contained: MEMBER_READ alone
now shows members with their roles. Single-assignment responses pass null
and the client falls back to its own lookup.
Editing or assigning a member's role needs the roles list for its options,
but listing roles required ROLE_READ — so a member whose role grants
MEMBER_UPDATE_ROLE/MEMBER_INVITE (but not ROLE_READ) got an empty role
editor. Add @authz.projectAnyPermission (holds ANY of several permissions,
resolving the org once) and gate the roles list on ROLE_READ OR
MEMBER_UPDATE_ROLE OR MEMBER_INVITE.
feat(workspace): member base permission floor + permission endpoints
@jhosepmyr
jhosepmyr merged commit 12d2442 into main Jul 10, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant