[{"data":1,"prerenderedAt":1428},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fexamples\u002Fkeycloak":3,"docs-nav":1205},{"id":4,"title":5,"body":6,"description":1196,"extension":1197,"meta":1198,"navigation":1200,"path":1201,"seo":1202,"stem":1203,"__hash__":1204},"docs\u002Fdocs\u002Fexamples\u002Fkeycloak\u002FREADME.md","Example — Keycloak → Tabularia group mapping",{"type":7,"value":8,"toc":1186},"minimark",[9,13,43,54,67,72,137,141,192,195,254,264,268,279,285,313,324,330,359,395,402,405,409,415,450,461,465,471,584,587,649,662,676,725,758,762,768,805,826,829,846,868,872,875,921,924,928,939,1182],[10,11,5],"h1",{"id":12},"example--keycloak--tabularia-group-mapping",[14,15,16,17,21,22,25,26,33,34,42],"p",{},"A runnable Keycloak realm whose ",[18,19,20],"strong",{},"groups"," land in the OIDC token as a ",[23,24,20],"code",{}," claim,\nmapped 1:1 onto Tabularia groups by name. The gateway side is implemented: see\n",[27,28,30],"a",{"href":29},"\u002Fdocs\u002Fdesign\u002Fsso-group-mapping",[23,31,32],{},"..\u002F..\u002Fdesign\u002Fsso-group-mapping.md"," for the design\nand ",[27,35,39],{"href":36,"rel":37},"https:\u002F\u002Fgithub.com\u002FJumpAsAService\u002FTabularia\u002Fblob\u002Fmain\u002Fgateway\u002Fapp\u002Fservices\u002Fsso.py",[38],"nofollow",[23,40,41],{},"gateway\u002Fapp\u002Fservices\u002Fsso.py"," for the code.",[14,44,45,46,49,50,53],{},"SSO is ",[18,47,48],{},"optional",": with ",[23,51,52],{},"OIDC__ISSUER"," unset, Tabularia keeps local email + password\nlogin and the login page shows no SSO button.",[14,55,56,57,61,62,66],{},"This folder is for trying SSO on a laptop with Docker Compose. To connect your company's\nKeycloak to Tabularia installed with Helm, follow\n",[27,58,60],{"href":59},"\u002Fdocs\u002Fsso-keycloak","Single sign-on with Keycloak",". The groups that arrive are ordinary Tabularia groups:\nwhat they may see and do is set on folders (",[27,63,65],{"href":64},"\u002Fdocs\u002Fgovernance\u002Fpermissions","Permissions",").",[68,69,71],"h2",{"id":70},"whats-here","What's here",[73,74,75,88],"table",{},[76,77,78],"thead",{},[79,80,81,85],"tr",{},[82,83,84],"th",{},"file",[82,86,87],{},"purpose",[89,90,91,102,120],"tbody",{},[79,92,93,99],{},[94,95,96],"td",{},[23,97,98],{},"docker-compose.keycloak.yml",[94,100,101],{},"a dev Keycloak that auto-imports the realm",[79,103,104,109],{},[94,105,106],{},[23,107,108],{},"tabularia-realm.json",[94,110,111,112,115,116,119],{},"realm ",[23,113,114],{},"tabularia",": confidential client ",[23,117,118],{},"tabularia-gateway"," with a group-membership mapper, and sample groups",[79,121,122,127],{},[94,123,124],{},[23,125,126],{},"gateway.env.example",[94,128,129,130,133,134],{},"the ",[23,131,132],{},"OIDC__*"," block to drop into ",[23,135,136],{},"infrastructure\u002F.env",[68,138,140],{"id":139},"1-boot-keycloak-with-the-realm","1. Boot Keycloak with the realm",[142,143,148],"pre",{"className":144,"code":145,"language":146,"meta":147,"style":147},"language-bash shiki shiki-themes github-dark-dimmed github-light","cd docs\u002Fexamples\u002Fkeycloak\ndocker compose -f docker-compose.keycloak.yml up -d\n# Admin console: http:\u002F\u002Flocalhost:8080  (admin \u002F admin)\n","bash","",[23,149,150,163,185],{"__ignoreMap":147},[151,152,155,159],"span",{"class":153,"line":154},"line",1,[151,156,158],{"class":157},"swcJU","cd",[151,160,162],{"class":161},"sXfbr"," docs\u002Fexamples\u002Fkeycloak\n",[151,164,166,170,173,176,179,182],{"class":153,"line":165},2,[151,167,169],{"class":168},"sqRhv","docker",[151,171,172],{"class":161}," compose",[151,174,175],{"class":157}," -f",[151,177,178],{"class":161}," docker-compose.keycloak.yml",[151,180,181],{"class":161}," up",[151,183,184],{"class":157}," -d\n",[151,186,188],{"class":153,"line":187},3,[151,189,191],{"class":190},"sgHix","# Admin console: http:\u002F\u002Flocalhost:8080  (admin \u002F admin)\n",[14,193,194],{},"The realm imports with:",[196,197,198,212,238],"ul",{},[199,200,201,204,205,207,208,211],"li",{},[18,202,203],{},"Client"," ",[23,206,118],{}," — confidential, standard (authorization-code) flow,\nPKCE (S256), redirect URI ",[23,209,210],{},"http:\u002F\u002Flocalhost:8000\u002Fauth\u002Fsso\u002Fcallback",".",[199,213,214,215,218,219,221,222,225,226,229,230,233,234,237],{},"A ",[18,216,217],{},"Group Membership"," mapper attached to the client, adding a ",[23,220,20],{}," claim to the\nID\u002Faccess token ",[18,223,224],{},"without"," the leading path (",[23,227,228],{},"Full group path = off","), so the claim\nis ",[23,231,232],{},"[\"analytics\"]",", not ",[23,235,236],{},"[\"\u002Fanalytics\"]",". It's on the client (not a separate client\nscope) so the claim is emitted on every token and no custom scope must be requested.",[199,239,240,243,244,247,248,247,251,211],{},[18,241,242],{},"Groups",": ",[23,245,246],{},"analytics",", ",[23,249,250],{},"finance",[23,252,253],{},"tabularia-admins",[14,255,256,257,259,260,263],{},"These group names are what you create in Tabularia (Admin → Groups). Membership of\n",[23,258,253],{}," is what ",[23,261,262],{},"OIDC__SUPERUSER_GROUP"," keys on.",[68,265,267],{"id":266},"2-one-address-for-both-sides-dev-only","2. One address for both sides (dev only)",[14,269,270,271,274,275,278],{},"The issuer must be reachable ",[18,272,273],{},"by the same URL"," from your browser and from inside the\ngateway container, because it is both the redirect target and the value signed into the\ntoken. ",[23,276,277],{},"localhost:8080"," means \"the host\" to your browser and \"the container itself\" to\nthe gateway, so it cannot work as is.",[14,280,281,284],{},[18,282,283],{},"Simplest: the bridge IP of the app network."," The host owns that address on its\nDocker bridge, and every container on the network reaches the host through it, while\nKeycloak publishes port 8080 on all interfaces:",[142,286,288],{"className":144,"code":287,"language":146,"meta":147,"style":147},"docker network inspect dataprep-network -f '{{range .IPAM.Config}}{{.Gateway}}{{end}}'\n# e.g. 172.22.0.1  →  OIDC__ISSUER=http:\u002F\u002F172.22.0.1:8080\u002Frealms\u002Ftabularia\n",[23,289,290,308],{"__ignoreMap":147},[151,291,292,294,297,300,303,305],{"class":153,"line":154},[151,293,169],{"class":168},[151,295,296],{"class":161}," network",[151,298,299],{"class":161}," inspect",[151,301,302],{"class":161}," dataprep-network",[151,304,175],{"class":157},[151,306,307],{"class":161}," '{{range .IPAM.Config}}{{.Gateway}}{{end}}'\n",[151,309,310],{"class":153,"line":165},[151,311,312],{"class":190},"# e.g. 172.22.0.1  →  OIDC__ISSUER=http:\u002F\u002F172.22.0.1:8080\u002Frealms\u002Ftabularia\n",[14,314,315,316,319,320,323],{},"No ",[23,317,318],{},"\u002Fetc\u002Fhosts"," edit, no compose change. Keycloak serves plain ",[23,321,322],{},"SameSite=Lax"," session\ncookies on that private address, which browsers accept over HTTP. The IP stays stable\nas long as the network is not recreated.",[14,325,326,329],{},[18,327,328],{},"Alternative: a hostname."," Map one to the host on both sides:",[142,331,333],{"className":144,"code":332,"language":146,"meta":147,"style":147},"echo \"127.0.0.1 keycloak.local\" | sudo tee -a \u002Fetc\u002Fhosts\n",[23,334,335],{"__ignoreMap":147},[151,336,337,340,343,347,350,353,356],{"class":153,"line":154},[151,338,339],{"class":157},"echo",[151,341,342],{"class":161}," \"127.0.0.1 keycloak.local\"",[151,344,346],{"class":345},"s6PUj"," |",[151,348,349],{"class":168}," sudo",[151,351,352],{"class":161}," tee",[151,354,355],{"class":157}," -a",[151,357,358],{"class":161}," \u002Fetc\u002Fhosts\n",[142,360,364],{"className":361,"code":362,"language":363,"meta":147,"style":147},"language-yaml shiki shiki-themes github-dark-dimmed github-light","  gateway:            # infrastructure\u002Fdocker-compose.yml\n    extra_hosts:\n      - \"keycloak.local:host-gateway\"\n","yaml",[23,365,366,379,387],{"__ignoreMap":147},[151,367,368,372,376],{"class":153,"line":154},[151,369,371],{"class":370},"shb1k","  gateway",[151,373,375],{"class":374},"ssh_m",":            ",[151,377,378],{"class":190},"# infrastructure\u002Fdocker-compose.yml\n",[151,380,381,384],{"class":153,"line":165},[151,382,383],{"class":370},"    extra_hosts",[151,385,386],{"class":374},":\n",[151,388,389,392],{"class":153,"line":187},[151,390,391],{"class":374},"      - ",[151,393,394],{"class":161},"\"keycloak.local:host-gateway\"\n",[14,396,397,398,401],{},"and use ",[23,399,400],{},"http:\u002F\u002Fkeycloak.local:8080\u002Frealms\u002Ftabularia"," as the issuer.",[14,403,404],{},"In production this wrinkle disappears: the IdP has one public DNS name.",[68,406,408],{"id":407},"3-create-a-test-user-and-assign-groups","3. Create a test user and assign groups",[14,410,411,412,414],{},"In the admin console (realm ",[18,413,114],{},"):",[416,417,418,432,438],"ol",{},[199,419,420,423,424,427,428,431],{},[18,421,422],{},"Users → Add user",": username ",[23,425,426],{},"alice",", email ",[23,429,430],{},"alice@example.com",", email verified on.",[199,433,434,437],{},[18,435,436],{},"Credentials",": set a password (temporary off).",[199,439,440,443,444,446,447,449],{},[18,441,442],{},"Groups → Join",": add ",[23,445,246],{}," (and ",[23,448,253],{}," to test superuser).",[14,451,452,453,456,457,460],{},"Mirror the group ",[18,454,455],{},"names"," in Tabularia so the mapping resolves (or set\n",[23,458,459],{},"OIDC__AUTO_CREATE_GROUPS=true"," to have them created on first login).",[68,462,464],{"id":463},"4-inspect-the-token-optional-sanity-check","4. Inspect the token (optional sanity check)",[14,466,467,468,470],{},"Confirm the ",[23,469,20],{}," claim shape with the direct-grant flow:",[142,472,474],{"className":144,"code":473,"language":146,"meta":147,"style":147},"curl -s http:\u002F\u002Flocalhost:8080\u002Frealms\u002Ftabularia\u002Fprotocol\u002Fopenid-connect\u002Ftoken \\\n  -d grant_type=password \\\n  -d client_id=tabularia-gateway \\\n  -d client_secret=tabularia-dev-secret \\\n  -d username=alice -d password=YOUR_PASSWORD \\\n  -d scope='openid profile email' | \\\n  cut -d. -f2 | base64 -d 2>\u002Fdev\u002Fnull | python3 -m json.tool\n",[23,475,476,491,501,510,520,536,548],{"__ignoreMap":147},[151,477,478,481,484,487],{"class":153,"line":154},[151,479,480],{"class":168},"curl",[151,482,483],{"class":157}," -s",[151,485,486],{"class":161}," http:\u002F\u002Flocalhost:8080\u002Frealms\u002Ftabularia\u002Fprotocol\u002Fopenid-connect\u002Ftoken",[151,488,490],{"class":489},"sxsTv"," \\\n",[151,492,493,496,499],{"class":153,"line":165},[151,494,495],{"class":157},"  -d",[151,497,498],{"class":161}," grant_type=password",[151,500,490],{"class":489},[151,502,503,505,508],{"class":153,"line":187},[151,504,495],{"class":157},[151,506,507],{"class":161}," client_id=tabularia-gateway",[151,509,490],{"class":489},[151,511,513,515,518],{"class":153,"line":512},4,[151,514,495],{"class":157},[151,516,517],{"class":161}," client_secret=tabularia-dev-secret",[151,519,490],{"class":489},[151,521,523,525,528,531,534],{"class":153,"line":522},5,[151,524,495],{"class":157},[151,526,527],{"class":161}," username=alice",[151,529,530],{"class":157}," -d",[151,532,533],{"class":161}," password=YOUR_PASSWORD",[151,535,490],{"class":489},[151,537,539,541,544,546],{"class":153,"line":538},6,[151,540,495],{"class":157},[151,542,543],{"class":161}," scope='openid profile email'",[151,545,346],{"class":345},[151,547,490],{"class":489},[151,549,551,554,557,560,562,565,567,570,573,575,578,581],{"class":153,"line":550},7,[151,552,553],{"class":168},"  cut",[151,555,556],{"class":157}," -d.",[151,558,559],{"class":157}," -f2",[151,561,346],{"class":345},[151,563,564],{"class":168}," base64",[151,566,530],{"class":157},[151,568,569],{"class":345}," 2>",[151,571,572],{"class":161},"\u002Fdev\u002Fnull",[151,574,346],{"class":345},[151,576,577],{"class":168}," python3",[151,579,580],{"class":157}," -m",[151,582,583],{"class":161}," json.tool\n",[14,585,586],{},"You should see:",[142,588,592],{"className":589,"code":590,"language":591,"meta":147,"style":147},"language-json shiki shiki-themes github-dark-dimmed github-light","{\n  \"email\": \"alice@example.com\",\n  \"name\": \"Alice ...\",\n  \"groups\": [\"analytics\", \"tabularia-admins\"]\n}\n","json",[23,593,594,599,613,625,644],{"__ignoreMap":147},[151,595,596],{"class":153,"line":154},[151,597,598],{"class":374},"{\n",[151,600,601,605,607,610],{"class":153,"line":165},[151,602,604],{"class":603},"sJK54","  \"email\"",[151,606,243],{"class":374},[151,608,609],{"class":161},"\"alice@example.com\"",[151,611,612],{"class":374},",\n",[151,614,615,618,620,623],{"class":153,"line":187},[151,616,617],{"class":603},"  \"name\"",[151,619,243],{"class":374},[151,621,622],{"class":161},"\"Alice ...\"",[151,624,612],{"class":374},[151,626,627,630,633,636,638,641],{"class":153,"line":512},[151,628,629],{"class":603},"  \"groups\"",[151,631,632],{"class":374},": [",[151,634,635],{"class":161},"\"analytics\"",[151,637,247],{"class":374},[151,639,640],{"class":161},"\"tabularia-admins\"",[151,642,643],{"class":374},"]\n",[151,645,646],{"class":153,"line":522},[151,647,648],{"class":374},"}\n",[14,650,651,652,654,655,658,659,211],{},"That ",[23,653,20],{}," array is what ",[23,656,657],{},"provision_and_sync()"," maps onto ",[23,660,661],{},"user_groups",[14,663,664,667,668,671,672,675],{},[18,665,666],{},"Roles instead of groups."," If the team keeps the access in Keycloak ",[18,669,670],{},"roles",", point\n",[23,673,674],{},"OIDC__GROUPS_CLAIM"," at them with the path they have in the token's JSON:",[73,677,678,689],{},[76,679,680],{},[79,681,682,685],{},[82,683,684],{},"What",[82,686,687],{},[23,688,674],{},[89,690,691,701,715],{},[79,692,693,696],{},[94,694,695],{},"realm roles",[94,697,698],{},[23,699,700],{},"realm_access.roles",[79,702,703,706],{},[94,704,705],{},"roles of a client",[94,707,708,711,712],{},[23,709,710],{},"resource_access.\u003Cclient id>.roles",", e.g. ",[23,713,714],{},"resource_access.tabularia-gateway.roles",[79,716,717,720],{},[94,718,719],{},"groups and realm roles together",[94,721,722],{},[23,723,724],{},"groups,realm_access.roles",[14,726,727,728,731,732,735,736,739,740,247,743,612,746,749,750,753,754,757],{},"Each name maps onto the Tabularia group with the same name, as groups do. Keycloak puts\n",[23,729,730],{},"realm_access"," and ",[23,733,734],{},"resource_access"," in the ",[18,737,738],{},"access token"," only, by default: the gateway\nreads them there (it checks the signature with the realm's keys, the issuer, the expiry\nand that it is the same person) when the id_token does not carry the path, then asks\nuserinfo. Keycloak adds its own realm roles too (",[23,741,742],{},"offline_access",[23,744,745],{},"uma_authorization",[23,747,748],{},"default-roles-\u003Crealm>","): with ",[23,751,752],{},"OIDC__AUTO_CREATE_GROUPS=false"," they match no group and\nare ignored; ",[23,755,756],{},"OIDC__GROUP_ALLOWLIST"," keeps out whatever else should not count.",[68,759,761],{"id":760},"5-point-the-gateway-at-keycloak","5. Point the gateway at Keycloak",[14,763,764,765,767],{},"Copy the OIDC block into ",[23,766,136],{},":",[142,769,771],{"className":144,"code":770,"language":146,"meta":147,"style":147},"cat docs\u002Fexamples\u002Fkeycloak\u002Fgateway.env.example >> infrastructure\u002F.env\ndocker compose -f infrastructure\u002Fdocker-compose.yml up -d gateway\n",[23,772,773,787],{"__ignoreMap":147},[151,774,775,778,781,784],{"class":153,"line":154},[151,776,777],{"class":168},"cat",[151,779,780],{"class":161}," docs\u002Fexamples\u002Fkeycloak\u002Fgateway.env.example",[151,782,783],{"class":345}," >>",[151,785,786],{"class":161}," infrastructure\u002F.env\n",[151,788,789,791,793,795,798,800,802],{"class":153,"line":165},[151,790,169],{"class":168},[151,792,172],{"class":161},[151,794,175],{"class":157},[151,796,797],{"class":161}," infrastructure\u002Fdocker-compose.yml",[151,799,181],{"class":161},[151,801,530],{"class":157},[151,803,804],{"class":161}," gateway\n",[14,806,807,808,810,811,814,815,821,822,825],{},"Adjust ",[23,809,52],{}," \u002F ",[23,812,813],{},"OIDC__REDIRECT_URI"," for your host, and ",[18,816,817,818],{},"rotate\n",[23,819,820],{},"OIDC__CLIENT_SECRET"," (the example secret is dev-only). The gateway self-configures\nfrom ",[23,823,824],{},"${OIDC__ISSUER}\u002F.well-known\u002Fopenid-configuration"," and refuses to start if the\nblock is half-filled.",[14,827,828],{},"Check it took:",[142,830,832],{"className":144,"code":831,"language":146,"meta":147,"style":147},"curl -s http:\u002F\u002Flocalhost:8000\u002Fauth\u002Fsso\u002Fconfig     # {\"enabled\":true,...}\n",[23,833,834],{"__ignoreMap":147},[151,835,836,838,840,843],{"class":153,"line":154},[151,837,480],{"class":168},[151,839,483],{"class":157},[151,841,842],{"class":161}," http:\u002F\u002Flocalhost:8000\u002Fauth\u002Fsso\u002Fconfig",[151,844,845],{"class":190},"     # {\"enabled\":true,...}\n",[14,847,848,849,853,854,857,858,247,861,864,865,211],{},"Then open ",[27,850,851],{"href":851,"rel":852},"http:\u002F\u002Flocalhost:3000\u002Flogin",[38]," — the SSO button is there. First sign-in creates\nthe Tabularia user and syncs the groups; the audit log records ",[23,855,856],{},"auth.sso_login"," with the\ngroup delta, and before it each change on its own (",[23,859,860],{},"user.create",[23,862,863],{},"group.member_add","…)\nwith ",[23,866,867],{},"\"via\": \"sso\"",[68,869,871],{"id":870},"entra-id-msal","Entra ID (MSAL)",[14,873,874],{},"Same flow, three differences:",[196,876,877,886,904],{},[199,878,879,882,883,211],{},[18,880,881],{},"Issuer"," is ",[23,884,885],{},"https:\u002F\u002Flogin.microsoftonline.com\u002F\u003Ctenant-id>\u002Fv2.0",[199,887,888,891,892,895,896,898,899,903],{},[18,889,890],{},"Prefer app roles over groups",": set ",[23,893,894],{},"OIDC__GROUPS_CLAIM=roles",". Entra's ",[23,897,20],{},"\nclaim carries group ",[900,901,902],"em",{},"object IDs",", not names, and overflows to a Graph lookup for users\nin many groups; app roles are readable names defined on the app registration.",[199,905,906,907,910,911,914,915,810,918,211],{},"Register the redirect URI under ",[18,908,909],{},"Web"," (not SPA) since the gateway is a confidential\nclient, and create a client secret for it. The ",[23,912,913],{},"email"," claim may be absent: the\ngateway falls back to ",[23,916,917],{},"preferred_username",[23,919,920],{},"upn",[14,922,923],{},"Everything else (JWKS validation, JIT provisioning, name-keyed group sync, the policy\ntoggles) is identical.",[68,925,927],{"id":926},"troubleshooting","Troubleshooting",[14,929,930,931,934,935,938],{},"A failed sign-in lands back on the login page with ",[23,932,933],{},"?sso_error=\u003Ccode>",", and for the\ncommon ones a sentence telling the person what to do; the full reason is in the gateway\nlog, and the code in the audit log (",[900,936,937],{},"Failed SSO login","), never more in the browser, to\navoid leaking IdP or config details.",[73,940,941,950],{},[76,942,943],{},[79,944,945,947],{},[82,946,23],{},[82,948,949],{},"meaning",[89,951,952,964,994,1004,1014,1043,1057,1070,1080,1118,1131,1148,1162,1172],{},[79,953,954,959],{},[94,955,956],{},[23,957,958],{},"sso_disabled",[94,960,961,963],{},[23,962,52],{}," not set on the gateway",[79,965,966,971],{},[94,967,968],{},[23,969,970],{},"idp_untrusted",[94,972,973,974,977,978,981,982,985,986,989,990,993],{},"the provider's certificate is signed by an authority the gateway does not trust (a company one): give its certificate in ",[23,975,976],{},"OIDC__CA_FILE"," (the chart's ",[23,979,980],{},"oidc.caCert"," or ",[23,983,984],{},"oidc.caConfigMap","), or accept it with ",[23,987,988],{},"OIDC__VERIFY_TLS=false"," — see ",[27,991,60],{"href":992},"\u002Fdocs\u002Fsso-keycloak#a-certificate-the-gateway-does-not-trust"," for what that costs",[79,995,996,1001],{},[94,997,998],{},[23,999,1000],{},"idp_unreachable",[94,1002,1003],{},"discovery or token endpoint not reachable from the gateway container (usually the hostname wrinkle in step 2)",[79,1005,1006,1011],{},[94,1007,1008],{},[23,1009,1010],{},"idp_denied",[94,1012,1013],{},"the IdP refused the login (consent, disabled account, policy)",[79,1015,1016,1021],{},[94,1017,1018],{},[23,1019,1020],{},"tx_missing",[94,1022,1023,1024,1027,1028,1030,1031,1034,1035,1038,1039,1042],{},"the sign-in's cookie did not come back with the callback. Almost always the sign-in ",[18,1025,1026],{},"started at another address"," than ",[23,1029,813],{},"'s: another host name for the same server, an IP, ",[23,1032,1033],{},"http"," against ",[23,1036,1037],{},"https",". The browser keeps the cookies of each address apart. The gateway logs ",[23,1040,1041],{},"sign-in started at \u003Caddress> but OIDC__REDIRECT_URI is on \u003Caddress>"," when it begins: have people open Tabularia at the redirect URI's address (or register theirs too). Otherwise the browser blocks cookies",[79,1044,1045,1050],{},[94,1046,1047],{},[23,1048,1049],{},"tx_expired",[94,1051,1052,1053,1056],{},"the sign-in took longer than ",[23,1054,1055],{},"OIDC__LOGIN_TX_TTL_SECONDS"," (10 minutes; a first sign-in that asks to set a password or a second factor can), or the clocks of the gateway's machines are more than a minute apart: check NTP on the nodes",[79,1058,1059,1064],{},[94,1060,1061],{},[23,1062,1063],{},"state_mismatch",[94,1065,129,1066,1069],{},[23,1067,1068],{},"state"," Keycloak sent back is not the one of this sign-in (a tampered link, or a callback of an older sign-in replayed): start again. A second sign-in started meanwhile — a double click, another tab — no longer causes it: each sign-in has its own cookie",[79,1071,1072,1077],{},[94,1073,1074],{},[23,1075,1076],{},"code_rejected",[94,1078,1079],{},"the IdP refused the code exchange: usually a wrong client secret or a redirect URI that does not match exactly",[79,1081,1082,1093],{},[94,1083,1084,247,1087,247,1090],{},[23,1085,1086],{},"token_invalid",[23,1088,1089],{},"nonce_mismatch",[23,1091,1092],{},"jwks_error",[94,1094,1095,1096,247,1099,247,1102,247,1105,1108,1109,1111,1112,1115,1116],{},"id_token failed validation (signature, ",[23,1097,1098],{},"iss",[23,1100,1101],{},"aud",[23,1103,1104],{},"exp",[23,1106,1107],{},"nonce","). The gateway allows a minute between its clock and Keycloak's; further apart, an id_token is \"not yet valid\" or already expired: check NTP on both. A wrong ",[23,1110,1098],{}," is usually Keycloak's hostname (",[23,1113,1114],{},"KC_HOSTNAME",") differing from ",[23,1117,52],{},[79,1119,1120,1125],{},[94,1121,1122],{},[23,1123,1124],{},"no_email",[94,1126,1127,1128,1130],{},"the token carries no email, UPN or ",[23,1129,917],{}," in email form",[79,1132,1133,1138],{},[94,1134,1135],{},[23,1136,1137],{},"no_identity",[94,1139,1140,1141,1143,1144,1147],{},"the token carries no ",[23,1142,1098],{},"\u002F",[23,1145,1146],{},"sub",", so it identifies nobody. Tabularia recognises a user by that stable pair, never by the email alone",[79,1149,1150,1155],{},[94,1151,1152],{},[23,1153,1154],{},"email_unverified",[94,1156,1157,1158,1161],{},"an account with this email already exists and the IdP does not assert the address as ",[18,1159,1160],{},"verified",", so it will not be claimed. Linking an existing account happens once and requires proof; a brand-new account does not",[79,1163,1164,1169],{},[94,1165,1166],{},[23,1167,1168],{},"identity_conflict",[94,1170,1171],{},"that account is already linked to a different identity of the IdP. Two people cannot claim the same user",[79,1173,1174,1179],{},[94,1175,1176],{},[23,1177,1178],{},"user_disabled",[94,1180,1181],{},"the user exists in Tabularia but is deactivated",[1183,1184,1185],"style",{},"html pre.shiki code .swcJU, html code.shiki .swcJU{--shiki-default:#6CB6FF;--shiki-light:#005CC5}html pre.shiki code .sXfbr, html code.shiki .sXfbr{--shiki-default:#96D0FF;--shiki-light:#032F62}html pre.shiki code .sqRhv, html code.shiki .sqRhv{--shiki-default:#F69D50;--shiki-light:#6F42C1}html pre.shiki code .sgHix, html code.shiki .sgHix{--shiki-default:#768390;--shiki-light:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .s6PUj, html code.shiki .s6PUj{--shiki-default:#F47067;--shiki-light:#D73A49}html pre.shiki code .shb1k, html code.shiki .shb1k{--shiki-default:#8DDB8C;--shiki-light:#22863A}html pre.shiki code .ssh_m, html code.shiki .ssh_m{--shiki-default:#ADBAC7;--shiki-light:#24292E}html pre.shiki code .sxsTv, html code.shiki .sxsTv{--shiki-default:#F47067;--shiki-light:#005CC5}html pre.shiki code .sJK54, html code.shiki .sJK54{--shiki-default:#8DDB8C;--shiki-light:#005CC5}",{"title":147,"searchDepth":165,"depth":187,"links":1187},[1188,1189,1190,1191,1192,1193,1194,1195],{"id":70,"depth":165,"text":71},{"id":139,"depth":165,"text":140},{"id":266,"depth":165,"text":267},{"id":407,"depth":165,"text":408},{"id":463,"depth":165,"text":464},{"id":760,"depth":165,"text":761},{"id":870,"depth":165,"text":871},{"id":926,"depth":165,"text":927},"A runnable Keycloak realm whose groups land in the OIDC token as a groups claim,\nmapped 1:1 onto Tabularia groups by name. The gateway side is implemented: see\n..\u002F..\u002Fdesign\u002Fsso-group-mapping.md for the design\nand gateway\u002Fapp\u002Fservices\u002Fsso.py for the code.","md",{"source":1199},"docs\u002Fexamples\u002Fkeycloak\u002FREADME.md",true,"\u002Fdocs\u002Fexamples\u002Fkeycloak",{"title":5,"description":1196},"docs\u002Fexamples\u002Fkeycloak\u002FREADME","pvRpiyvdSwlX7YxdR_NBYiXi87ZFYFFEo2vXCSeE_-4",[1206,1225,1281,1313,1345,1372,1394,1420],{"title":1207,"icon":1208,"intro":1209,"items":1210},"Getting started","start","Install it on one machine, learn the few ideas everything else is built on, and run a first flow.",[1211,1213,1216,1219,1222],{"path":1212,"title":1207},"\u002Fdocs\u002Fgetting-started",{"path":1214,"title":1215},"\u002Fdocs\u002Fgetting-started-docker-compose","Walkthrough with Docker Compose",{"path":1217,"title":1218},"\u002Fdocs\u002Fdeploy\u002Fminikube","Try it on your computer (minikube)",{"path":1220,"title":1221},"\u002Fdocs\u002Fconcepts","Concepts",{"path":1223,"title":1224},"\u002Fdocs\u002Foverview","Overview (README)",{"title":1226,"icon":1227,"intro":1228,"items":1229},"Capabilities","capabilities","What you can build: visual flows, analysis on any step, AI questions, pixel-perfect reports, dashboards, Python, R and SQL scripts, containers and the orchestration that ties them together.",[1230,1233,1236,1239,1242,1245,1248,1251,1254,1257,1260,1263,1266,1269,1272,1275,1278],{"path":1231,"title":1232},"\u002Fdocs\u002Fcapabilities","All capabilities",{"path":1234,"title":1235},"\u002Fdocs\u002Fcapabilities\u002Fflows","Low-code flows",{"path":1237,"title":1238},"\u002Fdocs\u002Fcapabilities\u002Fpreview-and-analysis","Preview and analysis",{"path":1240,"title":1241},"\u002Fdocs\u002Fcapabilities\u002Fask-your-data","Ask your data with AI",{"path":1243,"title":1244},"\u002Fdocs\u002Fcapabilities\u002Freports","Pixel-perfect reports",{"path":1246,"title":1247},"\u002Fdocs\u002Fcapabilities\u002Freport-functions","Report functions",{"path":1249,"title":1250},"\u002Fdocs\u002Fcapabilities\u002Fdashboards","Dashboards",{"path":1252,"title":1253},"\u002Fdocs\u002Fscripting","Python and R",{"path":1255,"title":1256},"\u002Fdocs\u002Fsql-scripts","SQL scripts",{"path":1258,"title":1259},"\u002Fdocs\u002Fcapabilities\u002Fnotebooks","marimo notebooks as apps",{"path":1261,"title":1262},"\u002Fdocs\u002Fcontainer-node","Docker images in flows",{"path":1264,"title":1265},"\u002Fdocs\u002Fcapabilities\u002Fflow-groups","Flow groups: orchestration",{"path":1267,"title":1268},"\u002Fdocs\u002Fcapabilities\u002Fscheduling","Scheduling",{"path":1270,"title":1271},"\u002Fdocs\u002Fpushmail","Pushmail: datasources, reports and dashboards by email",{"path":1273,"title":1274},"\u002Fdocs\u002Fplaceholders","Dynamic dates",{"path":1276,"title":1277},"\u002Fdocs\u002Fcapabilities\u002Fengines","Engines",{"path":1279,"title":1280},"\u002Fdocs\u002Fengines\u002Fengine-differences","Engine differences",{"title":1282,"icon":1283,"intro":1284,"items":1285},"Sources","sources","Every way data comes in: files, databases, buckets, SFTP folders, HTTP APIs, SharePoint, Google Sheets, and the results of your own flows.",[1286,1289,1292,1295,1298,1301,1304,1307,1310],{"path":1287,"title":1288},"\u002Fdocs\u002Fsources","All sources",{"path":1290,"title":1291},"\u002Fdocs\u002Fsources\u002Ffiles","Uploaded files",{"path":1293,"title":1294},"\u002Fdocs\u002Fsources\u002Fdatabases","Database tables and queries",{"path":1296,"title":1297},"\u002Fdocs\u002Fs3-datasource","S3 buckets",{"path":1299,"title":1300},"\u002Fdocs\u002Fsftp","SFTP folders",{"path":1302,"title":1303},"\u002Fdocs\u002Fapi-datasource","HTTP APIs",{"path":1305,"title":1306},"\u002Fdocs\u002Fsources\u002Fsharepoint","Excel on SharePoint",{"path":1308,"title":1309},"\u002Fdocs\u002Fsources\u002Fgoogle-sheets","Google Sheets",{"path":1311,"title":1312},"\u002Fdocs\u002Fsources\u002Fflow-outputs","Results of flows and scripts",{"title":1314,"icon":1315,"intro":1316,"items":1317},"Connections","connections","The systems Tabularia reads from and writes to, how each connection is set up, and who may use it.",[1318,1321,1324,1327,1330,1332,1333,1335,1336,1339,1342],{"path":1319,"title":1320},"\u002Fdocs\u002Fconnections","All connections",{"path":1322,"title":1323},"\u002Fdocs\u002Fconnections\u002Fdatabases","Databases",{"path":1325,"title":1326},"\u002Fdocs\u002Fsqlserver","Microsoft SQL Server driver",{"path":1328,"title":1329},"\u002Fdocs\u002Fconnections\u002Fs3","S3 and compatible storage",{"path":1299,"title":1331},"SFTP servers",{"path":1302,"title":1303},{"path":1305,"title":1334},"SharePoint",{"path":1308,"title":1309},{"path":1337,"title":1338},"\u002Fdocs\u002Fconnections\u002Fsmtp","SMTP email",{"path":1340,"title":1341},"\u002Fdocs\u002Fcontainer-registries","Container registries",{"path":1343,"title":1344},"\u002Fdocs\u002Fconnections\u002Fdestinations","Writing data out",{"title":1346,"icon":1347,"intro":1348,"items":1349},"Integrations","integrations","How Tabularia fits your stack: AI providers, the REST API and webhooks, dbt, OpenLineage, single sign-on, SIEM and monitoring.",[1350,1353,1356,1359,1362,1365,1366,1369],{"path":1351,"title":1352},"\u002Fdocs\u002Fintegrations","All integrations",{"path":1354,"title":1355},"\u002Fdocs\u002Fintegrations\u002Fai","AI: where it is used",{"path":1357,"title":1358},"\u002Fdocs\u002Fapi","API, tokens and webhooks",{"path":1360,"title":1361},"\u002Fdocs\u002Fdbt-export","Export to dbt",{"path":1363,"title":1364},"\u002Fdocs\u002Fopenlineage","OpenLineage",{"path":59,"title":60},{"path":1367,"title":1368},"\u002Fdocs\u002Fsiem-splunk","Audit log to Splunk",{"path":1370,"title":1371},"\u002Fdocs\u002Fintegrations\u002Fmonitoring","Monitoring",{"title":1373,"icon":1374,"intro":1375,"items":1376},"Security & governance","governance","Who may see and do what, what is recorded, and how data quality and personal data are kept in check.",[1377,1380,1381,1384,1387,1389,1391],{"path":1378,"title":1379},"\u002Fdocs\u002Fgovernance","Overview",{"path":64,"title":65},{"path":1382,"title":1383},"\u002Fdocs\u002Fgovernance\u002Faudit-log","Audit log",{"path":1385,"title":1386},"\u002Fdocs\u002Fgovernance\u002Fdata-contracts","Data contracts",{"path":29,"title":1388},"SSO and group mapping",{"path":1201,"title":1390},"Keycloak example on a laptop",{"path":1392,"title":1393},"\u002Fdocs\u002Fengines\u002Fclickhouse-ai-user","A ClickHouse user for the AI assistant",{"title":1395,"icon":1396,"intro":1397,"items":1398},"Install & operate","operate","Run it in production on Kubernetes, configure it, upgrade it and keep it healthy.",[1399,1402,1405,1408,1411,1414,1417],{"path":1400,"title":1401},"\u002Fdocs\u002Fdeploy\u002Fkubernetes","Kubernetes and Helm",{"path":1403,"title":1404},"\u002Fdocs\u002Fdeploy\u002Fkind","A company-like cluster on one computer (kind)",{"path":1406,"title":1407},"\u002Fdocs\u002Fdeploy\u002Fupgrading","Upgrading, backups and restore",{"path":1409,"title":1410},"\u002Fdocs\u002Fdeploy\u002Frelease-checklist","Install and upgrade checklist",{"path":1412,"title":1413},"\u002Fdocs\u002Fconfiguration","Configuration reference",{"path":1415,"title":1416},"\u002Fdocs\u002Fengine-settings","Settings from the panel",{"path":1418,"title":1419},"\u002Fdocs\u002Fdatabase-migrations","Database migrations",{"title":1421,"icon":1422,"intro":1423,"items":1424},"Reference","reference","Release process and the remaining reference pages.",[1425],{"path":1426,"title":1427},"\u002Fdocs\u002Frelease-process","Release process",1793656372204]