<!doctype html>
<html lang="en">
<head><base href="about:srcdoc"><script>(function(){function n(){parent.postMessage({t:'share:hash',h:location.hash},'*')}addEventListener('hashchange',n);addEventListener('message',function(e){if(e.source!==parent)return;var d=e.data||{};if(d.t==='share:setHash'&&typeof d.h==='string'&&d.h!==location.hash){location.hash=d.h}});addEventListener('DOMContentLoaded',function(){parent.postMessage({t:'share:ready',h:location.hash},'*')});})();</script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Why John sees what he sees in DS — Technical Brief</title>
<meta name="description" content="An access-control investigation around john.mwendwa@investkenya.go.ke, traced through Keycloak, BPA, Camunda and DS." />

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,300..700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />

<style>
  :root {
    --paper:        #F7F1E2;
    --paper-bright: #FBF6E8;
    --ink:          #1E1A14;
    --ink-soft:     #3C3528;
    --ink-light:    #6F6552;
    --rule:         #D4C9A8;
    --rule-strong:  #A89A75;
    --accent:       #8C2E25;
    --accent-deep:  #5E1F18;
    --green:        #4F5C36;

    --maxw:         78ch;
    --gutter:       clamp(1.25rem, 3vw, 2.5rem);
  }

  *, *::before, *::after { box-sizing: border-box; }
  html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
  body { margin: 0; }
  table { border-collapse: collapse; }
  section { scroll-margin-top: 2rem; }

  body {
    background: var(--paper);
    color: var(--ink);
    font-family: 'Source Serif 4', Georgia, 'Times New Roman', serif;
    font-size: 18.5px;
    line-height: 1.62;
    font-weight: 400;
    text-rendering: optimizeLegibility;
    -webkit-font-smoothing: antialiased;
    overflow-x: hidden;
  }

  /* Subtle paper grain — fixed, very low opacity */
  body::before {
    content: "";
    position: fixed;
    inset: 0;
    pointer-events: none;
    z-index: 1000;
    opacity: .035;
    mix-blend-mode: multiply;
    background-image:
      url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.2' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 .9 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  }

  a { color: var(--accent-deep); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; }
  a:hover { color: var(--accent); }
  ::selection { background: var(--accent); color: var(--paper-bright); }

  .doc {
    width: min(100% - 2rem, calc(var(--maxw) + 5rem));
    margin-inline: auto;
    padding-inline: var(--gutter);
    padding-block: clamp(2rem, 5vw, 3.5rem) 3rem;
  }

  /* ---------- Masthead ---------- */
  .masthead {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 1rem;
    padding-bottom: .9rem;
    border-bottom: 1px solid var(--ink);
    font-size: .72rem;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--ink-soft);
    font-weight: 500;
  }
  .masthead .right { color: var(--ink-light); }

  /* ---------- Hero ---------- */
  .hero {
    margin-block: 2.5rem 3rem;
  }
  .hero h1 {
    font-family: 'Source Serif 4', serif;
    font-weight: 600;
    font-size: clamp(2rem, 4.5vw, 2.95rem);
    line-height: 1.12;
    letter-spacing: -0.014em;
    margin: 0 0 1.2rem 0;
    color: var(--ink);
    max-width: 28ch;
  }
  .hero .standfirst {
    font-family: 'Source Serif 4', serif;
    font-style: italic;
    font-weight: 400;
    font-size: 1.12rem;
    line-height: 1.5;
    color: var(--ink-soft);
    max-width: 60ch;
    margin: 0;
  }

  .hero .meta {
    margin-top: 1.75rem;
    padding-top: 1.1rem;
    border-top: 1px solid var(--rule);
    display: grid;
    grid-template-columns: max-content 1fr;
    column-gap: 1.5rem;
    row-gap: .35rem;
    font-size: .85rem;
    max-width: 60ch;
  }
  .hero .meta dt {
    font-family: 'Source Serif 4', serif;
    font-size: .68rem;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--ink-light);
    font-weight: 600;
    align-self: baseline;
    padding-top: .15rem;
  }
  .hero .meta dd {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: .8rem;
    margin: 0;
    color: var(--ink);
    overflow-wrap: anywhere;
    word-break: break-word;
  }

  /* ---------- Sections ---------- */
  section { margin-block: 3.5rem; }

  .section-head {
    margin-bottom: 1.5rem;
  }
  .section-head .label {
    font-family: 'Source Serif 4', serif;
    font-size: .68rem;
    letter-spacing: .22em;
    text-transform: uppercase;
    color: var(--accent);
    font-weight: 600;
    margin-bottom: .35rem;
  }
  .section-head h2 {
    font-family: 'Source Serif 4', serif;
    font-weight: 600;
    font-size: 1.55rem;
    line-height: 1.22;
    letter-spacing: -0.006em;
    margin: 0 0 .65rem 0;
    color: var(--ink);
    max-width: 36ch;
  }
  .section-head::after {
    content: "";
    display: block;
    width: 2.5rem;
    height: 1px;
    background: var(--ink);
    margin-top: .8rem;
  }

  h3 {
    font-family: 'Source Serif 4', serif;
    font-weight: 600;
    font-size: 1.12rem;
    margin: 2.1rem 0 .65rem 0;
    color: var(--ink);
  }

  p { margin: 0 0 .95em; }
  p strong { font-weight: 600; color: var(--ink); }
  em { font-style: italic; }

  /* Inline code */
  code {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: .82em;
    background: var(--paper-bright);
    padding: .08em .35em;
    border: 1px solid var(--rule);
    border-radius: 2px;
    color: var(--accent-deep);
    overflow-wrap: anywhere;
  }

  /* Block code */
  pre {
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: .76rem;
    line-height: 1.55;
    background: var(--paper-bright);
    border: 1px solid var(--rule);
    border-left: 2px solid var(--ink);
    padding: .9rem 1.1rem;
    margin: 1.25rem 0;
    overflow-x: auto;
    color: var(--ink-soft);
  }
  pre code { background: none; border: 0; padding: 0; color: inherit; }

  /* Lists */
  ul, ol { margin: 0 0 1em 1.3em; padding: 0; }
  li { margin-bottom: .3em; }
  ul li::marker { color: var(--accent); }

  /* Blockquote — pull-quote / John conversation */
  blockquote {
    margin: 1.5rem 0;
    padding: 0 0 0 1.5rem;
    border-left: 2px solid var(--accent);
    font-style: italic;
    font-size: 1rem;
    line-height: 1.55;
    color: var(--ink-soft);
  }
  blockquote p:last-child { margin-bottom: 0; }

  /* ---------- Tables ---------- */
  .table-wrap {
    margin: 1.4rem 0;
    overflow-x: auto;
    border-top: 1.5px solid var(--ink);
    border-bottom: 1.5px solid var(--ink);
  }
  table {
    width: 100%;
    font-size: .9rem;
    line-height: 1.45;
  }
  thead tr { border-bottom: 1px solid var(--ink); }
  th {
    text-align: left;
    padding: .7rem .8rem;
    font-family: 'Source Serif 4', serif;
    font-weight: 600;
    font-size: .68rem;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--ink);
    vertical-align: bottom;
  }
  td {
    padding: .65rem .8rem;
    border-bottom: 1px solid var(--rule);
    vertical-align: top;
    color: var(--ink-soft);
  }
  td.num { font-family: 'JetBrains Mono', monospace; text-align: right; font-size: .85rem; color: var(--ink); }
  td strong { color: var(--ink); }
  td em { color: var(--ink-light); }
  tbody tr:last-child td { border-bottom: 0; }

  /* ---------- The four pillars (mental model diagram) ---------- */
  .pillars {
    margin: 1.5rem 0 1.75rem;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 0;
    border-top: 1.5px solid var(--ink);
    border-bottom: 1.5px solid var(--ink);
    background: var(--paper-bright);
  }
  .pillar {
    padding: 1.1rem 1rem 1.2rem;
    border-right: 1px solid var(--rule);
  }
  .pillar:last-child { border-right: 0; }
  .pillar .stage {
    font-family: 'Source Serif 4', serif;
    font-weight: 600;
    font-size: .62rem;
    letter-spacing: .18em;
    text-transform: uppercase;
    color: var(--ink-light);
    margin-bottom: .45rem;
  }
  .pillar .name {
    font-family: 'Source Serif 4', serif;
    font-weight: 600;
    font-size: 1rem;
    color: var(--ink);
    margin-bottom: .3rem;
  }
  .pillar .qa {
    font-style: italic;
    font-size: .8rem;
    line-height: 1.4;
    color: var(--accent-deep);
    margin-bottom: .7rem;
  }
  .pillar ul {
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: .76rem;
    line-height: 1.45;
    color: var(--ink-soft);
  }
  .pillar ul li { padding-left: .85rem; position: relative; margin-bottom: .2rem; }
  .pillar ul li::before { content: "—"; position: absolute; left: 0; color: var(--rule-strong); }
  .pillar ul li code { font-size: .72rem; }
  @media (max-width: 760px) {
    .pillars { grid-template-columns: 1fr; }
    .pillar { border-right: 0; border-bottom: 1px solid var(--rule); }
  }

  /* ---------- Group tree ---------- */
  .tree {
    margin: 1.5rem 0;
    padding: 1rem 1.25rem;
    background: var(--paper-bright);
    border: 1px solid var(--rule);
    border-left: 2px solid var(--ink);
    font-family: 'JetBrains Mono', monospace;
    font-size: .8rem;
    line-height: 1.6;
    color: var(--ink-soft);
  }
  .tree .root { color: var(--ink-light); }
  .tree .inst { color: var(--ink); font-weight: 700; }
  .tree .unit { color: var(--ink-soft); }
  .tree .unit.john { color: var(--accent); font-weight: 700; }
  .tree .annot {
    color: var(--ink-light);
    font-style: italic;
    font-family: 'Source Serif 4', serif;
    font-size: .76rem;
    padding-left: .5rem;
  }
  .tree .annot.john { color: var(--accent); }

  /* ---------- Scenario / timeline ---------- */
  .scenario {
    margin: 1.5rem 0;
    padding: 1rem 1.25rem;
    background: var(--paper-bright);
    border: 1px solid var(--rule);
    border-left: 2px solid var(--accent);
    font-family: 'JetBrains Mono', monospace;
    font-size: .78rem;
    line-height: 1.6;
    color: var(--ink-soft);
  }
  .scenario .stamp { color: var(--accent); font-weight: 700; }
  .scenario .arrow { color: var(--rule-strong); }
  .scenario .ok    { color: var(--green); font-weight: 700; }
  .scenario .stale { color: var(--accent); font-weight: 700; }

  /* ---------- Big number cards ---------- */
  .figures {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: .85rem;
    margin: 1.5rem 0;
  }
  .figure {
    padding: 1rem 1.1rem .95rem;
    background: var(--paper-bright);
    border: 1px solid var(--rule);
    border-top: 2px solid var(--rule-strong);
  }
  .figure.legacy { border-top-color: var(--accent); }
  .figure.modern { border-top-color: var(--green); }
  .figure .big {
    font-family: 'Source Serif 4', serif;
    font-weight: 500;
    font-size: 2.4rem;
    line-height: 1;
    letter-spacing: -.02em;
    color: var(--ink);
  }
  .figure.legacy .big { color: var(--accent); }
  .figure.modern .big { color: var(--green); }
  .figure .label {
    margin-top: .45rem;
    font-family: 'Source Serif 4', serif;
    font-weight: 600;
    font-size: .66rem;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--ink-light);
  }
  .figure .desc {
    margin-top: .35rem;
    font-style: italic;
    font-size: .82rem;
    line-height: 1.4;
    color: var(--ink-soft);
  }

  /* ---------- TL;DR ---------- */
  .tldr {
    margin: 1rem 0 0;
    padding: 1.5rem 1.75rem;
    background: var(--ink);
    color: var(--paper-bright);
  }
  .tldr p {
    margin: 0;
    font-size: .98rem;
    line-height: 1.6;
    color: var(--paper-bright);
  }
  .tldr p strong {
    color: #E5C9B5;
    font-weight: 600;
  }

  /* ---------- Footer ---------- */
  footer.colophon {
    margin-top: 4rem;
    padding-top: 1.1rem;
    border-top: 1px solid var(--ink);
    display: flex;
    justify-content: space-between;
    font-size: .68rem;
    letter-spacing: .14em;
    text-transform: uppercase;
    color: var(--ink-light);
    font-weight: 500;
  }

  /* Print */
  @media print {
    body { background: white; font-size: 11pt; }
    body::before { display: none; }
    .tldr { background: white; color: var(--ink); border: 1px solid var(--ink); }
    .tldr p, .tldr p strong { color: var(--ink); }
    .doc { width: auto; padding: 0 1.5cm; }
    section, .pillars { break-inside: avoid; }
  }

  @media (max-width: 540px) {
    body { font-size: 16px; }
    .hero h1 { font-size: 1.65rem; }
    .pillars { grid-template-columns: 1fr; }
  }

  /* ---------- Right-rail TOC (desktop) ---------- */
  .toc {
    position: fixed;
    top: 50%;
    right: 1.5rem;
    transform: translateY(-50%);
    width: 12.5rem;
    max-height: calc(100vh - 4rem);
    overflow-y: auto;
    padding: 1rem 1.1rem;
    background: var(--paper-bright);
    border: 1px solid var(--rule);
    border-left: 2px solid var(--accent);
    z-index: 50;
    font-size: .78rem;
    line-height: 1.4;
  }
  .toc .toc-label {
    font-family: 'Source Serif 4', serif;
    font-size: .6rem;
    letter-spacing: .2em;
    text-transform: uppercase;
    color: var(--ink-light);
    font-weight: 600;
    margin-bottom: .55rem;
    padding-bottom: .45rem;
    border-bottom: 1px solid var(--rule);
  }
  .toc ol {
    list-style: none;
    padding: 0;
    margin: 0;
    counter-reset: toc;
  }
  .toc ol li {
    counter-increment: toc;
    position: relative;
    padding: .35rem 0 .35rem 1.5rem;
    border-top: 1px solid var(--rule);
  }
  .toc ol li:first-child { border-top: 0; padding-top: .15rem; }
  .toc ol li::before {
    content: counter(toc, upper-roman) ".";
    position: absolute;
    left: 0;
    top: .35rem;
    font-family: 'Source Serif 4', serif;
    font-weight: 600;
    font-size: .7rem;
    color: var(--accent);
  }
  .toc ol li:first-child::before { top: .15rem; }
  .toc a {
    color: var(--ink-soft);
    text-decoration: none;
    display: block;
  }
  .toc a:hover { color: var(--accent); }

  /* Hide rail when viewport is too narrow to fit it next to the column */
  @media (max-width: 1380px) {
    .toc { display: none; }
  }

  /* ---------- Inline mini-TOC (mobile / narrow) ---------- */
  .toc-inline {
    display: none;
    margin: 1.5rem 0 0;
    padding: .9rem 1.1rem;
    background: var(--paper-bright);
    border: 1px solid var(--rule);
    border-left: 2px solid var(--accent);
    font-size: .82rem;
  }
  .toc-inline .toc-label {
    font-family: 'Source Serif 4', serif;
    font-size: .6rem;
    letter-spacing: .2em;
    text-transform: uppercase;
    color: var(--ink-light);
    font-weight: 600;
    margin-bottom: .5rem;
  }
  .toc-inline ol {
    list-style: none;
    padding: 0;
    margin: 0;
    column-count: 2;
    column-gap: 1rem;
  }
  .toc-inline ol li {
    break-inside: avoid;
    padding: .15rem 0;
  }
  .toc-inline a {
    color: var(--ink-soft);
    text-decoration: none;
  }
  .toc-inline a:hover { color: var(--accent); }
  @media (max-width: 1380px) {
    .toc-inline { display: block; }
  }
  @media (max-width: 480px) {
    .toc-inline ol { column-count: 1; }
  }

  /* ---------- Back-to-top floater ---------- */
  .top-link {
    position: fixed;
    right: 1.25rem;
    bottom: 1.25rem;
    z-index: 60;
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    padding: .45rem .75rem .5rem;
    background: var(--ink);
    color: var(--paper-bright);
    text-decoration: none;
    font-family: 'Source Serif 4', serif;
    font-size: .68rem;
    letter-spacing: .14em;
    text-transform: uppercase;
    font-weight: 600;
    border: 0;
    box-shadow: 0 6px 18px -8px rgba(30,26,20,.45);
    opacity: 0;
    pointer-events: none;
    transition: opacity .2s ease;
  }
  body.scrolled .top-link { opacity: 1; pointer-events: auto; }
  .top-link:hover { background: var(--accent); color: var(--paper-bright); }
  @media print { .top-link, .toc, .toc-inline { display: none !important; } }
