Skip to content

Auth Audit - fixes #81

Merged
Connorbelez merged 3 commits intomainfrom
Connorbelez/auth-audit
Mar 16, 2026
Merged

Auth Audit - fixes #81
Connorbelez merged 3 commits intomainfrom
Connorbelez/auth-audit

Conversation

@Connorbelez
Copy link
Copy Markdown
Owner

@Connorbelez Connorbelez commented Mar 16, 2026

TL;DR

Added new seed modules for mortgage, obligation, and onboarding request entities, improved authentication patterns, and enhanced error handling with type safety improvements.

What changed?

  • Added three new seed modules: seedMortgage, seedObligation, and seedOnboardingRequest to the API
  • Refactored getCurrentUser in auth.ts to use authedQuery instead of the generic convex.query()
  • Added TODO comments across document engine modules indicating plans to add permission gates for production
  • Removed unused runMutation parameter from requireFairLendAdminAction context type
  • Enhanced error handling in seedObligation.ts by using String() conversion for better type safety in error messages

How to test?

  • Verify the new seed modules are accessible through the API
  • Test authentication flows to ensure getCurrentUser still works correctly
  • Run existing document engine operations to confirm functionality remains intact
  • Test obligation seeding with various states to verify improved error handling

Why make this change?

The changes expand seeding capabilities for core business entities while improving code organization and type safety. The authentication refactoring provides cleaner patterns, and the TODO comments establish a clear roadmap for production security hardening. The error handling improvements prevent potential runtime issues with undefined values in error messages.

Summary by Sourcery

Implement document access control for generated documents with sensitivity tiers and update seeding, auth, and document engine scaffolding.

New Features:

  • Expose new mortgage, obligation, and onboarding request seed modules through the API.
  • Introduce document access control for generated documents based on entity relationships and sensitivity tiers.

Bug Fixes:

  • Improve type safety of obligation seeding errors by safely stringifying unsupported states.

Enhancements:

  • Implement full canAccessDocument logic that resolves generated documents to their parent entities and enforces entity, deal access, and permission checks.
  • Refine getCurrentUser to use the authenticated query helper for consistent auth handling.
  • Annotate document engine queries and actions with TODOs describing future permission gates for production hardening.
  • Simplify the FairLend admin action middleware context by removing an unused runMutation parameter.

Tests:

  • Add comprehensive tests covering document access for admins, missing documents, and public, private, and sensitive tiers across mortgages, deals, application packages, and provisional applications.

Summary by CodeRabbit

Release Notes

  • New Features

    • Document access now enforces three sensitivity tiers (public, private, sensitive) with corresponding access permissions.
    • Integrated deal access validation for document authorization workflows.
    • Enhanced permission-based access control for sensitive content.
  • Tests

    • Expanded test coverage for document access scenarios across entity types.
  • Refactor

    • Improved authentication module structure.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Mar 16, 2026

Reviewer's Guide

Implements full authorization logic for document access based on document sensitivity and parent entity, significantly expands tests around canAccessDocument, wires new mortgage/obligation/onboardingRequest seed modules into the API, tightens auth usage with authedQuery, and improves error handling and future permission-gate documentation in document engine modules.

Class diagram for document access helpers and related entities

classDiagram
  class Viewer {
    authId: string
    isFairLendAdmin: boolean
    permissions: Set_string
  }

  class GeneratedDocument {
    _id: Id_generatedDocuments
    entityType: string
    entityId: string
    sensitivityTier: string
  }

  class DealAccessRecord {
    _id: Id_dealAccess
    userId: Id_users
    dealId: Id_deals
    status: string
  }

  class Deal {
    _id: Id_deals
    mortgageId: Id_mortgages
  }

  class Mortgage {
    _id: Id_mortgages
  }

  class ApplicationPackage {
    _id: Id_applicationPackages
  }

  class ProvisionalApplication {
    _id: Id_provisionalApplications
    brokerId: Id_brokers
    borrowerId: Id_borrowers
  }

  class ResourceChecks {
    +canAccessDocument(ctx_db, viewer, documentId)
    +hasActiveDealAccess(ctx_db, viewer, entityType, entityId)
    +canAccessMortgage(ctx_db, viewer, mortgageId)
    +canAccessDeal(ctx_db, viewer, dealId)
    +canAccessApplicationPackage(ctx_db, viewer, applicationPackageId)
    +getBrokerByAuthId(ctx_db, authId)
    +getBorrowerByAuthId(ctx_db, authId)
  }

  class AuthContext {
    db: QueryDb
    viewer: Viewer
  }

  ResourceChecks --> GeneratedDocument : reads
  ResourceChecks --> DealAccessRecord : queries
  ResourceChecks --> Deal : queries
  ResourceChecks --> Mortgage : queries
  ResourceChecks --> ApplicationPackage : queries
  ResourceChecks --> ProvisionalApplication : queries
  AuthContext *-- Viewer
  AuthContext *-- QueryDb
  GeneratedDocument --> Deal : may_link_via_entityType_deal
  GeneratedDocument --> Mortgage : may_link_via_entityType_mortgage
  GeneratedDocument --> ApplicationPackage : may_link_via_entityType_applicationPackage
  GeneratedDocument --> ProvisionalApplication : may_link_via_entityType_provisionalApplication
  DealAccessRecord --> Deal : many_to_one
  DealAccessRecord --> Viewer : many_to_one_via_userId
  ProvisionalApplication --> Viewer : linked_via_broker_or_borrower
