Compare commits
6 Commits
ebe8e5d574
...
421911e41b
| Author | SHA1 | Date | |
|---|---|---|---|
| 421911e41b | |||
| 0e5de99f55 | |||
| 0dae53017d | |||
| 934937feb0 | |||
| dcb6850306 | |||
| f264f90f3b |
5
.vscode/launch.json
vendored
5
.vscode/launch.json
vendored
@@ -59,14 +59,15 @@
|
||||
"REDIS_HOST": "103.125.129.116",
|
||||
"REDIS_PORT": "7901",
|
||||
"REDIS_PASSWORD": "M@triXR3d1s@6202",
|
||||
"REDIS_DATABASE": "1",
|
||||
"REDIS_SSL": "false",
|
||||
"CYGNUS_CLOUD_BASE_URL": "http://localhost:8090",
|
||||
"CYGNUS_TOKEN_URL": "http://localhost:8090/oauth2/token",
|
||||
"CYGNUS_CLIENT_ID": "matrix",
|
||||
"CYGNUS_INSTALLATION_ID": "matrix-delhi-cygnus-01",
|
||||
"CYGNUS_CLIENT_ASSERTION": "file:${workspaceFolder}/matrix-installation/config/machine-assertion.jwt",
|
||||
"CYGNUS_CLIENT_ASSERTION": "file:${workspaceFolder}/config/clients/matrix/matrix-matrix-delhi-cygnus-01-assertion.jwt",
|
||||
"CYGNUS_LOGIN_KEY_ID": "cygnus-login-2026-01",
|
||||
"CYGNUS_LOGIN_PUBLIC_KEY": "file:${workspaceFolder}/matrix-installation/config/keys/login-public.pem",
|
||||
"CYGNUS_LOGIN_PUBLIC_KEY": "file:${workspaceFolder}/config/keys/login-public.pem",
|
||||
"CYGNUS_CLOUD_REQUEST_TIMEOUT": "PT10S",
|
||||
"CYGNUS_TOKEN_REFRESH_SKEW": "PT30S"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.cygnus.client;
|
||||
|
||||
import com.cygnus.client.model.CloudIdentitySession;
|
||||
import com.cygnus.client.model.CloudQueryResponse;
|
||||
import com.cygnus.client.model.LoginPayload;
|
||||
import com.cygnus.client.security.LoginEnvelopeEncryptor;
|
||||
import com.cygnus.client.security.MachineTokenProvider;
|
||||
@@ -50,4 +51,15 @@ public class CloudIdentityClient {
|
||||
.bodyToMono(CloudIdentitySession.class))
|
||||
.timeout(properties.requestTimeout());
|
||||
}
|
||||
|
||||
public Mono<CloudQueryResponse> fetchQuery(int queryId) {
|
||||
return tokenProvider.accessToken()
|
||||
.flatMap(token -> webClient.get()
|
||||
.uri(properties.baseUri().resolve("/api/v1/queries/" + queryId))
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.bodyToMono(CloudQueryResponse.class))
|
||||
.timeout(properties.requestTimeout());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.cygnus.client.model;
|
||||
|
||||
public record CloudQueryResponse(int queryId, String query) {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.cygnus.cloud.query;
|
||||
|
||||
public record CloudQuery(int queryId, String query) {
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.cygnus.cloud.query;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/queries")
|
||||
public class CloudQueryController {
|
||||
|
||||
private final QueryCatalogRepository repository;
|
||||
|
||||
public CloudQueryController(QueryCatalogRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@GetMapping("/{queryId}")
|
||||
public Mono<CloudQuery> query(
|
||||
@PathVariable @Min(1) int queryId) {
|
||||
return repository.findEnabled(queryId)
|
||||
.switchIfEmpty(Mono.error(new QueryNotFoundException(queryId)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.cygnus.cloud.query;
|
||||
|
||||
import com.cygnus.cloud.database.ReactiveDatabaseClient;
|
||||
import io.vertx.sqlclient.Tuple;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Repository
|
||||
public class QueryCatalogRepository {
|
||||
|
||||
private static final String INITIALIZE = """
|
||||
CREATE SCHEMA IF NOT EXISTS platform;
|
||||
CREATE TABLE IF NOT EXISTS platform.application_query (
|
||||
query_id integer PRIMARY KEY,
|
||||
query_text text NOT NULL,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT current_timestamp,
|
||||
updated_at timestamptz NOT NULL DEFAULT current_timestamp,
|
||||
CONSTRAINT ck_platform_application_query_id
|
||||
CHECK (query_id > 0),
|
||||
CONSTRAINT ck_platform_application_query_text
|
||||
CHECK (length(btrim(query_text)) > 0)
|
||||
)
|
||||
""";
|
||||
private static final String FIND = """
|
||||
SELECT query_id, query_text
|
||||
FROM platform.application_query
|
||||
WHERE query_id = $1
|
||||
AND enabled = true
|
||||
""";
|
||||
private final ReactiveDatabaseClient database;
|
||||
|
||||
public QueryCatalogRepository(ReactiveDatabaseClient database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
public Mono<Void> initialize() {
|
||||
return database.query(INITIALIZE).then();
|
||||
}
|
||||
|
||||
public Mono<CloudQuery> findEnabled(int queryId) {
|
||||
return database.preparedQuery(FIND, Tuple.of(queryId))
|
||||
.flatMapMany(rows -> reactor.core.publisher.Flux.fromIterable(rows))
|
||||
.next()
|
||||
.map(row -> new CloudQuery(
|
||||
row.getInteger("query_id"), row.getString("query_text")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.cygnus.cloud.query;
|
||||
|
||||
import java.time.Duration;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class QueryCatalogSchemaInitializer implements ApplicationRunner {
|
||||
|
||||
private final QueryCatalogRepository repository;
|
||||
|
||||
public QueryCatalogSchemaInitializer(QueryCatalogRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments arguments) {
|
||||
repository.initialize()
|
||||
.block(Duration.ofMinutes(2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cygnus.cloud.query;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public class QueryNotFoundException extends RuntimeException {
|
||||
|
||||
public QueryNotFoundException(int queryId) {
|
||||
super("Query was not found: " + queryId);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,8 @@ public class CloudSecurityConfiguration {
|
||||
.permitAll()
|
||||
.pathMatchers("/api/v1/identity/login")
|
||||
.hasAuthority("SCOPE_identity.login")
|
||||
.pathMatchers("/api/v1/queries/**")
|
||||
.hasAuthority("SCOPE_identity.login")
|
||||
.pathMatchers("/api/v1/admin/**")
|
||||
.hasAuthority("SCOPE_cygnus.admin")
|
||||
.anyExchange().authenticated())
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE SCHEMA IF NOT EXISTS platform;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS platform.application_query (
|
||||
query_id integer PRIMARY KEY,
|
||||
query_text text NOT NULL,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT current_timestamp,
|
||||
updated_at timestamptz NOT NULL DEFAULT current_timestamp,
|
||||
CONSTRAINT ck_platform_application_query_id
|
||||
CHECK (query_id > 0),
|
||||
CONSTRAINT ck_platform_application_query_text
|
||||
CHECK (length(btrim(query_text)) > 0)
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
DO $$
|
||||
DECLARE
|
||||
current_type text;
|
||||
BEGIN
|
||||
SELECT data_type
|
||||
INTO current_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'platform'
|
||||
AND table_name = 'application_query'
|
||||
AND column_name = 'query_id';
|
||||
|
||||
IF current_type IN ('character varying', 'text') THEN
|
||||
ALTER TABLE platform.application_query
|
||||
DROP CONSTRAINT IF EXISTS ck_platform_application_query_id;
|
||||
|
||||
ALTER TABLE platform.application_query
|
||||
ALTER COLUMN query_id TYPE integer
|
||||
USING regexp_replace(query_id, '^Query', '')::integer;
|
||||
|
||||
ALTER TABLE platform.application_query
|
||||
ADD CONSTRAINT ck_platform_application_query_id
|
||||
CHECK (query_id > 0);
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
@@ -30,6 +30,7 @@ public class DeploymentWriter {
|
||||
try {
|
||||
Path normalizedOutput = output.toAbsolutePath().normalize();
|
||||
Path config = normalizedOutput.resolve("config");
|
||||
protectCloudConfiguration(config, profile);
|
||||
Path keyDirectory = config.resolve("keys");
|
||||
Files.createDirectories(keyDirectory);
|
||||
|
||||
@@ -82,6 +83,23 @@ public class DeploymentWriter {
|
||||
}
|
||||
}
|
||||
|
||||
private void protectCloudConfiguration(Path deploymentConfig, ProductProfile profile) {
|
||||
if (isInside(deploymentConfig, profile.assertionEncryptionPublicKey())
|
||||
|| isInside(deploymentConfig, profile.loginEncryptionPublicKey())) {
|
||||
throw new IllegalArgumentException(
|
||||
"Refusing to write deployment files over the cloud-service "
|
||||
+ "configuration directory");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInside(Path directory, Path file) {
|
||||
if (file == null) {
|
||||
return false;
|
||||
}
|
||||
return file.toAbsolutePath().normalize().startsWith(
|
||||
directory.toAbsolutePath().normalize());
|
||||
}
|
||||
|
||||
private String installationConfiguration(
|
||||
UUID installationUuid,
|
||||
String installationCode,
|
||||
@@ -174,6 +192,7 @@ public class DeploymentWriter {
|
||||
appendEnvironment(env, "REDIS_HOST", runtime.redisHost());
|
||||
appendEnvironment(env, "REDIS_PORT", runtime.redisPort());
|
||||
appendEnvironment(env, "REDIS_PASSWORD", runtime.redisPassword());
|
||||
appendEnvironment(env, "REDIS_DATABASE", "1");
|
||||
appendEnvironment(env, "REDIS_SSL", Boolean.toString(runtime.redisSsl()));
|
||||
appendEnvironment(
|
||||
env, "CYGNUS_CLOUD_BASE_URL", normalizedCloudServiceUrl(runtime));
|
||||
|
||||
@@ -156,6 +156,7 @@ public class InstallationService {
|
||||
if (request.outputDirectory() == null) {
|
||||
throw new IllegalArgumentException("Output directory is required");
|
||||
}
|
||||
validateOutputIsolation(request.outputDirectory());
|
||||
RuntimeConfiguration runtime = request.runtimeConfiguration();
|
||||
if (runtime == null) {
|
||||
throw new IllegalArgumentException("Runtime configuration is required");
|
||||
@@ -204,6 +205,27 @@ public class InstallationService {
|
||||
}
|
||||
}
|
||||
|
||||
private void validateOutputIsolation(Path outputDirectory) {
|
||||
Path deploymentConfig = outputDirectory
|
||||
.toAbsolutePath()
|
||||
.normalize()
|
||||
.resolve("config");
|
||||
if (isInside(deploymentConfig, profile.assertionEncryptionPublicKey())
|
||||
|| isInside(deploymentConfig, profile.loginEncryptionPublicKey())) {
|
||||
throw new IllegalArgumentException(
|
||||
"Output directory would overwrite the cloud-service config folder. "
|
||||
+ "Select a dedicated deployment directory, such as "
|
||||
+ "'matrix-installation'.");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInside(Path directory, Path file) {
|
||||
if (file == null) {
|
||||
return false;
|
||||
}
|
||||
return file.toAbsolutePath().normalize().startsWith(directory);
|
||||
}
|
||||
|
||||
private String normalizedCloudServiceUrl(String value) {
|
||||
String normalized = value.trim();
|
||||
return normalized.endsWith("/")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.cygnus.installer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
@@ -108,6 +109,7 @@ class DeploymentWriterTest {
|
||||
.contains("MATRIX_IMAGE=\"registry.example.com/matrix:1.0.0\"")
|
||||
.contains("MATRIX_DB_URL=\"jdbc:postgresql://db:5432/matrix\"")
|
||||
.contains("REDIS_HOST=\"redis\"")
|
||||
.contains("REDIS_DATABASE=\"1\"")
|
||||
.contains("CYGNUS_CLOUD_BASE_URL=\"http://host.docker.internal:8090\"")
|
||||
.contains("CYGNUS_TOKEN_URL=\"http://host.docker.internal:8090/oauth2/token\"")
|
||||
.contains("CYGNUS_CLIENT_ID=\"matrix-client\"")
|
||||
@@ -121,4 +123,84 @@ class DeploymentWriterTest {
|
||||
assertThat(output.resolve("config/keys/client-signing-private.pem"))
|
||||
.isRegularFile();
|
||||
}
|
||||
|
||||
@Test
|
||||
void refusesToOverwriteCloudServiceConfiguration() throws Exception {
|
||||
Path cloudConfig = temporaryDirectory.resolve("config");
|
||||
Path cloudKeys = cloudConfig.resolve("keys");
|
||||
Files.createDirectories(cloudKeys);
|
||||
Path assertionPublic = cloudKeys.resolve("assertion-decryption-public.pem");
|
||||
Path loginPublic = cloudKeys.resolve("login-public.pem");
|
||||
Files.writeString(assertionPublic, "assertion-public-key");
|
||||
Files.writeString(loginPublic, "login-public-key");
|
||||
|
||||
var profile = new ProductProfile(
|
||||
"matrix",
|
||||
"Matrix",
|
||||
"matrix-onprem",
|
||||
"MATRIX_IMAGE",
|
||||
"matrix",
|
||||
"installation.yml",
|
||||
"MATRIX_INSTALLATION_CONFIG",
|
||||
"/srv/matrix/config/installation.yml",
|
||||
"8080:8080",
|
||||
"/matrix/",
|
||||
"/oauth2/token",
|
||||
assertionPublic,
|
||||
loginPublic,
|
||||
"cygnus-login-2026-01",
|
||||
List.of());
|
||||
|
||||
assertThatThrownBy(() -> new DeploymentWriter().write(
|
||||
temporaryDirectory,
|
||||
UUID.randomUUID(),
|
||||
"primary",
|
||||
"matrix-client",
|
||||
new ActivationDtos.ValidationResponse(
|
||||
"activation-token",
|
||||
OffsetDateTime.now().plusMinutes(5),
|
||||
UUID.randomUUID(),
|
||||
"matrix-client",
|
||||
"FULL",
|
||||
2),
|
||||
new ActivationDtos.RegistrationResponse(
|
||||
UUID.randomUUID(),
|
||||
UUID.randomUUID(),
|
||||
UUID.randomUUID(),
|
||||
null,
|
||||
"primary",
|
||||
1,
|
||||
"ACTIVE"),
|
||||
new InstallationKeyService().generate(),
|
||||
"assertion",
|
||||
new InstallerSettings(
|
||||
"matrix",
|
||||
URI.create("https://cloud.example.com"),
|
||||
URI.create("https://cloud.example.com"),
|
||||
"/api/v1/installations",
|
||||
"production",
|
||||
temporaryDirectory,
|
||||
temporaryDirectory,
|
||||
"1",
|
||||
new IniDocument(Map.of())),
|
||||
profile,
|
||||
"registry.example.com/matrix:1.0.0",
|
||||
Map.of(),
|
||||
new RuntimeConfiguration(
|
||||
"https://cloud.example.com",
|
||||
"jdbc:postgresql://db/matrix",
|
||||
"postgres",
|
||||
"secret",
|
||||
"redis",
|
||||
"7901",
|
||||
"secret",
|
||||
false,
|
||||
"PT10S",
|
||||
"PT30S")))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("cloud-service configuration");
|
||||
|
||||
assertThat(assertionPublic).hasContent("assertion-public-key");
|
||||
assertThat(loginPublic).hasContent("login-public-key");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -15,6 +15,7 @@ import io.lettuce.core.api.sync.RedisCommands;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import matrix.nimble.query.EncryptedQueryCache;
|
||||
|
||||
/**
|
||||
* Best-effort shared cache for the on-premises MVC application. Redis failures
|
||||
@@ -22,7 +23,7 @@ import org.springframework.stereotype.Service;
|
||||
* application.
|
||||
*/
|
||||
@Service
|
||||
public class OnPremRedisCacheService implements DisposableBean {
|
||||
public class OnPremRedisCacheService implements DisposableBean, EncryptedQueryCache {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(OnPremRedisCacheService.class.getName());
|
||||
|
||||
@@ -53,6 +54,21 @@ public class OnPremRedisCacheService implements DisposableBean {
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<String> getRaw(String key) {
|
||||
try {
|
||||
return Optional.ofNullable(commands().get(key));
|
||||
} catch (RedisException exception) {
|
||||
logUnavailable(exception);
|
||||
resetConnection();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> get(String queryId) {
|
||||
return getRaw(queryId);
|
||||
}
|
||||
|
||||
public <T> Optional<T> get(String namespace, String key, Class<T> type) {
|
||||
return get(namespace, key).flatMap(json -> deserialize(json, objectMapper.constructType(type)));
|
||||
}
|
||||
@@ -79,6 +95,37 @@ public class OnPremRedisCacheService implements DisposableBean {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean putRaw(String key, String value, Duration ttl) {
|
||||
try {
|
||||
commands().setex(key, ttl.toSeconds(), value);
|
||||
return true;
|
||||
} catch (RedisException exception) {
|
||||
logUnavailable(exception);
|
||||
resetConnection();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean put(String queryId, String encryptedQuery, Duration ttl) {
|
||||
return putRaw(queryId, encryptedQuery, ttl);
|
||||
}
|
||||
|
||||
public boolean evictRaw(String key) {
|
||||
try {
|
||||
return commands().del(key) > 0;
|
||||
} catch (RedisException exception) {
|
||||
logUnavailable(exception);
|
||||
resetConnection();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evict(String queryId) {
|
||||
return evictRaw(queryId);
|
||||
}
|
||||
|
||||
public boolean evict(String namespace, String key) {
|
||||
try {
|
||||
return commands().del(cacheKey(namespace, key)) > 0;
|
||||
|
||||
@@ -16,11 +16,16 @@ public class OnPremRedisConfiguration {
|
||||
@Value("${REDIS_HOST:192.168.0.111}") String host,
|
||||
@Value("${REDIS_PORT:7901}") int port,
|
||||
@Value("${REDIS_PASSWORD:}") String password,
|
||||
@Value("${REDIS_DATABASE:1}") int database,
|
||||
@Value("${REDIS_SSL:false}") boolean ssl,
|
||||
@Value("${REDIS_CONNECT_TIMEOUT_SECONDS:3}") long connectTimeoutSeconds) {
|
||||
if (database < 0) {
|
||||
throw new IllegalArgumentException("REDIS_DATABASE must be zero or greater");
|
||||
}
|
||||
RedisURI.Builder uri = RedisURI.builder()
|
||||
.withHost(host)
|
||||
.withPort(port)
|
||||
.withDatabase(database)
|
||||
.withSsl(ssl)
|
||||
.withTimeout(Duration.ofSeconds(connectTimeoutSeconds));
|
||||
if (password != null && !password.isBlank()) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package matrix.nimble.controller;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
//spring libraries
|
||||
import matrix.nimble.model.DownloadUploadSettings;
|
||||
@@ -19,109 +19,109 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.SessionAttributes;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||
|
||||
@Controller
|
||||
@SessionAttributes({"Sessvals"})
|
||||
@SessionAttributes({ "Sessvals" })
|
||||
public class CaseUpload {
|
||||
@RequestMapping(value="caseupload",method=RequestMethod.POST )
|
||||
public String UploadCases(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals)
|
||||
{
|
||||
model.addAttribute("portlist",FillPortList(Sessvals.getBranchID(),"download"));
|
||||
DownloadUploadSettings us=new DownloadUploadSettings();
|
||||
@RequestMapping(value = "caseupload", method = RequestMethod.POST)
|
||||
public String UploadCases(ModelMap model, HttpServletRequest request,
|
||||
@ModelAttribute(value = "Sessvals") Session Sessvals) {
|
||||
model.addAttribute("portlist", FillPortList(Sessvals.getBranchID(), "download"));
|
||||
DownloadUploadSettings us = new DownloadUploadSettings();
|
||||
us.setPortfolioid("-1");
|
||||
model.addAttribute("uploadsettings",us);
|
||||
model.addAttribute("uploadsettings", us);
|
||||
return "general/onlinedownload";
|
||||
}
|
||||
@RequestMapping(value="downloadsettings",method=RequestMethod.POST )
|
||||
public String FetchDownloadSettings(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals,@ModelAttribute(value="uploadsettings") DownloadUploadSettings us)
|
||||
{
|
||||
UploadHandler UH=new UploadHandler();
|
||||
|
||||
@RequestMapping(value = "downloadsettings", method = RequestMethod.POST)
|
||||
public String FetchDownloadSettings(ModelMap model, HttpServletRequest request,
|
||||
@ModelAttribute(value = "Sessvals") Session Sessvals,
|
||||
@ModelAttribute(value = "uploadsettings") DownloadUploadSettings us) {
|
||||
UploadHandler UH = new UploadHandler();
|
||||
UH.setErrCode("1111");
|
||||
UH.setProcessFlag(true);
|
||||
model.addAttribute("portlist",FillPortList(Sessvals.getBranchID(),"download"));
|
||||
UH.FetchSettings(us,158);
|
||||
model.addAttribute("portlist", FillPortList(Sessvals.getBranchID(), "download"));
|
||||
UH.FetchSettings(us, 158);
|
||||
model.addAttribute("uploadsettings", us);
|
||||
if(us.getDomainname().equals("localhost"))
|
||||
{
|
||||
if (us.getDomainname().equals("localhost")) {
|
||||
return "general/offlinedownload";
|
||||
} else {
|
||||
return "general/onlinedownload";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "general/onlinedownload";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/startexcelupload", method = RequestMethod.POST)
|
||||
public String StartExcelUpload(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals,@ModelAttribute(value="uploadsettings") DownloadUploadSettings us,@RequestParam MultipartFile file)
|
||||
{
|
||||
public String StartExcelUpload(ModelMap model, HttpServletRequest request,
|
||||
@ModelAttribute(value = "Sessvals") Session Sessvals,
|
||||
@ModelAttribute(value = "uploadsettings") DownloadUploadSettings us, @RequestParam MultipartFile file) {
|
||||
HSSFWorkbook excel = null;
|
||||
XSSFWorkbook excelx = null;
|
||||
UploadHandler UH=new UploadHandler();
|
||||
UploadHandler UH = new UploadHandler();
|
||||
UH.setErrCode("1111");
|
||||
UH.setProcessFlag(true);
|
||||
try
|
||||
{
|
||||
if(file.getOriginalFilename().endsWith("xlsx"))
|
||||
{
|
||||
try {
|
||||
if (file.getOriginalFilename().endsWith("xlsx")) {
|
||||
excelx = new XSSFWorkbook(file.getInputStream());
|
||||
UH.StartXLXUpload(us,Sessvals.getUserID(),Sessvals.getCompanyID(),Sessvals.getBranchID(),excelx);
|
||||
}
|
||||
else if(file.getOriginalFilename().endsWith("xls"))
|
||||
{
|
||||
UH.StartXLXUpload(us, Sessvals.getUserID(), Sessvals.getCompanyID(), Sessvals.getBranchID(), excelx);
|
||||
} else if (file.getOriginalFilename().endsWith("xls")) {
|
||||
excel = new HSSFWorkbook(file.getInputStream());
|
||||
UH.StartXLUpload(us,Sessvals.getUserID(),Sessvals.getCompanyID(),Sessvals.getBranchID(),excel);
|
||||
}
|
||||
else
|
||||
{
|
||||
UH.setErrMsg(UH.getErrCode()+"INFIL:error:Invlaid file format. Please check the format of file you are uploading.");
|
||||
UH.StartXLUpload(us, Sessvals.getUserID(), Sessvals.getCompanyID(), Sessvals.getBranchID(), excel);
|
||||
} else {
|
||||
UH.setErrMsg(UH.getErrCode()
|
||||
+ "INFIL:error:Invlaid file format. Please check the format of file you are uploading.");
|
||||
UH.setProcessFlag(false);
|
||||
}
|
||||
}catch(Exception exce)
|
||||
{
|
||||
UH.setErrMsg(UH.getErrCode()+"INFIL:error:"+exce.getMessage());
|
||||
} catch (Exception exce) {
|
||||
UH.setErrMsg(UH.getErrCode() + "INFIL:error:" + exce.getMessage());
|
||||
UH.setProcessFlag(false);
|
||||
}
|
||||
model.addAttribute("portlist",FillPortList(Sessvals.getBranchID(),"download"));
|
||||
model.addAttribute("portlist", FillPortList(Sessvals.getBranchID(), "download"));
|
||||
model.addAttribute("uploadsettings", us);
|
||||
model.addAttribute("msg",UH.getErrMsg());
|
||||
return "general/offlinedownload";
|
||||
model.addAttribute("msg", UH.getErrMsg());
|
||||
return "general/offlinedownload";
|
||||
}
|
||||
@RequestMapping(value="startupload",method=RequestMethod.POST )
|
||||
public String StartUpload(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals,@ModelAttribute(value="uploadsettings") DownloadUploadSettings us)
|
||||
{
|
||||
UploadHandler UH=new UploadHandler();
|
||||
|
||||
@RequestMapping(value = "startupload", method = RequestMethod.POST)
|
||||
public String StartUpload(ModelMap model, HttpServletRequest request,
|
||||
@ModelAttribute(value = "Sessvals") Session Sessvals,
|
||||
@ModelAttribute(value = "uploadsettings") DownloadUploadSettings us) {
|
||||
UploadHandler UH = new UploadHandler();
|
||||
UH.setErrCode("1111");
|
||||
UH.setProcessFlag(true);
|
||||
model.addAttribute("portlist",FillPortList(Sessvals.getBranchID(),"download"));
|
||||
UH.StartUpload(us,Sessvals.getUserID(),Sessvals.getCompanyID(),Sessvals.getBranchID());
|
||||
model.addAttribute("portlist", FillPortList(Sessvals.getBranchID(), "download"));
|
||||
UH.StartUpload(us, Sessvals.getUserID(), Sessvals.getCompanyID(), Sessvals.getBranchID());
|
||||
model.addAttribute("uploadsettings", us);
|
||||
model.addAttribute("msg",UH.getErrMsg());
|
||||
return "general/onlinedownload";
|
||||
model.addAttribute("msg", UH.getErrMsg());
|
||||
return "general/onlinedownload";
|
||||
}
|
||||
@RequestMapping(value="reportupload",method=RequestMethod.POST )
|
||||
public String UploadReports(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals)
|
||||
{
|
||||
model.addAttribute("portlist",FillPortList(Sessvals.getBranchID(),"upload"));
|
||||
DownloadUploadSettings us=new DownloadUploadSettings();
|
||||
|
||||
@RequestMapping(value = "reportupload", method = RequestMethod.POST)
|
||||
public String UploadReports(ModelMap model, HttpServletRequest request,
|
||||
@ModelAttribute(value = "Sessvals") Session Sessvals) {
|
||||
model.addAttribute("portlist", FillPortList(Sessvals.getBranchID(), "upload"));
|
||||
DownloadUploadSettings us = new DownloadUploadSettings();
|
||||
us.setPortfolioid("-1");
|
||||
model.addAttribute("uploadsettings",us);
|
||||
model.addAttribute("uploadsettings", us);
|
||||
return "general/uploadreports";
|
||||
}
|
||||
@RequestMapping(value="fileuploadsettings",method=RequestMethod.POST )
|
||||
public String FetchUploadSettings(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals,@ModelAttribute(value="uploadsettings") DownloadUploadSettings us)
|
||||
{
|
||||
UploadHandler UH=new UploadHandler();
|
||||
|
||||
@RequestMapping(value = "fileuploadsettings", method = RequestMethod.POST)
|
||||
public String FetchUploadSettings(ModelMap model, HttpServletRequest request,
|
||||
@ModelAttribute(value = "Sessvals") Session Sessvals,
|
||||
@ModelAttribute(value = "uploadsettings") DownloadUploadSettings us) {
|
||||
UploadHandler UH = new UploadHandler();
|
||||
UH.setErrCode("1112");
|
||||
UH.setProcessFlag(true);
|
||||
model.addAttribute("portlist",FillPortList(Sessvals.getBranchID(),"upload"));
|
||||
UH.FetchSettings(us,159);
|
||||
model.addAttribute("portlist", FillPortList(Sessvals.getBranchID(), "upload"));
|
||||
UH.FetchSettings(us, 159);
|
||||
UH.FetchFilesToUpload(us);
|
||||
model.addAttribute("uploadsettings", us);
|
||||
return "general/uploadreports";
|
||||
}
|
||||
public String [][] FillPortList(String BranchId,String Action)
|
||||
{
|
||||
return new ModuleFunctions("1001").GetResultArray(157,(BranchId+GlobalClass.ColDelim+Action+GlobalClass.ColDelim).split(GlobalClass.ColDelim));
|
||||
|
||||
public String[][] FillPortList(String BranchId, String Action) {
|
||||
return new ModuleFunctions("1001").GetResultArray(157,
|
||||
(BranchId + GlobalClass.ColDelim + Action + GlobalClass.ColDelim).split(GlobalClass.ColDelim));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,37 @@
|
||||
package matrix.nimble.controller;
|
||||
|
||||
import java.util.Map;
|
||||
package matrix.nimble.controller;
|
||||
|
||||
import matrix.nimble.cloud.identity.CloudAuthenticationException;
|
||||
import matrix.nimble.cloud.identity.CloudAuthenticationGateway;
|
||||
import matrix.nimble.model.Login;
|
||||
import matrix.nimble.model.Session;
|
||||
|
||||
//servlet libraries
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
|
||||
//spring libraries
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.SessionAttributes;
|
||||
|
||||
@Controller
|
||||
@SessionAttributes("Sessvals")
|
||||
|
||||
//servlet libraries
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
|
||||
//spring libraries
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.SessionAttributes;
|
||||
|
||||
@Controller
|
||||
@SessionAttributes("Sessvals")
|
||||
public class SessionController {
|
||||
private final CloudAuthenticationGateway cloudAuthenticationGateway;
|
||||
|
||||
public SessionController(CloudAuthenticationGateway cloudAuthenticationGateway) {
|
||||
this.cloudAuthenticationGateway = cloudAuthenticationGateway;
|
||||
}
|
||||
@RequestMapping(value="login",method=RequestMethod.POST )
|
||||
public String LoginPage(ModelMap model,@RequestHeader Map<String, String> headers)
|
||||
@RequestMapping(value="login", method={RequestMethod.GET, RequestMethod.POST})
|
||||
public String LoginPage(ModelMap model)
|
||||
{
|
||||
model.addAttribute("login", new Login());
|
||||
String host = headers.get("host").toString();
|
||||
if(!host.contains("192.168.10.205:8585") &&
|
||||
!host.contains("192.168.10.250:8585") &&
|
||||
!host.startsWith("localhost:") && !host.startsWith("127.0.0.1:")) {
|
||||
return "error";
|
||||
}
|
||||
return "login";
|
||||
}
|
||||
@RequestMapping(value="logout",method=RequestMethod.POST )
|
||||
@RequestMapping(value="logout",method=RequestMethod.POST )
|
||||
public String LogoutPage(HttpServletRequest request, ModelMap model,@ModelAttribute(value="Sessvals") Session Sessvals)
|
||||
{
|
||||
HttpSession session = request.getSession(false);
|
||||
@@ -69,17 +60,17 @@ public class SessionController {
|
||||
return "login";
|
||||
}
|
||||
}
|
||||
@RequestMapping(value="dashboard",method=RequestMethod.POST )
|
||||
public String Dashboard(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals)
|
||||
{
|
||||
if(Sessvals == null)
|
||||
{
|
||||
return "redirect:/logout";
|
||||
}
|
||||
else
|
||||
{
|
||||
model.addAttribute("Sessvals",Sessvals);
|
||||
return "dashboard";
|
||||
}
|
||||
}
|
||||
}
|
||||
@RequestMapping(value="dashboard",method=RequestMethod.POST )
|
||||
public String Dashboard(ModelMap model,HttpServletRequest request,@ModelAttribute(value="Sessvals") Session Sessvals)
|
||||
{
|
||||
if(Sessvals == null)
|
||||
{
|
||||
return "redirect:/logout";
|
||||
}
|
||||
else
|
||||
{
|
||||
model.addAttribute("Sessvals",Sessvals);
|
||||
return "dashboard";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public final class AesGcmQueryCipher implements QueryCipher {
|
||||
|
||||
private static final String VERSION = "v1";
|
||||
private static final int IV_LENGTH = 12;
|
||||
private static final int TAG_BITS = 128;
|
||||
|
||||
private final SecretKeySpec key;
|
||||
private final SecureRandom random;
|
||||
|
||||
public AesGcmQueryCipher(byte[] keyBytes) {
|
||||
this(keyBytes, new SecureRandom());
|
||||
}
|
||||
|
||||
AesGcmQueryCipher(byte[] keyBytes, SecureRandom random) {
|
||||
if (keyBytes == null || keyBytes.length != 32) {
|
||||
throw new IllegalArgumentException("Query cache AES key must contain 32 bytes");
|
||||
}
|
||||
this.key = new SecretKeySpec(keyBytes.clone(), "AES");
|
||||
this.random = random;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encrypt(String queryId, String query) {
|
||||
try {
|
||||
byte[] iv = new byte[IV_LENGTH];
|
||||
random.nextBytes(iv);
|
||||
Cipher cipher = cipher(Cipher.ENCRYPT_MODE, queryId, iv);
|
||||
byte[] encrypted = cipher.doFinal(query.getBytes(StandardCharsets.UTF_8));
|
||||
Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
|
||||
return VERSION + '.' + encoder.encodeToString(iv) + '.'
|
||||
+ encoder.encodeToString(encrypted);
|
||||
} catch (GeneralSecurityException exception) {
|
||||
throw new IllegalStateException("Unable to encrypt cached query", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decrypt(String queryId, String encryptedQuery) {
|
||||
try {
|
||||
String[] parts = encryptedQuery.split("\\.", -1);
|
||||
if (parts.length != 3 || !VERSION.equals(parts[0])) {
|
||||
throw new IllegalArgumentException("Unsupported encrypted query format");
|
||||
}
|
||||
Base64.Decoder decoder = Base64.getUrlDecoder();
|
||||
byte[] iv = decoder.decode(parts[1]);
|
||||
if (iv.length != IV_LENGTH) {
|
||||
throw new IllegalArgumentException("Invalid encrypted query IV");
|
||||
}
|
||||
Cipher cipher = cipher(Cipher.DECRYPT_MODE, queryId, iv);
|
||||
return new String(cipher.doFinal(decoder.decode(parts[2])), StandardCharsets.UTF_8);
|
||||
} catch (GeneralSecurityException | IllegalArgumentException exception) {
|
||||
throw new IllegalStateException("Unable to decrypt cached query", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private Cipher cipher(int mode, String queryId, byte[] iv)
|
||||
throws GeneralSecurityException {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(mode, key, new GCMParameterSpec(TAG_BITS, iv));
|
||||
cipher.updateAAD(queryId.getBytes(StandardCharsets.UTF_8));
|
||||
return cipher;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
import com.cygnus.client.CloudIdentityClient;
|
||||
|
||||
public final class CloudQuerySource implements QuerySource {
|
||||
|
||||
private final CloudIdentityClient client;
|
||||
|
||||
public CloudQuerySource(CloudIdentityClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fetch(int queryId) {
|
||||
return client.fetchQuery(queryId)
|
||||
.map(response -> response.query())
|
||||
.blockOptional()
|
||||
.filter(query -> !query.isBlank())
|
||||
.orElseThrow(() -> new QueryProviderException(
|
||||
"Cloud query was empty: " + queryId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface EncryptedQueryCache {
|
||||
|
||||
Optional<String> get(String queryId);
|
||||
|
||||
boolean put(String queryId, String encryptedQuery, Duration ttl);
|
||||
|
||||
boolean evict(String queryId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
public interface QueryCipher {
|
||||
|
||||
String encrypt(String queryId, String query);
|
||||
|
||||
String decrypt(String queryId, String encryptedQuery);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
public interface QueryProvider {
|
||||
|
||||
String getQuery(int queryId);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
import com.cygnus.client.CloudClientProperties;
|
||||
import com.cygnus.client.CloudIdentityClient;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Base64;
|
||||
import matrix.nimble.cloud.cache.OnPremRedisCacheService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
|
||||
@Configuration
|
||||
public class QueryProviderConfiguration implements DisposableBean {
|
||||
|
||||
private QueryProvider installed;
|
||||
|
||||
@Bean
|
||||
QueryProvider queryProvider(
|
||||
OnPremRedisCacheService cache,
|
||||
CloudIdentityClient cloudClient,
|
||||
CloudClientProperties cloudProperties,
|
||||
@Value("${CYGNUS_QUERY_CACHE_AES_KEY:}") String configuredKey) {
|
||||
byte[] key = queryCacheKey(configuredKey, cloudProperties.clientAssertion());
|
||||
installed = new RedisCachingQueryProvider(
|
||||
cache,
|
||||
new CloudQuerySource(cloudClient),
|
||||
new AesGcmQueryCipher(key));
|
||||
QueryProviders.install(installed);
|
||||
return installed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
if (installed != null) {
|
||||
QueryProviders.clear(installed);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] queryCacheKey(String configuredKey, String assertionLocation) {
|
||||
try {
|
||||
if (configuredKey != null && !configuredKey.isBlank()) {
|
||||
byte[] decoded = Base64.getDecoder().decode(configuredKey.trim());
|
||||
if (decoded.length != 32) {
|
||||
throw new IllegalStateException(
|
||||
"CYGNUS_QUERY_CACHE_AES_KEY must be a Base64-encoded 256-bit key");
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
String assertion = assertionLocation.startsWith("file:")
|
||||
? Files.readString(Path.of(assertionLocation.substring(5)),
|
||||
StandardCharsets.US_ASCII).trim()
|
||||
: assertionLocation;
|
||||
return MessageDigest.getInstance("SHA-256")
|
||||
.digest(assertion.getBytes(StandardCharsets.UTF_8));
|
||||
} catch (IllegalStateException exception) {
|
||||
throw exception;
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("Unable to initialize query cache encryption", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
public class QueryProviderException extends RuntimeException {
|
||||
|
||||
public QueryProviderException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public QueryProviderException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class QueryProviders {
|
||||
|
||||
private static final AtomicReference<QueryProvider> CURRENT = new AtomicReference<>();
|
||||
|
||||
private QueryProviders() {
|
||||
}
|
||||
|
||||
public static QueryProvider current() {
|
||||
QueryProvider provider = CURRENT.get();
|
||||
if (provider == null) {
|
||||
throw new QueryProviderException("QueryProvider has not been initialized");
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
public static void install(QueryProvider provider) {
|
||||
CURRENT.set(java.util.Objects.requireNonNull(provider));
|
||||
}
|
||||
|
||||
static void clear(QueryProvider provider) {
|
||||
CURRENT.compareAndSet(provider, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface QuerySource {
|
||||
|
||||
String fetch(int queryId);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public final class RedisCachingQueryProvider implements QueryProvider {
|
||||
|
||||
public static final Duration QUERY_TTL = Duration.ofHours(12);
|
||||
private static final Logger LOGGER = Logger.getLogger(RedisCachingQueryProvider.class.getName());
|
||||
private static final int LOCK_COUNT = 64;
|
||||
|
||||
private final EncryptedQueryCache cache;
|
||||
private final QuerySource cloudSource;
|
||||
private final QueryCipher cipher;
|
||||
private final Object[] locks = new Object[LOCK_COUNT];
|
||||
|
||||
public RedisCachingQueryProvider(
|
||||
EncryptedQueryCache cache,
|
||||
QuerySource cloudSource,
|
||||
QueryCipher cipher) {
|
||||
this.cache = cache;
|
||||
this.cloudSource = cloudSource;
|
||||
this.cipher = cipher;
|
||||
for (int index = 0; index < locks.length; index++) {
|
||||
locks[index] = new Object();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQuery(int queryId) {
|
||||
String cacheKey = cacheKey(queryId);
|
||||
Optional<String> cached = cached(cacheKey);
|
||||
if (cached.isPresent()) {
|
||||
return cached.get();
|
||||
}
|
||||
|
||||
synchronized (lock(cacheKey)) {
|
||||
cached = cached(cacheKey);
|
||||
if (cached.isPresent()) {
|
||||
return cached.get();
|
||||
}
|
||||
try {
|
||||
String query = cloudSource.fetch(queryId);
|
||||
cache.put(cacheKey, cipher.encrypt(cacheKey, query), QUERY_TTL);
|
||||
return query;
|
||||
} catch (QueryProviderException exception) {
|
||||
throw exception;
|
||||
} catch (RuntimeException exception) {
|
||||
throw new QueryProviderException(
|
||||
"Unable to retrieve query: " + queryId, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<String> cached(String queryId) {
|
||||
return cache.get(queryId).flatMap(encrypted -> {
|
||||
try {
|
||||
return Optional.of(cipher.decrypt(queryId, encrypted));
|
||||
} catch (RuntimeException exception) {
|
||||
LOGGER.log(Level.WARNING,
|
||||
"Discarding an invalid encrypted query cache entry: {0}", queryId);
|
||||
cache.evict(queryId);
|
||||
return Optional.empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Object lock(String queryId) {
|
||||
return locks[(queryId.hashCode() & Integer.MAX_VALUE) % locks.length];
|
||||
}
|
||||
|
||||
private String cacheKey(int queryId) {
|
||||
if (queryId <= 0) {
|
||||
throw new IllegalArgumentException("Invalid query ID");
|
||||
}
|
||||
return Integer.toString(queryId);
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,9 @@ import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import matrix.nimble.query.QueryProviders;
|
||||
|
||||
public class FileFunctions {
|
||||
private static final Object QUERY_CACHE_LOCK = new Object();
|
||||
private static volatile Map<Integer, String> queryCache = Collections.emptyMap();
|
||||
private static volatile long queryCacheLastModified = Long.MIN_VALUE;
|
||||
private static volatile String queryCachePath = "";
|
||||
private String ConnString;
|
||||
private String DBDriver;
|
||||
private String DBUser;
|
||||
@@ -207,53 +201,6 @@ public class FileFunctions {
|
||||
}
|
||||
}
|
||||
public String GetQuery(int QueryIndex) throws IOException {
|
||||
String realPath = (FileFunctions.class
|
||||
.getResource("FileFunctions.class")).toString()
|
||||
.replace("utilities/FileFunctions.class", "conf/")
|
||||
.replace("file:/", "");
|
||||
if(!isWindows())
|
||||
{
|
||||
realPath="/"+realPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
realPath=realPath.replace("%20", " ");
|
||||
}
|
||||
File queryFile = new File(realPath + "nimble.qry");
|
||||
refreshQueryCacheIfRequired(queryFile);
|
||||
return queryCache.get(QueryIndex);
|
||||
}
|
||||
|
||||
private static void refreshQueryCacheIfRequired(File queryFile) throws IOException {
|
||||
String absolutePath = queryFile.getAbsolutePath();
|
||||
long lastModified = queryFile.lastModified();
|
||||
if (absolutePath.equals(queryCachePath) && lastModified == queryCacheLastModified) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (QUERY_CACHE_LOCK) {
|
||||
if (absolutePath.equals(queryCachePath) && lastModified == queryCacheLastModified) {
|
||||
return;
|
||||
}
|
||||
Map<Integer, String> loadedQueries = new HashMap<>();
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(queryFile))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
int delimiterIndex = line.indexOf(GlobalClass.ColDelim);
|
||||
if (delimiterIndex <= 5 || !line.startsWith("Query")) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
int queryIndex = Integer.parseInt(line.substring(5, delimiterIndex));
|
||||
loadedQueries.put(queryIndex, line.substring(delimiterIndex + GlobalClass.ColDelim.length()));
|
||||
} catch (NumberFormatException ignored) {
|
||||
// Ignore malformed/non-query lines, matching the legacy lookup behavior.
|
||||
}
|
||||
}
|
||||
}
|
||||
queryCache = Collections.unmodifiableMap(loadedQueries);
|
||||
queryCachePath = absolutePath;
|
||||
queryCacheLastModified = lastModified;
|
||||
}
|
||||
return QueryProviders.current().getQuery(QueryIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
package matrix.nimble;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import matrix.nimble.utilities.FileFunctions;
|
||||
import matrix.nimble.query.QueryProviders;
|
||||
|
||||
class FileFunctionsQueryCacheTest {
|
||||
@Test
|
||||
void loadsQueriesFromNimbleQueryFileAndHandlesMissingCodes() throws Exception {
|
||||
void delegatesLegacyQueryLookupToQueryProvider() throws Exception {
|
||||
QueryProviders.install(queryId -> "provided:" + queryId);
|
||||
FileFunctions files = new FileFunctions("TEST");
|
||||
|
||||
String query = files.GetQuery(3);
|
||||
|
||||
assertTrue(query.startsWith("select!C0L!select portfolio_id"));
|
||||
assertNull(files.GetQuery(1));
|
||||
assertNull(files.GetQuery(Integer.MAX_VALUE));
|
||||
assertEquals("provided:3", files.GetQuery(3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.Arrays;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class AesGcmQueryCipherTest {
|
||||
|
||||
@Test
|
||||
void encryptsAndAuthenticatesQueryAndQueryId() {
|
||||
byte[] key = new byte[32];
|
||||
Arrays.fill(key, (byte) 7);
|
||||
AesGcmQueryCipher cipher = new AesGcmQueryCipher(key);
|
||||
|
||||
String encrypted = cipher.encrypt("3", "select * from portfolio");
|
||||
|
||||
assertNotEquals("select * from portfolio", encrypted);
|
||||
assertTrue(encrypted.startsWith("v1."));
|
||||
org.junit.jupiter.api.Assertions.assertEquals(
|
||||
"select * from portfolio", cipher.decrypt("3", encrypted));
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> cipher.decrypt("4", encrypted));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package matrix.nimble.query;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class RedisCachingQueryProviderTest {
|
||||
|
||||
@Test
|
||||
void usesEncryptedRedisValueAndThreeHourTtl() {
|
||||
MemoryCache cache = new MemoryCache();
|
||||
AtomicInteger cloudCalls = new AtomicInteger();
|
||||
QueryCipher cipher = new AesGcmQueryCipher(new byte[32]);
|
||||
RedisCachingQueryProvider provider = new RedisCachingQueryProvider(
|
||||
cache,
|
||||
queryId -> {
|
||||
cloudCalls.incrementAndGet();
|
||||
return "select!C0L!select 1";
|
||||
},
|
||||
cipher);
|
||||
|
||||
assertEquals("select!C0L!select 1", provider.getQuery(10));
|
||||
assertEquals("select!C0L!select 1", provider.getQuery(10));
|
||||
|
||||
assertEquals(1, cloudCalls.get());
|
||||
assertEquals(Duration.ofHours(3), cache.ttl.get());
|
||||
org.junit.jupiter.api.Assertions.assertNotEquals(
|
||||
"select!C0L!select 1", cache.values.get("10"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void collapsesConcurrentMissesForTheSameQuery() throws Exception {
|
||||
MemoryCache cache = new MemoryCache();
|
||||
AtomicInteger cloudCalls = new AtomicInteger();
|
||||
RedisCachingQueryProvider provider = new RedisCachingQueryProvider(
|
||||
cache,
|
||||
queryId -> {
|
||||
cloudCalls.incrementAndGet();
|
||||
try {
|
||||
Thread.sleep(30);
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return "select!C0L!select 1";
|
||||
},
|
||||
new AesGcmQueryCipher(new byte[32]));
|
||||
var executor = Executors.newFixedThreadPool(8);
|
||||
try {
|
||||
for (int index = 0; index < 20; index++) {
|
||||
executor.submit(() -> provider.getQuery(20));
|
||||
}
|
||||
} finally {
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(5, TimeUnit.SECONDS);
|
||||
}
|
||||
assertEquals(1, cloudCalls.get());
|
||||
}
|
||||
|
||||
private static final class MemoryCache implements EncryptedQueryCache {
|
||||
private final Map<String, String> values = new ConcurrentHashMap<>();
|
||||
private final AtomicReference<Duration> ttl = new AtomicReference<>();
|
||||
|
||||
@Override
|
||||
public Optional<String> get(String queryId) {
|
||||
return Optional.ofNullable(values.get(queryId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean put(String queryId, String encryptedQuery, Duration duration) {
|
||||
ttl.set(duration);
|
||||
values.put(queryId, encryptedQuery);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evict(String queryId) {
|
||||
return values.remove(queryId) != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,17 +0,0 @@
|
||||
MATRIX_IMAGE="hub.technobeesolutions.in/matrix-onprem:1.0.0"
|
||||
MATRIX_DB_URL="jdbc:postgresql://103.125.129.116:5333/matrix"
|
||||
MATRIX_DB_USERNAME="postgres"
|
||||
MATRIX_DB_PASSWORD="M@triXR3d1s@6202"
|
||||
REDIS_HOST="103.125.129.116"
|
||||
REDIS_PORT="7901"
|
||||
REDIS_PASSWORD="M@triXR3d1s@6202"
|
||||
REDIS_SSL="false"
|
||||
CYGNUS_CLOUD_BASE_URL="http://host.docker.internal:8090"
|
||||
CYGNUS_TOKEN_URL="http://host.docker.internal:8090/oauth2/token"
|
||||
CYGNUS_CLIENT_ID="matrix"
|
||||
CYGNUS_INSTALLATION_ID="matrix-delhi-cygnus-01"
|
||||
CYGNUS_CLIENT_ASSERTION="file:/opt/matrix/config/machine-assertion.jwt"
|
||||
CYGNUS_LOGIN_KEY_ID="cygnus-login-2026-01"
|
||||
CYGNUS_LOGIN_PUBLIC_KEY="file:/opt/matrix/config/keys/login-public.pem"
|
||||
CYGNUS_CLOUD_REQUEST_TIMEOUT="PT10S"
|
||||
CYGNUS_TOKEN_REFRESH_SKEW="PT30S"
|
||||
@@ -1,12 +0,0 @@
|
||||
services:
|
||||
matrix-onprem:
|
||||
image: ${MATRIX_IMAGE:?Set MATRIX_IMAGE}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./config:/opt/matrix/config:ro
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
MATRIX_INSTALLATION_CONFIG: /opt/matrix/config/installation.yml
|
||||
@@ -1,13 +0,0 @@
|
||||
matrix:
|
||||
product: "matrix"
|
||||
cloud-url: "http://host.docker.internal:8090"
|
||||
token-url: "http://host.docker.internal:8090/oauth2/token"
|
||||
tenant-id: "00000000-0000-4000-8000-000000000001"
|
||||
client-id: "matrix"
|
||||
installation-id: "5aecfd98-dcd0-4ac4-b973-2b65b7ceda08"
|
||||
installation-uuid: "579a040e-6803-4648-a9ef-74733cc94a21"
|
||||
installation-code: "matrix-delhi-cygnus-01"
|
||||
environment: "production"
|
||||
machine-assertion: "file:./config/machine-assertion.jwt"
|
||||
signing-private-key: "file:./config/keys/client-signing-private.pem"
|
||||
signing-public-key: "file:./config/keys/client-signing-public.pem"
|
||||
@@ -1,40 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
SO8FID8TwwH38mtCqMgIDLU=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,11 +0,0 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA6D2igA5GN29ey0MdZR6Z
|
||||
kf1UZfsqcmJ3n1bx06hIifXwuXqbJ/hlBSwOBeCMoQxAqcDDTpVtBpBcz2aSJRQi
|
||||
12h2jKwjVUDS9qGVw6BXCuiBKHfReXStII3RbwwikToTX1UCesLS5oBfaADveOot
|
||||
L1H6XTBq/qdixeCasqOcuwZShflatcBYqQhtqmhKjIsjh3e0Ce/mj2ymmV3WJYeN
|
||||
dCO4DnnznewvFTDZfGx0JqSoeIvfOzZTWjd7S0drXy8NjdcVgIo16WF3vJczWgrQ
|
||||
NXFZxDiLC8JhaEOtY30iB9dYfGasSFxbf9IH0lDaDN3J/ro5useIMKm3Dc97s4JY
|
||||
XsBybwlbs5pWUa9YgI8/YzqNUPbSTBKJ9ia+NVqKW8tfJoMJOsaHHmTKzQKUYjjQ
|
||||
PSJE9ReTeheo8EI2RZBBICquV99GsiH00/WGue2/uAQRFuoGDxJ8HGjD2kv+S9SM
|
||||
ZGdcwFTPRbOL8Ozi88DvqCJtR36Q4gYnU3+jHCwxVoTxAgMBAAE=
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -1,11 +0,0 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAvbjaZHePDwZQj1R1s23I
|
||||
CSR6SzUi707WsGHoJZLLcFruXGTreNWIonU8Ye+iHwCUIJVhhoz0d7eGdfUc70ZO
|
||||
Kn7dKfTfdsiWJi5RiM6loUk8iSAjrgh07chUgyL6luak7ZNevmWTCkSIuvVV9UZe
|
||||
ZiH5Xh99zXr6Y0cLs7TcB0Tfc3y72dtU9VLjqhcc7KYibzHDWWfwVcpG7mkmmkEM
|
||||
2tDHGmiZGoKk9uJoSMesh4w7XPE+UHxMEsK2wXpZLrwlN9ikxk56Cd/Z016MkkXZ
|
||||
mMAofaTQ8NtfV/UuJjALUER0REJ8jb3qXBq5Pgh2ZkpmSaysZQPpNdll5SSHjhVV
|
||||
t/UQ2QNgZiBh5bHZGpZJIvZ4E3RQ1Dtj2Q/QtSKgbffY3owNLu+2Lb/qQ90JV580
|
||||
dMg8D+89uy+MOL8JpQxHvTjKk5tqFlBhmVo+Y9i2KfPgv3rM1R3q+A4j9hN37x+a
|
||||
f2tulHI5quy0KLbANp/32qlVVgd6TgnMikWCkwNpo7YZAgMBAAE=
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -1 +0,0 @@
|
||||
eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.D21KmNzi4shZx538-fArwUt0frd6nViAjl3E4ptHYfVlxWb5700VrK5HuLBl5fi2mG2e7ymeEGun83Qg7NMCl0-H9Df5vmjDwoQWzzVPS2TEcn0PhsyrHXWi7d3H2pHZUT9o8vHcVRSg9ROIofrwO-yUzGSixDxoEeIRQm3k3MSuuGOBp6MwTGthskPpAhYTJbFs9LNvMgJGJb3Sg4F0Ycz2bSFP_eqfIwKvkCcDOh-sjXK-1F0bDuZpeG0-Wh3L6uX7tdQmAm6NIm1sxVKB9dKwff_oryMGu-PgsIzeiGwCD50UelQVe1LLGayew_ysNt8PoGnVAJQrXMsF9tZG11dcQiqS8Xze78U6Z2seiEhGAchE5A3UB60FHkY_iJ7JA_k9u-AhH994KF0eUGI8zcIFCj53vc2oNd6ortVq1CPyUn9GD3QD8xEIfrRreYWI94Fhf5od_i2YduMtVzY-MhGFTjElNKdnfaajCVQIRb-sMDOmxSDgeT_uVYiAcAU_.PQaHgpGtExOh36HB.9k2OZPRwFc5etToeQtTJ9cjT5n3RwMTnffLIw56Co0eEEZ-szIBJXiwdqJoOpp5MG7uz8kNXSwhfnz1v5JTD6FEqpJ0886wr30eJTpQE9Mi0XIwrzFS0IddAa4lgdOcgY_mgs4HQv9TAy-0a9eg7tZwpLjGutTCZHMW5oS_mzr-DIw2qkEanpK8M2ZH6uO8P15h2E2D-WPPCZ4lJes7qA5tOaU8c5UlauWtkTR0HL-pQw348UincBIYwap5dT5j1ToUrkhXUzo_rxfFdZ9H8_jEOnFbaltOH90aVhIMKGDfqKWQIgjdEXA8VgwfdcfwJxKuF-Jhz7FpRyEEoXDM4ios4D9a576dqM_1AtnsgmmHuCGvsgZ3aEuZlWDjOJT7-qkvWEDg4_tuiLYZV69ZIUNzUwxS9uqPizO_W79yV-e0DblGH0lSZwToFq3UHhorxXxAGn4hCqgW5l5xqjrF_sfooZwLYi9v2BKONRj_2TrVbbM7g7EJ4eoaEVftBzdTSARMogpR8NzNUTNcl08p59kiYsbx2yOFtBP6G6_jicjeyv-8fPopmPcLzpo0XCX6dF_toSL3bQsmcTSwmtv70lxKJk383cATJjVjI9ulDeOJbB5_ebudIkIFmQ_gMJuXp5A1rVC9aV_-Ppfq7lmtVv-o3SIft_qh_ihdOe8UxA99yt3IWq9-pfwQry0cH5gGqG4g43-CTNyt4xl_xAYEgFyBG9on2r6hIEFdQ7OJw32V308m3xPI-gHp5_z2yn82cBNskImS56a6ffj05kDuI-1e_1s1EhAZW8X-Z-weWCCv00Co2P1haS_9MjzGfcRW06tbAxsyrJ8xy8yIp97_8S9sgCHeYr3ig7CCDTnOCCIW5jwcFJDF8jWYP8H_3oKpAqgUH867Ar-eAktF4z6Kv6NGji4h4dCPWLGNdD_CglfM6ST0BKizE45leiyxEEDIkIuwBD7lxcy_ItJdVs9ejew.2oEBIqW9A6rXQbyMWu85ew
|
||||
@@ -228,60 +228,120 @@ register_client_in_database() {
|
||||
-d "${DB_NAME_VALUE}" \
|
||||
-X -v ON_ERROR_STOP=1 \
|
||||
-c "
|
||||
WITH account AS (
|
||||
WITH registration AS (
|
||||
SELECT registration_id
|
||||
FROM identity.client_registration_details
|
||||
WHERE lower(client_code) = lower(
|
||||
convert_from(decode('${client_id_b64}', 'base64'), 'UTF8'))
|
||||
AND status = 'ACTIVE'
|
||||
ORDER BY created_at
|
||||
LIMIT 1
|
||||
), account AS (
|
||||
INSERT INTO identity.client_account
|
||||
(tenant_id, client_slug, client_name, status)
|
||||
VALUES (
|
||||
(tenant_id, registration_id, client_slug, client_name, status)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
registration_id,
|
||||
convert_from(decode('${client_id_b64}', 'base64'), 'UTF8'),
|
||||
convert_from(decode('${client_name_b64}', 'base64'), 'UTF8'),
|
||||
'ACTIVE')
|
||||
'ACTIVE'
|
||||
FROM registration
|
||||
ON CONFLICT (client_slug) DO UPDATE SET
|
||||
registration_id = EXCLUDED.registration_id,
|
||||
client_name = EXCLUDED.client_name,
|
||||
status = 'ACTIVE',
|
||||
updated_at = now()
|
||||
RETURNING tenant_id
|
||||
), installation AS (
|
||||
INSERT INTO identity.client_installation
|
||||
(installation_id, tenant_id, client_id, installation_code,
|
||||
assertion_public_key, allowed_scopes, enabled)
|
||||
), new_license AS (
|
||||
INSERT INTO identity.client_license
|
||||
(license_id, tenant_id, license_type, package_code,
|
||||
valid_from, valid_until, status)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
tenant_id,
|
||||
convert_from(decode('${license_type_b64}', 'base64'), 'UTF8'),
|
||||
convert_from(decode('${package_code_b64}', 'base64'), 'UTF8'),
|
||||
now(),
|
||||
now() + make_interval(months => ${LICENSE_MONTHS}),
|
||||
'ACTIVE'
|
||||
FROM account
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM identity.client_license existing
|
||||
WHERE existing.tenant_id = account.tenant_id
|
||||
AND existing.status = 'ACTIVE'
|
||||
AND existing.valid_until > now()
|
||||
)
|
||||
RETURNING license_id, tenant_id
|
||||
), selected_license AS (
|
||||
SELECT existing.license_id, existing.tenant_id
|
||||
FROM identity.client_license existing
|
||||
JOIN account ON account.tenant_id = existing.tenant_id
|
||||
WHERE existing.status = 'ACTIVE'
|
||||
AND existing.valid_until > now()
|
||||
UNION ALL
|
||||
SELECT license_id, tenant_id
|
||||
FROM new_license
|
||||
ORDER BY license_id
|
||||
LIMIT 1
|
||||
), installation AS (
|
||||
INSERT INTO identity.client_installation
|
||||
(installation_id, tenant_id, client_id, installation_code,
|
||||
assertion_public_key, allowed_scopes, enabled, license_id,
|
||||
installation_uuid, installation_name, status,
|
||||
software_version, environment)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
account.tenant_id,
|
||||
convert_from(decode('${client_id_b64}', 'base64'), 'UTF8'),
|
||||
convert_from(decode('${installation_b64}', 'base64'), 'UTF8'),
|
||||
convert_from(decode('${public_key_b64}', 'base64'), 'UTF8'),
|
||||
ARRAY['identity.login']::text[],
|
||||
true
|
||||
true,
|
||||
selected_license.license_id,
|
||||
gen_random_uuid(),
|
||||
convert_from(decode('${client_name_b64}', 'base64'), 'UTF8')
|
||||
|| ' ' ||
|
||||
convert_from(decode('${installation_b64}', 'base64'), 'UTF8'),
|
||||
'ACTIVE',
|
||||
'development',
|
||||
'development'
|
||||
FROM account
|
||||
JOIN selected_license
|
||||
ON selected_license.tenant_id = account.tenant_id
|
||||
ON CONFLICT (client_id, installation_code) DO UPDATE SET
|
||||
tenant_id = EXCLUDED.tenant_id,
|
||||
assertion_public_key = EXCLUDED.assertion_public_key,
|
||||
allowed_scopes = EXCLUDED.allowed_scopes,
|
||||
enabled = true,
|
||||
license_id = EXCLUDED.license_id,
|
||||
status = 'ACTIVE',
|
||||
security_version = identity.client_installation.security_version + 1,
|
||||
updated_at = now()
|
||||
RETURNING tenant_id
|
||||
)
|
||||
INSERT INTO identity.client_license
|
||||
(license_id, tenant_id, license_type, package_code,
|
||||
valid_from, valid_until, status)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
tenant_id,
|
||||
convert_from(decode('${license_type_b64}', 'base64'), 'UTF8'),
|
||||
convert_from(decode('${package_code_b64}', 'base64'), 'UTF8'),
|
||||
now(),
|
||||
now() + make_interval(months => ${LICENSE_MONTHS}),
|
||||
'ACTIVE'
|
||||
FROM installation
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM identity.client_license existing
|
||||
WHERE existing.tenant_id = installation.tenant_id
|
||||
AND existing.status = 'ACTIVE'
|
||||
AND existing.valid_until > now()
|
||||
);"
|
||||
SELECT count(*) AS provisioned_installations
|
||||
FROM installation;"
|
||||
|
||||
local installation_count
|
||||
installation_count="$(
|
||||
PGPASSWORD="${DB_PASSWORD_VALUE}" "${PSQL_BIN}" \
|
||||
-h "${DB_HOST_VALUE}" \
|
||||
-p "${DB_PORT_VALUE}" \
|
||||
-U "${DB_USER_VALUE}" \
|
||||
-d "${DB_NAME_VALUE}" \
|
||||
-X -A -t \
|
||||
-c "
|
||||
SELECT count(*)
|
||||
FROM identity.client_installation
|
||||
WHERE client_id =
|
||||
convert_from(decode('${client_id_b64}', 'base64'), 'UTF8')
|
||||
AND installation_code =
|
||||
convert_from(decode('${installation_b64}', 'base64'), 'UTF8')
|
||||
AND enabled = true;"
|
||||
)"
|
||||
[[ "${installation_count//[[:space:]]/}" == "1" ]] || fail \
|
||||
"No active registration/account was found for '${CLIENT_ID}'. Create or activate client_registration_details first."
|
||||
}
|
||||
|
||||
generate_machine_assertion() {
|
||||
|
||||
Reference in New Issue
Block a user