3 - Auto-Gating Functions (The Attribute)
Gating A Function Without Writing The Check
Calling CheckPermission by hand in 400 functions is how you end up with 399 correct checks and one hole that owns the device. The point of the attribute is to declare the requirement once, on the function, and let the system enforce it before the body ever runs.
RequiresPermissionAttribute and PermissionEnforcer now exist in the PermissionHandler project (verified by smoke tests: allow-by-SecureData, allow-by-string, and deny-with-body-not-run). The attribute is still just a declaration — a bare attribute does nothing on its own. PermissionEnforcer is what reads it and runs the check, and the XRUIOS.Manager dispatch is what calls the enforcer. A ManagerStarterStub.cs scaffold is included to show the wiring; replace it with the real Manager.The shape
[RequiresPermission("Edit Camera Footage", Level = 5, RequesterArg = "requestId")]
public async Task TrimClip(string requestId, ClipRange range)
{
// If we got here, the requester is allowed at level >= 5.
// No manual CheckPermission needed.
}
Three things the attribute carries — all compile-time constants, which is the whole trick:
| Field | Meaning | Default |
|---|---|---|
| Permission name | The permission to check. If omitted, the function's own name is used | method name |
Level |
The minimum level required (>=) |
0 |
RequesterArg |
The name of the parameter that carries the requester ID | required |
SecureData values (the key, the requester, the store path) are never put on the attribute. Attribute arguments must be compile-time constants — you physically cannot write a SecureData in [...]. The live values are resolved at call time: the requester from the named parameter, and the store + per-app key from the Manager's context for that app.Where each value actually comes from
- Requester ID → pulled from the method argument named by
RequesterArg. The caller must point at or provide one — the system does not invent an identity. (This is the same "never trust the UI" rule from 1 - Welcome: the ID had to be Manager-issued in the first place.) - Permission name → the attribute's first argument, or the method name if you leave it off. So
"Edit Camera Footage"or justTrimClip. - Level → the attribute's
Level. - Save directory + per-app key → the Manager already knows these for the app the call belongs to; the dispatch layer supplies them. The app code never handles the raw key.
What the Manager does on each gated call
The enforcement lives in PermissionEnforcer. The Manager builds a per-app context once and calls the enforcer at its dispatch boundary:
var enforcer = new PermissionEnforcer(handler);
// Per-app context the Manager owns (it issued the key):
var ctx = new AppPermissionContext(saveDir, permClass, appKey);
// At the dispatch boundary — enforce, then invoke, in one call:
object? result = await enforcer.InvokeGuardedAsync(target, method, arguments, ctx);
// -> throws PermissionDeniedException if the requester isn't allowed; the body never runs.
If you already have your own invoke path, just enforce and invoke yourself:
await enforcer.EnforceAsync(method, arguments, ctx); // throws on denial
var result = method.Invoke(target, arguments);
Internally EnforceAsync:
- reads
[RequiresPermission]off the method (no attribute → nothing to enforce); permName = attr.PermissionName ?? method.Name;- pulls the requester from the argument named
attr.RequesterArg(aSecureDatais used as-is; astringis copied into aSecureDatawithout wiping your original); - calls
handler.CheckPermission(..., attr.Level); - throws
PermissionDeniedExceptionif not allowed.
That's it. The function author writes one attribute; enforcement is uniform and lives in exactly one place.
null requester argument is treated as a denial, not a pass — you cannot authorize nobody. A misconfigured attribute (missing RequesterArg, or naming a parameter that doesn't exist) throws InvalidOperationException loudly rather than silently allowing.How it gets consumed (implementation note)
Because XRUIOS is an orchestration layer, calls into app functions already pass through the Manager's dispatch. That dispatch boundary is where the attribute is read via reflection and enforced — no compile-time weaving required. If you ever call a gated method directly (bypassing dispatch), the attribute is not enforced, so keep gated functions behind the Manager.
See 2 - Security Model and Threat Notes for what this does and doesn't protect against.