---
name: strother-rbac
description: >
  Authoritative RBAC rules for the Strother Fabrication estimating app. ALWAYS load this skill
  before writing, editing, or reviewing ANY code that touches role checks, API resources,
  route middleware, controller authorization, or data returned to the frontend. Also load when
  adding new fields to models, new API endpoints, new Vue pages, or new audit/logging features —
  any of these can introduce a leak path. Do not rely on UI-level hiding to enforce access
  control. This skill defines the enforcement contract.
---

# Strother RBAC — Authoritative Rules

## 1. The Three Roles

| Role | Users | Access level |
|---|---|---|
| `admin` | Tyler, Chris | Everything — all costs, margins, overhead, grand totals, audit trail, settings |
| `estimator` | Ryan, future hires | Material costs, labor hours, equipment costs, Total Cost only |
| `office` | Tyler's wife | Job metadata, status, documents, proposal status — read-only on financials |

Role is stored on `users.role` (VARCHAR with CHECK constraint — not MySQL ENUM).

## 2. The Core Contract

> **Margin and overhead must be stripped at the API resource layer.
> UI-level hiding is not enforcement. It is decoration.**

If an estimator can obtain overhead_rate, margin_rate, or grand_total by:
- Changing a URL parameter
- Inspecting a network request
- Using a sort or filter parameter
- Reading an audit log note
- Any other means

...that is a security failure, not a UI gap.

## 3. What Each Role Sees — Field-Level Rules

### Fields NEVER returned to `estimator` or `office` roles

Strip these at the Eloquent API Resource layer before the response leaves the server:

- `overhead_rate` (scope)
- `margin_rate` (scope)
- `material_overhead_rate` (scope)
- `grand_total` (scope — formula-derived)
- `manual_sell_price` (scope)
- `customer_facing_total` (scope)
- `tax_amount` (scope)
- `default_overhead_rate` (system_settings)
- `default_margin_rate` (system_settings)
- Any pricing snapshot fields that contain the above

### Fields visible to `estimator`

- `material_total` (scope)
- `equipment_total` (scope)
- `labor_total` (scope)
- `total_cost` (material + equipment + labor — this is Ryan's deliverable)
- All line-item data: scope_materials, scope_equipment, scope_labor, scope_labor_tasks
- All job metadata fields
- Scope name, estimate_type, status, notes (internal-flagged notes visible, include_in_proposal toggle visible)

### Fields visible to `office`

- Job metadata: customer, job name, address, dates, status, assigned estimator
- Document list (name, type, upload date)
- Proposal status (draft/sent/approved) — NOT the dollar amounts
- No cost data of any kind

## 4. Laravel Enforcement Pattern

### Route middleware

```php
// routes/web.php
Route::middleware(['auth', 'role:admin'])->group(function () {
    // Pricing panel, system settings, tax rates, terms
});

Route::middleware(['auth', 'role:admin,estimator'])->group(function () {
    // Estimate builder, scope editing, materials/labor/equipment
});

Route::middleware(['auth'])->group(function () {
    // Job list, job detail (role-scoped response via Resource)
    // Office can reach here; Resource strips financials
});
```

### API Resource stripping pattern

```php
// app/Http/Resources/ScopeResource.php
public function toArray(Request $request): array
{
    $data = [
        'id'             => $this->id,
        'name'           => $this->name,
        'estimate_type'  => $this->estimate_type,
        'status'         => $this->status,
        'material_total' => $this->material_total,
        'equipment_total'=> $this->equipment_total,
        'labor_total'    => $this->labor_total,
        'total_cost'     => $this->total_cost,
        // ... non-sensitive fields
    ];

    // Admin-only fields — stripped for all other roles
    if ($request->user()->role === 'admin') {
        $data['overhead_rate']          = $this->overhead_rate;
        $data['margin_rate']            = $this->margin_rate;
        $data['material_overhead_rate'] = $this->material_overhead_rate;
        $data['grand_total']            = $this->grand_total;
        $data['manual_sell_price']      = $this->manual_sell_price;
        $data['customer_facing_total']  = $this->customer_facing_total;
    }

    return $data;
}
```

**Never use `when()` with a client-side flag to toggle sensitive fields.**
**Always use `$request->user()->role === 'admin'` server-side.**

## 5. Known Leak Paths — Check Every One Before Shipping

These are confirmed attack surfaces. Review each when adding related features:

### L1 — Filter/Sort Oracle
If the API allows `?sort=grand_total` or `?filter[grand_total][gt]=50000`, an estimator
can binary-search to derive the grand total without seeing it directly.
**Fix:** Whitelist sortable/filterable fields per role. Reject admin-only fields in query params for non-admins.

### L2 — Audit Log Free-Text
If Tyler writes "raised margin from 25% to 30% because labor was underpriced" in an audit
note, and estimators can read audit notes, the margin is leaked.
**Fix:** Audit notes on admin-only fields are visible to admin only. The `audit_log` viewer
must filter by field sensitivity, not just by user.

### L3 — Settings Deny-by-Default
`system_settings` contains `default_overhead_rate` and `default_margin_rate`.
Any endpoint that returns system_settings must strip those keys for non-admins.
**Fix:** SystemSettingResource applies the same role-strip pattern.

### L4 — Pricing Snapshot Fields
`proposals` and any snapshot table contain grand_total, margin_rate, etc.
Proposal endpoints used by estimator (e.g., to see proposal status) must not return
snapshot pricing fields.
**Fix:** ProposalResource returns only status, version, sent_at for non-admins.

### L5 — `proposal_sends` Pre-Ruling
Sending a proposal requires knowing the grand total is finalized. The send action
itself must be admin-only — an estimator should never be able to trigger a send.
**Fix:** `proposals.send` route is inside the `role:admin` middleware group.

## 6. Office Capabilities (P1 Sprint 1)

Office CAN:
- Create and edit job records (cover sheet fields)
- Upload documents to a job
- View job status and assigned estimator
- View proposal status (sent/not sent, version number)

Office CANNOT:
- See any cost, total, rate, or pricing field
- Edit scope data
- Approve estimates
- Send proposals
- Access system settings

## 7. Admin-Only Features (enforce at route level)

These routes must be inside `middleware(['auth', 'role:admin'])`:

- `/settings` — system settings CRUD
- `/tax-rates` — tax rate management
- `/terms` — terms and conditions management
- `/jobs/{job}/pricing` — back-office pricing panel
- `/jobs/{job}/scopes/{scope}/pricing` — per-scope overhead/margin
- `/proposals/{proposal}/approve` — approval action
- `/proposals/{proposal}/send` — send action
- `/users` — user management (create, edit, deactivate)
- `/audit-log` — full audit trail viewer

## 8. Estimator Scope Restrictions

Estimators see only jobs where `assigned_estimator = auth()->id()` by default.
Admin sees all jobs.

```php
// JobController@index
$query = Job::query();
if (auth()->user()->role === 'estimator') {
    $query->where('assigned_estimator', auth()->id());
}
```

This filter uses `assigned_estimator` (the correct column name on `jobs_estimating`).
Do NOT use `estimator_id` — that column does not exist.

## 9. What This Skill Overrides

If you see UI-only role checks (e.g., `v-if="user.role === 'admin'"` on a price field
with no server-side strip), flag it as a security gap. The UI check can stay for UX
purposes but the server-side strip is mandatory and non-negotiable.
