{"id":"access-policy","title":"Extend a permission engine","summary":"Tenant isolation, deny rules, inherited roles.","phases":["Repair access decisions. Input: {roles:[{id,parents:[],rules:[\n{effect:'allow'|'deny',tenant,resource,action}]}], users:[{id,roles:[id]}],\nqueries:[{user,tenant,resource,action,time:integer}]}. IDs are unique ASCII\nidentifiers. All assigned roles exist, but a query user may not exist. Return\nan array of 'allow'/'deny', one per query. A rule matches only if tenant is EXACT\nand resource and action each equal the query or are the whole-string wildcard\n'*'. No substring or prefix matching. Any matching deny overrides ALL matching\nallows. Otherwise allow iff at least one allow matches. Unknown users and no\nmatching rules deny. Role assignment order and duplicates do not matter.\nPhase one has no inheritance or expiry. Do not mutate input.","Extend the engine. Roles may inherit parents (all IDs exist,\nduplicates allowed). Collect all rules transitively, sharing ancestors safely.\nIf ANY cycle is reachable from a user's assigned roles, deny every query for\nthat user, even when a direct allow matches. Ignore unrelated cycles. Rules\ncan have until:integer: they match only when query.time < until. Missing until\nmeans no expiry. Expired deny rules do not override active allows. Tenants\nstill match exactly, including a literal '*' tenant. All phase-one rules remain\nrequired. Avoid enumerating paths in shared inheritance graphs."],"prompts":["Work in this repository. Implement synchronous CommonJS JavaScript.\nThe entry point is src/index.js, exporting { solve }. solve(input) must return a\nJSON-serializable value. Use only relative .js modules within src/. No packages,\nfilesystem, network, timers, process, eval, or async in submitted source.\nSubmit at most 24 .js files totaling at most 128 KiB.\nYou may inspect files, edit code, add tests, and run node --test. Change src/ and\ntest/ only. Do not change the existing test/smoke.test.js. Each hidden case runs\nin a fresh QuickJS context with 32 MiB memory and 500 ms CPU time. Object key\norder does not matter; array order does. Numbers are safe integers. All inputs\nsatisfy the stated contract. No Internet or external tools are needed.\nFinish by describing the change briefly. The code on disk is the submission.\n\nWORK ORDER 1\nRepair access decisions. Input: {roles:[{id,parents:[],rules:[\n{effect:'allow'|'deny',tenant,resource,action}]}], users:[{id,roles:[id]}],\nqueries:[{user,tenant,resource,action,time:integer}]}. IDs are unique ASCII\nidentifiers. All assigned roles exist, but a query user may not exist. Return\nan array of 'allow'/'deny', one per query. A rule matches only if tenant is EXACT\nand resource and action each equal the query or are the whole-string wildcard\n'*'. No substring or prefix matching. Any matching deny overrides ALL matching\nallows. Otherwise allow iff at least one allow matches. Unknown users and no\nmatching rules deny. Role assignment order and duplicates do not matter.\nPhase one has no inheritance or expiry. Do not mutate input.","Work in this repository. Implement synchronous CommonJS JavaScript.\nThe entry point is src/index.js, exporting { solve }. solve(input) must return a\nJSON-serializable value. Use only relative .js modules within src/. No packages,\nfilesystem, network, timers, process, eval, or async in submitted source.\nSubmit at most 24 .js files totaling at most 128 KiB.\nYou may inspect files, edit code, add tests, and run node --test. Change src/ and\ntest/ only. Do not change the existing test/smoke.test.js. Each hidden case runs\nin a fresh QuickJS context with 32 MiB memory and 500 ms CPU time. Object key\norder does not matter; array order does. Numbers are safe integers. All inputs\nsatisfy the stated contract. No Internet or external tools are needed.\nFinish by describing the change briefly. The code on disk is the submission.\n\nWORK ORDER 2\nRepair access decisions. Input: {roles:[{id,parents:[],rules:[\n{effect:'allow'|'deny',tenant,resource,action}]}], users:[{id,roles:[id]}],\nqueries:[{user,tenant,resource,action,time:integer}]}. IDs are unique ASCII\nidentifiers. All assigned roles exist, but a query user may not exist. Return\nan array of 'allow'/'deny', one per query. A rule matches only if tenant is EXACT\nand resource and action each equal the query or are the whole-string wildcard\n'*'. No substring or prefix matching. Any matching deny overrides ALL matching\nallows. Otherwise allow iff at least one allow matches. Unknown users and no\nmatching rules deny. Role assignment order and duplicates do not matter.\nPhase one has no inheritance or expiry. Do not mutate input.\n\nExtend the engine. Roles may inherit parents (all IDs exist,\nduplicates allowed). Collect all rules transitively, sharing ancestors safely.\nIf ANY cycle is reachable from a user's assigned roles, deny every query for\nthat user, even when a direct allow matches. Ignore unrelated cycles. Rules\ncan have until:integer: they match only when query.time < until. Missing until\nmeans no expiry. Expired deny rules do not override active allows. Tenants\nstill match exactly, including a literal '*' tenant. All phase-one rules remain\nrequired. Avoid enumerating paths in shared inheritance graphs."],"starter":{"src/index.js":"module.exports = require(\"./engine.js\");\n","src/engine.js":"// Rules are currently evaluated in assignment order.\nfunction solve(input) {\n  return input.queries.map(q=>{\n    const user=input.users.find(u=>u.id===q.user);if(!user)return 'deny';\n    for(const id of user.roles){\n      const role=input.roles.find(r=>r.id===id);\n      for(const r of role.rules){\n        if(r.tenant===q.tenant && (r.resource==='*'||r.resource===q.resource) &&\n          (r.action==='*'||r.action===q.action))return r.effect;\n      }\n    }\n    return 'deny';\n  });\n}\nmodule.exports={solve};\n"}}