changes for allowed cidrs; refactor

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
Abhishek Kumar 2026-03-20 15:14:36 +05:30
parent 38c8b70cf3
commit 50403f7548
8 changed files with 334 additions and 144 deletions

View File

@ -29,6 +29,7 @@ import javax.servlet.http.HttpServletRequest;
import org.apache.cloudstack.utils.server.ServerPropertiesUtil;
import org.apache.cloudstack.veeam.api.ApiService;
import org.apache.cloudstack.veeam.filter.AllowedClientCidrsFilter;
import org.apache.cloudstack.veeam.filter.BearerOrBasicAuthFilter;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
@ -43,6 +44,7 @@ import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.RequestLogHandler;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.ssl.SslContextFactory;
@ -51,12 +53,14 @@ import org.jetbrains.annotations.NotNull;
public class VeeamControlServer {
private static final Logger LOGGER = LogManager.getLogger(VeeamControlServer.class);
private final VeeamControlService veeamControlService;
private Server server;
private List<RouteHandler> routeHandlers;
public VeeamControlServer(List<RouteHandler> routeHandlers) {
public VeeamControlServer(List<RouteHandler> routeHandlers, VeeamControlService veeamControlService) {
this.routeHandlers = new ArrayList<>(routeHandlers);
this.routeHandlers.sort((a, b) -> Integer.compare(b.priority(), a.priority()));
this.veeamControlService = veeamControlService;
}
public void startIfEnabled() throws Exception {
@ -118,8 +122,15 @@ public class VeeamControlServer {
new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
ctx.setContextPath(ctxPath);
// CIDR filter for all routes
AllowedClientCidrsFilter cidrFilter = new AllowedClientCidrsFilter(veeamControlService);
FilterHolder cidrHolder = new FilterHolder(cidrFilter);
ctx.addFilter(cidrHolder, ApiService.BASE_ROUTE + "/*", EnumSet.of(DispatcherType.REQUEST));
// Bearer or Basic Auth for all routes
ctx.addFilter(BearerOrBasicAuthFilter.class, ApiService.BASE_ROUTE + "/*", EnumSet.of(DispatcherType.REQUEST));
BearerOrBasicAuthFilter authFilter = new BearerOrBasicAuthFilter(veeamControlService);
FilterHolder authHolder = new FilterHolder(authFilter);
ctx.addFilter(authHolder, ApiService.BASE_ROUTE + "/*", EnumSet.of(DispatcherType.REQUEST));
// Front controller servlet
ctx.addServlet(new ServletHolder(new VeeamControlServlet(routeHandlers)), "/*");

View File

@ -17,6 +17,8 @@
package org.apache.cloudstack.veeam;
import java.util.List;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
@ -31,9 +33,9 @@ public interface VeeamControlService extends PluggableService, Configurable {
"8090", "Port for Veeam Integration REST API server", false);
ConfigKey<String> ContextPath = new ConfigKey<>("Advanced", String.class, "integration.veeam.control.context.path",
"/ovirt-engine", "Context path for Veeam Integration REST API server", false);
ConfigKey<String> Username = new ConfigKey<>("Advanced", String.class, "integration.veeam.control.api.username",
ConfigKey<String> Username = new ConfigKey<>("Secure", String.class, "integration.veeam.control.api.username",
"veeam", "Username for Basic Auth on Veeam Integration REST API server", true);
ConfigKey<String> Password = new ConfigKey<>("Advanced", String.class, "integration.veeam.control.api.password",
ConfigKey<String> Password = new ConfigKey<>("Secure", String.class, "integration.veeam.control.api.password",
"change-me", "Password for Basic Auth on Veeam Integration REST API server", true);
ConfigKey<String> ServiceAccountId = new ConfigKey<>("Advanced", String.class,
"integration.veeam.control.service.account", "",
@ -46,4 +48,13 @@ public interface VeeamControlService extends PluggableService, Configurable {
"false", "Attempt to assign restored Instance to the owner based on OVF and network " +
"details. If the assignment fails or set to false then the Instance will remain owned by the service " +
"account", true);
ConfigKey<String> AllowedClientCidrs = new ConfigKey<>("Advanced", String.class,
"integration.veeam.control.allowed.client.cidrs",
"", "Comma-separated list of CIDR blocks representing clients allowed to access the API. " +
"If empty, all clients will be allowed. Example: '192.168.1.1/24,192.168.2.100/32", true);
List<String> getAllowedClientCidrs();
boolean validateCredentials(String username, String password);
}

View File

@ -17,17 +17,43 @@
package org.apache.cloudstack.veeam;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.utils.cache.SingleCache;
import org.apache.cloudstack.veeam.utils.DataUtil;
import org.apache.commons.lang3.StringUtils;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.net.NetUtils;
public class VeeamControlServiceImpl extends ManagerBase implements VeeamControlService {
private List<RouteHandler> routeHandlers;
private VeeamControlServer veeamControlServer;
private SingleCache<List<String>> allowedClientCidrsCache;
protected List<String> getAllowedClientCidrsInternal() {
String allowedClientCidrsStr = AllowedClientCidrs.value();
if (StringUtils.isBlank(allowedClientCidrsStr)) {
return Collections.emptyList();
}
List<String> allowedClientCidrs = List.of(allowedClientCidrsStr.split(","));
// Sanitize and remove any incorrect CIDR entries
allowedClientCidrs = allowedClientCidrs.stream()
.map(String::trim)
.filter(StringUtils::isNotBlank)
.filter(cidr -> {
boolean valid = NetUtils.isValidIp4Cidr(cidr);
if (!valid) {
logger.warn("Invalid CIDR entry '{}' in allowed client CIDRs, ignoring", cidr);
}
return valid;
}).collect(Collectors.toList());
return allowedClientCidrs;
}
public List<RouteHandler> getRouteHandlers() {
return routeHandlers;
@ -37,9 +63,21 @@ public class VeeamControlServiceImpl extends ManagerBase implements VeeamControl
this.routeHandlers = routeHandlers;
}
@Override
public List<String> getAllowedClientCidrs() {
return allowedClientCidrsCache.get();
}
@Override
public boolean validateCredentials(String username, String password) {
return DataUtil.constantTimeEquals(Username.value(), username) &&
DataUtil.constantTimeEquals(Password.value(), password);
}
@Override
public boolean start() {
veeamControlServer = new VeeamControlServer(getRouteHandlers());
allowedClientCidrsCache = new SingleCache<>(30, this::getAllowedClientCidrsInternal);
veeamControlServer = new VeeamControlServer(getRouteHandlers(), this);
try {
veeamControlServer.startIfEnabled();
} catch (Exception e) {
@ -71,14 +109,15 @@ public class VeeamControlServiceImpl extends ManagerBase implements VeeamControl
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey[] {
Enabled,
BindAddress,
Port,
ContextPath,
Username,
Password,
ServiceAccountId,
InstanceRestoreAssignOwner
Enabled,
BindAddress,
Port,
ContextPath,
Username,
Password,
ServiceAccountId,
InstanceRestoreAssignOwner,
AllowedClientCidrs
};
}
}

View File

@ -0,0 +1,100 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.veeam.filter;
import java.io.IOException;
import java.net.InetAddress;
import java.util.List;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.cloudstack.veeam.VeeamControlService;
import org.apache.commons.collections.CollectionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.cloud.utils.net.NetUtils;
public class AllowedClientCidrsFilter implements Filter {
private static final Logger LOGGER = LogManager.getLogger(AllowedClientCidrsFilter.class);
private final VeeamControlService veeamControlService;
public AllowedClientCidrsFilter(VeeamControlService veeamControlService) {
this.veeamControlService = veeamControlService;
}
@Override
public void init(FilterConfig filterConfig) {
// no-op
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
chain.doFilter(request, response);
return;
}
final HttpServletRequest req = (HttpServletRequest) request;
final HttpServletResponse resp = (HttpServletResponse) response;
if (veeamControlService == null) {
LOGGER.warn("Failed to inject VeeamControlService, allowing request by default");
chain.doFilter(request, response);
return;
}
final List<String> cidrList = veeamControlService.getAllowedClientCidrs();
if (CollectionUtils.isEmpty(cidrList)) {
chain.doFilter(request, response);
return;
}
final String remoteAddr = req.getRemoteAddr();
try {
final InetAddress clientIp = InetAddress.getByName(remoteAddr);
final boolean allowed = NetUtils.isIpInCidrList(clientIp, cidrList.toArray(new String[0]));
if (!allowed) {
LOGGER.warn("Rejected request from client IP {} not in allowed CIDRs {}", remoteAddr, cidrList);
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
return;
}
} catch (Exception e) {
LOGGER.warn("Rejected request failed to parse client IP {}: {}", remoteAddr, e.getMessage());
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
return;
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
// no-op
}
}

View File

@ -21,11 +21,8 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
@ -36,22 +33,30 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.cloudstack.veeam.VeeamControlService;
import org.apache.cloudstack.veeam.sso.SsoService;
import org.apache.cloudstack.veeam.utils.DataUtil;
import org.apache.cloudstack.veeam.utils.JwtUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
public class BearerOrBasicAuthFilter implements Filter {
// Keep these aligned with SsoService (move to ConfigKeys later)
public static final List<String> REQUIRED_SCOPES = List.of("ovirt-app-admin", "ovirt-app-portal");
public static final String ISSUER = "veeam-control";
public static final String HMAC_SECRET = "change-this-super-secret-key-change-this";
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
@Override public void init(FilterConfig filterConfig) {}
@Override public void destroy() {}
private final VeeamControlService veeamControlService;
public BearerOrBasicAuthFilter(VeeamControlService veeamControlService) {
this.veeamControlService = veeamControlService;
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
@ -89,9 +94,6 @@ public class BearerOrBasicAuthFilter implements Filter {
}
private boolean verifyBasic(String b64) {
final String expectedUser = VeeamControlService.Username.value();
final String expectedPass = VeeamControlService.Password.value();
final String decoded;
try {
decoded = new String(Base64.getDecoder().decode(b64), StandardCharsets.UTF_8);
@ -105,7 +107,7 @@ public class BearerOrBasicAuthFilter implements Filter {
final String user = decoded.substring(0, idx);
final String pass = decoded.substring(idx + 1);
return constantTimeEquals(user, expectedUser) && constantTimeEquals(pass, expectedPass);
return veeamControlService != null && veeamControlService.validateCredentials(user, pass);
}
/**
@ -114,9 +116,6 @@ public class BearerOrBasicAuthFilter implements Filter {
* - "iss" matches
* - "exp" not expired
* - "scope" contains REQUIRED_SCOPES (space-separated)
*
* NOTE: This does not parse JSON robustly; its sufficient for the token you mint in SsoService.
* If you want robust parsing, switch to Nimbus and keep the rest the same.
*/
private boolean verifyJwtHs256(String token) {
final String[] parts = token.split("\\.");
@ -128,8 +127,8 @@ public class BearerOrBasicAuthFilter implements Filter {
final byte[] expectedSig;
try {
expectedSig = hmacSha256((headerB64 + "." + payloadB64).getBytes(StandardCharsets.UTF_8),
HMAC_SECRET.getBytes(StandardCharsets.UTF_8));
expectedSig = JwtUtil.hmacSha256((headerB64 + "." + payloadB64).getBytes(StandardCharsets.UTF_8),
SsoService.HMAC_SECRET.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
return false;
}
@ -141,21 +140,22 @@ public class BearerOrBasicAuthFilter implements Filter {
return false;
}
if (!constantTimeEquals(expectedSig, providedSig)) return false;
if (!DataUtil.constantTimeEquals(expectedSig, providedSig)) return false;
Map<String, Object> payloadMap;
try {
String payloadJson = new String(Base64.getUrlDecoder().decode(payloadB64), StandardCharsets.UTF_8);
payloadMap = JSON_MAPPER.readValue(
payloadJson,
new TypeReference<>() {}
new TypeReference<>() {
}
);
} catch (IllegalArgumentException | JsonProcessingException e) {
return false;
}
final String iss = (String)payloadMap.get("iss");
final String scope = (String)payloadMap.get("scope");
final String iss = (String) payloadMap.get("iss");
final String scope = (String) payloadMap.get("scope");
final Object expObj = payloadMap.get("exp");
Long exp = null;
if (expObj instanceof Number) {
@ -163,10 +163,11 @@ public class BearerOrBasicAuthFilter implements Filter {
} else if (expObj instanceof String) {
try {
exp = Long.parseLong((String) expObj);
} catch (NumberFormatException ignored) {}
} catch (NumberFormatException ignored) {
}
}
if (!ISSUER.equals(iss)) {
if (!JwtUtil.ISSUER.equals(iss)) {
return false;
}
if (exp == null || Instant.now().getEpochSecond() >= exp) {
@ -177,7 +178,7 @@ public class BearerOrBasicAuthFilter implements Filter {
private static boolean hasRequiredScopes(String scope) {
String[] scopes = scope.split("\\s+");
for (String required : REQUIRED_SCOPES) {
for (String required : SsoService.REQUIRED_SCOPES) {
if (!hasScope(scopes, required)) return false;
}
return true;
@ -192,22 +193,15 @@ public class BearerOrBasicAuthFilter implements Filter {
return false;
}
private static byte[] hmacSha256(byte[] data, byte[] key) throws Exception {
final Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(data);
}
private static void unauthorized(HttpServletRequest req, HttpServletResponse resp,
String error, String desc) throws IOException {
// IMPORTANT: dont throw (your current filter throws and Jetty turns it into 500) :contentReference[oaicite:3]{index=3}
resp.resetBuffer();
resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// Helpful for OAuth clients:
resp.setHeader("WWW-Authenticate",
"Bearer realm=\"Veeam Integration\", error=\"" + esc(error) + "\", error_description=\"" + esc(desc) + "\"");
"Bearer realm=\"Veeam Integration\", error=\"" + DataUtil.jsonEscape(error) +
"\", error_description=\"" + DataUtil.jsonEscape(desc) + "\"");
final String accept = req.getHeader("Accept");
final boolean wantsJson = accept != null && accept.toLowerCase().contains("application/json");
@ -215,27 +209,12 @@ public class BearerOrBasicAuthFilter implements Filter {
resp.setCharacterEncoding("UTF-8");
if (wantsJson) {
resp.setContentType("application/json; charset=UTF-8");
resp.getWriter().write("{\"error\":\"" + esc(error) + "\",\"error_description\":\"" + esc(desc) + "\"}");
resp.getWriter().write("{\"error\":\"" + DataUtil.jsonEscape(error) +
"\",\"error_description\":\"" + DataUtil.jsonEscape(desc) + "\"}");
} else {
resp.setContentType("text/html; charset=UTF-8");
resp.getWriter().write("<html><head><title>Error</title></head><body>Unauthorized</body></html>");
}
resp.getWriter().flush();
}
private static String esc(String s) {
return s == null ? "" : s.replace("\\", "\\\\").replace("\"", "\\\"");
}
private static boolean constantTimeEquals(String a, String b) {
if (a == null || b == null) return false;
return constantTimeEquals(a.getBytes(StandardCharsets.UTF_8), b.getBytes(StandardCharsets.UTF_8));
}
private static boolean constantTimeEquals(byte[] x, byte[] y) {
if (x.length != y.length) return false;
int r = 0;
for (int i = 0; i < x.length; i++) r |= x[i] ^ y[i];
return r == 0;
}
}

View File

@ -20,27 +20,30 @@ package org.apache.cloudstack.veeam.sso;
import java.io.IOException;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.cloudstack.veeam.RouteHandler;
import org.apache.cloudstack.veeam.VeeamControlService;
import org.apache.cloudstack.veeam.VeeamControlServlet;
import org.apache.cloudstack.veeam.filter.BearerOrBasicAuthFilter;
import org.apache.cloudstack.veeam.utils.JwtUtil;
import org.apache.cloudstack.veeam.utils.Negotiation;
import org.apache.commons.lang3.StringUtils;
import com.cloud.utils.component.ManagerBase;
public class SsoService extends ManagerBase implements RouteHandler {
private static final String BASE_ROUTE = "/sso";
private static final long DEFAULT_TTL_SECONDS = 3600;
public static final List<String> REQUIRED_SCOPES = List.of("ovirt-app-admin", "ovirt-app-portal");
public static final String HMAC_SECRET = "change-this-super-secret-key-change-this";
// Replace with your real credential validation (CloudStack account, config, etc.)
private final PasswordAuthenticator authenticator = new StaticPasswordAuthenticator();
@Inject
VeeamControlService veeamControlService;
@Override
public boolean canHandle(String method, String path) {
@ -48,7 +51,8 @@ public class SsoService extends ManagerBase implements RouteHandler {
}
@Override
public void handle(HttpServletRequest req, HttpServletResponse resp, String path, Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException {
public void handle(HttpServletRequest req, HttpServletResponse resp, String path, Negotiation.OutFormat outFormat,
VeeamControlServlet io) throws IOException {
final String sanitizedPath = getSanitizedPath(path);
if (sanitizedPath.equals(BASE_ROUTE + "/oauth/token")) {
handleToken(req, resp, outFormat, io);
@ -62,54 +66,56 @@ public class SsoService extends ManagerBase implements RouteHandler {
Negotiation.OutFormat outFormat, VeeamControlServlet io) throws IOException {
if (!"POST".equalsIgnoreCase(req.getMethod())) {
// oVirt-like: 405 is fine; if you strictly want 400, change to SC_BAD_REQUEST
resp.setHeader("Allow", "POST");
io.getWriter().write(resp, HttpServletResponse.SC_METHOD_NOT_ALLOWED,
Map.of("error", "method_not_allowed", "message", "token endpoint requires POST"), outFormat);
Map.of("error", "method_not_allowed",
"message", "token endpoint requires POST"), outFormat);
return;
}
// OAuth password grant uses x-www-form-urlencoded. With servlet containers this usually populates getParameter().
final String grantType = trimToNull(req.getParameter("grant_type"));
final String scope = trimToNull(req.getParameter("scope")); // typically "ovirt-app-api"
final String scope = trimToNull(req.getParameter("scope"));
final String username = trimToNull(req.getParameter("username"));
final String password = trimToNull(req.getParameter("password"));
if (grantType == null) {
io.getWriter().write(resp, HttpServletResponse.SC_BAD_REQUEST,
Map.of("error", "invalid_request", "error_description", "Missing parameter: grant_type"), outFormat);
Map.of("error", "invalid_request",
"error_description", "Missing parameter: grant_type"), outFormat);
return;
}
if (!"password".equals(grantType)) {
io.getWriter().write(resp, HttpServletResponse.SC_BAD_REQUEST,
Map.of("error", "unsupported_grant_type", "error_description", "Only grant_type=password is supported"), outFormat);
Map.of("error", "unsupported_grant_type",
"error_description", "Only grant_type=password is supported"), outFormat);
return;
}
if (username == null || password == null) {
io.getWriter().write(resp, HttpServletResponse.SC_BAD_REQUEST,
Map.of("error", "invalid_request", "error_description", "Missing username/password"), outFormat);
Map.of("error", "invalid_request",
"error_description", "Missing username/password"), outFormat);
return;
}
if (!authenticator.authenticate(username, password)) {
// 401 for bad creds
if (!veeamControlService.validateCredentials(username, password)) {
io.getWriter().write(resp, HttpServletResponse.SC_UNAUTHORIZED,
Map.of("error", "invalid_grant", "error_description", "Invalid credentials"), outFormat);
Map.of("error", "invalid_grant",
"error_description", "Invalid credentials"), outFormat);
return;
}
final String effectiveScope = (scope == null) ? "ovirt-app-api" : scope;
final String effectiveScope = (scope == null) ? StringUtils.join(REQUIRED_SCOPES, " ") : scope;
final long ttl = DEFAULT_TTL_SECONDS;
long nowMillis = Instant.now().toEpochMilli();
long expMillis = nowMillis + ttl * 1000L;
final String token;
try {
token = JwtUtil.issueHs256Jwt(BearerOrBasicAuthFilter.ISSUER, username, effectiveScope, ttl,
BearerOrBasicAuthFilter.HMAC_SECRET);
token = JwtUtil.issueHs256Jwt(username, effectiveScope, ttl, HMAC_SECRET);
} catch (Exception e) {
io.getWriter().write(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
Map.of("error", "server_error", "error_description", "Failed to issue token"), outFormat);
Map.of("error", "server_error",
"error_description", "Failed to issue token"), outFormat);
return;
}
@ -128,61 +134,4 @@ public class SsoService extends ManagerBase implements RouteHandler {
s = s.trim();
return s.isEmpty() ? null : s;
}
// ---------- Minimal auth helpers (replace later) ----------
interface PasswordAuthenticator {
boolean authenticate(String username, String password);
}
static final class StaticPasswordAuthenticator implements PasswordAuthenticator {
StaticPasswordAuthenticator() {
}
@Override
public boolean authenticate(String username, String password) {
return VeeamControlService.Username.value().equals(username) &&
VeeamControlService.Password.value().equals(password);
}
}
// ---------- Minimal JWT HS256 without extra libs ----------
// (If you prefer Nimbus, I can convert this to nimbus-jose-jwt; this keeps dependencies tiny.)
static final class JwtUtil {
static String issueHs256Jwt(String issuer, String subject, String scope, long ttlSeconds, String secret) throws Exception {
long now = Instant.now().getEpochSecond();
long exp = now + ttlSeconds;
String headerJson = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
String payloadJson =
"{"
+ "\"iss\":\"" + jsonEscape(issuer) + "\","
+ "\"sub\":\"" + jsonEscape(subject) + "\","
+ "\"scope\":\"" + jsonEscape(scope) + "\","
+ "\"iat\":" + now + ","
+ "\"exp\":" + exp
+ "}";
String header = b64Url(headerJson.getBytes("UTF-8"));
String payload = b64Url(payloadJson.getBytes("UTF-8"));
String signingInput = header + "." + payload;
byte[] sig = hmacSha256(signingInput.getBytes("UTF-8"), secret.getBytes("UTF-8"));
return signingInput + "." + b64Url(sig);
}
static byte[] hmacSha256(byte[] data, byte[] key) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(data);
}
static String b64Url(byte[] in) {
return java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(in);
}
static String jsonEscape(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"");
}
}
}

View File

@ -0,0 +1,44 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.veeam.utils;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class DataUtil {
public static String b64Url(byte[] in) {
return Base64.getUrlEncoder().withoutPadding().encodeToString(in);
}
public static String jsonEscape(String s) {
return s == null ? "" : s.replace("\\", "\\\\").replace("\"", "\\\"");
}
public static boolean constantTimeEquals(String a, String b) {
if (a == null || b == null) return false;
return constantTimeEquals(a.getBytes(StandardCharsets.UTF_8), b.getBytes(StandardCharsets.UTF_8));
}
public static boolean constantTimeEquals(byte[] x, byte[] y) {
if (x.length != y.length) return false;
int r = 0;
for (int i = 0; i < x.length; i++) r |= x[i] ^ y[i];
return r == 0;
}
}

View File

@ -0,0 +1,57 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.veeam.utils;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class JwtUtil {
public static final String ALGORITHM = "HmacSHA256";
public static final String ISSUER = "veeam-control";
public static String issueHs256Jwt(String subject, String scope, long ttlSeconds, String secret) throws Exception {
long now = Instant.now().getEpochSecond();
long exp = now + ttlSeconds;
String headerJson = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
String payloadJson =
"{"
+ "\"iss\":\"" + DataUtil.jsonEscape(ISSUER) + "\","
+ "\"sub\":\"" + DataUtil.jsonEscape(subject) + "\","
+ "\"scope\":\"" + DataUtil.jsonEscape(scope) + "\","
+ "\"iat\":" + now + ","
+ "\"exp\":" + exp
+ "}";
String header = DataUtil.b64Url(headerJson.getBytes(StandardCharsets.UTF_8));
String payload = DataUtil.b64Url(payloadJson.getBytes(StandardCharsets.UTF_8));
String signingInput = header + "." + payload;
byte[] sig = hmacSha256(signingInput.getBytes(StandardCharsets.UTF_8), secret.getBytes(StandardCharsets.UTF_8));
return signingInput + "." + DataUtil.b64Url(sig);
}
public static byte[] hmacSha256(byte[] data, byte[] key) throws Exception {
final Mac mac = Mac.getInstance(ALGORITHM);
mac.init(new SecretKeySpec(key, ALGORITHM));
return mac.doFinal(data);
}
}