</style>
</head>
<body id="top">

<!-- ─────────── Right-rail TOC (desktop only, hidden < 1280px) ─────────── -->
<nav class="toc" aria-label="Sections">
  <div class="toc-label">On this page</div>
  <ol>
    <li><a href="#s1">The mental model</a></li>
    <li><a href="#s2">John's identity, traced</a></li>
    <li><a href="#s3">Anomaly A — Part A is not gated</a></li>
    <li><a href="#s4">Anomaly B — what John sees in Part B</a></li>
    <li><a href="#s5">The architectural question</a></li>
    <li><a href="#s6">Conversation with John</a></li>
    <li><a href="#s7">Order of operations</a></li>
    <li><a href="#s8">In one paragraph</a></li>
  </ol>
</nav>

<a href="#top" class="top-link" aria-label="Back to top">↑ Top</a>

<div class="doc">

  <!-- ─────────── Masthead ─────────── -->
  <header class="masthead">
    <div>Technical Brief · Invest Kenya</div>
    <div class="right">2026‑05‑06</div>
  </header>

  <!-- ─────────── Hero ─────────── -->
  <section class="hero">
    <h1>Why John sees what he sees in DS, and what to do about it.</h1>
    <p class="standfirst">An access‑control investigation around <code>john.mwendwa@investkenya.go.ke</code>,
      traced through the four systems that decide it: Keycloak, BPA, Camunda, and DS.</p>

    <dl class="meta">
      <dt>User</dt>
      <dd>john.mwendwa@investkenya.go.ke</dd>
      <dt>Realm</dt>
      <dd>ke (Kenya)</dd>
      <dt>Country services</dt>
      <dd>7 published</dd>
    </dl>

    <!-- Inline mini-TOC (visible on narrow screens only) -->
    <nav class="toc-inline" aria-label="Sections (mobile)">
      <div class="toc-label">Contents</div>
      <ol>
        <li>I. <a href="#s1">The mental model</a></li>
        <li>II. <a href="#s2">John's identity, traced</a></li>
        <li>III. <a href="#s3">Anomaly A — Part A is not gated</a></li>
        <li>IV. <a href="#s4">Anomaly B — Part B view</a></li>
        <li>V. <a href="#s5">The architectural question</a></li>
        <li>VI. <a href="#s6">Conversation with John</a></li>
        <li>VII. <a href="#s7">Order of operations</a></li>
        <li>VIII. <a href="#s8">In one paragraph</a></li>
      </ol>
    </nav>
  </section>

  <!-- ═══════════════ §1 ═══════════════ -->
  <section aria-labelledby="s1">
    <header class="section-head">
      <div class="label">Section I</div>
      <h2 id="s1">The mental model — four moving parts</h2>
    </header>

    <p>Access in eRegistrations is decided across a chain of four systems. Each holds one piece of the
       picture; none has the whole.</p>

    <div class="pillars" aria-label="The four moving parts">
      <div class="pillar">
        <div class="stage">Identity</div>
        <div class="name">Keycloak</div>
        <div class="qa">Who the user is.</div>
        <ul>
          <li>Realm roles (<code>partb</code>…)</li>
          <li>Group memberships</li>
          <li><code>/institutions/&lt;inst&gt;/&lt;unit&gt;</code></li>
        </ul>
      </div>
      <div class="pillar">
        <div class="stage">Configuration</div>
        <div class="name">BPA</div>
        <div class="qa">Who is allowed in theory.</div>
        <ul>
          <li>Each role's home institution + unit</li>
          <li>Re‑published when a service is changed</li>
        </ul>
      </div>
      <div class="pillar">
        <div class="stage">Runtime</div>
        <div class="name">Camunda</div>
        <div class="qa">What is alive right now.</div>
        <ul>
          <li>Identity links on each running task</li>
          <li>Stamped at task creation, not re‑evaluated</li>
        </ul>
      </div>
      <div class="pillar">
        <div class="stage">Presentation</div>
        <div class="name">DS</div>
        <div class="qa">What the user is shown.</div>
        <ul>
          <li>Menu items, file list</li>
          <li>Filtered by user/groups</li>
        </ul>
      </div>
    </div>

    <p><strong>The crucial thing to understand.</strong> Camunda is the only system that <em>actually</em>
       decides what John can act on, because it holds the live identity links of every running task. BPA
       <em>tells</em> Camunda what those identity links should be — but only when a process starts. If
       you change BPA later, already‑running processes keep the old rules.</p>

    <h3>Two‑level institution structure</h3>

    <p>In Keycloak each institution is modelled as a group. A user is placed in <em>a unit</em> (a leaf),
       not in the institution itself.</p>

    <div class="tree" role="img" aria-label="KIA institution group tree in Keycloak">
      <div><span class="root">/institutions/</span></div>
      <div>&nbsp;&nbsp;└── <span class="inst">Kenya Investment Authority</span><span class="annot">— the institution</span></div>
      <div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;├── <span class="unit john">CEO</span><span class="annot john">— John</span></div>
      <div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;├── <span class="unit john">Managing Director</span><span class="annot john">— John</span></div>
      <div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;├── <span class="unit">Investor Services</span><span class="annot">— a unit (not John)</span></div>
      <div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;├── <span class="unit">Bank review, Enquiry, Head OSC, Registrar</span></div>
    </div>

    <p>In BPA, when an admin defines a role and chooses "Institution: KIA / Unit in charge: Investor
       Services", BPA stores that pair and tells Camunda: "for this role, the candidate group is the
       <em>unit</em> id". If the admin leaves "Unit in charge" blank, BPA stores
       <code>(institution, no unit)</code> and Camunda gets the <em>institution</em> id as the candidate
       group. That difference — institution‑level versus unit‑level binding — is the hinge on which
       everything below turns.</p>
  </section>

  <!-- ═══════════════ §2 ═══════════════ -->
  <section aria-labelledby="s2">
    <header class="section-head">
      <div class="label">Section II</div>
      <h2 id="s2">John's identity, traced through every layer</h2>
    </header>

    <p>Each layer holds one fact about John. Read them as a stack.</p>

    <div class="table-wrap">
      <table>
        <thead>
          <tr>
            <th style="width:14%">Layer</th>
            <th style="width:34%">Question</th>
            <th>Answer for John</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td><strong>Keycloak</strong></td>
            <td>What realm roles does he have?</td>
            <td><code>partb</code>, plus the standard defaults (<code>uma_authorization</code>,
                <code>offline_access</code>, <code>default-roles-ke</code>). Nothing else.</td>
          </tr>
          <tr>
            <td><strong>Keycloak</strong></td>
            <td>What groups is he in?</td>
            <td>Two — <code>/Kenya Investment Authority/CEO</code> and
                <code>/Kenya Investment Authority/Managing Director</code>.
                Not Investor Services, not any other unit.</td>
          </tr>
          <tr>
            <td><strong>BPA</strong></td>
            <td>What roles do the seven country services have?</td>
            <td>Around thirty user‑facing roles. The breakdown by binding is in the next table.</td>
          </tr>
          <tr>
            <td><strong>Camunda</strong></td>
            <td>For each in‑flight task, who is a candidate?</td>
            <td>The candidate group is the institution id (if BPA left "unit" blank) or the unit id (if
                BPA assigned one) at the moment that task was created.</td>
          </tr>
          <tr>
            <td><strong>DS</strong></td>
            <td>What does it show John?</td>
            <td>Every service whose roles intersect his groups, and every file whose current task does.</td>
          </tr>
        </tbody>
      </table>
    </div>

    <p>Across the seven services, here is the full set of BPA roles for which John is a legitimate
       Camunda candidate, and the path that makes him match:</p>

    <div class="table-wrap">
      <table>
        <thead>
          <tr>
            <th style="width:36%">Service</th>
            <th>Roles John matches</th>
            <th style="width:16%">Match path</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>Get Immigration support for investors</td>
            <td><code>immigrationLetterUpload</code></td>
            <td><em>KIA institution</em></td>
          </tr>
          <tr>
            <td>Get support for waiver of import duty &amp; taxes</td>
            <td><code>immigrationLetterUpload</code></td>
            <td><em>KIA institution</em></td>
          </tr>
          <tr>
            <td>Register your investment project with Invest Kenya</td>
            <td>9 KIA‑institution roles (<code>investmentCreate</code>, <code>investmentRegUpload</code>,
                <code>lettersUpload</code>, <code>brsCreate</code>, and 5 problem‑* roles)
                <strong>+ 2 CEO‑unit roles</strong> (<code>conditionalIcApproval</code>,
                <code>conditionalCertificateUpload</code>)</td>
            <td><em>KIA + CEO</em></td>
          </tr>
          <tr>
            <td>Update Investment Project Information</td>
            <td><code>investmentCreate</code>, <code>investmentRegUpload</code>,
                <code>problemInvRegUpload</code>, <code>problemRoleInvestmentCreate</code></td>
            <td><em>KIA institution</em></td>
          </tr>
          <tr>
            <td>Request Investment certificate</td>
            <td><code>certificateUpload</code> (KIA) <strong>+ <code>icApprovalCeo</code> (CEO‑unit)</strong></td>
            <td><em>KIA + CEO</em></td>
          </tr>
          <tr>
            <td>Enquiry</td>
            <td><code>enquiryUpdate</code>, <code>enquiryResponseNjau</code></td>
            <td><em>KIA institution</em></td>
          </tr>
          <tr>
            <td>Tax registration for foreigners (PIN)</td>
            <td><code>problemPinCreate</code>, <code>problemUpdateGdb</code> &nbsp;<em>(plus 21 in‑flight
                <code>investorReview</code>/<code>statusReview</code> tasks under a stale candidate — see §IV)</em></td>
            <td><em>KIA institution</em></td>
          </tr>
        </tbody>
      </table>
    </div>

    <p>Two patterns jump out. First, John is correctly his‑unit's candidate for three roles —
       <code>icApprovalCeo</code>, <code>conditionalIcApproval</code>,
       <code>conditionalCertificateUpload</code> — that BPA explicitly bound to the CEO unit. Second,
       every one of the seven services has at least one role bound to KIA at <em>institution level</em>
       (no unit). That is what makes the whole catalogue visible to him.</p>
  </section>

  <!-- ═══════════════ §3 ═══════════════ -->
  <section aria-labelledby="s3">
    <header class="section-head">
      <div class="label">Section III</div>
      <h2 id="s3">Anomaly A — Part A is not gated by code</h2>
    </header>

    <p>John can navigate to the Part A applicant dashboard, and so can any other Part B user. The cause
       is straightforward but not where you'd first look: <strong>the page itself has no role guard.</strong>
       The menu link is just convenience — even if removed, John can type <code>/</code> in the URL bar
       and the dashboard renders.</p>

    <h3>What the code actually does</h3>

    <p>The only role‑based decision the system makes about Part A vs. Part B is a post‑login redirect,
       in <code>ds-backend/apps/profile/views.py</code>:</p>

