mirror of
https://github.com/iflytek/skillhub.git
synced 2026-11-14 09:01:16 +00:00
feat: opt in to protected business metrics in Compose and Helm
Signed-off-by: 千乘妍 (Xiaoyaner) <xiaoyaner0201@users.noreply.github.com>
This commit is contained in:
parent
294410d324
commit
07d3c31482
10 changed files with 97 additions and 1 deletions
|
|
@ -79,6 +79,8 @@ spec:
|
|||
value: {{ $profiles | quote }}
|
||||
- name: SKILLHUB_SUITE_REVIEW_WRITES_ENABLED
|
||||
value: {{ .Values.server.suiteReviewWritesEnabled | quote }}
|
||||
- name: SKILLHUB_ACTUATOR_EXPOSURE_INCLUDE
|
||||
value: {{ .Values.server.actuatorExposureInclude | default "health,info" | quote }}
|
||||
{{- $businessLogs := .Values.server.businessLogs | default dict }}
|
||||
- name: SKILLHUB_BUSINESS_LOGS_ENABLED
|
||||
value: {{ $businessLogs.enabled | default false | quote }}
|
||||
|
|
|
|||
|
|
@ -633,3 +633,25 @@ print('Helm application Secret render/rotation contracts passed')
|
|||
PY_SECRET_RENDER
|
||||
|
||||
echo "Helm configuration contract tests passed"
|
||||
|
||||
# Business Logs and metrics remain opt-in; credentials reference an existing Secret.
|
||||
render business-default "$CHART_DIR" --show-only templates/server-deployment.yaml >"$TMP_DIR/business-default.yaml"
|
||||
grep -A1 -F 'name: SKILLHUB_ACTUATOR_EXPOSURE_INCLUDE' "$TMP_DIR/business-default.yaml" | grep -Fq 'value: "health,info"'
|
||||
grep -A1 -F 'name: SKILLHUB_BUSINESS_LOGS_ENABLED' "$TMP_DIR/business-default.yaml" | grep -Fq 'value: "false"'
|
||||
assert_rejected business-no-endpoint --set server.businessLogs.enabled=true
|
||||
assert_rejected actuator-wildcard --set-string 'server.actuatorExposureInclude=*'
|
||||
cat >"$TMP_DIR/business-values.yaml" <<'BUSINESS_VALUES'
|
||||
server:
|
||||
actuatorExposureInclude: health,info,metrics,prometheus
|
||||
businessLogs:
|
||||
enabled: true
|
||||
endpoint: https://receiver.example.com/v1/logs
|
||||
headersSecretRef:
|
||||
name: business-logs-credentials
|
||||
key: headers
|
||||
BUSINESS_VALUES
|
||||
render business-enabled "$CHART_DIR" -f "$TMP_DIR/business-values.yaml" --show-only templates/server-deployment.yaml >"$TMP_DIR/business-enabled.yaml"
|
||||
grep -A1 -F 'name: SKILLHUB_ACTUATOR_EXPOSURE_INCLUDE' "$TMP_DIR/business-enabled.yaml" | grep -Fq 'value: "health,info,metrics,prometheus"'
|
||||
grep -A6 -F 'name: SKILLHUB_BUSINESS_LOGS_HEADERS' "$TMP_DIR/business-enabled.yaml" | grep -Fq 'name: "business-logs-credentials"'
|
||||
grep -A6 -F 'name: SKILLHUB_BUSINESS_LOGS_HEADERS' "$TMP_DIR/business-enabled.yaml" | grep -Fq 'optional: false'
|
||||
echo 'Helm business Logs / metrics contracts passed'
|
||||
|
|
|
|||
|
|
@ -452,6 +452,7 @@
|
|||
"enabled": { "type": "boolean" },
|
||||
"replicaCount": { "type": "integer", "minimum": 1 },
|
||||
"suiteReviewWritesEnabled": { "type": "boolean" },
|
||||
"actuatorExposureInclude": { "enum": ["health,info", "health,info,metrics,prometheus"] },
|
||||
"businessLogs": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
|
|
|||
|
|
@ -312,6 +312,9 @@ server:
|
|||
# Server instance supports typed review subjects; fresh non-rolling installs may enable it directly.
|
||||
suiteReviewWritesEnabled: false
|
||||
|
||||
# Opt in with health,info,metrics,prometheus. No wildcard exposure.
|
||||
actuatorExposureInclude: health,info
|
||||
|
||||
# Independent of tracing; optional OTLP Logs receiver. Other budgets use server.extraEnv.
|
||||
businessLogs:
|
||||
enabled: false
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ services:
|
|||
SKILLHUB_SCAN_STREAM_RECLAIM_MIN_IDLE: ${SKILLHUB_SCAN_STREAM_RECLAIM_MIN_IDLE:-PT16M}
|
||||
SKILLHUB_BUILTIN_SKILLS_ENABLED: ${SKILLHUB_BUILTIN_SKILLS_ENABLED:-true}
|
||||
SKILLHUB_AUTH_DIRECT_ENABLED: ${SKILLHUB_AUTH_DIRECT_ENABLED:-false}
|
||||
SKILLHUB_ACTUATOR_EXPOSURE_INCLUDE: ${SKILLHUB_ACTUATOR_EXPOSURE_INCLUDE:-health,info}
|
||||
SKILLHUB_TRACING_MODE: ${SKILLHUB_TRACING_MODE:-none}
|
||||
SKILLHUB_BUSINESS_LOGS_ENABLED: ${SKILLHUB_BUSINESS_LOGS_ENABLED:-false}
|
||||
SKILLHUB_BUSINESS_LOGS_ENDPOINT: ${SKILLHUB_BUSINESS_LOGS_ENDPOINT:-}
|
||||
|
|
|
|||
|
|
@ -348,6 +348,10 @@ validate_boolean BOOTSTRAP_ADMIN_ENABLED
|
|||
validate_boolean SKILLHUB_TRUST_FORWARDED_PROTO
|
||||
validate_boolean SKILLHUB_BUILTIN_SKILLS_ENABLED
|
||||
validate_boolean SKILLHUB_BUSINESS_LOGS_ENABLED
|
||||
case "${SKILLHUB_ACTUATOR_EXPOSURE_INCLUDE:-health,info}" in
|
||||
health,info|health,info,metrics,prometheus) ;;
|
||||
*) error "SKILLHUB_ACTUATOR_EXPOSURE_INCLUDE must be health,info or health,info,metrics,prometheus" ;;
|
||||
esac
|
||||
# Do not echo the endpoint or authentication headers. Full URL/budget validation is at startup.
|
||||
if [ "${SKILLHUB_BUSINESS_LOGS_ENABLED:-false}" = "true" ]; then
|
||||
require_non_empty SKILLHUB_BUSINESS_LOGS_ENDPOINT
|
||||
|
|
|
|||
|
|
@ -357,7 +357,7 @@ management:
|
|||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
include: ${SKILLHUB_ACTUATOR_EXPOSURE_INCLUDE:health,info}
|
||||
endpoint:
|
||||
health:
|
||||
show-details: when-authorized
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
package com.iflytek.skillhub.metrics;
|
||||
|
||||
import com.iflytek.skillhub.TestRedisConfig;
|
||||
import com.iflytek.skillhub.auth.device.DeviceAuthService;
|
||||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability
|
||||
@SpringBootTest(properties = "SKILLHUB_ACTUATOR_EXPOSURE_INCLUDE=health,info,metrics,prometheus")
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@Import(TestRedisConfig.class)
|
||||
class BusinessMetricsSecurityTest {
|
||||
@Autowired MockMvc mockMvc;
|
||||
@Autowired MeterRegistry registry;
|
||||
@MockBean NamespaceMemberRepository namespaceMemberRepository;
|
||||
@MockBean DeviceAuthService deviceAuthService;
|
||||
|
||||
static RequestPostProcessor session(String... roles) {
|
||||
var principal = new PlatformPrincipal("synthetic-metrics-user", "Metrics test", null, null, "local", Set.of(roles));
|
||||
var authorities = java.util.Arrays.stream(roles).map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList();
|
||||
return authentication(new UsernamePasswordAuthenticationToken(principal, null, authorities));
|
||||
}
|
||||
|
||||
@Test
|
||||
void optInExposesOnlyMetricsToAuthorizedSessions() throws Exception {
|
||||
registry.counter("skillhub.business.logs.enqueued").increment();
|
||||
for (String path : List.of("/actuator/metrics", "/actuator/metrics/skillhub.business.logs.enqueued", "/actuator/prometheus")) {
|
||||
mockMvc.perform(get(path).with(session("SUPER_ADMIN"))).andExpect(status().isOk());
|
||||
mockMvc.perform(get(path).with(session())).andExpect(status().isForbidden());
|
||||
mockMvc.perform(get(path)).andExpect(status().isUnauthorized());
|
||||
}
|
||||
mockMvc.perform(get("/actuator/env").with(session("SUPER_ADMIN"))).andExpect(status().isNotFound());
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,14 @@ class PrometheusSecurityTest {
|
|||
@MockBean
|
||||
private DeviceAuthService deviceAuthService;
|
||||
|
||||
@Test
|
||||
void metricsRemainUnexposedByDefaultEvenForAdmin() throws Exception {
|
||||
for (String path : java.util.List.of("/actuator/metrics", "/actuator/prometheus")) {
|
||||
mockMvc.perform(get(path).with(BusinessMetricsSecurityTest.session("SUPER_ADMIN")))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void prometheusEndpointShouldNotBeAnonymous() throws Exception {
|
||||
mockMvc.perform(get("/actuator/prometheus"))
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ public class RouteSecurityPolicyRegistry {
|
|||
RouteAuthorizationPolicy.permitAll(null, "/swagger-ui/**"),
|
||||
RouteAuthorizationPolicy.permitAll(null, "/.well-known/**"),
|
||||
RouteAuthorizationPolicy.roles(null, "/actuator/prometheus", "SUPER_ADMIN", "AUDITOR"),
|
||||
RouteAuthorizationPolicy.roles(null, "/actuator/metrics", "SUPER_ADMIN"),
|
||||
RouteAuthorizationPolicy.roles(null, "/actuator/metrics/**", "SUPER_ADMIN"),
|
||||
RouteAuthorizationPolicy.authenticated(HttpMethod.GET, "/api/v1/skills/*/star"),
|
||||
RouteAuthorizationPolicy.authenticated(HttpMethod.PUT, "/api/v1/skills/*/star"),
|
||||
RouteAuthorizationPolicy.authenticated(HttpMethod.DELETE, "/api/v1/skills/*/star"),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue