mirror of
https://github.com/ruvnet/RuView
synced 2026-08-01 19:01:42 +00:00
docs(adr): correct a false claim about browser scope; pin the decision in code
A cross-vendor pre-merge sweep found no merge blockers, but did surface an
error in ADR-271 that I wrote — and a coherence gap behind it.
ADR-271 P2 stated that capping the browser session at `sensing:read` was
"considered and rejected, because the dashboard genuinely performs admin
operations". That is wrong. `/oauth/start` (main.rs:9206) already requests
`SENSING_READ` and nothing else, deliberately, with a comment saying so. The
browser session is ALREADY read-only, so the breakage I claimed capping would
cause is simply the current behaviour.
Two consequences, now stated in the ADR instead of left to be discovered:
1. The UI's admin controls do not work from a browser OAuth session.
`model.service.js:136` issues DELETE /api/v1/models/{id}, which 401s. Admin
work needs the CLI (`login --admin`) or a pasted admin bearer. A gap in a new
feature, not a regression — the token-paste path is unchanged.
2. The ADMIN_REVERIFY_SECS step-up control added in the previous commit guards
a case that cannot currently arise. No browser session holds `sensing:admin`,
so the freshness branch never fires in production. Its tests pass because the
crate-internal seam mints an admin cookie the real flow never produces.
That second point is worth being blunt about: it is the same shape as several
defects this branch already fixed — correct code, green tests, unreachable call
site. The difference is that here the guard is deliberately ahead of the need
rather than mistakenly behind it, and saying which one it is matters.
So the constant is now named `BROWSER_SIGNIN_SCOPE` rather than inlined, with
the cost of widening it documented at the definition, and two tests:
`browser_sign_in_stays_read_only_until_someone_decides_otherwise` pins the
value, and `the_authorize_url_actually_carries_that_scope` proves it reaches the
wire — asserting on the constant alone would pass even if `begin` were called
with something else, which is exactly the isolation failure being guarded
against.
The ADR also records the coherent way to add browser-side admin if wanted:
escalate-on-demand via the RFC 6750 challenge, keeping least privilege by
default rather than asking every user to consent to delete capability to watch
a stream. Not bundled here — it needs a scope parameter and a UI affordance.
Sweep verdict: no merge blockers. Two other non-blocking risks it raised are
accurate and unchanged: concurrent JWKS refresh at the stale boundary is not
atomic (a duplicated idempotent GET, already documented as an accepted cost),
and the service worker's SHELL_ASSETS use root-relative paths while the UI
mounts under /ui, so offline shell precaching is incomplete — pre-existing,
unrelated to this branch.
Verified: workspace 176 suites clean.
Co-Authored-By: Ruflo & AQE
This commit is contained in:
@@ -81,6 +81,27 @@ pub const SESSION_TTL_SECS: i64 = 3600;
|
||||
/// fresh `auth_time` and, if their Cognitum session is live, no prompt.
|
||||
pub const ADMIN_REVERIFY_SECS: i64 = 300;
|
||||
|
||||
/// The scope `/oauth/start` requests. Read-only, deliberately.
|
||||
///
|
||||
/// Named rather than inlined because it is a decision, not a detail, and it has
|
||||
/// two consequences that are easy to widen by accident:
|
||||
///
|
||||
/// 1. **The UI's admin controls do not work from a browser session.**
|
||||
/// `model.service.js` issues `DELETE /api/v1/models/{id}`; from a
|
||||
/// Cognitum-signed-in browser that is a 401. Admin work goes through the CLI
|
||||
/// (`wifi-densepose login --admin`) or a pasted admin bearer.
|
||||
/// 2. **[`ADMIN_REVERIFY_SECS`] therefore guards a case that cannot yet arise.**
|
||||
/// No browser session holds `sensing:admin`, so the freshness branch never
|
||||
/// fires in production today. It becomes load-bearing the instant this
|
||||
/// constant grows, which is the right ordering — but do not mistake its
|
||||
/// passing tests for evidence that the control is exercised.
|
||||
///
|
||||
/// Widening this to include `sensing:admin` would make every browser sign-in
|
||||
/// consent to delete capability just to watch a stream. The coherent way to add
|
||||
/// browser-side admin is escalate-on-demand: keep this read-only and let the
|
||||
/// RFC 6750 challenge send the user back through `/oauth/start` asking for more.
|
||||
pub const BROWSER_SIGNIN_SCOPE: &str = ruview_auth::scope::SENSING_READ;
|
||||
|
||||
fn now() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
@@ -785,6 +806,31 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_sign_in_stays_read_only_until_someone_decides_otherwise() {
|
||||
// Pins the decision documented on BROWSER_SIGNIN_SCOPE. Widening it is
|
||||
// legitimate, but it must be a choice: it makes every browser sign-in
|
||||
// consent to delete capability, and it activates the ADMIN_REVERIFY_SECS
|
||||
// branch that is currently unreachable in production.
|
||||
assert_eq!(BROWSER_SIGNIN_SCOPE, ruview_auth::scope::SENSING_READ);
|
||||
assert!(
|
||||
!BROWSER_SIGNIN_SCOPE.split_whitespace().any(|s| s == ruview_auth::scope::SENSING_ADMIN),
|
||||
"browser sign-in must not silently request admin: {BROWSER_SIGNIN_SCOPE}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_authorize_url_actually_carries_that_scope() {
|
||||
// The constant is only worth pinning if it reaches the wire. Asserting
|
||||
// on the constant alone would pass even if `begin` were called with
|
||||
// something else — the same "tested in isolation, call site untested"
|
||||
// shape that produced several defects in this branch.
|
||||
init_secret_for_tests();
|
||||
let (url, _) = begin("https://a.example", "ruview", BROWSER_SIGNIN_SCOPE, false).unwrap();
|
||||
assert!(url.contains("scope=sensing%3Aread"), "{url}");
|
||||
assert!(!url.contains("sensing%3Aadmin"), "{url}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clearing_cookies_expires_them_immediately() {
|
||||
for c in [clear_session(false), clear_transaction(false)] {
|
||||
|
||||
@@ -9202,8 +9202,9 @@ async fn oauth_start(
|
||||
};
|
||||
let secure = request_is_tls(&headers);
|
||||
// Least privilege: a browser session asks for read. Admin work goes through
|
||||
// the CLI, which requires an explicit --admin.
|
||||
match bs::begin(&issuer, &auth.primary_client_id(), ruview_auth::scope::SENSING_READ, secure) {
|
||||
// the CLI, which requires an explicit --admin. See BROWSER_SIGNIN_SCOPE for
|
||||
// what widening this would cost.
|
||||
match bs::begin(&issuer, &auth.primary_client_id(), bs::BROWSER_SIGNIN_SCOPE, secure) {
|
||||
Ok((location, cookie)) => (
|
||||
axum::http::StatusCode::FOUND,
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user