<pre><code>def _should_redirect_to_part_b(user, auth_service, token_data):
    if not (user and token_data):
        return False
    roles = auth_service.get_roles_from_user_data(token_data)
    return "partb" in [role.lower() for role in roles]</code></pre>

    <p>If the user has the realm role <code>partb</code>, the server redirects them to
       <code>/{lang}/part-b</code> immediately after login. Otherwise they land on <code>/</code>.
       <strong>That is the only place a role is consulted.</strong> After the redirect, nothing prevents
       the user from coming back.</p>

    <p>The Angular router confirms it:</p>

<pre><code>const routes: Routes = [
  { path: '',     component: DashboardComponent },     // Part A — no guard
  { path: 'part-b', loadChildren: …PartBModule },      // no guard
  { path: 'admin/users',     canActivate: [SysadminGuard] },
  { path: 'financial-report', canActivate: [FinancialReportGuard] },
  { path: '**',   component: DashboardComponent }      // wildcard → Part A
];</code></pre>

    <p>Out of the entire route table, only three paths have any guard. The root path is open and the
       wildcard funnels every unmatched URL to the Part A dashboard.</p>

    <h3>What the fix looks like</h3>

    <p>Wrapping the menu <code>&lt;li&gt;</code> in <code>*ngIf</code> only hides the link. The route
       itself needs a guard. Two ingredients:</p>

    <ol>
      <li><strong>A route guard on <code>''</code> (and on the wildcard).</strong> Same shape as
          the existing <code>SysadminGuard</code>. The simplest contract: if the user has
          <code>partb</code>, redirect them to <code>/part-b</code>. That mirrors the existing post‑login
          logic and needs no new role.</li>
      <li><strong>Hide the menu entry on the same condition</strong>, for cosmetic consistency.</li>
    </ol>

    <p>This change is small, isolated to ds‑frontend, and does not require any Keycloak changes.</p>
  </section>

  <!-- ═══════════════ §4 ═══════════════ -->
  <section aria-labelledby="s4">
    <header class="section-head">
      <div class="label">Section IV</div>
      <h2 id="s4">Anomaly B — what John sees in Part B has two compounding causes</h2>
    </header>

    <p>What John sees in Part B is two distinct things layered on top of each other. Separating them is
       the only way to choose the right remedy for each.</p>

    <h3>Cause 1 — institution‑level role bindings (by configuration)</h3>

    <p>Across the seven Kenya services, most user‑facing roles are bound at the <em>institution</em>
       level — i.e. their "Unit in charge" field was left empty when the role was defined in BPA. When
       BPA publishes such a role, the candidate group is the institution UUID. Anyone in any sub‑group
       of that institution becomes a legitimate candidate.</p>

    <p>John, as a member of <code>/KIA/CEO</code> and <code>/KIA/Managing Director</code>, is a member of
       KIA in the only sense the system reasons about. He therefore matches every KIA‑institution‑level
       role published in any Kenya service, by design. That is what makes <em>all seven services</em>
       appear in his dropdown and most of the files in those services appear in his list.</p>

    <p>This is not a bug. It is a configuration choice, and whether it is the right one is the
       question in §V.</p>

    <h3>Cause 2 — a republish migration gap (today, only on Tax‑PIN)</h3>

    <p>The mechanic itself is general. When BPA publishes a service, candidate groups are written into
       the BPMN definition; Camunda copies them onto each task at task creation. <em>Republishing does
       not re‑stamp running tasks.</em> So whenever any service in any country has a role binding
       tightened, every then‑in‑flight task for that role keeps the older, broader rule until it
       completes.</p>

    <p>To know whether this is theoretical or actual on Kenya today, we audited the candidate groups on
       every running task across all seven services and compared them to BPA's current declared
       candidates. Result:</p>

    <div class="table-wrap">
      <table>
        <thead>
          <tr>
            <th>Service</th>
            <th style="width:18%">Active tasks</th>
            <th style="width:18%">Stale tasks</th>
          </tr>
        </thead>
        <tbody>
          <tr><td>Get Immigration support for investors</td><td class="num">0</td><td class="num">0</td></tr>
          <tr><td>Get support for waiver of import duty &amp; taxes</td><td class="num">0</td><td class="num">0</td></tr>
          <tr><td>Register your investment project with Invest Kenya</td><td class="num">79</td><td class="num">0</td></tr>
          <tr><td>Update Investment Project Information</td><td class="num">0</td><td class="num">0</td></tr>
          <tr><td>Request Investment certificate</td><td class="num">0</td><td class="num">0</td></tr>
          <tr><td>Enquiry</td><td class="num">24</td><td class="num">0</td></tr>
          <tr><td><strong>Tax registration for foreigners (PIN)</strong></td><td class="num">60</td><td class="num"><strong>21</strong></td></tr>
        </tbody>
      </table>
    </div>

    <p>So the scope of stale state on Kenya right now is contained: <strong>21 in‑flight Tax‑PIN tasks
       — 14 <code>investorReview</code> and 7 <code>statusReview</code></strong>, all of them tightened
       from "(KIA, no unit)" to "(KIA, Investor Services unit)" on 14 April 2026. The remaining six
       services are aligned with their current BPA configuration. But this is only today's snapshot;
       the same drift can re‑accumulate the next time any role on any service is tightened.</p>

    <div class="figures">
      <div class="figure legacy">
        <div class="big">21</div>
        <div class="label">Stale Tax‑PIN tasks</div>
        <div class="desc">Started before 14 April. Candidate still <code>[KIA]</code>. <strong>John matches</strong> — under the rule that was in force when these started.</div>
      </div>
      <div class="figure modern">
        <div class="big">14</div>
        <div class="label">Aligned Tax‑PIN tasks</div>
        <div class="desc">Started after 14 April. Candidate <code>[Investor&nbsp;Services]</code>. <strong>John does not match.</strong></div>
      </div>
      <div class="figure">
        <div class="big">24</div>
        <div class="label">Applicant tasks</div>
        <div class="desc">Citizens — irrelevant to John.</div>
      </div>
    </div>

    <div class="scenario">
      <div><span class="stamp">Before 14 Apr 2026</span></div>
      <div>&nbsp;&nbsp;Investor Review bound to <em>(KIA, no unit)</em></div>
      <div>&nbsp;&nbsp;<span class="arrow">→</span>&nbsp;Camunda stamps tasks with candidate = <strong>KIA</strong></div>
      <br>
      <div><span class="stamp">14 Apr 2026</span> &nbsp;Admin tightens to <em>(KIA, Investor Services)</em>, republishes</div>
      <div>&nbsp;&nbsp;<span class="arrow">→</span>&nbsp;<span class="ok">New</span> processes get candidate = Investor Services ✓</div>
      <div>&nbsp;&nbsp;<span class="arrow">→</span>&nbsp;<span class="stale">21 in‑flight</span> tasks keep candidate = KIA</div>
    </div>

    <h3>Why John sees a small inconsistency on Tax‑PIN specifically</h3>

    <p>The Part B header reads from two different sources of truth. The "Roles 2" badge reads BPA's
       <em>current</em> configuration and correctly reports the two Problem‑* roles that John still
       matches. The file table reads Camunda's <em>running</em> identity links and includes the 21
       legacy files where the current task still carries the old KIA candidate. Both views are
       individually correct; they describe different points in time. Migrating those 21 tasks closes
       the gap on Tax‑PIN. It does not address Cause 1 above — for that, BPA configuration itself
       has to change.</p>
  </section>

  <!-- ═══════════════ §5 ═══════════════ -->
  <section aria-labelledby="s5">
    <header class="section-head">
      <div class="label">Section V</div>
      <h2 id="s5">The architectural question — institution‑wide or unit‑bound?</h2>
    </header>

    <p>One configuration question has to be answered, because the answer changes which fixes are
       appropriate:</p>

    <blockquote>
      <p>Should institution‑level role bindings — which currently grant access to every member of every
         unit, by design — continue to be the default? Or should every role in BPA be assigned to a
         specific unit, so that visibility is unit‑scoped for everyone?</p>
    </blockquote>

    <p>The system today behaves <em>symmetrically</em> across every unit, not selectively. When BPA
       leaves "Unit in charge" blank for a role, the role's Camunda candidate group becomes the
       institution UUID — and every user in any sub‑group of that institution becomes a candidate.
       The matching logic in <code>utils/auth_service.py</code> treats CEO, Managing Director, Investor
       Services, Bank Review, Registrar, and the Enquiry desk identically: each, found as a sub‑group
       of KIA, is recorded as a KIA member.</p>

    <p>So the broad visibility John sees is <strong>not a privilege of his title</strong>; it is shared
       by every KIA staff member regardless of which unit they belong to. An Investor Services agent
       sees the same institution‑level tasks John sees — plus their unit‑bound
       <code>investorReview</code> / <code>statusReview</code> / <code>pinCreate</code> /
       <code>updateGdb</code>. A Bank Review agent sees the same institution‑level tasks plus their
       unit‑bound roles. And so on. John's situation looks unusual only because the question came up
       around his account.</p>

    <p>If the intended answer is <strong>"institution‑wide is fine"</strong>, the current behaviour is
       right. What needs to change is the perception: the file list isn't a bug — it is the institution
       view that every KIA member is entitled to. The only loose end is the 21‑task republish gap on
       Tax‑PIN, which should be closed.</p>

    <p>If the intended answer is <strong>"every role should be unit‑scoped"</strong>, the BPA
       configuration itself needs auditing. For each service, every role currently bound to
       "(institution, no unit)" should be reviewed and either left as institution‑wide (when that is
       intentional, e.g. "anyone in KIA can upload supporting letters") or assigned to a specific unit.
       After each tightening, running tasks must be migrated, otherwise the same drift recurs. This is
       a BPA‑admin exercise across all seven services, applied uniformly to every institution and unit
       — not just KIA.</p>

    <p>A reasonable hybrid is to keep institution‑wide binding for low‑sensitivity tasks (uploads,
       problem flows, enquiries) and tighten only the high‑sensitivity ones (reviews, approvals).</p>
  </section>

  <!-- ═══════════════ §6 ═══════════════ -->
  <section aria-labelledby="s6">
    <header class="section-head">
      <div class="label">Section VI</div>
      <h2 id="s6">What this means for the conversation with John</h2>
    </header>

    <p>Plain language for him:</p>

    <blockquote>
      <p>"John, two distinct things are happening here, and neither is really about you specifically.</p>
      <p>First — your ability to navigate to the applicant view. The system never blocks anyone from
         that page; the only role decision it makes is to send Part B users straight to
         <code>/part-b</code> after login, and after that nothing is gated. We have a small fix planned
         that adds a real route guard so the page is no longer reachable.</p>
      <p>Second — your Part B file list. Most of what you see is intentional, and it is not specific
         to your role. BPA was configured so that many tasks belong to Kenya Investment Authority as a
         whole rather than to a particular unit. Every KIA staff member — yourself, your colleagues in
         Investor Services, Bank Review, Registrar, the Enquiry desk — is treated identically as a KIA
         member and is a legitimate processor for those tasks. The institution‑wide view you see is the
         same view they see; you stand out only because the question was raised on your account.
         Separately, a small handful of tasks (e.g. <code>icApprovalCeo</code>) are bound by BPA to
         specific units, and those reach you by unit, not by your KIA membership.</p>
      <p>On the Tax registration service, two reviews were tightened from "anyone in KIA" to "Investor
         Services only" on 14 April; 21 in‑flight files still carry the older rule and so still appear
         to you. We can migrate those 21 in one go. If you want the broader institution‑wide visibility
         narrowed across the board, that is a BPA configuration decision that applies symmetrically to
         every role, every unit, and every institution — not just yours."</p>
    </blockquote>
  </section>

  <!-- ═══════════════ §7 ═══════════════ -->
  <section aria-labelledby="s7">
    <header class="section-head">
      <div class="label">Section VII</div>
      <h2 id="s7">Recommended order of operations</h2>
    </header>

    <p>Five moves, grouped by who acts. The first three close the discrete drift; the last two are
       strategic. Move 1 alone measurably reduces John's confusion on Tax‑PIN today; the rest depend on
       the answer to §V.</p>

    <div class="table-wrap">
      <table>
        <thead>
          <tr>
            <th style="width:4%">#</th>
            <th style="width:34%">Action</th>
            <th style="width:14%">Owner</th>
            <th style="width:12%">Effort</th>
            <th>Effect</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td class="num">1</td>
            <td><strong>Migrate the 21 stale Tax‑PIN tasks</strong> — re‑stamp candidate group from <code>KIA</code> to <code>Investor Services unit</code> via Camunda's task identity‑link API.</td>
            <td>DevOps</td>
            <td>Small (one‑shot script, reversible)</td>
            <td>The 21 stale Investor / Status Review files leave John's view immediately.</td>
          </tr>
          <tr>
            <td class="num">2</td>
            <td><strong>Add a role guard on the Angular root route</strong> (and the wildcard). Simplest contract: if the user has <code>partb</code>, redirect to <code>/part-b</code>.</td>
            <td>Frontend</td>
            <td>Small</td>
            <td>The applicant page is no longer reachable by Part B users.</td>
          </tr>
          <tr>
            <td class="num">3</td>
            <td><strong>Hide the menu entry on the same condition</strong>, for cosmetic consistency.</td>
            <td>Frontend</td>
            <td>One‑line</td>
            <td>The "Applicant's view" entry disappears from John's dropdown.</td>
          </tr>
          <tr>
            <td class="num">4</td>
            <td><strong>Decide §V (institution‑wide vs unit‑bound)</strong> and, if tightening is wanted, audit BPA role bindings across the seven Kenya services and assign units where appropriate. Then re‑apply move 1's migration logic to whatever active tasks the new bindings produce.</td>
            <td>BPA admin (Kenya) + DevOps</td>
            <td>Medium — discovery first, then per‑role decisions</td>
            <td>John's Part B file list narrows to what his unit owns; the service dropdown narrows to services where he has at least one matching role.</td>
          </tr>
          <tr>
            <td class="num">5</td>
            <td><strong>Fix BPA's republish path</strong> so future role tightenings migrate active tasks automatically — preventing the next batch of stale tasks from accumulating on any service.</td>
            <td>Platform team</td>
            <td>Larger — needs careful testing</td>
            <td>Prevents this class of drift across every service in every country, going forward.</td>
          </tr>
        </tbody>
      </table>
    </div>
  </section>

  <!-- ═══════════════ §8 ═══════════════ -->
  <section aria-labelledby="s8">
    <header class="section-head">
      <div class="label">Section VIII</div>
      <h2 id="s8">In one paragraph</h2>
    </header>

    <div class="tldr">
      <p>John has the realm role <strong>partb</strong> and is a member of two KIA units. What he sees
         in DS is two unrelated things. <strong>One:</strong> Part A is not gated by code anywhere — the
         menu link is unguarded, and so is the route itself. The only role decision is to send Part B
         users straight to <code>/part-b</code> after login; nothing prevents them from coming back to
         <code>/</code>. The fix is a small route guard. <strong>Two:</strong> John's Part B view is
         largely the legitimate consequence of BPA configuration — most Kenya roles are bound to KIA at
         institution level, so as a KIA member he is a legitimate candidate everywhere; only a handful
         (notably <code>icApprovalCeo</code>) are bound to his CEO unit specifically. Layered on top is
         a small drift, contained to one service: <strong>21 in‑flight Tax‑PIN tasks</strong> still
         carry the older "(KIA, no unit)" rule that was tightened to "(KIA, Investor Services)" on 14
         April. The other six services audited clean today, but the same drift can re‑accumulate on any
         service the next time a role is tightened — so migrating those 21 fixes John, and fixing BPA's
         republish path prevents recurrence. Whether John's broader visibility should narrow further is
         a configuration decision the BPA admin needs to make, role by role.</p>
    </div>
  </section>

  <footer class="colophon">
    <div>Invest Kenya · BPA v413</div>
    <div>2026‑05‑06</div>
  </footer>

</div>

<script>
  // Show "↑ Top" only after the user has scrolled past the hero.
  (function () {
    var threshold = 600;
    var ticking = false;
    function update() {
      document.body.classList.toggle('scrolled', window.scrollY > threshold);
      ticking = false;
    }
    window.addEventListener('scroll', function () {
      if (!ticking) {
        window.requestAnimationFrame(update);
        ticking = true;
      }
    }, { passive: true });
  })();
</script>

</body>
</html>