Skip to content

Commit c0d3992

Browse files
authored
fix(scheduler): Read Health Credentials Correctly (#3948)
* fix scheduler health endpoint to read correct properties * added acceptance test for scheduler health endpoint with basic auth * test /health endpoint with basic authentication * consider health configs from VCAP_Services and load in enviornment * simplify CloudFOundryPostProcessor * add required property in bosh based scheduler config
1 parent 2c95f0d commit c0d3992

File tree

11 files changed

+311
-208
lines changed

11 files changed

+311
-208
lines changed

jobs/scheduler/templates/scheduler.yml.erb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ server:
174174
cfserver:
175175
validOrgGuid: <%= p("autoscaler.scheduler.cf_server.xfcc.valid_org_guid") %>
176176
validSpaceGuid: <%= p("autoscaler.scheduler.cf_server.xfcc.valid_space_guid") %>
177+
healthserver:
178+
username: "<%=p('autoscaler.scheduler.health.username') %>"
179+
password: "<%=p('autoscaler.scheduler.health.password') %>"
177180

178181

179182
#User added properties

src/autoscaler/integration/scheduler_application.template.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ server:
3737
cfserver:
3838
validOrgGuid: some-org-guid
3939
validSpaceGuid: some-space-guid
40+
healthserver:
41+
username: health-test-user
42+
password: health-test-password
4043
spring:
4144
aop:
4245
auto: false

src/autoscaler/scheduler/src/main/java/org/cloudfoundry/autoscaler/scheduler/conf/CfHttpConfiguration.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.cloudfoundry.autoscaler.scheduler.conf;
22

3+
import lombok.Getter;
34
import lombok.Setter;
45
import org.apache.catalina.connector.Connector;
56
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -8,9 +9,11 @@
89
import org.springframework.context.annotation.Bean;
910
import org.springframework.context.annotation.Configuration;
1011

12+
// TODO: may be move this to a better place e.g. CfServerConfiguration.java
1113
@Configuration
1214
@ConfigurationProperties(prefix = "server.http")
1315
@Setter
16+
@Getter
1417
public class CfHttpConfiguration {
1518

1619
private int port;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.cloudfoundry.autoscaler.scheduler.conf;
2+
3+
import lombok.Data;
4+
import org.springframework.boot.context.properties.ConfigurationProperties;
5+
import org.springframework.context.annotation.Configuration;
6+
7+
@Data
8+
@Configuration
9+
@ConfigurationProperties(prefix = "cfserver")
10+
public class CfServerConfiguration {
11+
private String validOrgGuid;
12+
private String validSpaceGuid;
13+
private HealthServer healthserver;
14+
15+
@Data
16+
public static class HealthServer {
17+
private String username;
18+
private String password;
19+
}
20+
}

src/autoscaler/scheduler/src/main/java/org/cloudfoundry/autoscaler/scheduler/conf/CloudFoundryConfigurationProcessor.java

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,13 @@ public void postProcessEnvironment(
5252
allConfigs.putAll(schedulerConfig);
5353
}
5454

55-
// Process VCAP_APPLICATION for org GUID (this should override scheduler-config)
56-
Map<String, Object> vcapAppConfig = extractVcapApplicationConfig(environment);
57-
if (vcapAppConfig != null && !vcapAppConfig.isEmpty()) {
58-
logger.info("Found VCAP_APPLICATION, applying cfserver configuration overrides");
59-
allConfigs.putAll(vcapAppConfig);
55+
Map<String, Object> vcapOrgAndGuid = getOrgAndSpaceGuidFromVcap(environment);
56+
Map<String, Object> cfServerConfig = (Map<String, Object>) allConfigs.get("cfserver");
57+
if (cfServerConfig != null && !cfServerConfig.isEmpty()) {
58+
logger.info(
59+
"Found org and space guid service in VCAP_APPLICATIONS, applying configuration"
60+
+ " overrides");
61+
cfServerConfig.putAll(vcapOrgAndGuid);
6062
}
6163

6264
// Process database services
@@ -90,36 +92,28 @@ public void postProcessEnvironment(
9092
}
9193
}
9294

93-
private Map<String, Object> extractVcapApplicationConfig(ConfigurableEnvironment environment) {
95+
private Map<String, Object> getOrgAndSpaceGuidFromVcap(ConfigurableEnvironment environment) {
96+
Map<String, Object> orgSpaceMap;
9497
try {
9598
String vcapApplication = environment.getProperty(VCAP_APPLICATION);
9699

97100
if (vcapApplication == null || vcapApplication.trim().isEmpty()) {
98101
logger.debug("VCAP_APPLICATION not found or empty, skipping org GUID extraction");
99102
return null;
100103
}
101-
102-
TypeReference<Map<String, Object>> typeRef = new TypeReference<Map<String, Object>>() {};
104+
TypeReference<Map<String, Object>> typeRef = new TypeReference<>() {};
103105
Map<String, Object> vcapApp = objectMapper.readValue(vcapApplication, typeRef);
104106

105-
Map<String, Object> config = new java.util.HashMap<>();
106-
boolean foundConfig = false;
107+
orgSpaceMap = new java.util.HashMap<>();
107108

108109
// Extract organization_id
109110
Object organizationId = vcapApp.get("organization_id");
110111
if (organizationId instanceof String && !((String) organizationId).trim().isEmpty()) {
111112
String orgGuid = (String) organizationId;
112113
logger.info(
113114
"Setting cfserver.validOrgGuid from VCAP_APPLICATION organization_id: {}", orgGuid);
115+
orgSpaceMap.put("validOrgGuid", orgGuid);
114116

115-
// Create nested structure to match scheduler config format
116-
Map<String, Object> cfserverConfig = (Map<String, Object>) config.get("cfserver");
117-
if (cfserverConfig == null) {
118-
cfserverConfig = new java.util.HashMap<>();
119-
config.put("cfserver", cfserverConfig);
120-
}
121-
cfserverConfig.put("validOrgGuid", orgGuid);
122-
foundConfig = true;
123117
} else {
124118
logger.warn("organization_id not found or empty in VCAP_APPLICATION");
125119
}
@@ -130,24 +124,15 @@ private Map<String, Object> extractVcapApplicationConfig(ConfigurableEnvironment
130124
String spaceGuid = (String) spaceId;
131125
logger.info(
132126
"Setting cfserver.validSpaceGuid from VCAP_APPLICATION space_id: {}", spaceGuid);
133-
134-
// Create nested structure to match scheduler config format
135-
Map<String, Object> cfserverConfig = (Map<String, Object>) config.get("cfserver");
136-
if (cfserverConfig == null) {
137-
cfserverConfig = new java.util.HashMap<>();
138-
config.put("cfserver", cfserverConfig);
139-
}
140-
cfserverConfig.put("validSpaceGuid", spaceGuid);
141-
foundConfig = true;
127+
orgSpaceMap.put("validSpaceGuid", spaceGuid);
142128
} else {
143129
logger.warn("space_id not found or empty in VCAP_APPLICATION");
144130
}
145-
146-
return foundConfig ? config : null;
147131
} catch (Exception e) {
148132
logger.error("Failed to parse VCAP_APPLICATION JSON", e);
149133
return null;
150134
}
135+
return orgSpaceMap;
151136
}
152137

153138
private Map<String, Object> extractSchedulerConfig(String vcapServices) {
@@ -420,11 +405,9 @@ private Map<String, Object> extractCfInstanceCertificates(ConfigurableEnvironmen
420405

421406
private Map<String, Object> flattenConfiguration(String prefix, Map<String, Object> config) {
422407
Map<String, Object> flattened = new java.util.HashMap<>();
423-
424408
config.forEach(
425409
(key, value) -> {
426410
String fullKey = prefix.isEmpty() ? key : prefix + "." + key;
427-
428411
if (value instanceof Map) {
429412
@SuppressWarnings("unchecked")
430413
Map<String, Object> nestedMap = (Map<String, Object>) value;
@@ -450,6 +433,6 @@ private Map<String, Object> extractCredentialsFromService(Map<String, Object> se
450433
Map<String, Object> credentialsMap = (Map<String, Object>) credentials;
451434
return credentialsMap;
452435
}
453-
return Map.<String, Object>of();
436+
return Map.of();
454437
}
455438
}

src/autoscaler/scheduler/src/main/java/org/cloudfoundry/autoscaler/scheduler/filter/HttpAuthFilter.java

Lines changed: 76 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -6,145 +6,128 @@
66
import jakarta.servlet.http.HttpServletResponse;
77
import java.io.ByteArrayInputStream;
88
import java.io.IOException;
9+
import java.security.cert.CertificateException;
910
import java.security.cert.CertificateFactory;
1011
import java.security.cert.X509Certificate;
1112
import java.util.Base64;
12-
import lombok.Setter;
13+
import lombok.RequiredArgsConstructor;
14+
import org.cloudfoundry.autoscaler.scheduler.conf.CfServerConfiguration;
1315
import org.slf4j.Logger;
1416
import org.slf4j.LoggerFactory;
15-
import org.springframework.beans.factory.annotation.Value;
16-
import org.springframework.boot.context.properties.ConfigurationProperties;
1717
import org.springframework.core.annotation.Order;
1818
import org.springframework.stereotype.Component;
1919
import org.springframework.web.filter.OncePerRequestFilter;
2020

2121
@Component
2222
@Order(0)
23-
@ConfigurationProperties(prefix = "cfserver")
24-
@Setter
23+
@RequiredArgsConstructor
2524
public class HttpAuthFilter extends OncePerRequestFilter {
26-
private Logger logger = LoggerFactory.getLogger(this.getClass());
27-
28-
private String validSpaceGuid;
29-
private String validOrgGuid;
30-
31-
@Value("${cfserver.healthserver.username}")
32-
private String healthServerUsername;
3325

34-
@Value("${cfserver.healthserver.password}")
35-
private String healthServerPassword;
36-
37-
public void setHealthServerUsername(String healthServerUsername) {
38-
this.healthServerUsername = healthServerUsername;
39-
}
26+
private static final String AUTHORIZATION_HEADER = "Authorization";
27+
private static final String BASIC_AUTH_PREFIX = "Basic ";
28+
private static final String XFCC_HEADER = "X-Forwarded-Client-Cert";
29+
private static final String HEALTH_ENDPOINT = "/health";
4030

41-
public void setHealthServerPassword(String healthServerPassword) {
42-
this.healthServerPassword = healthServerPassword;
43-
}
31+
private Logger logger = LoggerFactory.getLogger(this.getClass());
32+
private final CfServerConfiguration cfServerConfiguration;
4433

4534
@Override
4635
protected void doFilterInternal(
4736
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
4837
throws ServletException, IOException {
4938

50-
logger.info(
51-
"Received request with request "
52-
+ request.getRequestURI()
53-
+ " method"
54-
+ request.getMethod());
55-
56-
// Debug logging
5739
String forwardedProto = request.getHeader("X-Forwarded-Proto");
58-
boolean isHealthEndpoint = request.getRequestURI().contains("/health");
40+
boolean isHealthEndpoint = request.getRequestURI().contains(HEALTH_ENDPOINT);
41+
5942
logger.info(
60-
"DEBUG: scheme={}, X-Forwarded-Proto={}, isHealthEndpoint={}, healthServerUsername={},"
61-
+ " healthServerPassword={}",
43+
"Received {} request, scheme={},X-Forwarded-Proto={}, isHealthEndpoint={}, username={}",
44+
request.getMethod(),
6245
request.getScheme(),
6346
forwardedProto,
6447
isHealthEndpoint,
65-
healthServerUsername,
66-
healthServerPassword);
67-
68-
// Skip filter if X-Forwarded-Client-Cert is missing and not a health request
69-
String xfccHeader = request.getHeader("X-Forwarded-Client-Cert");
70-
if ((xfccHeader == null || xfccHeader.isEmpty()) && !isHealthEndpoint) {
71-
logger.info(
72-
"DEBUG: Skipping request without X-Forwarded-Client-Cert - URI={}",
73-
request.getRequestURI());
74-
filterChain.doFilter(request, response);
75-
return;
76-
}
77-
78-
// handles /health endpoint with basic auth
79-
if (request.getRequestURI().contains("/health")) {
80-
logger.info("DEBUG: Processing health endpoint request");
81-
// parse request basic auth header
82-
String authHeader = request.getHeader("Authorization");
83-
logger.info("DEBUG: Authorization header: {}", authHeader != null ? "present" : "missing");
84-
if (authHeader == null || !authHeader.startsWith("Basic ")) {
85-
logger.warn("Missing or invalid Authorization header for health check request");
86-
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
87-
return;
88-
}
89-
String[] credentials =
90-
new String(Base64.getDecoder().decode(authHeader.substring(6))).split(":");
91-
92-
if (credentials.length != 2) {
93-
logger.warn("Invalid Authorization header format for health check request");
94-
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Bad Request");
95-
return;
96-
}
97-
if (!credentials[0].equals(healthServerUsername)
98-
|| !credentials[1].equals(healthServerPassword)) {
99-
logger.warn("Invalid credentials for health check request");
100-
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
101-
return;
102-
} else {
103-
response.setStatus(HttpServletResponse.SC_OK);
104-
response.setContentType("application/json");
105-
response.getWriter().write("{\"status\":\"UP\"}");
106-
response.getWriter().flush();
107-
}
48+
cfServerConfiguration.getHealthserver().getUsername());
10849

50+
if (isHealthEndpoint) {
51+
handleHealthEndpoint(request, response);
10952
return;
11053
}
111-
54+
// Check for XFCC header
55+
String xfccHeader = request.getHeader(XFCC_HEADER);
11256
if (xfccHeader == null || xfccHeader.isEmpty()) {
113-
logger.warn("Missing X-Forwarded-Client-Cert header");
114-
response.sendError(
115-
HttpServletResponse.SC_BAD_REQUEST,
116-
"Missing X-Forwarded-Client-Cert header in the request");
57+
logger.warn("Missing X-Forwarded-Client-Cert header, URI={}", request.getRequestURI());
58+
filterChain.doFilter(request, response);
11759
return;
11860
}
119-
logger.info(
120-
"X-Forwarded-Client-Cert header received ... checking authorized org and space in OU");
121-
logger.info("X-Forwarded-Client-Cert header: " + xfccHeader);
61+
validateOrganizationAndSpace(xfccHeader, response);
62+
filterChain.doFilter(request, response);
63+
}
12264

65+
private void validateOrganizationAndSpace(String xfccHeader, HttpServletResponse response)
66+
throws IOException {
67+
logger.info(
68+
"X-Forwarded-Client-Cert header received ... checking authorized cf organization and space"
69+
+ " in Organizational Unit");
12370
try {
12471
String organizationalUnit = extractOrganizationalUnit(xfccHeader);
125-
12672
// Validate both key-value pairs in OrganizationalUnit
12773
if (!isValidOrganizationalUnit(organizationalUnit)) {
12874
logger.warn("Unauthorized OrganizationalUnit: " + organizationalUnit);
12975
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Unauthorized OrganizationalUnit");
130-
return;
13176
}
132-
} catch (Exception e) {
77+
} catch (CertificateException e) {
13378
logger.warn("Invalid certificate: " + e.getMessage());
13479
response.sendError(
13580
HttpServletResponse.SC_BAD_REQUEST, "Invalid certificate: " + e.getMessage());
81+
}
82+
}
83+
84+
private void handleHealthEndpoint(HttpServletRequest request, HttpServletResponse response)
85+
throws IOException {
86+
logger.info("Handling health check request with Basic Auth");
87+
String authHeader = request.getHeader(AUTHORIZATION_HEADER);
88+
logger.info("Authorization header: {}", authHeader != null ? "present" : "missing");
89+
90+
if (authHeader == null || !authHeader.startsWith(BASIC_AUTH_PREFIX)) {
91+
logger.warn("Missing or invalid Authorization header for health check request");
92+
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
13693
return;
13794
}
138-
// Proceed with the request
139-
filterChain.doFilter(request, response);
95+
String[] credentials = decodeBasicAuth(authHeader);
96+
if (credentials.length != 2) {
97+
logger.warn("Invalid Authorization header format for health check request");
98+
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Bad Request");
99+
return;
100+
}
101+
if (!credentials[0].equals(cfServerConfiguration.getHealthserver().getUsername())
102+
|| !credentials[1].equals(cfServerConfiguration.getHealthserver().getPassword())) {
103+
logger.warn("Invalid credentials for health check request");
104+
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
105+
return;
106+
}
107+
108+
response.setStatus(HttpServletResponse.SC_OK);
109+
response.setContentType("application/json");
110+
response.getWriter().write("{\"status\":\"UP\"}");
111+
response.getWriter().flush();
112+
}
113+
114+
private String[] decodeBasicAuth(String authHeader) {
115+
try {
116+
return new String(
117+
Base64.getDecoder().decode(authHeader.substring(BASIC_AUTH_PREFIX.length())))
118+
.split(":");
119+
} catch (IllegalArgumentException e) {
120+
logger.warn("Failed to decode Basic Auth header: {}", e.getMessage());
121+
return null;
122+
}
140123
}
141124

142-
private String extractOrganizationalUnit(String certValue) throws Exception {
125+
private String extractOrganizationalUnit(String certValue) throws CertificateException {
143126
X509Certificate certificate = parseCertificate(certValue);
144127
return certificate.getSubjectX500Principal().getName();
145128
}
146129

147-
private X509Certificate parseCertificate(String certValue) throws Exception {
130+
private X509Certificate parseCertificate(String certValue) throws CertificateException {
148131
// Extract the base64-encoded certificate from the XFCC header
149132
String base64Cert =
150133
certValue
@@ -159,8 +142,10 @@ private X509Certificate parseCertificate(String certValue) throws Exception {
159142
}
160143

161144
private boolean isValidOrganizationalUnit(String organizationalUnit) {
162-
boolean isSpaceValid = organizationalUnit.contains("space:" + validSpaceGuid);
163-
boolean isOrgValid = organizationalUnit.contains("organization:" + validOrgGuid);
145+
boolean isSpaceValid =
146+
organizationalUnit.contains("space:" + cfServerConfiguration.getValidSpaceGuid());
147+
boolean isOrgValid =
148+
organizationalUnit.contains("organization:" + cfServerConfiguration.getValidOrgGuid());
164149
return isSpaceValid && isOrgValid;
165150
}
166151
}

0 commit comments

Comments
 (0)