mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
feat(security): gate Swagger/OpenAPI UI behind mateclaw.openapi.expose-ui flag
- explicit SecurityConfig authorization for /swagger-ui*, /v3/api-docs*, /webjars/** - public for local/default profile; admin-only (ROLE_ADMIN) by default in production DB profiles - override via MATECLAW_OPENAPI_EXPOSE_UI; add RANDOM_PORT integration tests and docs
This commit is contained in:
parent
865513a3b6
commit
982c6c048c
@ -34,6 +34,11 @@ MATECLAW_CORS_ALLOWED_ORIGINS=
|
|||||||
# 留空时回退到当前请求的 host,再退回相对路径。反代后部署建议显式设置。
|
# 留空时回退到当前请求的 host,再退回相对路径。反代后部署建议显式设置。
|
||||||
MATECLAW_PUBLIC_BASE_URL=
|
MATECLAW_PUBLIC_BASE_URL=
|
||||||
|
|
||||||
|
# 是否公开 Swagger UI / OpenAPI 文档(/swagger-ui.html、/v3/api-docs)。
|
||||||
|
# 生产数据库 profile(mysql/kingbase/postgres)默认 false —— 匿名无法浏览全部
|
||||||
|
# 端点结构,需全局管理员(ROLE_ADMIN)。仅在内网/预发临时调试时设为 true。
|
||||||
|
MATECLAW_OPENAPI_EXPOSE_UI=
|
||||||
|
|
||||||
# SearXNG 会话密钥(容器内部用,留空会用开发默认值)。生产部署请设成 32+ 位随机串。
|
# SearXNG 会话密钥(容器内部用,留空会用开发默认值)。生产部署请设成 32+ 位随机串。
|
||||||
# openssl rand -hex 32
|
# openssl rand -hex 32
|
||||||
SEARXNG_SECRET=
|
SEARXNG_SECRET=
|
||||||
|
|||||||
@ -27,11 +27,12 @@ import java.util.List;
|
|||||||
* Personal Access Token 以 {@code mc_} 开头(PAT_PREFIX)。因此 Swagger
|
* Personal Access Token 以 {@code mc_} 开头(PAT_PREFIX)。因此 Swagger
|
||||||
* UI 的 Authorize 按钮只需填入任意一种 token 即可。
|
* UI 的 Authorize 按钮只需填入任意一种 token 即可。
|
||||||
*
|
*
|
||||||
* <h3>注意:Swagger 当前公开可访问</h3>
|
* <h3>访问控制(在 SecurityConfig,不在本类)</h3>
|
||||||
* {@link SecurityConfig#filterChain} 中 {@code /api/**} 要求认证,但
|
* Swagger UI / OpenAPI 文档路径({@code /swagger-ui*}、{@code /v3/api-docs*}、
|
||||||
* {@code /swagger-ui*}、{@code /v3/api-docs*}、{@code /webjars/**} 落到
|
* {@code /webjars/**})的鉴权由 {@link SecurityConfig#filterChain} 通过
|
||||||
* {@code .anyRequest().permitAll()},即 Swagger UI 当前是公开的。如生产环境
|
* {@code mateclaw.openapi.expose-ui} 开关控制:本地/默认 profile 公开,
|
||||||
* 需要收口,应在 SecurityConfig 显式加规则,而不是改本类。
|
* 生产数据库 profile(mysql/kingbase/postgres)默认要求全局管理员
|
||||||
|
* ({@code ROLE_ADMIN})。访问规则只属于 SecurityConfig,不要加到本类。
|
||||||
*
|
*
|
||||||
* <h3>未做的事(与「全局配置 + 安全方案」范围一致)</h3>
|
* <h3>未做的事(与「全局配置 + 安全方案」范围一致)</h3>
|
||||||
* 不逐个 Controller 补 {@code @Parameter} / {@code @ApiResponse} /
|
* 不逐个 Controller 补 {@code @Parameter} / {@code @ApiResponse} /
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package vip.mate.config;
|
package vip.mate.config;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
@ -28,6 +29,22 @@ public class SecurityConfig {
|
|||||||
|
|
||||||
private final JwtAuthFilter jwtAuthFilter;
|
private final JwtAuthFilter jwtAuthFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SpringDoc Swagger UI / OpenAPI document paths. These serve the full REST
|
||||||
|
* surface (every endpoint plus request/response schemas), so they are gated
|
||||||
|
* explicitly instead of relying on the {@code .anyRequest().permitAll()}
|
||||||
|
* fallthrough. Whether they are public or admin-only is driven by
|
||||||
|
* {@code mateclaw.openapi.expose-ui} (see {@link #filterChain}).
|
||||||
|
*/
|
||||||
|
private static final String[] OPENAPI_PATHS = {
|
||||||
|
"/swagger-ui.html",
|
||||||
|
"/swagger-ui/**",
|
||||||
|
"/v3/api-docs",
|
||||||
|
"/v3/api-docs/**",
|
||||||
|
"/v3/api-docs.yaml",
|
||||||
|
"/webjars/**"
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 密码编码器独立配置(打破 SecurityConfig → JwtAuthFilter → AuthService → BCryptPasswordEncoder 循环)
|
* 密码编码器独立配置(打破 SecurityConfig → JwtAuthFilter → AuthService → BCryptPasswordEncoder 循环)
|
||||||
*/
|
*/
|
||||||
@ -39,8 +56,19 @@ public class SecurityConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the security filter chain.
|
||||||
|
*
|
||||||
|
* @param exposeOpenApiUi when {@code true} the Swagger UI / OpenAPI document
|
||||||
|
* paths are public; when {@code false} they require a global admin
|
||||||
|
* ({@code ROLE_ADMIN}). Defaults to {@code false} (locked down) when the
|
||||||
|
* property is absent — the base {@code application.yml} enables it for
|
||||||
|
* local dev while the production database profiles keep it off.
|
||||||
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(
|
||||||
|
HttpSecurity http,
|
||||||
|
@Value("${mateclaw.openapi.expose-ui:false}") boolean exposeOpenApiUi) throws Exception {
|
||||||
http
|
http
|
||||||
.csrf(AbstractHttpConfigurer::disable)
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
.headers(headers -> headers
|
.headers(headers -> headers
|
||||||
@ -48,10 +76,10 @@ public class SecurityConfig {
|
|||||||
.frameOptions(frame -> frame.sameOrigin())
|
.frameOptions(frame -> frame.sameOrigin())
|
||||||
)
|
)
|
||||||
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> {
|
||||||
// GET /settings/language stays anonymous (first-paint i18n). PUT
|
// GET /settings/language stays anonymous (first-paint i18n). PUT
|
||||||
// requires login + admin (see @RequireGlobalAdmin on the controller).
|
// requires login + admin (see @RequireGlobalAdmin on the controller).
|
||||||
.requestMatchers(HttpMethod.GET, "/api/v1/settings/language").permitAll()
|
auth.requestMatchers(HttpMethod.GET, "/api/v1/settings/language").permitAll()
|
||||||
// 公开 API 接口
|
// 公开 API 接口
|
||||||
.requestMatchers(
|
.requestMatchers(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
@ -66,12 +94,20 @@ public class SecurityConfig {
|
|||||||
// expire after GeneratedFileCache.TTL (7 days) — delayed access (e.g. an
|
// expire after GeneratedFileCache.TTL (7 days) — delayed access (e.g. an
|
||||||
// IM-delivered link opened later) is intentional, the UUID is the guard.
|
// IM-delivered link opened later) is intentional, the UUID is the guard.
|
||||||
"/api/v1/files/generated/**"
|
"/api/v1/files/generated/**"
|
||||||
).permitAll()
|
).permitAll();
|
||||||
|
// Swagger UI / OpenAPI document — explicit rule rather than the
|
||||||
|
// permitAll() fallthrough. Public for local dev, admin-only in
|
||||||
|
// production, driven by mateclaw.openapi.expose-ui.
|
||||||
|
if (exposeOpenApiUi) {
|
||||||
|
auth.requestMatchers(OPENAPI_PATHS).permitAll();
|
||||||
|
} else {
|
||||||
|
auth.requestMatchers(OPENAPI_PATHS).hasRole("ADMIN");
|
||||||
|
}
|
||||||
// 所有其他 API 接口需要认证
|
// 所有其他 API 接口需要认证
|
||||||
.requestMatchers("/api/**").authenticated()
|
auth.requestMatchers("/api/**").authenticated()
|
||||||
// 非 API 请求(前端路由、静态资源、Swagger、H2 Console 等)全部放行
|
// 非 API 请求(前端路由、静态资源、H2 Console 等)全部放行
|
||||||
.anyRequest().permitAll()
|
.anyRequest().permitAll();
|
||||||
)
|
})
|
||||||
.exceptionHandling(ex -> ex
|
.exceptionHandling(ex -> ex
|
||||||
.authenticationEntryPoint((request, response, authException) -> {
|
.authenticationEntryPoint((request, response, authException) -> {
|
||||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
|||||||
@ -69,3 +69,11 @@ mybatis-plus:
|
|||||||
mate:
|
mate:
|
||||||
wiki:
|
wiki:
|
||||||
require-allowed-roots: true
|
require-allowed-roots: true
|
||||||
|
|
||||||
|
# Production hardening: lock down the Swagger UI / OpenAPI document so it is not
|
||||||
|
# anonymously browsable. Requires a global admin (ROLE_ADMIN); SecurityConfig
|
||||||
|
# enforces it. Set MATECLAW_OPENAPI_EXPOSE_UI=true to re-open it for an
|
||||||
|
# internal/staging host.
|
||||||
|
mateclaw:
|
||||||
|
openapi:
|
||||||
|
expose-ui: ${MATECLAW_OPENAPI_EXPOSE_UI:false}
|
||||||
|
|||||||
@ -44,3 +44,11 @@ mate:
|
|||||||
allowed-source-roots: ${MATE_WIKI_ALLOWED_SOURCE_ROOTS:}
|
allowed-source-roots: ${MATE_WIKI_ALLOWED_SOURCE_ROOTS:}
|
||||||
watcher-enabled: ${MATE_WIKI_WATCHER_ENABLED:false}
|
watcher-enabled: ${MATE_WIKI_WATCHER_ENABLED:false}
|
||||||
watcher-interval-ms: ${MATE_WIKI_WATCHER_INTERVAL_MS:300000}
|
watcher-interval-ms: ${MATE_WIKI_WATCHER_INTERVAL_MS:300000}
|
||||||
|
|
||||||
|
# Production hardening: lock down the Swagger UI / OpenAPI document so it is not
|
||||||
|
# anonymously browsable. Requires a global admin (ROLE_ADMIN); SecurityConfig
|
||||||
|
# enforces it. Set MATECLAW_OPENAPI_EXPOSE_UI=true to re-open it for an
|
||||||
|
# internal/staging host.
|
||||||
|
mateclaw:
|
||||||
|
openapi:
|
||||||
|
expose-ui: ${MATECLAW_OPENAPI_EXPOSE_UI:false}
|
||||||
|
|||||||
@ -69,3 +69,11 @@ mate:
|
|||||||
allowed-source-roots: ${MATE_WIKI_ALLOWED_SOURCE_ROOTS:}
|
allowed-source-roots: ${MATE_WIKI_ALLOWED_SOURCE_ROOTS:}
|
||||||
watcher-enabled: ${MATE_WIKI_WATCHER_ENABLED:false}
|
watcher-enabled: ${MATE_WIKI_WATCHER_ENABLED:false}
|
||||||
watcher-interval-ms: ${MATE_WIKI_WATCHER_INTERVAL_MS:300000}
|
watcher-interval-ms: ${MATE_WIKI_WATCHER_INTERVAL_MS:300000}
|
||||||
|
|
||||||
|
# Production hardening: lock down the Swagger UI / OpenAPI document so it is not
|
||||||
|
# anonymously browsable. Requires a global admin (ROLE_ADMIN); SecurityConfig
|
||||||
|
# enforces it. Set MATECLAW_OPENAPI_EXPOSE_UI=true to re-open it for an
|
||||||
|
# internal/staging host.
|
||||||
|
mateclaw:
|
||||||
|
openapi:
|
||||||
|
expose-ui: ${MATECLAW_OPENAPI_EXPOSE_UI:false}
|
||||||
|
|||||||
@ -145,6 +145,11 @@ mateclaw:
|
|||||||
server-url: ${MATECLAW_OPENAPI_SERVER_URL:}
|
server-url: ${MATECLAW_OPENAPI_SERVER_URL:}
|
||||||
# description 留空则使用 OpenApiConfig 中的内置默认描述
|
# description 留空则使用 OpenApiConfig 中的内置默认描述
|
||||||
description: ${MATECLAW_OPENAPI_DESCRIPTION:}
|
description: ${MATECLAW_OPENAPI_DESCRIPTION:}
|
||||||
|
# 是否公开 Swagger UI / OpenAPI 文档路径(/swagger-ui*、/v3/api-docs*、/webjars/**)。
|
||||||
|
# true = 任何人可浏览(本地开发 / 内网默认);
|
||||||
|
# false = 需要全局管理员(ROLE_ADMIN)才能访问,由 SecurityConfig 强制。
|
||||||
|
# 生产数据库 profile(mysql/kingbase/postgres)默认覆盖为 false。
|
||||||
|
expose-ui: ${MATECLAW_OPENAPI_EXPOSE_UI:true}
|
||||||
jwt:
|
jwt:
|
||||||
secret: ${JWT_SECRET:MateClaw-JWT-Secret-Key-2024-Please-Change-In-Production}
|
secret: ${JWT_SECRET:MateClaw-JWT-Secret-Key-2024-Please-Change-In-Production}
|
||||||
expiration: 86400000
|
expiration: 86400000
|
||||||
|
|||||||
@ -55,16 +55,24 @@ mateclaw:
|
|||||||
version: ${MATECLAW_OPENAPI_VERSION:1.0}
|
version: ${MATECLAW_OPENAPI_VERSION:1.0}
|
||||||
server-url: ${MATECLAW_OPENAPI_SERVER_URL:} # empty → derived from request host
|
server-url: ${MATECLAW_OPENAPI_SERVER_URL:} # empty → derived from request host
|
||||||
description: ${MATECLAW_OPENAPI_DESCRIPTION:} # empty → built-in default
|
description: ${MATECLAW_OPENAPI_DESCRIPTION:} # empty → built-in default
|
||||||
|
expose-ui: ${MATECLAW_OPENAPI_EXPOSE_UI:true} # whether the Swagger/OpenAPI paths are public, see the security section below
|
||||||
```
|
```
|
||||||
|
|
||||||
When `server-url` is empty, SpringDoc derives it from the request host so "Try it out" hits the right address; for fixed production URLs (e.g. behind a reverse proxy), set `MATECLAW_OPENAPI_SERVER_URL=https://mate.example.com`.
|
When `server-url` is empty, SpringDoc derives it from the request host so "Try it out" hits the right address; for fixed production URLs (e.g. behind a reverse proxy), set `MATECLAW_OPENAPI_SERVER_URL=https://mate.example.com`.
|
||||||
|
|
||||||
## ⚠️ Security note: Swagger is currently public
|
## 🔒 Access control: Swagger is locked down by default in production
|
||||||
|
|
||||||
In `SecurityConfig.filterChain`, `/api/**` requires authentication, but `/swagger-ui*`, `/v3/api-docs*`, and `/webjars/**` fall through to `.anyRequest().permitAll()` — meaning **Swagger UI is publicly accessible** without login; anyone can browse the full endpoint surface (including request/response schemas).
|
Access to the Swagger UI / OpenAPI document paths (`/swagger-ui*`, `/v3/api-docs*`, `/webjars/**`) is controlled by the `mateclaw.openapi.expose-ui` flag and enforced explicitly in `SecurityConfig.filterChain` (no longer relying on the `.anyRequest().permitAll()` fallthrough):
|
||||||
|
|
||||||
- Local dev / intranet deployments: usually acceptable.
|
| `expose-ui` | Behavior | Default profile |
|
||||||
- Public production deployments: consider adding an explicit auth rule for the Swagger paths in `SecurityConfig` (e.g. require `@RequireGlobalAdmin`). Don't rely on the network layer alone. When locking it down, edit `SecurityConfig`, not `OpenApiConfig`.
|
|---|---|---|
|
||||||
|
| `true` | Publicly accessible — anyone can browse the full endpoint surface (incl. request/response schemas) without login | Local / default profile (H2, desktop) |
|
||||||
|
| `false` | Requires a global admin (`ROLE_ADMIN`); anonymous → 401, non-admin → 403 | Production database profiles (`mysql` / `kingbase` / `postgres`) |
|
||||||
|
|
||||||
|
- Local dev: defaults to `true`, so `http://localhost:18088/swagger-ui.html` is reachable directly.
|
||||||
|
- Public production: defaults to `false` (locked down). To temporarily open it on an internal/staging host, set `MATECLAW_OPENAPI_EXPOSE_UI=true`.
|
||||||
|
- Note: once locked, opening `/swagger-ui.html` in a browser won't automatically carry the SPA's JWT (the token lives in localStorage, not a cookie), so even an admin cannot open it from the browser directly. To debug, either set `expose-ui=true` temporarily, or fetch `/v3/api-docs` with a client that sends the `Authorization` header.
|
||||||
|
- The access rule lives only in `SecurityConfig`, not `OpenApiConfig`.
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|
||||||
|
|||||||
@ -55,16 +55,24 @@ mateclaw:
|
|||||||
version: ${MATECLAW_OPENAPI_VERSION:1.0}
|
version: ${MATECLAW_OPENAPI_VERSION:1.0}
|
||||||
server-url: ${MATECLAW_OPENAPI_SERVER_URL:} # 留空则从请求 host 推导
|
server-url: ${MATECLAW_OPENAPI_SERVER_URL:} # 留空则从请求 host 推导
|
||||||
description: ${MATECLAW_OPENAPI_DESCRIPTION:} # 留空则用内置默认描述
|
description: ${MATECLAW_OPENAPI_DESCRIPTION:} # 留空则用内置默认描述
|
||||||
|
expose-ui: ${MATECLAW_OPENAPI_EXPOSE_UI:true} # 是否公开 Swagger/OpenAPI 路径,见下方安全章节
|
||||||
```
|
```
|
||||||
|
|
||||||
`server-url` 留空时由 SpringDoc 从请求 host 推导,避免 "Try it out" 打到错误地址;生产若需固定(如反代后),设 `MATECLAW_OPENAPI_SERVER_URL=https://mate.example.com`。
|
`server-url` 留空时由 SpringDoc 从请求 host 推导,避免 "Try it out" 打到错误地址;生产若需固定(如反代后),设 `MATECLAW_OPENAPI_SERVER_URL=https://mate.example.com`。
|
||||||
|
|
||||||
## ⚠️ 安全提示:Swagger 当前公开可访问
|
## 🔒 访问控制:Swagger 生产默认收口
|
||||||
|
|
||||||
`SecurityConfig.filterChain` 中 `/api/**` 要求认证,但 `/swagger-ui*`、`/v3/api-docs*`、`/webjars/**` 落到 `.anyRequest().permitAll()` —— 即 **Swagger UI 当前是公开可访问的**,无需登录即可浏览全部端点结构(含请求/响应 schema)。
|
Swagger UI / OpenAPI 文档路径(`/swagger-ui*`、`/v3/api-docs*`、`/webjars/**`)的访问由 `mateclaw.openapi.expose-ui` 开关控制,并由 `SecurityConfig.filterChain` 显式强制(不再依赖 `.anyRequest().permitAll()` 兜底):
|
||||||
|
|
||||||
- 本地开发 / 内网部署:通常可接受。
|
| `expose-ui` | 行为 | 默认生效的场景 |
|
||||||
- 公网生产部署:建议在 `SecurityConfig` 显式给 Swagger 路径加鉴权规则(如要求 `@RequireGlobalAdmin`),**不要**仅靠网络层防护。收口时应改 `SecurityConfig`,而非 `OpenApiConfig`。
|
|---|---|---|
|
||||||
|
| `true` | 公开可访问,无需登录即可浏览全部端点结构(含请求/响应 schema) | 本地 / 默认 profile(H2、桌面版) |
|
||||||
|
| `false` | 需要全局管理员(`ROLE_ADMIN`);匿名访问返回 401,非管理员返回 403 | 生产数据库 profile(`mysql` / `kingbase` / `postgres`) |
|
||||||
|
|
||||||
|
- 本地开发:默认 `true`,`http://localhost:18088/swagger-ui.html` 直接可访问。
|
||||||
|
- 公网生产:默认 `false`,已收口。如确需在内网/预发环境临时打开,设 `MATECLAW_OPENAPI_EXPOSE_UI=true`。
|
||||||
|
- 注意:锁定后浏览器直接访问 `/swagger-ui.html` 不会自动携带 SPA 的 JWT(token 存于 localStorage 而非 Cookie),因此即使管理员也无法在浏览器里直接打开;如需调试,临时置 `expose-ui=true` 或改用带 `Authorization` 头的客户端拉取 `/v3/api-docs`。
|
||||||
|
- 访问规则只在 `SecurityConfig`,不在 `OpenApiConfig`。
|
||||||
|
|
||||||
## 关联
|
## 关联
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
package vip.mate.config;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local/dev posture: with {@code mateclaw.openapi.expose-ui=true} (the base
|
||||||
|
* {@code application.yml} default) the Swagger UI / OpenAPI document stays
|
||||||
|
* anonymously reachable so developers can browse and debug without a login.
|
||||||
|
*/
|
||||||
|
@SpringBootTest(
|
||||||
|
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||||
|
properties = {
|
||||||
|
"spring.flyway.enabled=true",
|
||||||
|
"spring.flyway.locations=classpath:db/migration/h2",
|
||||||
|
"mateclaw.openapi.expose-ui=true"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
class OpenApiExposedAccessTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TestRestTemplate rest;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Anonymous OpenAPI JSON is reachable (200) when expose-ui=true")
|
||||||
|
void anonymousApiDocsReachable() {
|
||||||
|
ResponseEntity<String> resp = rest.getForEntity("/v3/api-docs", String.class);
|
||||||
|
assertEquals(HttpStatus.OK, resp.getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
package vip.mate.config;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Production posture: with {@code mateclaw.openapi.expose-ui=false} the Swagger
|
||||||
|
* UI / OpenAPI document paths must NOT be anonymously reachable. They fall under
|
||||||
|
* an explicit {@code hasRole('ADMIN')} rule in {@link SecurityConfig}, so an
|
||||||
|
* unauthenticated request is rejected by the authentication entry point (401)
|
||||||
|
* instead of leaking the full API surface.
|
||||||
|
*
|
||||||
|
* <p>Uses a real embedded servlet container ({@code RANDOM_PORT}) because the app
|
||||||
|
* registers a WebSocket endpoint that requires a servlet {@code ServerContainer},
|
||||||
|
* which the MockMvc-only environment does not provide.
|
||||||
|
*/
|
||||||
|
@SpringBootTest(
|
||||||
|
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||||
|
properties = {
|
||||||
|
"spring.flyway.enabled=true",
|
||||||
|
"spring.flyway.locations=classpath:db/migration/h2",
|
||||||
|
"mateclaw.openapi.expose-ui=false"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
class OpenApiLockedDownAccessTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TestRestTemplate rest;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Anonymous OpenAPI JSON is blocked (401) when expose-ui=false")
|
||||||
|
void anonymousApiDocsBlocked() {
|
||||||
|
ResponseEntity<String> resp = rest.getForEntity("/v3/api-docs", String.class);
|
||||||
|
assertEquals(HttpStatus.UNAUTHORIZED, resp.getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Anonymous Swagger UI is blocked (401) when expose-ui=false")
|
||||||
|
void anonymousSwaggerUiBlocked() {
|
||||||
|
ResponseEntity<String> resp = rest.getForEntity("/swagger-ui/index.html", String.class);
|
||||||
|
assertEquals(HttpStatus.UNAUTHORIZED, resp.getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("A genuinely public endpoint stays reachable when Swagger is locked")
|
||||||
|
void publicEndpointStillReachable() {
|
||||||
|
// GET /api/v1/settings/language is permitAll (first-paint i18n); proves
|
||||||
|
// the lockdown is scoped to the OpenAPI paths, not a blanket denial.
|
||||||
|
ResponseEntity<String> resp = rest.getForEntity("/api/v1/settings/language", String.class);
|
||||||
|
assertEquals(HttpStatus.OK, resp.getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,6 +8,8 @@ import org.junit.jupiter.api.condition.DisabledOnOs;
|
|||||||
import org.junit.jupiter.api.condition.OS;
|
import org.junit.jupiter.api.condition.OS;
|
||||||
import vip.mate.tool.builtin.ToolExecutionContext;
|
import vip.mate.tool.builtin.ToolExecutionContext;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
@ -26,8 +28,16 @@ class WorkspacePathGuardShellTest {
|
|||||||
private static final String WORKSPACE = "/tmp/ws-guard-shell-test";
|
private static final String WORKSPACE = "/tmp/ws-guard-shell-test";
|
||||||
private static final String SKILL_ROOT = "/tmp/ws-guard-skill-root";
|
private static final String SKILL_ROOT = "/tmp/ws-guard-skill-root";
|
||||||
|
|
||||||
|
// The global fallback sandbox root is process-wide mutable static state that
|
||||||
|
// another test (or the app context, in a full-suite run) may have set. Save
|
||||||
|
// and restore it so the "no workspace configured" cases here are deterministic
|
||||||
|
// rather than depending on whatever the previous test left behind.
|
||||||
|
private Path savedDefaultRoot;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setup() {
|
void setup() {
|
||||||
|
savedDefaultRoot = WorkspacePathGuard.getDefaultRoot();
|
||||||
|
WorkspacePathGuard.setDefaultRoot(null);
|
||||||
ToolExecutionContext.set("conv-test", "test-user", WORKSPACE);
|
ToolExecutionContext.set("conv-test", "test-user", WORKSPACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,6 +45,7 @@ class WorkspacePathGuardShellTest {
|
|||||||
void teardown() {
|
void teardown() {
|
||||||
ToolExecutionContext.clear();
|
ToolExecutionContext.clear();
|
||||||
WorkspacePathGuard.setSkillRoot(null);
|
WorkspacePathGuard.setSkillRoot(null);
|
||||||
|
WorkspacePathGuard.setDefaultRoot(savedDefaultRoot == null ? null : savedDefaultRoot.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== No-op when sandbox absent ====================
|
// ==================== No-op when sandbox absent ====================
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user