8 Commits

Author SHA1 Message Date
Sharang Parnerkar bd6d4572e0 feat(keycloak): add tenant-context client scope + M7.1 test users
CI / Tests (push) Has been skipped
CI / Format (pull_request) Successful in 4s
CI / Format (push) Successful in 21s
CI / Clippy (push) Successful in 2m59s
CI / Security Audit (push) Has been skipped
CI / Clippy (pull_request) Successful in 2m59s
CI / Security Audit (pull_request) Has been skipped
CI / Tests (pull_request) Has been skipped
CI / Deploy (push) Has been skipped
CI / Deploy (pull_request) Has been skipped
CI / E2E Tests (push) Has been skipped
CI / E2E Tests (pull_request) Has been skipped
Adds the breakpilot platform multi-tenancy claims to the dev realm
so M7.x products (starting with compliance-scanner-agent) can
authenticate against the local CERTifAI stack end-to-end.

New tenant-context client scope, included by default on all three
clients, with six protocol mappers backed by user attributes:

  tenant_id, tenant_slug, tenant_status, plan (strings)
  org_roles, products (multi-valued)

Five test users cover every tenant_status branch:

  admin@certifai.local  (acme, active, IT_ADMIN + CXO)
  user@certifai.local   (acme, active, USER)
  trial@acme.local      (trialco, trial)
  frozen@acme.local     (frozenco, frozen)   -> 402 on writes
  archived@acme.local   (archiveco, archived) -> 410 always