Loading

Flow diagram for hasActiveDealAccess helper

flowchart TD
  B_start[Start hasActiveDealAccess]
  B1{entityType}
  B2[Query dealAccess using by_user_and_deal index
     filter userId == viewer.authId and dealId == entityId as Id_deals]
  B3[Collect records]
  B4{Any record.status == active?}
  B5[Return true]
  B6[Return false]
  B7[Query deals using by_mortgage index
     filter mortgageId == entityId as Id_mortgages]
  B8[Collect deals]
  B9[For each deal]
  B10[Query dealAccess using by_user_and_deal index
       filter userId == viewer.authId and dealId == deal._id]
  B11[Collect records for deal]
  B12{Any record.status == active?}
  B13[Return true]
  B14[After loop, return false]
  B15[Return true for pre_deal entities
      applicationPackage and provisionalApplication]

  B_start --> B1
  B1 -->|deal| B2 --> B3 --> B4
  B4 -->|yes| B5
  B4 -->|no| B6

  B1 -->|mortgage| B7 --> B8 --> B9 --> B10 --> B11 --> B12
  B12 -->|yes| B13
  B12 -->|no| B9
  B9 --> B14

  B1 -->|applicationPackage or provisionalApplication or other| B15
Loading

File-Level Changes

Change Details Files
Implement real canAccessDocument authorization with sensitivity tiers and deal access checks, plus comprehensive tests.
  • Replace stubbed canAccessDocument with implementation that resolves generatedDocuments to parent entities and checks entity-level access via canAccessMortgage/canAccessDeal/canAccessApplicationPackage or provisionalApplication ownership.
  • Introduce hasActiveDealAccess helper to enforce active dealAccess records for deal- and mortgage-scoped documents for private/sensitive tiers.
  • Require documents:sensitive_access permission for sensitive tier documents after entity and deal access checks.
  • Update canAccessDocument signature to accept a generatedDocuments Id and handle missing document as a denied access case.
  • Add extensive unit tests for canAccessDocument covering admin access, non-existent documents, public/private/sensitive tiers across mortgage, deal, applicationPackage, and provisionalApplication entities, including dealAccess permutations and permissions.
convex/auth/resourceChecks.ts
convex/auth/__tests__/resourceChecks.test.ts
Improve type safety and robustness of obligation seeding error handling.
  • Wrap unsupported obligation state values in String() when constructing ConvexError messages for state path resolution.
  • Wrap unsupported obligation date state values in String() when constructing ConvexError messages for obligationDates.
convex/seed/seedObligation.ts
Standardize current-user lookup on authedQuery instead of the generic convex.query().
  • Switch getCurrentUser to use authedQuery, keeping the handler logic that delegates to authKit.getAuthUser unchanged.
convex/auth.ts
Annotate document engine read paths with future permission-gate TODOs while keeping current behavior based on authenticated-only access.
  • Add TODO comments to basePdfs, generation, dataModelEntities, systemVariables, templateGroups, templateVersions, and templates indicating that current authedQuery/authedAction usage is authentication-only and that requirePermission-based gates (e.g., document:view, document:generate, document:upload, entity:view, variable:view) should be added for production.
  • Clarify which operations are considered read queries vs mutating/document-generation operations in those modules via comments.
convex/documentEngine/basePdfs.ts
convex/documentEngine/generation.ts
convex/documentEngine/dataModelEntities.ts
convex/documentEngine/systemVariables.ts
convex/documentEngine/templateGroups.ts
convex/documentEngine/templateVersions.ts
convex/documentEngine/templates.ts
Expose new seed modules for mortgage, obligation, and onboardingRequest through the generated API typings.
  • Register seedMortgage, seedObligation, and seedOnboardingRequest modules in the generated fullApi typings so they are callable via the Convex API.
convex/_generated/api.d.ts
Simplify the requireFairLendAdminAction middleware context to only carry the Viewer.
  • Remove unused runMutation field from the middleware context type used by requireFairLendAdminAction.
convex/fluent.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 16, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f8eacae5-abc3-4920-a7b0-17d16c1646ab

📥 Commits

Reviewing files that changed from the base of the PR and between ae60b5b and 20a9861.

⛔ Files ignored due to path filters (1)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (11)
  • convex/auth.ts
  • convex/auth/__tests__/resourceChecks.test.ts
  • convex/auth/resourceChecks.ts
  • convex/documentEngine/basePdfs.ts
  • convex/documentEngine/dataModelEntities.ts
  • convex/documentEngine/generation.ts
  • convex/documentEngine/systemVariables.ts
  • convex/documentEngine/templateGroups.ts
  • convex/documentEngine/templateVersions.ts
  • convex/documentEngine/templates.ts
  • convex/seed/seedObligation.ts

