Skip to content

Consult the host's mail policy on preview and attachment routes - #87

Open
MACscr wants to merge 2 commits into
backstagephp:mainfrom
MACscr:per-record-mail-authorization
Open

MACscr wants to merge 2 commits into
backstagephp:mainfrom
MACscr:per-record-mail-authorization

Conversation

@MACscr

@MACscr MACscr commented Aug 26, 2026 •

Copy link
Copy Markdown

Problem

#84 / #86 put the preview and attachment routes behind Authenticate and canManageMails(). That answers "may this user use the mail log at all?", which is the same question every Resource, Page and Widget asks — but the controllers still resolve the mail by id and serve it:

$mail = $mailModel::findOrFail($request->route('mail'));

return response($mail->html, …);

Hosts that need a second answer — "may this user see this mail?" — have no hook. In a multi-tenant application every user who may open the mail log can read any tenant's mail, and download its attachments, by changing the id in the URL. Wrapping the routes in extra middleware from the host is possible but means re-declaring Mails::routes() and its middleware by hand, which is exactly the class of drift #84 was written to prevent.

Most applications are not multi-tenant and do not need that second answer.

Fix

When the host has registered a policy for the mail model, both controllers authorize the view ability for the authenticated user against the requested mail before serving it:

if (Gate::getPolicyFor($mail) !== null) {
    Gate::authorize('view', $mail);
}
  • Hosts without a policy are unaffected — Gate::getPolicyFor() returns null and nothing changes.
  • Hosts with a policy get the same per-record rule on the raw routes that they already apply in their own resources. The policy receives the authenticated user and that mail.
  • canManageMails() keeps its role as the coarse gate; this runs after it, inside the same middleware stack.

The README section on canManageMails() now documents the policy hook with a tenant example, and states that applications which never register a policy are unchanged.

Tests

Two cases in tests/MailRouteSecurityTest.php using an AllowSpecificUserPolicy fixture. The same mail is forbidden for one user and served for another, on both the preview and an attachment download. Hosts that do not register a policy are covered by the existing route tests, which still pass with no policy registered.

canManageMails() answers whether a user may use the mail log at all. Hosts
that need a per-mail answer as well - a multi-tenant application where a
user may only open mails belonging to their own tenant - have no hook: both
controllers resolve the mail by id and serve it.

When the host has registered a policy for the mail model, authorize the
view ability against the requested mail before serving the preview or an
attachment. Hosts without a policy are unaffected.
@Casmo

Casmo commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

What is the DenyOddMailPolicy class testing exactly? There is no Gate check for the $user.

The even/odd fixture never looked at the user, so it did not show that a host policy can allow one person and deny another for the same mail. Apps that never register a policy stay unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
@MACscr

MACscr commented Sep 25, 2026

Copy link
Copy Markdown
Author

The even/odd fixture was only a deterministic allow/deny, so it never looked at $user. That made it look like the routes were not actually authorizing the authenticated user.

Most apps are not multi-tenant and never register a mail policy. For those, canManageMails() is still the only check and these routes are unchanged. A host that does need a per-mail rule registers a policy, and view then receives that user and that mail.

The fixture is now AllowSpecificUserPolicy: the same mail is forbidden for one user and served for another, on both the preview and the attachment download.

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