Enables Direct Access Grants on certifai-dashboard so password-
grant requests work for local API testing. This is the dev realm
only (KC_DB: dev-mem); prod realms inherit nothing from this file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:21:04 +02:00
Sharang Parnerkar 78b215bb77 ci: retrigger after transient clippy failure
CI / Format (push) Successful in 4s
CI / Clippy (push) Successful in 2m54s
CI / Security Audit (push) Successful in 1m45s
CI / Tests (push) Successful in 4m11s
CI / Deploy (push) Successful in 1m28s
CI / E2E Tests (push) Failing after 1s
2026-04-08 16:20:43 +02:00
Sharang Parnerkar 58428892a8 ci: log orca webhook response so the step isnt silent on success
CI / Format (push) Successful in 5s
CI / Clippy (push) Failing after 2s
CI / Security Audit (push) Successful in 1m52s
CI / Tests (push) Has been skipped
CI / Deploy (push) Has been skipped
CI / E2E Tests (push) Has been skipped
2026-04-08 15:09:01 +02:00
Sharang Parnerkar 24b604ce51 ci: install openssl for orca webhook HMAC signing
CI / Format (push) Successful in 5s
CI / Clippy (push) Successful in 2m59s
CI / Security Audit (push) Successful in 1m54s
CI / Tests (push) Successful in 4m16s
CI / Deploy (push) Successful in 1m25s
CI / E2E Tests (push) Failing after 1s
2026-04-08 14:56:12 +02:00
Sharang Parnerkar a02827a34b ci: smoke test full deploy cycle (build → push → orca redeploy)
CI / Format (push) Successful in 4s
CI / Clippy (push) Successful in 3m4s
CI / Security Audit (push) Successful in 1m43s
CI / Tests (push) Successful in 3m59s
CI / Deploy (push) Failing after 1m19s
CI / E2E Tests (push) Has been skipped
2026-04-08 12:58:53 +02:00
Sharang Parnerkar 5b431f65dc chore(deps): cargo update to fix audit vulnerabilities
CI / Format (push) Successful in 4s
CI / Clippy (push) Successful in 2m46s
CI / Security Audit (push) Successful in 1m44s
CI / Tests (push) Successful in 3m53s
CI / Deploy (push) Successful in 13m45s
CI / E2E Tests (push) Failing after 15s
Bumps transitive deps (aws-lc-sys, quinn-proto, rustls-webpki, etc.)
to versions without RUSTSEC advisories. Two unmaintained-warning
deps remain (fxhash via scraper, instant via async-stripe) but
those are non-blocking warnings only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:05:36 +02:00
Sharang Parnerkar b5ee887387 ci: replace coolify webhook with orca deploy
CI / Format (push) Successful in 22s
CI / Clippy (push) Successful in 2m49s
CI / Security Audit (push) Failing after 1m43s
CI / Tests (push) Has been skipped
CI / Deploy (push) Has been skipped
CI / E2E Tests (push) Has been skipped
Build and push image to registry, then trigger orca redeploy via
HMAC-signed webhook. Coolify webhook is no longer the source of truth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 09:31:43 +02:00
sharang 75a35dbb85 feat(sidebar): add compliance scanner link from env config (#19)
CI / Format (push) Successful in 4s
CI / Clippy (push) Successful in 2m43s
CI / Security Audit (push) Successful in 1m37s
CI / Tests (push) Successful in 3m54s
CI / Deploy (push) Successful in 2s
CI / E2E Tests (push) Failing after 31s
2026-03-09 08:39:00 +00:00
14 changed files with 658 additions and 350 deletions
+5
View File
@@ -74,6 +74,11 @@ LANGGRAPH_URL=
LANGFLOW_URL=
LANGFUSE_URL=
# ---------------------------------------------------------------------------
# Compliance scanner (external tool, opens in new tab) [OPTIONAL]
# ---------------------------------------------------------------------------
COMPLIANCE_SCANNER_URL=
# ---------------------------------------------------------------------------
# Vector database [OPTIONAL]
# ---------------------------------------------------------------------------
+25 -5
View File
@@ -262,10 +262,30 @@ jobs:
needs: [test]
if: github.ref == 'refs/heads/main'
container:
image: alpine:latest
image: docker:27-cli
steps:
- name: Trigger Coolify deploy
- name: Checkout
run: |
apk add --no-cache curl
curl -sf "${{ secrets.COOLIFY_WEBHOOK }}" \
-H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}"
apk add --no-cache git curl openssl
git init
git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
git fetch --depth=1 origin "${GITHUB_SHA}"
git checkout FETCH_HEAD
- name: Build and push image
run: |
IMAGE=registry.meghsakha.com/certifai-dashboard
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
docker login registry.meghsakha.com -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
docker build -t "$IMAGE:latest" -t "$IMAGE:${GITHUB_SHA}" .
docker push "$IMAGE:latest"
docker push "$IMAGE:${GITHUB_SHA}"
- name: Trigger orca redeploy
run: |
PAYLOAD=$(printf '{"ref":"refs/heads/main","repository":{"full_name":"sharang/certifai"},"head_commit":{"id":"%s","message":"CI deploy"}}' "${GITHUB_SHA}")
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "${{ secrets.ORCA_WEBHOOK_SECRET }}" | awk '{print $2}')
echo "Calling orca webhook for sharang/certifai@${GITHUB_SHA}"
RESP=$(curl -fsS -w "\nHTTP %{http_code}" -X POST "http://46.225.100.82:6880/api/v1/webhooks/github" \
-H "Content-Type: application/json" \
-H "X-Hub-Signature-256: sha256=$SIG" \
-d "$PAYLOAD")
echo "$RESP"
Generated
+393 -332
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -46,7 +46,8 @@
"agents": "Agenten",
"flow": "Flow",
"analytics": "Analytics",
"pricing": "Preise"
"pricing": "Preise",
"compliance": "Compliance"
},
"auth": {
"redirecting_login": "Weiterleitung zur Anmeldung...",
+2 -1
View File
@@ -46,7 +46,8 @@
"agents": "Agents",
"flow": "Flow",
"analytics": "Analytics",
"pricing": "Pricing"
"pricing": "Pricing",
"compliance": "Compliance"
},
"auth": {
"redirecting_login": "Redirecting to login...",
+2 -1
View File
@@ -46,7 +46,8 @@
"agents": "Agentes",
"flow": "Flujo",
"analytics": "Estadisticas",
"pricing": "Precios"
"pricing": "Precios",
"compliance": "Cumplimiento"
},
"auth": {
"redirecting_login": "Redirigiendo al inicio de sesion...",
+2 -1
View File
@@ -46,7 +46,8 @@
"agents": "Agents",
"flow": "Flux",
"analytics": "Analytique",
"pricing": "Tarifs"
"pricing": "Tarifs",
"compliance": "Conformite"
},
"auth": {
"redirecting_login": "Redirection vers la connexion...",
+2 -1
View File
@@ -46,7 +46,8 @@
"agents": "Agentes",
"flow": "Fluxo",
"analytics": "Analise",
"pricing": "Precos"
"pricing": "Precos",
"compliance": "Conformidade"
},
"auth": {
"redirecting_login": "A redirecionar para o inicio de sessao...",
+202 -6
View File
@@ -53,7 +53,7 @@
"description": "CERTifAI administration dashboard",
"enabled": true,
"publicClient": true,
"directAccessGrantsEnabled": false,
"directAccessGrantsEnabled": true,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"serviceAccountsEnabled": false,
@@ -73,7 +73,8 @@
"defaultClientScopes": [
"openid",
"profile",
"email"
"email",
"tenant-context"
],
"optionalClientScopes": [
"offline_access"
@@ -106,7 +107,8 @@
"defaultClientScopes": [
"openid",
"profile",
"email"
"email",
"tenant-context"
],
"optionalClientScopes": [
"offline_access"
@@ -139,7 +141,8 @@
"defaultClientScopes": [
"openid",
"profile",
"email"
"email",
"tenant-context"
],
"optionalClientScopes": [
"offline_access"
@@ -269,6 +272,105 @@
}
}
]
},
{
"name": "tenant-context",
"description": "Breakpilot platform tenant + org claims (M7.1)",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "false"
},
"protocolMappers": [
{
"name": "tenant_id",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"user.attribute": "tenant_id",
"claim.name": "tenant_id",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true",
"jsonType.label": "String"
}
},
{
"name": "tenant_slug",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"user.attribute": "tenant_slug",
"claim.name": "tenant_slug",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true",
"jsonType.label": "String"
}
},
{
"name": "tenant_status",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"user.attribute": "tenant_status",
"claim.name": "tenant_status",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true",
"jsonType.label": "String"
}
},
{
"name": "plan",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"user.attribute": "plan",
"claim.name": "plan",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true",
"jsonType.label": "String"
}
},
{
"name": "org_roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"user.attribute": "org_roles",
"claim.name": "org_roles",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true",
"jsonType.label": "String",
"multivalued": "true",
"aggregate.attrs": "true"
}
},
{
"name": "products",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"user.attribute": "products",
"claim.name": "products",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true",
"jsonType.label": "String",
"multivalued": "true",
"aggregate.attrs": "true"
}
}
]
}
],
"users": [
@@ -289,7 +391,15 @@
"realmRoles": [
"admin",
"user"
]
],
"attributes": {
"tenant_id": ["00000000-0000-0000-0000-000000000001"],
"tenant_slug": ["acme"],
"tenant_status": ["active"],
"plan": ["professional"],
"org_roles": ["IT_ADMIN", "CXO"],
"products": ["compliance", "certifai"]
}
},
{
"username": "user@certifai.local",
@@ -307,7 +417,93 @@
],
"realmRoles": [
"user"
]
],
"attributes": {
"tenant_id": ["00000000-0000-0000-0000-000000000001"],
"tenant_slug": ["acme"],
"tenant_status": ["active"],
"plan": ["professional"],
"org_roles": ["USER"],
"products": ["compliance"]
}
},
{
"username": "frozen@acme.local",
"email": "frozen@acme.local",
"firstName": "Frozen",
"lastName": "Tenant",
"enabled": true,
"emailVerified": true,
"credentials": [
{
"type": "password",
"value": "frozen",
"temporary": false
}
],
"realmRoles": [
"user"
],
"attributes": {
"tenant_id": ["00000000-0000-0000-0000-000000000002"],
"tenant_slug": ["frozenco"],
"tenant_status": ["frozen"],
"plan": ["starter"],
"org_roles": ["IT_ADMIN"],
"products": ["compliance"]
}
},
{
"username": "archived@acme.local",
"email": "archived@acme.local",
"firstName": "Archived",
"lastName": "Tenant",
"enabled": true,
"emailVerified": true,
"credentials": [
{
"type": "password",
"value": "archived",
"temporary": false
}
],
"realmRoles": [
"user"
],
"attributes": {
"tenant_id": ["00000000-0000-0000-0000-000000000003"],
"tenant_slug": ["archiveco"],
"tenant_status": ["archived"],
"plan": ["starter"],
"org_roles": ["IT_ADMIN"],
"products": ["compliance"]
}
},
{
"username": "trial@acme.local",
"email": "trial@acme.local",
"firstName": "Trial",
"lastName": "Tenant",
"enabled": true,
"emailVerified": true,
"credentials": [
{
"type": "password",
"value": "trial",
"temporary": false
}
],
"realmRoles": [
"user"
],
"attributes": {
"tenant_id": ["00000000-0000-0000-0000-000000000004"],
"tenant_slug": ["trialco"],
"tenant_status": ["trial"],
"plan": ["starter"],
"org_roles": ["IT_ADMIN"],
"products": ["compliance"]
}
}
]
}
+1
View File
@@ -76,6 +76,7 @@ pub fn AppShell() -> Element {
name: info.name,
avatar_url: info.avatar_url,
librechat_url: info.librechat_url,
compliance_scanner_url: info.compliance_scanner_url,
class: sidebar_cls,
on_nav: move |_| mobile_menu_open.set(false),
}
+13 -2
View File
@@ -1,7 +1,7 @@
use dioxus::prelude::*;
use dioxus_free_icons::icons::bs_icons::{
BsBoxArrowRight, BsBuilding, BsChatDots, BsCloudArrowUp, BsCodeSlash, BsGithub, BsGlobe2,
BsGrid, BsHouseDoor, BsMoonFill, BsSunFill,
BsGrid, BsHouseDoor, BsMoonFill, BsShieldCheck, BsSunFill,
};
use dioxus_free_icons::Icon;
@@ -44,13 +44,14 @@ pub fn Sidebar(
email: String,
avatar_url: String,
#[props(default = "http://localhost:3080".to_string())] librechat_url: String,
#[props(default)] compliance_scanner_url: String,
#[props(default = "sidebar".to_string())] class: String,
#[props(default)] on_nav: EventHandler<()>,
) -> Element {
let locale = use_context::<Signal<Locale>>();
let locale_val = *locale.read();
let nav_items: Vec<NavItem> = vec![
let mut nav_items: Vec<NavItem> = vec![
NavItem {
key: "dashboard",
label: t(locale_val, "nav.dashboard"),
@@ -84,6 +85,16 @@ pub fn Sidebar(
},
];
// Only show the compliance scanner link when a URL is configured.
if !compliance_scanner_url.is_empty() {
nav_items.push(NavItem {
key: "compliance",
label: t(locale_val, "nav.compliance"),
target: NavTarget::External(compliance_scanner_url.clone()),
icon: rsx! { Icon { icon: BsShieldCheck, width: 18, height: 18 } },
});
}
// Determine current path to highlight the active nav link.
let current_route = use_route::<Route>();
let logout_label = t(locale_val, "common.logout");
+2
View File
@@ -35,6 +35,7 @@ pub async fn check_auth() -> Result<AuthInfo, ServerFnError> {
let langgraph_url = state.services.langgraph_url.clone();
let langflow_url = state.services.langflow_url.clone();
let langfuse_url = state.services.langfuse_url.clone();
let compliance_scanner_url = state.services.compliance_scanner_url.clone();
Ok(AuthInfo {
authenticated: true,
@@ -46,6 +47,7 @@ pub async fn check_auth() -> Result<AuthInfo, ServerFnError> {
langgraph_url,
langflow_url,
langfuse_url,
compliance_scanner_url,
})
}
None => Ok(AuthInfo::default()),
+3
View File
@@ -168,6 +168,8 @@ pub struct ServiceUrls {
pub s3_access_key: String,
/// S3 secret key (wrapped for debug safety).
pub s3_secret_key: SecretString,
/// Compliance scanner URL (external tool opened in a new tab).
pub compliance_scanner_url: String,
}
impl ServiceUrls {
@@ -194,6 +196,7 @@ impl ServiceUrls {
s3_url: optional_env("S3_URL"),
s3_access_key: optional_env("S3_ACCESS_KEY"),
s3_secret_key: SecretString::from(optional_env("S3_SECRET_KEY")),
compliance_scanner_url: optional_env("COMPLIANCE_SCANNER_URL"),
})
}
}
+4
View File
@@ -30,6 +30,8 @@ pub struct AuthInfo {
pub langflow_url: String,
/// Langfuse observability URL (empty if not configured)
pub langfuse_url: String,
/// Compliance scanner URL (empty if not configured)
pub compliance_scanner_url: String,
}
/// Per-user LLM provider configuration stored in MongoDB.
@@ -100,6 +102,7 @@ mod tests {
assert_eq!(info.langgraph_url, "");
assert_eq!(info.langflow_url, "");
assert_eq!(info.langfuse_url, "");
assert_eq!(info.compliance_scanner_url, "");
}
#[test]
@@ -114,6 +117,7 @@ mod tests {
langgraph_url: "http://localhost:8123".into(),
langflow_url: "http://localhost:7860".into(),
langfuse_url: "http://localhost:3000".into(),
compliance_scanner_url: "http://localhost:9090".into(),
};
let json = serde_json::to_string(&info).expect("serialize AuthInfo");
let back: AuthInfo = serde_json::from_str(&json).expect("deserialize AuthInfo");