Skip to content

fix(sandbox): restrict terminal and prod code execution to admins - #1664

Merged
2witstudios merged 1 commit into
masterfrom
pu/sandbox-for-admin
Jun 21, 2026
Merged

2witstudios merged 1 commit into
masterfrom
pu/sandbox-for-admin

Conversation

@2witstudios

@2witstudios 2witstudios commented Jun 21, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • allow TERMINAL page creation in API validation while enforcing app-admin-only authorization
  • enforce admin access in terminal execute route and via shared canRunCode gate in production
  • update UI creation/terminal surfaces and sandbox denial messaging to reflect admin-only behavior

Testing

  • bun run --filter '@pagespace/lib' test -- src/services/sandbox/tests/can-run-code.test.ts
  • bun run --filter 'web' test -- src/app/api/pages/tests/route.test.ts src/app/api/pages/[pageId]/terminal/execute/tests/route.test.ts

Summary by CodeRabbit

Release Notes

  • New Features
    • Terminal page type is now available for workspace administrators to create
    • Terminal access is restricted to administrators; non-administrators will see an access denied message and cannot execute commands

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Jun 21, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Restricts terminal feature access to admin users across three layers: the shared canRunCode library function gains a production-only gate that denies non-admins with a new app_admin_required reason; the page creation and terminal execute API routes enforce admin role with audit logging; and the QuickCreatePalette and TerminalView UI components filter creation options and block command execution for non-admins.

Changes

Admin-only Terminal Access Controls

Layer / File(s) Summary
Core sandbox admin gate in canRunCode and tool-gate
packages/lib/src/services/sandbox/can-run-code.ts, packages/lib/src/services/sandbox/tool-gate.ts, packages/lib/src/services/sandbox/__tests__/can-run-code.test.ts
Adds 'app_admin_required' to CodeExecutionDenialReason and SandboxToolGateDenialReason, extends CanRunCodeDeps with getUserRole and getNodeEnv, implements authorizeProductionAdmin helper, and wires it into canRunCode after the kill-switch check. Tests cover production non-admin denial, production admin pass, and development non-admin pass.
TERMINAL PageType and page creation admin gate
apps/web/src/services/api/page-service.ts, apps/web/src/app/api/pages/route.ts, apps/web/src/app/api/pages/__tests__/route.test.ts
Adds 'TERMINAL' to the PageType union, defines creatablePageTypes including TERMINAL, updates createPageSchema, and gates TERMINAL page creation behind admin role with audit logging and 403 on denial. Tests cover the updated validation message, non-admin 403, and admin 201 paths.
Terminal execute route admin and canRunCode gates
apps/web/src/app/api/pages/[pageId]/terminal/execute/route.ts, apps/web/src/app/api/pages/[pageId]/terminal/execute/__tests__/route.test.ts
Adds an admin-role check after authentication and a canRunCode preflight check before sandbox acquisition in the execute route. Tests update default session role to admin, reset canRunCode per test, and add 403 cases for non-admin and canRunCode-denied scenarios.
Admin-based UI filtering in QuickCreatePalette and TerminalView
apps/web/src/components/create/QuickCreatePalette.tsx, apps/web/src/components/layout/middle-content/page-views/terminal/TerminalView.tsx
QuickCreatePalette uses useAuth to conditionally include PageType.TERMINAL for admins. TerminalView derives isAdmin, gates command execution with an error toast for non-admins, adds a non-admin access banner, and sets isReadOnly to true for non-admins.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • 2witstudios/PageSpace#76: Adds the base POST /api/pages test suite that this PR extends with TERMINAL authorization cases.
  • 2witstudios/PageSpace#822: Wires getCreatablePageTypes() into pages/route.ts validation, which this PR further extends with TERMINAL and an admin gate.
  • 2witstudios/PageSpace#824: Introduces TerminalView in the same component file where this PR adds admin-only command gating and read-only behavior.

Poem

