# Admin can view but not modify tenant data without an audit trail

- **Pattern:** `ab-002352` (`saas-multi-tenancy.tenant-management.admin-view-no-modify-without-trail`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002352
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002352)

## Why it matters

Platform admin routes that can write tenant data without logging are invisible to both the tenant and to compliance auditors. CWE-778 (Insufficient Logging) and OWASP A09 (Security Logging and Monitoring Failures) both apply; SOC 2 CC6.8 explicitly requires that privileged actions be logged with who, what, and when. The practical risk is that an insider threat — a rogue admin or compromised admin account — can modify or delete tenant data with no forensic trail, making incident response and regulatory defense impossible.

## Severity rationale

Low because admin routes are access-controlled, but an unlogged admin write operation eliminates any forensic trail for insider-threat scenarios or compliance audits.

## Remediation

Wrap every admin write route in a logging function that captures the actor, action, affected tenant, and resource before returning:

```ts
// src/lib/admin-audit.ts
async function adminUpdateUser(
  adminId: string,
  targetUserId: string,
  changes: Partial<User>
) {
  const updated = await db.user.update({
    where: { id: targetUserId },
    data: changes
  })
  await db.adminAuditLog.create({
    data: {
      adminUserId: adminId,
      action: 'user.update',
      targetUserId,
      targetOrganizationId: updated.organizationId,
      changes: JSON.stringify(changes), // redact sensitive fields like passwords
      timestamp: new Date()
    }
  })
  return updated
}
```

In `src/app/api/admin/`, require this wrapper pattern for every `POST`, `PATCH`, `PUT`, and `DELETE` handler. Read-only `GET` handlers do not require the same trail but should still be access-controlled.

## Detection

- **ID:** `admin-view-no-modify-without-trail`
- **Severity:** `low`
- **What to look for:** Examine admin or super-admin routes — routes accessible only to platform administrators (not tenant admins). Check whether admin actions that modify tenant data (editing records, overriding settings, impersonating users, deleting data on behalf of a tenant) are logged with sufficient detail to reconstruct what was done and by whom. Look for admin route handlers that perform write operations without any audit log creation.
- **Pass criteria:** Enumerate all admin endpoints. At least 100% of admin write operations against tenant data must create an audit log entry that includes: the admin's user ID, the action taken, the tenant affected, the resource modified, and a timestamp. Admin view-only access (read operations) does not require the same logging, but read access to sensitive tenant data should at minimum be access-controlled.
- **Fail criteria:** Admin write operations on tenant data execute without creating any audit trail. An admin can modify a tenant's data and there is no record of the action in any log or table.
- **Skip (N/A) when:** No admin or super-admin routes are detected. Signal: no admin route group, no admin role check in any route handler, no "impersonate" or "act as tenant" functionality.
- **Detail on fail:** Example: `"POST /api/admin/users/[id] can update any user's data but writes no audit log entry. No record is kept of admin modifications to tenant user accounts."`
- **Remediation:** Wrap admin write operations with audit logging:

  ```ts
  async function adminUpdateUser(adminId: string, targetUserId: string, changes: Partial<User>) {
    const updated = await db.user.update({
      where: { id: targetUserId },
      data: changes
    })
    // Always log admin mutations
    await db.adminAuditLog.create({
      data: {
        adminUserId: adminId,
        action: 'user.update',
        targetUserId,
        targetOrganizationId: updated.organizationId,
        changes: JSON.stringify(changes), // consider redacting sensitive fields
        timestamp: new Date()
      }
    })
    return updated
  }
  ```

---

## External references

- cwe CWE-778
- owasp A09
- nist AU-2
- soc2 CC6.8

Taxons: observability, access-control

HTML version: https://auditbuffet.com/patterns/ab-002352
