fix(bridge): grant with a null owner and read every grant back (D85)

EnsureGrant and EnsureGroupPermission passed `this` as the owner. Given an
owner, Oxide first asks PermissionExists(perm, owner) — whether THAT plugin
registered the name — and returns silently when it did not, so every
site-authored grant of another plugin's permission was a no-op on Oxide that
the report counted as applied (PLAN.md §27.6).

Both now pass null and read the store back. A grant the store does not hold
afterwards is reported in the new `notLanded` list instead of counted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-24 00:37:45 -05:00
parent dcf4e34fee
commit c106ba826b

View File

@@ -2775,6 +2775,13 @@ namespace Oxide.Plugins
public readonly List<object> Foreign = new List<object>();
public readonly List<string> Pending = new List<string>();
/// <summary>
/// Grants the store did not hold after being told to (<c>subject:permission</c>).
/// Reported rather than counted, so the site can never again be told a grant
/// landed that did not (D85).
/// </summary>
public readonly List<string> NotLanded = new List<string>();
/// <summary>
/// Group permissions an event lease holds (protocol 8). Neither applied to nor
/// reported foreign; what the site asked for is kept on the hold instead.
@@ -3138,12 +3145,22 @@ namespace Oxide.Plugins
if (permission.GroupHasPermission(op.Subject, op.Object))
{
job.AlreadyCorrect++;
break;
}
else
{
permission.GrantGroupPermission(op.Subject, op.Object, this);
// **`null`, not `this`, as the owner (D85).** With an owner, Oxide's grant
// first asks `PermissionExists(perm, owner)` — whether THAT plugin registered
// the name — and returns without a word when it did not. Every permission the
// site grants belongs to some other plugin, so from phase 7 until phase 13 this
// call did nothing on Oxide and was counted as applied (PLAN.md §27.6). The
// read-back is what makes the count true: only a grant the store now holds
// is reported as one.
permission.GrantGroupPermission(op.Subject, op.Object, null);
if (permission.GroupHasPermission(op.Subject, op.Object))
job.GroupPermissions++;
}
else
job.NotLanded.Add(op.Subject + ":" + op.Object);
break;
@@ -3173,12 +3190,16 @@ namespace Oxide.Plugins
if (HoldsDirectly(op.Subject, op.Object))
{
job.AlreadyCorrect++;
break;
}
else
{
permission.GrantUserPermission(op.Subject, op.Object, this);
// The same owner trap as the group grant above (D85), and the same read-back.
permission.GrantUserPermission(op.Subject, op.Object, null);
if (HoldsDirectly(op.Subject, op.Object))
job.Grants++;
}
else
job.NotLanded.Add(op.Subject + ":" + op.Object);
break;
@@ -3478,6 +3499,7 @@ namespace Oxide.Plugins
report["foreign"] = job.Foreign;
report["leased"] = job.Leased;
report["pending"] = job.Pending;
report["notLanded"] = job.NotLanded;
report["operations"] = job.Ops.Count;
if (!string.IsNullOrEmpty(job.SetId))
@@ -3490,7 +3512,7 @@ namespace Oxide.Plugins
$"ops={job.Ops.Count} grants={job.Grants} revokes={job.Revokes} " +
$"groups={job.GroupsCreated} members={job.Members} same={job.AlreadyCorrect} " +
$"unresolved={job.Unresolved.Count} foreign={job.Foreign.Count} " +
$"pending={job.Pending.Count} at={DateTime.UtcNow:HH:mm:ss}Z";
$"pending={job.Pending.Count} notLanded={job.NotLanded.Count} at={DateTime.UtcNow:HH:mm:ss}Z";
EmitFrame(report);
}