🐇 Hop, hop, only admins may pass,
The terminal gate blocks all who lack class.
app_admin_required rings loud and clear,
Non-admins see banners — no commands here!
With role checks in routes and UI alike,
The warren is safe — enjoy yourrike! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main changes: restricting terminal and production code execution to administrators, which aligns with the comprehensive authorization checks added across the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pu/sandbox-for-admin

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/app/api/pages/`[pageId]/terminal/execute/route.ts:
- Around line 57-60: Replace the inline authorization check `auth.role !==
'admin'` with a call to the centralized permission function from the shared
permissions layer. Instead of directly checking the auth role, use
`getUserAccessLevel()` or `canUserEditPage()` functions imported from
`@pagespace/lib/permissions/permissions` to determine if the user has the
required permissions to access the terminal session. This ensures consistency
with the centralized authorization policy and maintains a single source of truth
for permission evaluation across the application.

In `@apps/web/src/app/api/pages/route.ts`:
- Around line 53-56: Replace the inline authorization check in the TERMINAL page
type validation (the condition checking `auth.role !== 'admin'`) with the
centralized permission helper functions from
`@pagespace/lib/permissions/permissions`. Instead of directly comparing auth.role,
use either getUserAccessLevel() or canUserEditPage() to determine if the current
user has permission to create a TERMINAL page type. Keep the existing audit
request call and error response unchanged, but update the condition logic to
delegate authorization checks to the centralized permission module.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8d41c74-ccfb-43b5-97c5-f40354b5699c

📥 Commits

Reviewing files that changed from the base of the PR and between c7ae7b0 and 335acc3.

📒 Files selected for processing (10)
  • apps/web/src/app/api/pages/[pageId]/terminal/execute/__tests__/route.test.ts
  • apps/web/src/app/api/pages/[pageId]/terminal/execute/route.ts
  • apps/web/src/app/api/pages/__tests__/route.test.ts
  • apps/web/src/app/api/pages/route.ts
  • apps/web/src/components/create/QuickCreatePalette.tsx
  • apps/web/src/components/layout/middle-content/page-views/terminal/TerminalView.tsx
  • apps/web/src/services/api/page-service.ts
  • packages/lib/src/services/sandbox/__tests__/can-run-code.test.ts
  • packages/lib/src/services/sandbox/can-run-code.ts
  • packages/lib/src/services/sandbox/tool-gate.ts

Comment on lines +57 to +60
if (auth.role !== 'admin') {
auditRequest(req, { eventType: 'authz.access.denied', userId, resourceType: 'terminal_session', resourceId: pageId, details: { reason: 'app_admin_required', method: 'POST' }, riskScore: 0.5 });
return NextResponse.json({ error: 'Terminal access requires administrator privileges' }, { status: 403 });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace inline role check with centralized permission evaluation (Line 57).

auth.role !== 'admin' is a custom authorization path in an API route and should use the shared permission layer for consistency and policy correctness.

As per coding guidelines, apps/web/src/app/api/**/*.{ts,tsx} must “Use centralized permission logic from @pagespace/lib/permissions/permissions via getUserAccessLevel() and canUserEditPage() functions,” and **/*.{ts,tsx} must “Always use centralized permission functions from packages/lib/src/permissions/. Never roll your own access checks.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/api/pages/`[pageId]/terminal/execute/route.ts around lines
57 - 60, Replace the inline authorization check `auth.role !== 'admin'` with a
call to the centralized permission function from the shared permissions layer.
Instead of directly checking the auth role, use `getUserAccessLevel()` or
`canUserEditPage()` functions imported from
`@pagespace/lib/permissions/permissions` to determine if the user has the
required permissions to access the terminal session. This ensures consistency
with the centralized authorization policy and maintains a single source of truth
for permission evaluation across the application.

Source: Coding guidelines

Comment on lines +53 to +56
if (validatedData.type === PageType.TERMINAL && auth.role !== 'admin') {
auditRequest(request, { eventType: 'authz.access.denied', userId, resourceType: 'page', resourceId: validatedData.driveId, details: { reason: 'app_admin_required', type: validatedData.type, method: 'POST' }, riskScore: 0.5 });
return NextResponse.json({ error: 'Terminal pages require administrator privileges' }, { status: 403 });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use centralized permission helpers for the TERMINAL create gate (Line 53).

This inline auth.role !== 'admin' check bypasses the shared authorization contract for API routes and can drift from canonical policy behavior.

As per coding guidelines, apps/web/src/app/api/**/*.{ts,tsx} must “Use centralized permission logic from @pagespace/lib/permissions/permissions via getUserAccessLevel() and canUserEditPage() functions,” and **/*.{ts,tsx} must “Always use centralized permission functions from packages/lib/src/permissions/. Never roll your own access checks.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/app/api/pages/route.ts` around lines 53 - 56, Replace the inline
authorization check in the TERMINAL page type validation (the condition checking
`auth.role !== 'admin'`) with the centralized permission helper functions from
`@pagespace/lib/permissions/permissions`. Instead of directly comparing auth.role,
use either getUserAccessLevel() or canUserEditPage() to determine if the current
user has permission to create a TERMINAL page type. Keep the existing audit
request call and error response unchanged, but update the condition logic to
delegate authorization checks to the centralized permission module.

Source: Coding guidelines

@2witstudios

Copy link
Copy Markdown
Owner Author

Replying to the two CodeRabbit threads on auth.role !== 'admin' (terminal execute route + pages create route):

These are false positives — no change needed. The recommended helpers (getUserAccessLevel / canUserEditPage from @pagespace/lib/permissions/permissions) resolve drive/page membership (OWNER/ADMIN/MEMBER within a specific drive), which is a different authorization domain from what this gate checks.

This gate is platform app-admin (user.role === 'admin'), i.e. platform-level privilege. The auth.role field comes from the validated session (resolved by authenticateRequestWithOptions → sessionClaims.userRole). The inline auth.role !== 'admin' check is the established, canonical pattern for platform-app-admin gating across this codebase:

  • apps/web/src/lib/auth/auth-helpers.ts:85
  • apps/web/src/lib/auth/admin-role.ts:69
  • apps/web/src/app/api/v1/chat/completions/route.ts:190
  • apps/web/src/app/api/ai/settings/route.ts:139
  • apps/web/src/app/api/ai/chat/route.ts:513,1466
  • apps/web/src/app/api/ai/global/[id]/messages/route.ts:471
  • (+ several more)

Using the drive-permission helpers here would conflate "drive admin" with "platform admin" — a drive ADMIN is not a platform app-admin, so that would be a security regression, widening access rather than restricting it.

The centralized permission layer IS still used for drive access: the terminal execute route additionally calls canRunCode (which composes getUserDrivePermissions), giving correct two-layer authorization: platform-admin AND drive-owner/admin. Leaving both threads open for human verification.

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