📝 Walkthrough

Walkthrough

This pull request refactors authentication in auth.ts by replacing convex.query() with authedQuery, implements a new polymorphic document access control model in resourceChecks.ts that supports multiple entity types and sensitivity tiers with deal-access validation, expands test coverage significantly, and adds TODO comments across document engine files indicating planned permission gates.

Changes

Cohort / File(s) Summary
Auth Module Refactoring
convex/auth.ts
Replaced convex import with authedQuery and updated getCurrentUser export to use authedQuery.handler(...).public() instead of convex.query().handler(...).public().
Document Access Control
convex/auth/resourceChecks.ts
Introduced polymorphic canAccessDocument function with DocumentEntityType alias, three-tier sensitivity model (public/private/sensitive), and entity-type specific access dispatching (mortgage, deal, applicationPackage, provisionalApplication). Added internal hasActiveDealAccess helper for deal-scoped permission validation.
Document Access Tests
convex/auth/__tests__/resourceChecks.test.ts
Expanded canAccessDocument test suite with comprehensive fixtures (insertDocumentPrereqs, insertGeneratedDocument) covering multiple entity types, sensitivity tiers, admin/non-existent document handling, ownership/relation-based access, and deal-access integration scenarios.
Document Engine Production TODOs
convex/documentEngine/basePdfs.ts, convex/documentEngine/dataModelEntities.ts, convex/documentEngine/generation.ts, convex/documentEngine/systemVariables.ts, convex/documentEngine/templateGroups.ts, convex/documentEngine/templateVersions.ts, convex/documentEngine/templates.ts
Added TODO comments across files indicating planned authentication (authedQuery/authedAction) and permission gate (requirePermission) additions for production; no functional changes.
Seed Utility Refactoring
convex/seed/seedObligation.ts
Replaced string cast type assertions with String(state) in error messages for buildStatePath and obligationDates functions.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant canAccessDocument as canAccessDocument Handler
    participant DB as Database
    participant EntityHandler as Entity-Type Handler<br/>(Mortgage/Deal/etc.)
    participant DealAccess as Deal Access Validator

    Caller->>canAccessDocument: Request document access<br/>with documentId
    canAccessDocument->>DB: Fetch generatedDocument
    DB-->>canAccessDocument: Document with entityType & sensitivity
    
    canAccessDocument->>canAccessDocument: Check entity-level access<br/>via type dispatcher
    canAccessDocument->>EntityHandler: canAccessMortgage<br/>canAccessDeal<br/>canAccessApplicationPackage<br/>canAccessProvisionalApplication
    EntityHandler-->>canAccessDocument: Entity access granted?
    
    alt Entity Access Denied
        canAccessDocument-->>Caller: Access Denied
    end
    
    alt Public Sensitivity
        canAccessDocument-->>Caller: Access Granted
    end
    
    alt Private or Sensitive
        canAccessDocument->>DealAccess: hasActiveDealAccess<br/>(entityType, entityId)
        DealAccess->>DB: Query deal state & permissions
        DB-->>DealAccess: Deal access records
        DealAccess-->>canAccessDocument: Active deal access?
        
        alt No Active Deal Access
            canAccessDocument-->>Caller: Access Denied
        end
        
        alt Sensitive Tier
            canAccessDocument->>canAccessDocument: Check permission<br/>documents:sensitive_access
            alt Permission Denied
                canAccessDocument-->>Caller: Access Denied
            end
        end
    end
    
    canAccessDocument-->>Caller: Access Granted
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

  • PR #71: Introduces the generatedDocuments schema that the new canAccessDocument logic directly queries and depends on for document entity-type resolution.
  • PR #74: Directly modifies the same convex/auth/resourceChecks.ts and test files, likely containing overlapping or complementary access-control logic.
  • PR #66: Expands entity-type definitions and entity-table mappings that the polymorphic document access dispatcher relies upon for entity-type specific access checks.

Poem

🐰 A rabbit hops through access gates,
Three tiers of docs—each type awaits,
With deals in mind and checks so fine,
Permissions bloom in guard and line,
Security's dance, both swift and bright!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Auth Audit - fixes' is vague and does not clearly convey the primary changes. It uses generic phrasing ('fixes') without specifying what was fixed or the main focus of the changeset. Revise the title to be more specific and descriptive of the main changes, such as 'Implement document access control and refactor auth to authedQuery' or 'Add canAccessDocument logic and auth refinements'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch Connorbelez/auth-audit
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Copy link
Copy Markdown
Owner Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@Connorbelez Connorbelez changed the title auth and type fixes Auth Audit - fixes Mar 16, 2026
@Connorbelez Connorbelez marked this pull request as ready for review March 16, 2026 21:05
Copilot AI review requested due to automatic review settings March 16, 2026 21:05
Copy link
Copy Markdown
Owner Author

