Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1.4k
feat(webapp): consolidate auth path + add comprehensive auth tests#3499
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
396cbade27a397e331f75fae54b4e27562994b5f07d4693d1329ecdefb8cba337484d95e8a3a2088f52dc5aeab29a58cbc1268e7edc4feb11d747084598d9ac4d4f526d5e71ecf67c17d5924c7cd7c54ca7f7897e382b04752de61bb0b12990d8d5a16b47a0686fccfda0938b9e55c4330da5332a2deeae2350af39154ce69fdfbeadeb403978217b409855c6d67f5920db9e4cd3715fc335d428ffabfc198bb3de0f9a4402935343dc4a7f75d1d134fadb39e9954c47de5da51a8d7287b4fe05a6edc9a398e8c913a5b2efa09f353e31796f3e61083af4e7782c97002b8060dd56d22a1a04eca210f1df988aecaf995d75f4e18a2144c04e44bab6386b980b68182aec1fb32483bc812a0bda30a8c7797da75ac55b3b25aa98015fdea72d02369dd5d43f1ec19465486db63337c6db41892fdaa4baab432c94377b2a80bff6ee3ccc76af8d6456e531327f057efad0e0187fc7e381afa056b493704b07282757fb5e18fe83065884054263f0a2094dFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@trigger.dev/plugins": patch | ||
| --- | ||
| The public interfaces for a plugin system. Initially consolidated authentication and authorization interfaces. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| name: "🛡️ E2E Tests: Webapp Auth (full)" | ||
| # Comprehensive RBAC auth test suite — see TRI-8731. Runs separately from | ||
| # the smoke e2e-webapp.yml because it covers every route family with a | ||
| # pass/fail matrix and would otherwise dominate per-PR CI time. | ||
| # | ||
| # Triggered: | ||
| # - Manually via workflow_dispatch. | ||
| # - Nightly via schedule. | ||
| # - On pull requests touching auth-relevant files only (paths filter). | ||
| permissions: | ||
| contents: read | ||
| on: | ||
| workflow_dispatch: | ||
| schedule: | ||
| - cron: "0 4 * * *" # 04:00 UTC daily | ||
| pull_request: | ||
| paths: | ||
| - "apps/webapp/app/services/routeBuilders/**" | ||
| - "apps/webapp/app/services/rbac.server.ts" | ||
| - "apps/webapp/app/services/apiAuth.server.ts" | ||
| - "apps/webapp/app/services/personalAccessToken.server.ts" | ||
| - "apps/webapp/app/services/sessionStorage.server.ts" | ||
| - "apps/webapp/app/routes/api.v*.**" | ||
| - "apps/webapp/app/routes/realtime.v*.**" | ||
| - "apps/webapp/test/**/*.e2e.full.test.ts" | ||
| - "apps/webapp/test/setup/global-e2e-full-setup.ts" | ||
| - "apps/webapp/test/helpers/sharedTestServer.ts" | ||
| - "apps/webapp/test/helpers/seedTestSession.ts" | ||
| - "apps/webapp/vitest.e2e.full.config.ts" | ||
| - "internal-packages/rbac/**" | ||
| - "packages/plugins/**" | ||
| - ".github/workflows/e2e-webapp-auth-full.yml" | ||
| jobs: | ||
| e2eAuthFull: | ||
| name: "🛡️ E2E Auth Tests (full)" | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| env: | ||
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | ||
| steps: | ||
| - name: 🔧 Disable IPv6 | ||
| run: | | ||
| sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1 | ||
| sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1 | ||
| sudo sysctl -w net.ipv6.conf.lo.disable_ipv6=1 | ||
| - name: 🔧 Configure docker address pool | ||
| run: | | ||
| CONFIG='{ | ||
| "default-address-pools" : [ | ||
| { | ||
| "base" : "172.17.0.0/12", | ||
| "size" : 20 | ||
| }, | ||
| { | ||
| "base" : "192.168.0.0/16", | ||
| "size" : 24 | ||
| } | ||
| ] | ||
| }' | ||
| mkdir -p /etc/docker | ||
| echo "$CONFIG" | sudo tee /etc/docker/daemon.json | ||
| - name: 🔧 Restart docker daemon | ||
| run: sudo systemctl restart docker | ||
| - name: ⬇️ Checkout repo | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| fetch-depth: 0 | ||
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Uh oh!There was an error while loading. Please reload this page. | ||
| # Don't leave the GITHUB_TOKEN in .git/config — this job | ||
| # doesn't need to push and the persisted creds would be | ||
| # readable from any subsequent step (zizmor/artipacked). | ||
| persist-credentials: false | ||
| - name: ⎔ Setup pnpm | ||
| uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 | ||
| with: | ||
| version: 10.33.2 | ||
| - name: ⎔ Setup node | ||
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | ||
| with: | ||
| node-version: 20.20.0 | ||
| cache: "pnpm" | ||
| - name: 🐳 Login to DockerHub | ||
| if: ${{ env.DOCKERHUB_USERNAME }} | ||
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | ||
| with: | ||
| username: ${{ secrets.DOCKERHUB_USERNAME }} | ||
| password: ${{ secrets.DOCKERHUB_TOKEN }} | ||
| - name: 🐳 Skipping DockerHub login (no secrets available) | ||
| if: ${{ !env.DOCKERHUB_USERNAME }} | ||
| run: echo "DockerHub login skipped because secrets are not available." | ||
| - name: 🐳 Pre-pull testcontainer images | ||
| if: ${{ env.DOCKERHUB_USERNAME }} | ||
| run: | | ||
| docker pull postgres:14 | ||
| docker pull redis:7.2 | ||
| docker pull testcontainers/ryuk:0.11.0 | ||
| - name: 📥 Download deps | ||
| run: pnpm install --frozen-lockfile | ||
| - name: 📀 Generate Prisma Client | ||
| run: pnpm run generate | ||
| - name: 🏗️ Build Webapp | ||
| run: pnpm run build --filter webapp | ||
| - name: 🛡️ Run Webapp Full Auth E2E Tests | ||
| run: cd apps/webapp && pnpm exec vitest run --config vitest.e2e.full.config.ts --reporter=default | ||
| env: | ||
| WEBAPP_TEST_VERBOSE: "1" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| area: webapp | ||
| type: improvement | ||
| --- | ||
| Webapp now supports a plugin system. Initially consolidates authentication and authorization paths. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| import { type Prisma, prisma } from "~/db.server"; | ||
| import { createEnvironment } from "./organization.server"; | ||
| import { customAlphabet } from "nanoid"; | ||
| import { logger } from "~/services/logger.server"; | ||
| import { rbac } from "~/services/rbac.server"; | ||
| const tokenValueLength = 40; | ||
| const tokenGenerator = customAlphabet("123456789abcdefghijkmnopqrstuvwxyz", tokenValueLength); | ||
| @@ -86,10 +88,19 @@ export async function inviteMembers({ | ||
| slug, | ||
| emails, | ||
| userId, | ||
| rbacRoleId, | ||
| }: { | ||
| slug: string; | ||
| emails: string[]; | ||
| userId: string; | ||
| /** | ||
| * Optional RBAC role to attach to the invite. When set, accepted | ||
| * invites trigger `rbac.setUserRole(rbacRoleId)` after the OrgMember | ||
| * is created. | ||
| * | ||
| * `OrgMemberInvite.role` is still set if the plugin isn't installed. | ||
| */ | ||
| rbacRoleId?: string | null; | ||
| }) { | ||
| const org = await prisma.organization.findFirst({ | ||
| where: { slug, members: { some: { userId } } }, | ||
| @@ -107,6 +118,7 @@ export async function inviteMembers({ | ||
| organizationId: org.id, | ||
| inviterId: userId, | ||
| role: "MEMBER", | ||
| rbacRoleId: rbacRoleId ?? null, | ||
| } satisfies Prisma.OrgMemberInviteCreateManyInput) | ||
| ); | ||
| @@ -163,7 +175,7 @@ export async function acceptInvite({ | ||
| user: { id: string; email: string }; | ||
| inviteId: string; | ||
| }) { | ||
| return await prisma.$transaction(async (tx) => { | ||
| const result = await prisma.$transaction(async (tx) => { | ||
| // 1. Delete the invite and get the invite details | ||
| const invite = await tx.orgMemberInvite.delete({ | ||
| where: { | ||
| @@ -207,8 +219,32 @@ export async function acceptInvite({ | ||
| }, | ||
| }); | ||
| return { remainingInvites, organization: invite.organization }; | ||
| return { | ||
| remainingInvites, | ||
| organization: invite.organization, | ||
| inviteRole: invite.role, | ||
| rbacRoleId: invite.rbacRoleId, | ||
| }; | ||
| }); | ||
| // If the invite carried an explicit RBAC role. Errors are logged, not fatal. | ||
| if (result.rbacRoleId) { | ||
| const roleResult = await rbac.setUserRole({ | ||
| userId: user.id, | ||
| organizationId: result.organization.id, | ||
| roleId: result.rbacRoleId, | ||
| }); | ||
| if (!roleResult.ok) { | ||
| logger.error("acceptInvite: skipped RBAC role assignment", { | ||
| organizationId: result.organization.id, | ||
| userId: user.id, | ||
| rbacRoleId: result.rbacRoleId, | ||
| reason: roleResult.error, | ||
| }); | ||
| } | ||
| } | ||
matt-aitken marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| return { remainingInvites: result.remainingInvites, organization: result.organization }; | ||
| } | ||
| export async function declineInvite({ | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.