Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/secure-tenancy-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hephaestus": patch
---

Workspace isolation checks now fail closed by default instead of allowing a detected unscoped query to continue.
2 changes: 2 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Hephaestus is pre-1.0 and released continuously from `main`; **only the latest r

In scope: the code in this repository — the Spring Boot application server (including the webhook receiver), the React webapp, and the deployment/Docker configuration we ship.

See the [published threat model](https://ls1intum.github.io/Hephaestus/admin/threat-model) for trust boundaries, attacker assumptions, controls, and residual risks.

Out of scope:

- Vulnerabilities in third-party dependencies without a Hephaestus-specific exploit path — report those upstream
Expand Down
153 changes: 153 additions & 0 deletions docs/admin/threat-model.mdx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,10 @@ above are not actually defense-in-depth and we need stronger SQL parsing or
controller-layer enforcement only); or
`tenancy.parse_failure.total` starts non-zero in prod (would mean the regex hits a
pathological input class worth handling explicitly).

## Update — 2026-08-30 ([issue #1603](https://github.com/ls1intum/Hephaestus/issues/1603))

`throw` is now the default in every profile. The canary period described above has ended; `log` and
`off` remain explicit diagnostic overrides and are not production settings. This changes only the
default response to a violation the inspector already detects. The documented parser carve-outs and
the requirement for workspace authorization remain unchanged.
1 change: 1 addition & 0 deletions docs/sidebars.admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const sidebars: SidebarsConfig = {
{ type: "doc", id: "compatibility-policy", label: "Compatibility Policy" },
{ type: "doc", id: "runtime-roles", label: "Runtime Roles" },
{ type: "doc", id: "configuration-readiness", label: "Configuration Readiness" },
{ type: "doc", id: "threat-model", label: "Threat Model" },
{ type: "doc", id: "release-image-lock", label: "Release image lock" },
{ type: "doc", id: "buildpacks-cds-decision", label: "Server image build (Buildpacks + CDS)" },
{ type: "doc", id: "legal-pages", label: "Legal Pages" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ private MentorChatMetrics.Outcome runTurnInternal(
// Both gates run before ANYTHING persists, so a refused turn leaves no partial rows and never
// warms a sandbox. Budget runs after admission: which purse applies depends on who pays for
// the bound model.
MentorLlmConfig llmConfig = resolveLlmConfig(request.workspaceId());
MentorLlmConfig llmConfig = resolveWorkspaceLlmConfig(request.workspaceId());

FundingSource mentorFunding =
Objects.requireNonNull(llmConfig.connectionScope(), "Mentor model must have a funding source");
Expand Down Expand Up @@ -663,13 +663,7 @@ private Map<String, byte[]> buildMentorContext(MentorTurnRequest request, User u
request.workspaceId(), user.getId(), request.threadId(), currentUserMessageId));
}

/**
* Resolve exactly the workspace's own mentor binding, failing closed rather than substituting
* another — a silent swap would change provider, model and price mid-conversation. SECURITY: the
* workspace-scoped finder, never a bare {@code findById}: prod tenancy enforcement only logs, so
* this query is the real cross-tenant guard.
*/
private MentorLlmConfig resolveLlmConfig(long workspaceId) {
private MentorLlmConfig resolveWorkspaceLlmConfig(long workspaceId) {
WorkspaceAgentBinding binding = agentBindingRepository
.findByWorkspaceIdAndPurpose(workspaceId, AgentPurpose.MENTOR)
.orElseThrow(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
package de.tum.cit.aet.hephaestus.core.tenancy;

/**
* Enforcement mode for {@link WorkspaceStatementInspector}. Configured via
* {@code hephaestus.tenancy.enforcement}; defaults to {@link #THROW} in test, {@link #LOG}
* elsewhere. Production flips to {@link #THROW} after a staging canary week of clean
* {@code tenancy.violation.total} counter readings.
*/
/** Enforcement mode for {@link WorkspaceStatementInspector}. */
public enum TenancyEnforcement {
/** Throw {@code TenancyViolationException} on any unguarded scoped-table query. */
THROW,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
/**
* {@code hephaestus.tenancy.*} configuration properties.
*
* <p>Default {@link TenancyEnforcement#LOG} in production-ish profiles (staging canary
* before flipping to THROW after one calendar week of clean counter readings);
* {@code application-test.yml} overrides to THROW so tests fail loudly on tenancy bugs.
* <p>Defaults to {@link TenancyEnforcement#THROW}. The {@code LOG} and {@code OFF} modes are
* explicit diagnostic overrides that weaken isolation enforcement.
*/
@ConfigurationProperties(prefix = "hephaestus.tenancy")
public record TenancyEnforcementProperties(TenancyEnforcement enforcement) {
public TenancyEnforcementProperties {
if (enforcement == null) {
enforcement = TenancyEnforcement.LOG;
enforcement = TenancyEnforcement.THROW;
}
}
}
14 changes: 6 additions & 8 deletions server/application/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -275,16 +275,14 @@ hephaestus:
# ═══════════════════════════════════════════════════════════════════════════
# TENANCY ENFORCEMENT (WorkspaceStatementInspector)
# ═══════════════════════════════════════════════════════════════════════════
# SQL-layer enforcement that every query against a workspace-scoped table
# carries a workspace_id predicate. See core/tenancy/* and ADR 0004.
# SQL-shape defect detection for workspace-scoped tables. This complements, but does
# not replace, workspace authorization. See core/tenancy/* and ADR 0004.
#
# throw — TenancyViolationException on violations (fail loud; default in test)
# log — log + Micrometer counter, query passes (default outside test —
# one calendar week of clean counter readings before flipping to throw
# in prod)
# off — disable inspection (only for perf debugging)
# throw — TenancyViolationException on violations (default)
# log — log + Micrometer counter, query passes (diagnostics only)
# off — disable inspection entirely (diagnostics only)
tenancy:
enforcement: ${HEPHAESTUS_TENANCY_ENFORCEMENT:log}
enforcement: ${HEPHAESTUS_TENANCY_ENFORCEMENT:throw}

# Provider-agnostic sync scheduler (SyncSchedulerProperties). The GitHub-specific sync
# tuning (GraphQL timeouts, incremental buffer, backfill page size) lives under
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package de.tum.cit.aet.hephaestus.core.tenancy;

import static org.assertj.core.api.Assertions.assertThat;

import de.tum.cit.aet.hephaestus.testconfig.BaseUnitTest;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Configuration;

class TenancyEnforcementPropertiesTest extends BaseUnitTest {

private final ApplicationContextRunner contextRunner =
new ApplicationContextRunner().withUserConfiguration(TestConfiguration.class);

@Test
void defaultsToThrow() {
contextRunner.run(context -> assertThat(
context.getBean(TenancyEnforcementProperties.class).enforcement())
.isEqualTo(TenancyEnforcement.THROW));
}

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(TenancyEnforcementProperties.class)
static class TestConfiguration {}
}
6 changes: 3 additions & 3 deletions webapp/src/api/@tanstack/react-query.gen.ts

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions webapp/src/api/client.gen.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts

import { type Client, type ClientOptions, type Config, createClient, createConfig } from './client';
import type { ClientOptions as ClientOptions2 } from './types.gen';
import { type Client, type ClientOptions, type Config, createClient, createConfig } from './client/index.js';
import type { ClientOptions as ClientOptions2 } from './types.gen.js';

/**
* The `createClientConfig()` function will be called on client initialization
Expand Down
10 changes: 5 additions & 5 deletions webapp/src/api/client/client.gen.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// This file is auto-generated by @hey-api/openapi-ts

import { createSseClient } from '../core/serverSentEvents.gen';
import type { HttpMethod } from '../core/types.gen';
import { getValidRequestBody } from '../core/utils.gen';
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen';
import { createSseClient } from '../core/serverSentEvents.gen.js';
import type { HttpMethod } from '../core/types.gen.js';
import { getValidRequestBody } from '../core/utils.gen.js';
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen.js';
import {
buildUrl,
createConfig,
Expand All @@ -12,7 +12,7 @@ import {
mergeConfigs,
mergeHeaders,
setAuthParams,
} from './utils.gen';
} from './utils.gen.js';

type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
body?: any;
Expand Down
20 changes: 10 additions & 10 deletions webapp/src/api/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// This file is auto-generated by @hey-api/openapi-ts

export type { Auth } from '../core/auth.gen';
export type { QuerySerializerOptions } from '../core/bodySerializer.gen';
export type { Auth } from '../core/auth.gen.js';
export type { QuerySerializerOptions } from '../core/bodySerializer.gen.js';
export {
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export type { ServerSentEventsResult } from '../core/serverSentEvents.gen';
export type { ClientMeta } from '../core/types.gen';
export { createClient } from './client.gen';
} from '../core/bodySerializer.gen.js';
export { buildClientParams } from '../core/params.gen.js';
export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen.js';
export type { ServerSentEventsResult } from '../core/serverSentEvents.gen.js';
export type { ClientMeta } from '../core/types.gen.js';
export { createClient } from './client.gen.js';
export type {
Client,
ClientOptions,
Expand All @@ -23,5 +23,5 @@ export type {
ResolvedRequestOptions,
ResponseStyle,
TDataShape,
} from './types.gen';
export { createConfig, mergeHeaders } from './utils.gen';
} from './types.gen.js';
export { createConfig, mergeHeaders } from './utils.gen.js';
8 changes: 4 additions & 4 deletions webapp/src/api/client/types.gen.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// This file is auto-generated by @hey-api/openapi-ts

import type { Auth } from '../core/auth.gen';
import type { Auth } from '../core/auth.gen.js';
import type {
ServerSentEventsOptions,
ServerSentEventsResult,
} from '../core/serverSentEvents.gen';
import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen';
import type { Middleware } from './utils.gen';
} from '../core/serverSentEvents.gen.js';
import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen.js';
import type { Middleware } from './utils.gen.js';

export type ResponseStyle = 'data' | 'fields';

Expand Down
12 changes: 6 additions & 6 deletions webapp/src/api/client/utils.gen.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// This file is auto-generated by @hey-api/openapi-ts

import { getAuthToken } from '../core/auth.gen';
import type { QuerySerializerOptions } from '../core/bodySerializer.gen';
import { jsonBodySerializer } from '../core/bodySerializer.gen';
import { getAuthToken } from '../core/auth.gen.js';
import type { QuerySerializerOptions } from '../core/bodySerializer.gen.js';
import { jsonBodySerializer } from '../core/bodySerializer.gen.js';
import {
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from '../core/pathSerializer.gen';
import { getUrl } from '../core/utils.gen';
import type { Client, ClientOptions, Config, RequestOptions } from './types.gen';
} from '../core/pathSerializer.gen.js';
import { getUrl } from '../core/utils.gen.js';
import type { Client, ClientOptions, Config, RequestOptions } from './types.gen.js';

export const createQuerySerializer = <T = unknown>({
parameters = {},
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/api/core/bodySerializer.gen.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// This file is auto-generated by @hey-api/openapi-ts

import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen';
import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen.js';

export type QuerySerializer = (query: Record<string, unknown>) => string;

Expand Down
2 changes: 1 addition & 1 deletion webapp/src/api/core/serverSentEvents.gen.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// This file is auto-generated by @hey-api/openapi-ts

import type { Config } from './types.gen';
import type { Config } from './types.gen.js';

export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, 'method'> &
Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & {
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/api/core/types.gen.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts

import type { Auth, AuthToken } from './auth.gen';
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen';
import type { Auth, AuthToken } from './auth.gen.js';
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen.js';

export type HttpMethod =
| 'connect'
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/api/core/utils.gen.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// This file is auto-generated by @hey-api/openapi-ts

import type { BodySerializer, QuerySerializer } from './bodySerializer.gen';
import type { BodySerializer, QuerySerializer } from './bodySerializer.gen.js';
import {
type ArraySeparatorStyle,
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from './pathSerializer.gen';
} from './pathSerializer.gen.js';

export interface PathSerializer {
path: Record<string, unknown>;
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/api/index.ts

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions webapp/src/api/sdk.gen.ts

Large diffs are not rendered by default.

Loading
Loading