@greptile please review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 5 issues, and left some high level feedback:

  • In canAccessDocument / hasActiveDealAccess, the comment says applicationPackage/provisionalApplication only require entity-level access for all tiers, but sensitive documents still go through the documents:sensitive_access permission check; consider either special-casing these entity types in the sensitive tier branch or updating the comment/tests to match the actual behavior.
  • The mortgage branch of hasActiveDealAccess issues one dealAccess query per related deal, which can become N+1 queries for mortgages with many deals; consider adding a more suitable index or restructuring the query to check all relevant dealAccess records in a single query if this path is performance-sensitive.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `canAccessDocument` / `hasActiveDealAccess`, the comment says applicationPackage/provisionalApplication only require entity-level access for all tiers, but sensitive documents still go through the `documents:sensitive_access` permission check; consider either special-casing these entity types in the sensitive tier branch or updating the comment/tests to match the actual behavior.
- The mortgage branch of `hasActiveDealAccess` issues one `dealAccess` query per related deal, which can become N+1 queries for mortgages with many deals; consider adding a more suitable index or restructuring the query to check all relevant `dealAccess` records in a single query if this path is performance-sensitive.

## Individual Comments

### Comment 1
<location path="convex/auth/resourceChecks.ts" line_range="318-327" />
<code_context>
+//   private   → entity access + dealAccess record required
+//   sensitive → entity access + dealAccess + documents:sensitive_access permission
+
+async function hasActiveDealAccess(
+	ctx: { db: QueryCtx["db"] },
+	viewer: Viewer,
+	entityType: string,
+	entityId: string
+): Promise<boolean> {
+	if (entityType === "deal") {
+		const records = await ctx.db
+			.query("dealAccess")
+			.withIndex("by_user_and_deal", (q) =>
+				q.eq("userId", viewer.authId).eq("dealId", entityId as Id<"deals">)
+			)
+			.collect();
+		return records.some((r) => r.status === "active");
+	}
+
+	if (entityType === "mortgage") {
+		const deals = await ctx.db
+			.query("deals")
</code_context>
<issue_to_address>
**suggestion:** Consider tightening the `entityType` parameter type in `hasActiveDealAccess` to a union of known entity types instead of `string`.

Since this function only has specific logic for "deal" and "mortgage" and otherwise defaults to `true` (for applicationPackage/provisionalApplication), keeping `entityType` as `string` makes it easy to pass an unsupported value and silently get the default branch. A union of the supported values (or reusing `doc.entityType`’s type) would prevent that at compile time.

Suggested implementation:

```typescript
async function hasActiveDealAccess(
	ctx: { db: QueryCtx["db"] },
	viewer: Viewer,
	entityType: "deal" | "mortgage" | "applicationPackage" | "provisionalApplication",
	entityId: string
): Promise<boolean> {

```

If there is an existing type for the `entityType` field on the underlying document (e.g. a `GeneratedDocument["entityType"]` union or similar), it would be preferable to:
1. Import that type into this file, and
2. Replace the inline union with that type alias for better reuse and single-source-of-truth for supported entity types.

Call sites of `hasActiveDealAccess` will also need to be adjusted if they currently pass arbitrary strings or values that are not part of this union.
</issue_to_address>

### Comment 2
<location path="convex/auth.ts" line_range="296" />
<code_context>

-export const getCurrentUser = convex
-	.query()
+export const getCurrentUser = authedQuery
 	.handler(async (ctx) => {
 		const user = await authKit.getAuthUser(ctx);
</code_context>
<issue_to_address>
**question (bug_risk):** Switching `getCurrentUser` from a generic query to `authedQuery` changes its behavior for unauthenticated callers.

Using `authedQuery` means unauthenticated callers will now be rejected at the middleware layer instead of being able to invoke `getCurrentUser`. If any existing flows call this before login (e.g., to probe for an existing session), those clients may now break. If this change is intentional, no action needed; otherwise, please verify the call sites and adjust expectations or behavior accordingly.
</issue_to_address>

### Comment 3
<location path="convex/auth/__tests__/resourceChecks.test.ts" line_range="1018" />
<code_context>
+	describe("canAccessDocument", () => {
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test case for admin access when the document does not exist

Currently admin access is only tested when the document exists. Since `canAccessDocument` short-circuits on `viewer.isFairLendAdmin` before any DB lookup, please add a case like `"admin — non-existent document — true"` that passes a deleted/non-existent `generatedDocuments` id and asserts `true` to protect against regressions where admin access starts depending on document existence.
</issue_to_address>

### Comment 4
<location path="convex/auth/__tests__/resourceChecks.test.ts" line_range="1363" />
<code_context>
+		});
+	});
+
+	it("private + applicationPackage — entity access sufficient (pre-deal)", async () => {
+		const t = convexTest(schema, modules);
+		await t.run(async (ctx) => {
</code_context>
<issue_to_address>
**suggestion (testing):** Add coverage for sensitive-tier applicationPackage access with and without permission

`applicationPackage` comments and `hasActiveDealAccess` imply entity-level access should be enough for all tiers, but the core logic still enforces `documents:sensitive_access` for `sensitive` docs. Right now only `public` and `private` applicationPackage flows are tested. Please add tests for the sensitive tier, e.g.: (1) `sensitive + applicationPackage` as `sr_underwriter` with `documents:sensitive_access``true`, and (2) the same without `documents:sensitive_access``false`, to verify pre-deal behavior matches the intended security model.

Suggested implementation:

```typescript
			// Broker has entity access to mortgage but no dealAccess
			const viewer = makeViewer({ authId: "broker-auth" });
			const result = await canAccessDocument(ctx, viewer, docId);
			expect(result).toBe(false);
		});
	});

	it("sensitive + applicationPackage — entity access sufficient with documents:sensitive_access (pre-deal)", async () => {
		const t = convexTest(schema, modules);

		await t.run(async (ctx) => {
			const { templateId, storageId } = await insertDocumentPrereqs(ctx);

			const underwriterUserId = await insertUser(ctx, {
				authId: "sr-underwriter-auth",
				role: "sr_underwriter",
			});
			const brokerId = await insertBroker(ctx, underwriterUserId);
			const propId = await insertProperty(ctx);
			const mortgageId = await insertMortgage(ctx, propId, brokerId);

			const docId = await insertGeneratedDocument(
				ctx,
				templateId,
				storageId,
				"mortgage",
				"sensitive"
			);

			const viewer = makeViewer({
				authId: "sr-underwriter-auth",
				role: "sr_underwriter",
				permissions: ["documents:sensitive_access"],
				applicationPackage: {
					propertyId: propId,
				},
			});

			const result = await canAccessDocument(ctx, viewer, docId);
			expect(result).toBe(true);
		});
	});

	it("sensitive + applicationPackage — entity access insufficient without documents:sensitive_access (pre-deal)", async () => {
		const t = convexTest(schema, modules);

		await t.run(async (ctx) => {
			const { templateId, storageId } = await insertDocumentPrereqs(ctx);

			const underwriterUserId = await insertUser(ctx, {
				authId: "sr-underwriter-no-sensitive-auth",
				role: "sr_underwriter",
			});
			const brokerId = await insertBroker(ctx, underwriterUserId);
			const propId = await insertProperty(ctx);
			const mortgageId = await insertMortgage(ctx, propId, brokerId);

			const docId = await insertGeneratedDocument(
				ctx,
				templateId,
				storageId,
				"mortgage",
				"sensitive"
			);

			const viewer = makeViewer({
				authId: "sr-underwriter-no-sensitive-auth",
				role: "sr_underwriter",
				// NOTE: no documents:sensitive_access here
				permissions: [],
				applicationPackage: {
					propertyId: propId,
				},
			});

			const result = await canAccessDocument(ctx, viewer, docId);
			expect(result).toBe(false);
		});
	});

```

You may need to align the helper usage with the rest of the file:

1. **Viewer shape**:  
   - If `makeViewer` expects different keys (e.g. `roles` instead of `role`, `scopes` instead of `permissions`, or a different shape for `applicationPackage`), adjust the viewer objects accordingly to match how the existing `public/private + applicationPackage` tests construct viewers.
2. **User / broker relationship**:  
   - If underwriters aren’t modeled via `insertBroker`, replace that with the appropriate helper used for `sr_underwriter` in other tests (or drop the broker entirely if not needed for entity access in the application package flow).
3. **Tier argument**:  
   - Ensure `insertGeneratedDocument`’s final `"sensitive"` argument matches the actual enum/string used elsewhere for sensitive-tier documents.
4. **Auth IDs and roles**:  
   - If the role identifier is different (e.g. `"sr_underwriter_role"` or comes from a constant), substitute the correct value so the tests reflect how `sr_underwriter` is represented in your auth model.
</issue_to_address>

### Comment 5
<location path="convex/auth/__tests__/resourceChecks.test.ts" line_range="1406" />
<code_context>
+
+	// ── Sensitive tier: entity + dealAccess + permission ──────────
+
+	it("sensitive + deal — dealAccess + permission — true", async () => {
+		const t = convexTest(schema, modules);
+		await t.run(async (ctx) => {
</code_context>
<issue_to_address>
**suggestion (testing):** Extend sensitive-tier coverage to mortgage-backed documents as well

Current tests only cover sensitive-tier logic for `deal` docs. Since mortgages can also be sensitive and depend on `hasActiveDealAccess` via related deals, please add coverage for at least: (a) `sensitive + mortgage` with related-deal access and permission → `true`, (b) same but without permission → `false`, and ideally (c) entity access but no active deal access → `false`, to validate the combined entity/dealAccess/permission checks for mortgages as well.

Suggested implementation:

```typescript
	// ── Sensitive tier: entity + dealAccess + permission ──────────

	it("sensitive + mortgage — dealAccess + permission — true", async () => {
		const t = convexTest(schema, modules);
		await t.run(async (ctx) => {
			const { templateId, storageId } = await insertDocumentPrereqs(ctx);
			const brokerUserId = await insertUser(ctx, { authId: "broker-auth" });
			const brokerId = await insertBroker(ctx, brokerUserId);
			const propId = await insertProperty(ctx);
			const mortgageId = await insertMortgage(ctx, propId, brokerId);
			const docId = await insertGeneratedDocument(
				ctx,
				templateId,
				storageId,
				"mortgage",
				mortgageId,
				{
					sensitivityTier: "sensitive",
				},
			);

			const viewer = await insertViewer(ctx, {
				authId: "sr-uw-auth",
				roles: new Set(["sr_underwriter"]),
			});

			const result = await canAccessDocument(ctx, viewer, docId);
			expect(result).toBe(true);
		});
	});

	it("sensitive + mortgage — dealAccess + no permission — false", async () => {
		const t = convexTest(schema, modules);
		await t.run(async (ctx) => {
			const { templateId, storageId } = await insertDocumentPrereqs(ctx);
			const brokerUserId = await insertUser(ctx, { authId: "broker-auth" });
			const brokerId = await insertBroker(ctx, brokerUserId);
			const propId = await insertProperty(ctx);
			const mortgageId = await insertMortgage(ctx, propId, brokerId);
			const docId = await insertGeneratedDocument(
				ctx,
				templateId,
				storageId,
				"mortgage",
				mortgageId,
				{
					sensitivityTier: "sensitive",
				},
			);

			const viewer = await insertViewer(ctx, {
				authId: "no-sensitive-auth",
				roles: new Set(), // viewer without sensitive-docs permission
			});

			const result = await canAccessDocument(ctx, viewer, docId);
			expect(result).toBe(false);
		});
	});

	it("sensitive + mortgage — entity access only, no active deal access — false", async () => {
		const t = convexTest(schema, modules);
		await t.run(async (ctx) => {
			const { templateId, storageId } = await insertDocumentPrereqs(ctx);
			const brokerUserId = await insertUser(ctx, { authId: "broker-auth" });
			const brokerId = await insertBroker(ctx, brokerUserId);
			const propId = await insertProperty(ctx);
			const mortgageId = await insertMortgage(ctx, propId, brokerId);
			const docId = await insertGeneratedDocument(
				ctx,
				templateId,
				storageId,
				"mortgage",
				mortgageId,
				{
					sensitivityTier: "sensitive",
				},
			);

			// Viewer associated with the same entity but without active deal access
			const viewer = await insertViewer(ctx, {
				authId: "entity-only-auth",
				roles: new Set(["sr_underwriter"]),
			});

			const result = await canAccessDocument(ctx, viewer, docId);
			expect(result).toBe(false);
		});
	});

```

Because only a fragment of the test file is visible, you should:

1. Align the viewer/role setup with whatever the existing “sensitive + deal” tests use:
   - If there is a dedicated helper for seeding permissions (e.g. `grantPermission` or similar), swap out the simple role changes above for that helper so that “with permission”/“without permission” exactly mirror the deal tests.
2. Ensure that mortgages are actually tied to deals for the `hasActiveDealAccess` logic:
   - If the existing deal tests explicitly create a deal and call a helper like `insertDealAccess`/`insertActiveDealAccess`, replicate that pattern here so that `mortgageId` is associated with the same deal and the viewer’s deal access matches the intended scenario in each test.
3. If `insertViewer` has a different name or the viewer factory takes additional required fields, update the viewer creation blocks to match the existing tests’ convention.
4. If `sensitivityTier` is set differently in your existing tests (constant, enum, or nested field), update the `insertGeneratedDocument` calls to follow that same shape so the tests correctly exercise the sensitive-tier path.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread convex/auth/resourceChecks.ts
Comment thread convex/auth.ts
Comment thread convex/auth/__tests__/resourceChecks.test.ts
Comment thread convex/auth/__tests__/resourceChecks.test.ts Outdated
Comment thread convex/auth/__tests__/resourceChecks.test.ts
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements the canAccessDocument resource check (T-009 / ENG-144), replacing a stub that blocked all non-admin access with a full three-tier sensitivity model (public, private, sensitive). It also refactors getCurrentUser to use authedQuery, adds TODO comments for production permission gates in the document engine, cleans up an unused type constraint in requireFairLendAdminAction, and registers three new seed modules.

Changes:

  • Implemented canAccessDocument with entity-level access checks, deal-access checks, and sensitive-tier permission checks, along with comprehensive tests covering all tiers and entity types.
  • Refactored getCurrentUser from convex.query() to authedQuery, adding authentication enforcement.
  • Added TODO comments across document engine modules for future production permission gates, removed unused runMutation type from requireFairLendAdminAction, and registered new seed modules (seedMortgage, seedObligation, seedOnboardingRequest).

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
convex/auth/resourceChecks.ts Replaced canAccessDocument stub with full three-tier sensitivity implementation and hasActiveDealAccess helper
convex/auth/__tests__/resourceChecks.test.ts Added ~530 lines of tests covering admin, non-existent doc, public/private/sensitive tiers across all entity types
convex/auth.ts Changed getCurrentUser from convex.query() to authedQuery
convex/fluent.ts Removed unused runMutation from requireFairLendAdminAction context type
convex/seed/seedObligation.ts Wrapped error template literals with String() for type safety
convex/documentEngine/*.ts Added TODO comments for production permission gates
convex/_generated/api.d.ts Registered three new seed modules

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread convex/auth.ts
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 16, 2026

Greptile Summary

This PR implements the canAccessDocument access control logic (ENG-144) with a three-tier sensitivity model (public/private/sensitive), adds three new seed modules, refactors getCurrentUser to use authedQuery, and annotates document engine endpoints with production-readiness TODOs.

Key changes:

  • canAccessDocument in resourceChecks.ts now fully resolves a generatedDocument to its parent entity and enforces tiered access — entity check → deal access check → permission check
  • getCurrentUser in auth.ts switched from convex.query() to authedQuery, which now throws for unauthenticated callers instead of returning null — this is a behavioral change for a .public() endpoint that may be used as an auth probe
  • hasActiveDealAccess unconditionally returns true for applicationPackage and provisionalApplication entities, bypassing the deal access gate for the private tier on those entity types — this appears intentional but is not tested for the sensitive tier denied case
  • runMutation removed from requireFairLendAdminAction's $context<> type — safe since runMutation is a native action context property
  • Seed modules for mortgage, obligation, and onboarding request are now registered in the generated API types

Confidence Score: 3/5

  • This PR is risky to merge without verifying the getCurrentUser behavioral change and confirming the intent behind the private-tier bypass for pre-deal entities.
  • The canAccessDocument logic is well-structured and the tests provide good coverage for the happy paths. However, two concerns lower confidence: (1) getCurrentUser switching to authedQuery will throw for unauthenticated clients, which is a silent breaking change for a public endpoint; (2) hasActiveDealAccess returning true for applicationPackage/provisionalApplication intentionally bypasses deal access for private-tier documents, but this design decision lacks a test for the sensitive-tier denied path.
  • convex/auth.ts (getCurrentUser behavioral change) and convex/auth/resourceChecks.ts (hasActiveDealAccess pre-deal bypass design)

Important Files Changed

Filename Overview
convex/auth.ts Replaced convex.query() with authedQuery for getCurrentUser, which now throws for unauthenticated callers — potentially breaking the intended auth-probe pattern of this public endpoint.
convex/auth/resourceChecks.ts Full canAccessDocument implementation with three-tier sensitivity model; hasActiveDealAccess always returns true for pre-deal entities, which bypasses the deal access gate for private tier on applicationPackage/provisionalApplication documents.
convex/auth/tests/resourceChecks.test.ts Comprehensive new tests for canAccessDocument covering all entity types and sensitivity tiers for deal/mortgage; missing coverage for sensitive + applicationPackage denied case.
convex/fluent.ts Removed unused runMutation from requireFairLendAdminAction context type — safe cleanup as runMutation is a native action context property, not middleware-injected.
convex/seed/seedObligation.ts Added String() conversion for error messages in buildStatePath and obligationDates — minor type-safety improvement, no functional change.
convex/_generated/api.d.ts Auto-generated type declarations updated to include three new seed modules: seedMortgage, seedObligation, and seedOnboardingRequest.
convex/documentEngine/generation.ts Added TODO comments noting that prepareGeneration, generateUploadUrl, generateFromTemplate, and generateFromGroup need requirePermission("document:generate") gates before production.
convex/documentEngine/basePdfs.ts TODO comment added documenting that read queries use auth-only middleware and need permission gates before production deployment.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[canAccessDocument called] --> B{viewer.isFairLendAdmin?}
    B -- Yes --> Z[return true]
    B -- No --> C{doc exists?}
    C -- No --> F[return false]
    C -- Yes --> D[Step 1: Entity-level access check]

    D --> D1{entityType?}
    D1 -- mortgage --> E1[canAccessMortgage]
    D1 -- deal --> E2[canAccessDeal]
    D1 -- applicationPackage --> E3[canAccessApplicationPackage]
    D1 -- provisionalApplication --> E4[check broker/borrower ownership]

    E1 --> G{hasEntityAccess?}
    E2 --> G
    E3 --> G
    E4 --> G

    G -- No --> F
    G -- Yes --> H{sensitivityTier = public?}

    H -- Yes --> Z
    H -- No --> I[Step 3: hasActiveDealAccess check]

    I --> I1{entityType?}
    I1 -- deal --> I2[query dealAccess by userId + dealId]
    I1 -- mortgage --> I3[find related deals, check any active dealAccess]
    I1 -- applicationPackage / provisionalApplication --> I4[return true bypass]

    I2 --> J{hasDealAccess?}
    I3 --> J
    I4 --> J

    J -- No --> F
    J -- Yes --> K{sensitivityTier = sensitive?}

    K -- No --> Z
    K -- Yes --> L{has documents:sensitive_access permission?}

    L -- Yes --> Z
    L -- No --> F
Loading

Comments Outside Diff (1)

  1. convex/auth.ts, line 296-301 (link)

    getCurrentUser now throws for unauthenticated callers

    authedQuery composes authMiddleware, which calls ctx.auth.getUserIdentity() and throws ConvexError("Unauthorized: sign in required") when no identity is present. The previous convex.query() had no auth middleware, so an unauthenticated caller would fall through to authKit.getAuthUser(ctx) and receive null.

    getCurrentUser is a .public() endpoint, meaning it is intentionally callable from the client — commonly used to determine whether the user is currently logged in. Any client code that calls this query unconditionally (e.g. on app load, to hydrate auth state) will now receive an unhandled error for unauthenticated sessions instead of null.

    If the endpoint is only ever called from already-authenticated contexts, this change is safe. But if any client code uses this query as an auth probe, it will break silently at runtime.

    Consider keeping the endpoint on the un-authenticated builder for the unauthenticated case:

    export const getCurrentUser = convex
        .query()
        .handler(async (ctx) => {
            const user = await authKit.getAuthUser(ctx);
            return user;
        })
        .public();

Last reviewed commit: 1a9ffcc

Comment thread convex/auth/resourceChecks.ts Outdated
Comment on lines +355 to +357
// applicationPackage / provisionalApplication are pre-deal entities —
// entity-level access (underwriting RBAC) is sufficient for all tiers.
return true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

hasActiveDealAccess always returns true for applicationPackage / provisionalApplication — bypasses the deal access gate for private tier

The comment states "entity-level access (underwriting RBAC) is sufficient for all tiers." For the sensitive tier this still works correctly because Step 4 independently checks viewer.permissions.has("documents:sensitive_access"). However, for the private tier the entire hasDealAccessResult gate is bypassed — any user who passes the entity-level check (e.g. any sr_underwriter) automatically satisfies the private-tier requirement with no dealAccess record needed.

If this is intentional, it should be clearly documented that private docs on pre-deal entities intentionally have no deal access gate. If it is not intentional and private documents on an applicationPackage should still require a deal access record when a related deal exists, the fallback should return false (or look up the linked deal) rather than true.

There is also no test covering sensitive + applicationPackage denied when documents:sensitive_access is missing, which would help verify the permission gate is still active for this path.

@Connorbelez Connorbelez force-pushed the Connorbelez/auth-audit branch from ae60b5b to 20a9861 Compare March 16, 2026 22:21
@Connorbelez Connorbelez merged commit c51c678 into main Mar 16, 2026
0 of 2 checks passed
Connorbelez added a commit that referenced this pull request Apr 20, 2026
### TL;DR

Added new seed modules for mortgage, obligation, and onboarding request entities, improved authentication patterns, and enhanced error handling with type safety improvements.

### What changed?

- Added three new seed modules: `seedMortgage`, `seedObligation`, and `seedOnboardingRequest` to the API
- Refactored `getCurrentUser` in auth.ts to use `authedQuery` instead of the generic `convex.query()`
- Added TODO comments across document engine modules indicating plans to add permission gates for production
- Removed unused `runMutation` parameter from `requireFairLendAdminAction` context type
- Enhanced error handling in `seedObligation.ts` by using `String()` conversion for better type safety in error messages

### How to test?

- Verify the new seed modules are accessible through the API
- Test authentication flows to ensure `getCurrentUser` still works correctly
- Run existing document engine operations to confirm functionality remains intact
- Test obligation seeding with various states to verify improved error handling

### Why make this change?

The changes expand seeding capabilities for core business entities while improving code organization and type safety. The authentication refactoring provides cleaner patterns, and the TODO comments establish a clear roadmap for production security hardening. The error handling improvements prevent potential runtime issues with undefined values in error messages.

## Summary by Sourcery

Implement document access control for generated documents with sensitivity tiers and update seeding, auth, and document engine scaffolding.

New Features:
- Expose new mortgage, obligation, and onboarding request seed modules through the API.
- Introduce document access control for generated documents based on entity relationships and sensitivity tiers.

Bug Fixes:
- Improve type safety of obligation seeding errors by safely stringifying unsupported states.

Enhancements:
- Implement full canAccessDocument logic that resolves generated documents to their parent entities and enforces entity, deal access, and permission checks.
- Refine getCurrentUser to use the authenticated query helper for consistent auth handling.
- Annotate document engine queries and actions with TODOs describing future permission gates for production hardening.
- Simplify the FairLend admin action middleware context by removing an unused runMutation parameter.

Tests:
- Add comprehensive tests covering document access for admins, missing documents, and public, private, and sensitive tiers across mortgages, deals, application packages, and provisional applications.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
  * Implemented document access control with three sensitivity tiers (public, private, sensitive) and role-based permission checks based on user roles and entity relationships.

* **Tests**
  * Expanded document access control test coverage with comprehensive scenarios across multiple entity types, roles, and permission combinations.

* **Chores**
  * Added notes for future permission gate implementations and improved error message formatting.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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.

2 participants