Phase 4: SSO scaffolding, RBAC enforcement, tenant-scoped dashboards, audit logging, K8s deployment
RBAC (api/internal/authz) is live on /query and /dashboards, backed by a new enterprise/ module (session issuance, audit logging, RBAC storage, OIDC/SAML protocol wiring) that core never imports -- only calls over HTTP. Found and fixed a real cross-tenant vulnerability in dashboards (no tenant_id filtering at all) while writing the threat model doc. Two things are explicitly NOT done, documented rather than hidden: tenant isolation for log data itself (/query still shares one ClickHouse connection and Tantivy index across every tenant -- RBAC controls who can query, not what a query can see), and human SSO login (protocol wiring exists, no HTTP handler calls it yet). See docs/security/threat-model.md and docs/phase-4-runbook.md. Also adds deploy/ (Go Operator + Helm chart, validated offline only -- no cluster was reachable in this environment).
This commit is contained in:
@@ -5,6 +5,12 @@
|
||||
|
||||
export const apiBase = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8080';
|
||||
export const alertingBase = import.meta.env.VITE_ALERTING_API_BASE_URL ?? 'http://localhost:8081';
|
||||
// Optional third backend (Phase 4, commercial-license) -- undefined in a
|
||||
// deployment that hasn't built/deployed enterprise-auth, same "runtime
|
||||
// capability check" shape /docs/phase-4-rbac-design.md's Web UI boundary
|
||||
// section describes. getAuthFeatures below treats a missing base URL the
|
||||
// same as a failed fetch: everything reports disabled, no broken links.
|
||||
export const enterpriseAuthBase = import.meta.env.VITE_ENTERPRISE_AUTH_BASE_URL as string | undefined;
|
||||
|
||||
export type Language = '' | 'sql' | 'spl';
|
||||
|
||||
@@ -118,6 +124,31 @@ export function deletePanel(dashboardId: string, panelId: string): Promise<void>
|
||||
return request(`/dashboards/${dashboardId}/panels/${panelId}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export type AuthFeatures = { sso_configured: boolean; oidc_enabled: boolean; saml_enabled: boolean };
|
||||
|
||||
// getAuthFeatures never throws -- an absent/unreachable enterprise-auth
|
||||
// is a normal, expected deployment shape (Phase 0-3-style single-tenant,
|
||||
// no enterprise/ deployed), not an error condition the settings page
|
||||
// should surface. Deliberately no `credentials: 'include'` here: this
|
||||
// endpoint needs no session, and CORS_ALLOWED_ORIGIN's dev default of
|
||||
// "*" can't be combined with credentialed fetches anyway (the browser
|
||||
// rejects that combination) -- the same reason api.ts's other requests
|
||||
// don't send credentials yet. Wiring session cookies through is part of
|
||||
// the deferred login-flow work, alongside tightening CORS_ALLOWED_ORIGIN
|
||||
// to a real origin, done together as one change so neither breaks the
|
||||
// other.
|
||||
export async function getAuthFeatures(): Promise<AuthFeatures> {
|
||||
const disabled = { sso_configured: false, oidc_enabled: false, saml_enabled: false };
|
||||
if (!enterpriseAuthBase) return disabled;
|
||||
try {
|
||||
const res = await fetch(`${enterpriseAuthBase}/auth/features`);
|
||||
if (!res.ok) return disabled;
|
||||
return await res.json();
|
||||
} catch {
|
||||
return disabled;
|
||||
}
|
||||
}
|
||||
|
||||
export function exportDashboard(id: string): Promise<Dashboard> {
|
||||
return request(`/dashboards/${id}/export`);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<a href="/">Query</a>
|
||||
<a href="/dashboards">Dashboards</a>
|
||||
<a href="/alerts">Alerts</a>
|
||||
<a href="/settings">Settings</a>
|
||||
</nav>
|
||||
|
||||
{@render children()}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { getAuthFeatures, enterpriseAuthBase, type AuthFeatures } from '$lib/api';
|
||||
|
||||
let loading = $state(true);
|
||||
let features = $state<AuthFeatures>({ sso_configured: false, oidc_enabled: false, saml_enabled: false });
|
||||
|
||||
async function load() {
|
||||
loading = true;
|
||||
features = await getAuthFeatures();
|
||||
loading = false;
|
||||
}
|
||||
load();
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<h1>Settings</h1>
|
||||
|
||||
<section>
|
||||
<h2>Core</h2>
|
||||
<p>Single-tenant deployment settings live here. Nothing configurable yet.</p>
|
||||
</section>
|
||||
|
||||
{#if loading}
|
||||
<p>Loading…</p>
|
||||
{:else if features.sso_configured}
|
||||
<section>
|
||||
<h2>Single sign-on</h2>
|
||||
<ul>
|
||||
{#if features.oidc_enabled}<li>OIDC configured</li>{/if}
|
||||
{#if features.saml_enabled}<li>SAML configured</li>{/if}
|
||||
</ul>
|
||||
<p class="note">
|
||||
User/role management isn't built yet -- an Admin or Owner will manage tenant members here once
|
||||
it is.
|
||||
</p>
|
||||
</section>
|
||||
{:else if enterpriseAuthBase}
|
||||
<section>
|
||||
<h2>Single sign-on</h2>
|
||||
<p class="note">enterprise-auth is deployed but no SSO provider is configured yet.</p>
|
||||
</section>
|
||||
{/if}
|
||||
<!-- No section at all when enterpriseAuthBase is unset -- a Phase 0-3-style
|
||||
single-tenant deployment with no enterprise/ deployed sees a settings
|
||||
page with core content only, no dead "upgrade to unlock" link. See
|
||||
/docs/phase-4-rbac-design.md's "Web UI boundary" section. -->
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
font-family: system-ui, sans-serif;
|
||||
max-width: 960px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
section {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.note {
|
||||
color: #666;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
// Same shape as dashboards/+page.ts: no route params, data comes from a
|
||||
// client-side fetch.
|
||||
export const prerender = true;
|
||||
Reference in New Issue
Block a user