fix(cli): M3 re-check: provider denials answer 403; A2 test owner carries rootOwner (#1873)
The rehearsal provider matches routes on the parsed pathname, so a query string no longer turns authorize or token into a 404. Authorize and token answer 403 to any method before the GET check, and only discovery and the JWKS answer 405 to another method. R20 now sends a POST to token and a GET to authorize with a query, and expects 403 for both. The upstream A2 agent-levels write test stubbed an owner session without rootOwner. M3 sets the hook's owner flag from rootOwner, as sso.mjs grants it, so the stub now carries rootOwner: true, as system-types-http does. No check changes.
This commit was merged in pull request #1918.
This commit is contained in:
1 parent
9acc378555
commit
d0cfebe286
4 files changed
+17
-10
No files matched your search
+2
-1
@@ -827,7 +827,8 @@ Before that, a stamped run starts a rehearsal provider and prints
|
||||
HTTPS with a certificate made for this run, under `provider/` (mode 0700). For
|
||||
each live provider id it serves discovery and a JWKS whose public key it made
|
||||
in memory for the first algorithm it can make from that provider's list. Its
|
||||
authorize and token routes answer 403: it issues no code and no token, and
|
||||
authorize and token routes answer 403 to any method and query: it issues no
|
||||
code and no token, and
|
||||
it reads no live secret. It logs each request to `provider/requests.log`,
|
||||
deletes its TLS key file once it has read it, and exits on SIGTERM or when
|
||||
the rehearsal's process is gone. `provider-start` fails when it exits early,
|
||||
|
||||
@@ -2148,7 +2148,8 @@ function rHost(){
|
||||
// M3 (#1873): under --sso it runs its copy's real createSso on the scratch console.sso, with the
|
||||
// scratch slot placeholders, as the real Console does, and /healthz carries sso.health(). Faults:
|
||||
// console-hold waits for $HOME/release before it starts; console-no-sso reports every provider
|
||||
// ready without asking any; console-asks-token asks provider a's token endpoint once.
|
||||
// ready without asking any; console-asks-token POSTs to provider a's token endpoint, then asks its
|
||||
// authorize endpoint with a query, once each.
|
||||
async function rConsole(){
|
||||
const fs=process.getBuiltinModule('node:fs'),path=process.getBuiltinModule('node:path'),http=process.getBuiltinModule('node:http');
|
||||
const id=path.basename(path.resolve(import.meta.dirname,'..','..','..')),home=process.env.HOME,a=process.argv.slice(2);
|
||||
@@ -2168,7 +2169,10 @@ async function rConsole(){
|
||||
let sso;try{sso=await createSso(c.console.sso,{secrets,log:()=>{}})}catch(err){console.error(`fixture Console: ${err.message}`);process.exit(2)}
|
||||
health=()=>sso.health();
|
||||
}
|
||||
if(fault('console-asks-token'))await fetch(`${ps[0].issuer}token`).catch(()=>{});
|
||||
if(fault('console-asks-token')){
|
||||
await fetch(`${ps[0].issuer}token`,{method:'POST',headers:{'content-type':'application/x-www-form-urlencoded'},body:'grant_type=authorization_code&code=x'}).catch(()=>{});
|
||||
await fetch(`${ps[0].issuer}authorize?client_id=rehearsal&response_type=code`).catch(()=>{});
|
||||
}
|
||||
}
|
||||
const s=http.createServer((q,r)=>{
|
||||
const ok=q.url==='/healthz'&&!fault(`healthz-${id}`);r.writeHead(ok?200:500,{'content-type':'application/json'});
|
||||
@@ -2728,7 +2732,7 @@ test('M3 R20 the rehearsal provider\'s failure controls: no start, no trust, no
|
||||
// passes and provider-discovery fails.
|
||||
for(const [f,why] of [
|
||||
['console-no-sso','no GET /a/.well-known/openid-configuration 200; no GET /a/jwks 200'],
|
||||
['console-asks-token','a request for GET /a/token 403'],
|
||||
['console-asks-token','a request for POST /a/token 403; a request for GET /a/authorize?client_id=rehearsal&response_type=code 403'],
|
||||
]){
|
||||
const e=await smk(t);rfault(e,f);
|
||||
const r=await rehearse(e,B,'--backup',e.act,'--port',await freePort(),'--timeout','10');
|
||||
|
||||
@@ -23,7 +23,8 @@ async function fixture(t, { send = true, granted = false } = {}) {
|
||||
const rolesDir = rolesCopy(root);
|
||||
const file = writeJson(join(root, 'businesses/demo.json'), businessDoc(root, 'demo'));
|
||||
const trail = [];
|
||||
const session = { provider: 'fixture', iss: 'https://issuer.example', sub: 'owner-sub', name: 'jason' };
|
||||
// M3 (#1873): the hook fence's owner flag is the session's root authority (rootOwner), as sso.mjs sets it.
|
||||
const session = { provider: 'fixture', iss: 'https://issuer.example', sub: 'owner-sub', name: 'jason', rootOwner: true };
|
||||
const sso = { publicOrigin: '', publicHost: '', formTargets: () => [], session: () => session, handle: () => {}, health: () => ({}) };
|
||||
const organizations = {
|
||||
list: async () => [{ id: 'demo', label: 'Demo', busHealthy: true }],
|
||||
|
||||
@@ -2104,8 +2104,8 @@ process.stdout.write(JSON.stringify(c,null,2)+"\n");'
|
||||
# read into memory and its file removed before the server listens; cert.pem stays for the scratch
|
||||
# Console's NODE_EXTRA_CA_CERTS. Per configured provider, at /<id>/, it serves discovery and a JWKS
|
||||
# holding one public key made in memory for the first of the provider's algorithms it can make.
|
||||
# Authorization, token and every other route answer 403 or 404: it issues no code, no token and no
|
||||
# sign-in. Each request is a line in requests.log. It writes origin when listening and exits when
|
||||
# Authorization and token answer 403 to any method and query; another method on discovery or the
|
||||
# JWKS is 405 and any other route 404: it issues no code, no token and no sign-in. Each request is a line in requests.log. It writes origin when listening and exits when
|
||||
# PARENT (the rehearsal) is gone, on SIGTERM, or on an error.
|
||||
R_PROVIDER_JS='
|
||||
const fs=require("node:fs"),path=require("node:path"),https=require("node:https"),crypto=require("node:crypto"),{spawnSync}=require("node:child_process");
|
||||
@@ -2136,13 +2136,14 @@ const log=(q,status)=>fs.appendFileSync(path.join(dir,"requests.log"),`${q.metho
|
||||
let origin;
|
||||
const server=https.createServer({key,cert,requestTimeout:5000,headersTimeout:5000},(q,res)=>{
|
||||
const send=(status,body)=>{log(q,status);res.writeHead(status,{"content-type":"application/json","cache-control":"no-store"});res.end(JSON.stringify(body))};
|
||||
if(q.method!=="GET")return send(405,{error:"GET only"});
|
||||
const m=/^\/([^/?#]+)\/(\.well-known\/openid-configuration|jwks|authorize|token)$/.exec(q.url);
|
||||
let at;try{at=new URL(q.url,origin).pathname}catch{at=""}
|
||||
const m=/^\/([^/]+)\/(\.well-known\/openid-configuration|jwks|authorize|token)$/.exec(at);
|
||||
const jwk=m?providers.get(m[1]):undefined;
|
||||
if(!jwk)return send(404,{error:"not found"});
|
||||
if(m[2]==="authorize"||m[2]==="token")return send(403,{error:"the rehearsal provider issues no code and no token"});
|
||||
if(q.method!=="GET")return send(405,{error:"GET only"});
|
||||
const issuer=`${origin}/${m[1]}/`;
|
||||
if(m[2]==="jwks")return send(200,{keys:[jwk]});
|
||||
if(m[2]!==".well-known/openid-configuration")return send(403,{error:"the rehearsal provider issues no code and no token"});
|
||||
send(200,{issuer,authorization_endpoint:`${issuer}authorize`,token_endpoint:`${issuer}token`,jwks_uri:`${issuer}jwks`,response_types_supported:["code"],subject_types_supported:["public"],id_token_signing_alg_values_supported:[jwk.alg],code_challenge_methods_supported:["S256"]});
|
||||
});
|
||||
server.on("error",(e)=>die(e.message));
|
||||
|
||||
Reference in new issue
Block a user