This is an automated email from the ASF dual-hosted git repository. yasith pushed a commit to branch service-layer-improvements in repository https://gitbox.apache.org/repos/asf/airavata.git
commit 80485503025d8d46a39b710997e276723b58c713 Author: yasithdev <[email protected]> AuthorDate: Sun Dec 14 02:14:14 2025 -0600 migrations - dbcp2 to hikari, guice to spring, quartz to spring quartz, shiro to spring security, setup mapstruct to migrate dozer --- .../plans/spring_migration_plan_c41f8113.plan.md | 215 +++++++++++++++++++++ airavata-api/pom.xml | 52 +++-- .../org/apache/airavata/common/utils/DBUtil.java | 24 ++- .../org/apache/airavata/common/utils/JPAUtils.java | 2 +- .../org/apache/airavata/common/utils/JSONUtil.java | 143 ++++++++------ .../java/org/apache/airavata/config/JpaConfig.java | 23 ++- .../task/parsing/models/ParsingTaskInputs.java | 18 +- .../task/parsing/models/ParsingTaskOutputs.java | 18 +- .../metadata/analyzer/DataInterpreterService.java | 8 +- .../rescheduler/ProcessReschedulingService.java | 8 +- .../cluster/ClusterStatusMonitorJobScheduler.java | 10 +- .../ComputationalResourceMonitoringService.java | 8 +- .../realtime/parser/RealtimeJobStatusParser.java | 10 +- .../utils/migration/MappingToolRunner.java | 2 +- .../security/interceptor/SecurityCheck.java | 3 +- .../security/interceptor/SecurityModule.java | 84 ++++++-- .../airavata/security/userstore/JDBCUserStore.java | 144 ++++++++++---- .../airavata/security/userstore/LDAPUserStore.java | 117 ++++++++--- pom.xml | 11 ++ 19 files changed, 688 insertions(+), 212 deletions(-) diff --git a/.cursor/plans/spring_migration_plan_c41f8113.plan.md b/.cursor/plans/spring_migration_plan_c41f8113.plan.md new file mode 100644 index 0000000000..9aa6f6d679 --- /dev/null +++ b/.cursor/plans/spring_migration_plan_c41f8113.plan.md @@ -0,0 +1,215 @@ +--- +name: Spring Migration Plan +overview: "Migrate non-Spring dependencies to Spring equivalents: Guice → Spring DI, Dozer → MapStruct, Commons DBCP2 → HikariCP, Quartz → Spring Scheduling, Apache Shiro → Spring Security, and Gson → Jackson." +todos: [] +--- + +# Spring Dependency Migration Plan + +## Overview + +Migrate dependencies in `airavata-api` to Spring equivalents to align with Spring Boot architecture and reduce external dependencies. + +## Dependencies to Migrate + +### 1. Google Guice → Spring Dependency Injection + +**Current Usage:** + +- `SecurityModule.java` - Uses Guice `AbstractModule` for AOP interceptor binding +- `@SecurityCheck` annotation - Uses Guice `@BindingAnnotation` +- Security interceptor registration via Guice + +**Migration Strategy:** + +- Replace `SecurityModule` with Spring `@Configuration` class +- Convert `@SecurityCheck` to Spring AOP annotation (e.g., custom `@SecurityCheck` with Spring AOP) +- Use Spring AOP `@Aspect` and `@Around` instead of Guice interceptors +- Update `SecurityInterceptor` to use Spring `MethodInterceptor` (already uses it, but needs Spring AOP setup) + +**Files to Modify:** + +- `airavata-api/src/main/java/org/apache/airavata/security/interceptor/SecurityModule.java` - Replace with Spring config +- `airavata-api/src/main/java/org/apache/airavata/security/interceptor/SecurityCheck.java` - Update annotation +- `airavata-api/src/main/java/org/apache/airavata/security/interceptor/SecurityInterceptor.java` - Already Spring-compatible +- Remove Guice dependency from `pom.xml` + +--- + +### 2. Dozer → MapStruct (Recommended) or ModelMapper + +**Current Usage:** + +- 69+ service classes use `com.github.dozermapper.core.Mapper` +- `DozerMapperConfig.java` - Spring bean configuration +- `dozer_mapping.xml` - XML mapping configuration +- Custom converters: `StorageDateConverter`, `CsvStringConverter` +- Custom bean factory: `CustomBeanFactory` + +**Migration Strategy:** + +- **Option A (Recommended): MapStruct** - Compile-time mapping, type-safe, better performance +- Add `mapstruct` and `mapstruct-processor` dependencies +- Create mapper interfaces with `@Mapper` annotation +- MapStruct generates implementation at compile time +- Replace all `mapper.map()` calls with generated mapper methods +- Migrate custom converters to MapStruct `@AfterMapping` or custom methods + +- **Option B: ModelMapper** - Runtime mapping, easier migration +- Replace Dozer with ModelMapper +- Similar API, less refactoring needed +- Lower performance than MapStruct + +**Files to Modify:** + +- All 69+ service classes using Dozer mapper +- `airavata-api/src/main/java/org/apache/airavata/config/DozerMapperConfig.java` - Replace with MapStruct/ModelMapper config +- `airavata-api/src/main/resources/dozer_mapping.xml` - Convert to MapStruct mappers or ModelMapper configuration +- `airavata-api/src/main/java/org/apache/airavata/registry/utils/DozerConverter/*.java` - Convert to MapStruct custom methods +- `airavata-api/src/main/java/org/apache/airavata/registry/utils/CustomBeanFactory.java` - May not be needed with MapStruct +- Remove Dozer dependency from `pom.xml` + +--- + +### 3. Commons DBCP2 → HikariCP (Spring Boot Default) + +**Current Usage:** + +- `JpaConfig.java` - Creates `BasicDataSource` from Commons DBCP2 +- `JPAUtils.java` - Uses DBCP2 driver name +- `DBUtil.java` - Uses DBCP2 +- `MappingToolRunner.java` - Uses DBCP2 + +**Migration Strategy:** + +- Replace `BasicDataSource` with HikariCP `HikariDataSource` +- HikariCP is already included in Spring Boot starter +- Update connection pool configuration +- HikariCP has better performance and is Spring Boot default + +**Files to Modify:** + +- `airavata-api/src/main/java/org/apache/airavata/config/JpaConfig.java` - Replace `BasicDataSource` with `HikariDataSource` +- `airavata-api/src/main/java/org/apache/airavata/common/utils/JPAUtils.java` - Update driver name +- `airavata-api/src/main/java/org/apache/airavata/common/utils/DBUtil.java` - Update to HikariCP +- `airavata-api/src/main/java/org/apache/airavata/registry/utils/migration/MappingToolRunner.java` - Update driver name +- Remove `commons-dbcp2` and `commons-pool2` dependencies from `pom.xml` + +--- + +### 4. Quartz → Spring Scheduling + +**Current Usage:** + +- `ComputationalResourceMonitoringService.java` - Uses Quartz for compute resource monitoring +- `ClusterStatusMonitorJobScheduler.java` - Cluster status monitoring +- `ProcessReschedulingService.java` - Process scanning jobs +- `DataInterpreterService.java` - Data analysis jobs +- Multiple job classes: `MonitoringJob`, `ClusterStatusMonitorJob`, `ProcessScannerImpl`, `DataAnalyzerImpl` + +**Migration Strategy:** + +- **Option A: Spring @Scheduled** - For simple periodic tasks +- Use `@Scheduled` annotation on methods +- Enable with `@EnableScheduling` +- Convert job classes to Spring `@Component` with scheduled methods + +- **Option B: Spring Quartz Integration** - For complex scheduling needs +- Use `spring-boot-starter-quartz` +- Keep Quartz but integrate with Spring +- Use Spring-managed job beans +- Better for dynamic job scheduling + +**Recommendation:** Use Spring Quartz integration (Option B) since jobs are complex and need dynamic scheduling. + +**Files to Modify:** + +- `airavata-api/src/main/java/org/apache/airavata/monitor/compute/ComputationalResourceMonitoringService.java` +- `airavata-api/src/main/java/org/apache/airavata/monitor/cluster/ClusterStatusMonitorJobScheduler.java` +- `airavata-api/src/main/java/org/apache/airavata/metascheduler/process/scheduling/engine/rescheduler/ProcessReschedulingService.java` +- `airavata-api/src/main/java/org/apache/airavata/metascheduler/metadata/analyzer/DataInterpreterService.java` +- All job classes implementing `org.quartz.Job` +- Add `spring-boot-starter-quartz` dependency +- Keep `quartz` dependency but use Spring-managed version + +--- + +### 5. Apache Shiro → Spring Security + +**Current Usage:** + +- `LDAPUserStore.java` - Uses Shiro `JndiLdapRealm` for LDAP authentication +- `JDBCUserStore.java` - Uses Shiro `JdbcRealm` for JDBC authentication +- Limited usage (only 2 classes) + +**Migration Strategy:** + +- Replace Shiro realms with Spring Security authentication providers +- Use Spring Security LDAP support (`spring-boot-starter-security` + `spring-ldap-core`) +- Use Spring Security JDBC authentication +- Create custom `AuthenticationProvider` implementations + +**Files to Modify:** + +- `airavata-api/src/main/java/org/apache/airavata/security/userstore/LDAPUserStore.java` - Replace with Spring Security LDAP +- `airavata-api/src/main/java/org/apache/airavata/security/userstore/JDBCUserStore.java` - Replace with Spring Security JDBC +- Add Spring Security dependencies +- Remove `shiro-core` dependency from `pom.xml` + +**Note:** This is a lower priority migration since Shiro usage is minimal and isolated. + +--- + +### 6. Gson → Jackson (Already Available) + +**Current Usage:** + +- Gson dependency exists but usage is minimal (only in `pom.xml`, no actual usage found) + +**Migration Strategy:** + +- Remove Gson dependency +- Use Jackson (already included via Spring Boot) for any JSON processing +- Search for any Gson usage and replace with Jackson `ObjectMapper` + +**Files to Modify:** + +- Remove `gson` dependency from `pom.xml` +- Search and replace any Gson usage with Jackson + +--- + +## Migration Order (Recommended) + +1. **Gson → Jackson** (Simplest, no code changes if unused) +2. **Commons DBCP2 → HikariCP** (Straightforward replacement) +3. **Google Guice → Spring DI** (Medium complexity, affects security) +4. **Quartz → Spring Quartz Integration** (Medium complexity, many files) +5. **Dozer → MapStruct** (Most complex, 69+ files, but high value) +6. **Apache Shiro → Spring Security** (Low priority, minimal usage) + +## Testing Strategy + +- Unit tests for each migrated component +- Integration tests for security interceptor +- Performance tests for mapper migration (MapStruct should be faster) +- Verify all scheduled jobs still work +- Test database connection pooling + +## Dependencies to Remove + +After migration, remove from `pom.xml`: + +- `com.google.inject:guice` +- `com.github.dozermapper:dozer-core` +- `org.apache.commons:commons-dbcp2` +- `org.apache.commons:commons-pool2` +- `com.google.code.gson:gson` +- `org.apache.shiro:shiro-core` (after Shiro migration) + +## Dependencies to Add + +- `org.mapstruct:mapstruct` and `mapstruct-processor` (for Dozer replacement) +- `org.springframework.boot:spring-boot-starter-quartz` (for Quartz integration) +- `org.springframework.boot:spring-boot-starter-security` (for Shiro replacement) +- `org.springframework.ldap:spring-ldap-core` (for LDAP support) \ No newline at end of file diff --git a/airavata-api/pom.xml b/airavata-api/pom.xml index 3041d2e948..f259ae73c2 100644 --- a/airavata-api/pom.xml +++ b/airavata-api/pom.xml @@ -52,10 +52,7 @@ under the License. <groupId>org.apache.httpcomponents.core5</groupId> <artifactId>httpcore5</artifactId> </dependency> - <dependency> - <groupId>com.google.inject</groupId> - <artifactId>guice</artifactId> - </dependency> + <!-- Guice removed - using Spring DI and AOP --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> @@ -78,14 +75,7 @@ under the License. </dependency> <!-- Database & pooling --> - <dependency> - <groupId>org.apache.commons</groupId> - <artifactId>commons-dbcp2</artifactId> - </dependency> - <dependency> - <groupId>org.apache.commons</groupId> - <artifactId>commons-pool2</artifactId> - </dependency> + <!-- DBCP2 removed - using HikariCP (included in Spring Boot) --> <dependency> <groupId>org.apache.derby</groupId> <artifactId>derby</artifactId> @@ -116,6 +106,7 @@ under the License. <groupId>commons-cli</groupId> <artifactId>commons-cli</artifactId> </dependency> + <!-- Dozer - being migrated to MapStruct --> <dependency> <groupId>com.github.dozermapper</groupId> <artifactId>dozer-core</artifactId> @@ -126,6 +117,18 @@ under the License. </exclusion> </exclusions> </dependency> + <!-- MapStruct for object mapping (replacing Dozer) --> + <dependency> + <groupId>org.mapstruct</groupId> + <artifactId>mapstruct</artifactId> + <version>1.5.5.Final</version> + </dependency> + <dependency> + <groupId>org.mapstruct</groupId> + <artifactId>mapstruct-processor</artifactId> + <version>1.5.5.Final</version> + <scope>provided</scope> + </dependency> <dependency> <groupId>org.apache.openjpa</groupId> <artifactId>openjpa</artifactId> @@ -136,10 +139,7 @@ under the License. </dependency> <!-- JSON, Kafka, XML --> - <dependency> - <groupId>com.google.code.gson</groupId> - <artifactId>gson</artifactId> - </dependency> + <!-- Gson removed - using Jackson (included in Spring Boot) --> <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> @@ -197,9 +197,20 @@ under the License. <artifactId>amqp-client</artifactId> </dependency> + <!-- Shiro removed - using Spring Security --> + <!-- Spring Security for authentication --> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-security</artifactId> + </dependency> + <!-- Spring LDAP for LDAP authentication --> <dependency> - <groupId>org.apache.shiro</groupId> - <artifactId>shiro-core</artifactId> + <groupId>org.springframework.ldap</groupId> + <artifactId>spring-ldap-core</artifactId> + </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-ldap</artifactId> </dependency> <dependency> @@ -224,9 +235,10 @@ under the License. <artifactId>api-all</artifactId> </dependency> + <!-- Quartz - using Spring-managed version --> <dependency> - <groupId>org.quartz-scheduler</groupId> - <artifactId>quartz</artifactId> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-quartz</artifactId> </dependency> <dependency> <groupId>com.hierynomus</groupId> diff --git a/airavata-api/src/main/java/org/apache/airavata/common/utils/DBUtil.java b/airavata-api/src/main/java/org/apache/airavata/common/utils/DBUtil.java index 9732d2e83a..66eaa13773 100644 --- a/airavata-api/src/main/java/org/apache/airavata/common/utils/DBUtil.java +++ b/airavata-api/src/main/java/org/apache/airavata/common/utils/DBUtil.java @@ -19,6 +19,8 @@ */ package org.apache.airavata.common.utils; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; @@ -28,7 +30,6 @@ import java.util.Properties; import javax.sql.DataSource; import org.apache.airavata.common.exception.ApplicationSettingsException; import org.apache.airavata.config.AiravataServerProperties; -import org.apache.commons.dbcp2.BasicDataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -216,18 +217,23 @@ public class DBUtil { } /** - * Gets a new DBCP data source. + * Gets a new HikariCP data source. * * @return A new data source. */ public DataSource getDataSource() { - BasicDataSource ds = new BasicDataSource(); - ds.setDriverClassName(this.driverName); - ds.setUsername(this.databaseUserName); - ds.setPassword(this.databasePassword); - ds.setUrl(this.jdbcUrl); - - return ds; + HikariConfig config = new HikariConfig(); + config.setDriverClassName(this.driverName); + config.setUsername(this.databaseUserName); + config.setPassword(this.databasePassword); + config.setJdbcUrl(this.jdbcUrl); + config.setMinimumIdle(2); + config.setMaximumPoolSize(10); + config.setConnectionTimeout(30000); + config.setIdleTimeout(600000); + config.setMaxLifetime(1800000); + + return new HikariDataSource(config); } /** diff --git a/airavata-api/src/main/java/org/apache/airavata/common/utils/JPAUtils.java b/airavata-api/src/main/java/org/apache/airavata/common/utils/JPAUtils.java index d4fde12179..9bd35f4847 100644 --- a/airavata-api/src/main/java/org/apache/airavata/common/utils/JPAUtils.java +++ b/airavata-api/src/main/java/org/apache/airavata/common/utils/JPAUtils.java @@ -37,7 +37,7 @@ public class JPAUtils { static { Map<String, String> properties = new HashMap<String, String>(); - properties.put("openjpa.ConnectionDriverName", "org.apache.commons.dbcp2.BasicDataSource"); + properties.put("openjpa.ConnectionDriverName", "com.zaxxer.hikari.HikariDataSource"); // Allow unenhanced classes at runtime - this is needed for Spring Data JPA integration // where metamodel is accessed before EntityManagerFactory is fully initialized // "supported" allows unenhanced classes but may have performance implications diff --git a/airavata-api/src/main/java/org/apache/airavata/common/utils/JSONUtil.java b/airavata-api/src/main/java/org/apache/airavata/common/utils/JSONUtil.java index 7ae435d81d..1223a0b726 100644 --- a/airavata-api/src/main/java/org/apache/airavata/common/utils/JSONUtil.java +++ b/airavata-api/src/main/java/org/apache/airavata/common/utils/JSONUtil.java @@ -19,71 +19,95 @@ */ package org.apache.airavata.common.utils; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonPrimitive; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.Reader; -import java.util.Map; -import java.util.Set; +import java.util.Iterator; public class JSONUtil { + private static final ObjectMapper objectMapper = new ObjectMapper(); - public static void saveJSON(JsonElement jsonElement, File file) throws IOException { - IOUtil.writeToFile(jsonElementToString(jsonElement), file); + public static void saveJSON(JsonNode jsonNode, File file) throws IOException { + IOUtil.writeToFile(jsonNodeToString(jsonNode), file); } - public static JsonObject stringToJSONObject(String workflowString) { - return JsonParser.parseString(workflowString).getAsJsonObject(); + public static ObjectNode stringToJSONObject(String workflowString) { + try { + JsonNode node = objectMapper.readTree(workflowString); + if (node.isObject()) { + return (ObjectNode) node; + } + throw new IllegalArgumentException("String does not represent a JSON object"); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to parse JSON string", e); + } } - public static JsonObject loadJSON(File file) throws IOException { + public static ObjectNode loadJSON(File file) throws IOException { return loadJSON(new FileReader(file)); } - public static JsonObject loadJSON(Reader reader) throws IOException { - return JsonParser.parseReader(reader).getAsJsonObject(); + public static ObjectNode loadJSON(Reader reader) throws IOException { + try { + JsonNode node = objectMapper.readTree(reader); + if (node.isObject()) { + return (ObjectNode) node; + } + throw new IllegalArgumentException("File does not contain a JSON object"); + } catch (JsonProcessingException e) { + throw new IOException("Failed to parse JSON from reader", e); + } } - public static String jsonElementToString(JsonElement jsonElement) { - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - return gson.toJson(jsonElement); + public static String jsonNodeToString(JsonNode jsonNode) { + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to convert JsonNode to string", e); + } } - public static boolean isEqual(JsonObject originalJsonObject, JsonObject newJsonObject) { + public static boolean isEqual(ObjectNode originalJsonObject, ObjectNode newJsonObject) { // TODO - Implement this method if (originalJsonObject == null && newJsonObject == null) { return true; } else if (originalJsonObject == null || newJsonObject == null) { return false; } else { - // check the number of childs - Set<Map.Entry<String, JsonElement>> entrySetOfOriginalJson = originalJsonObject.entrySet(); - Set<Map.Entry<String, JsonElement>> entrySetOfNewJson = newJsonObject.entrySet(); - if (entrySetOfOriginalJson.size() != entrySetOfNewJson.size()) { + // check the number of children + if (originalJsonObject.size() != newJsonObject.size()) { return false; } - for (Map.Entry<String, JsonElement> keyString : entrySetOfOriginalJson) { - JsonElement valueOrig = keyString.getValue(); - JsonElement valueNew = newJsonObject.get(keyString.getKey()); - if (valueOrig instanceof JsonObject - && valueNew instanceof JsonObject - && !isEqual((JsonObject) valueOrig, (JsonObject) valueNew)) { + Iterator<String> fieldNames = originalJsonObject.fieldNames(); + while (fieldNames.hasNext()) { + String key = fieldNames.next(); + JsonNode valueOrig = originalJsonObject.get(key); + JsonNode valueNew = newJsonObject.get(key); + + if (valueNew == null) { return false; - } else if (valueOrig instanceof JsonArray - && valueNew instanceof JsonArray - && !isEqual((JsonArray) valueOrig, (JsonArray) valueNew)) { - return false; - } else if (valueOrig instanceof JsonPrimitive - && valueNew instanceof JsonPrimitive - && !isEqual((JsonPrimitive) valueOrig, (JsonPrimitive) valueNew)) { + } + + if (valueOrig.isObject() && valueNew.isObject()) { + if (!isEqual((ObjectNode) valueOrig, (ObjectNode) valueNew)) { + return false; + } + } else if (valueOrig.isArray() && valueNew.isArray()) { + if (!isEqual((ArrayNode) valueOrig, (ArrayNode) valueNew)) { + return false; + } + } else if (valueOrig.isValueNode() && valueNew.isValueNode()) { + if (!isEqual(valueOrig, valueNew)) { + return false; + } + } else { return false; } } @@ -91,29 +115,31 @@ public class JSONUtil { return true; } - private static boolean isEqual(JsonArray arrayOriginal, JsonArray arrayNew) { + private static boolean isEqual(ArrayNode arrayOriginal, ArrayNode arrayNew) { if (arrayOriginal == null && arrayNew == null) { return true; } else if (arrayOriginal == null || arrayNew == null) { return false; } else { - // check the number of element + // check the number of elements if (arrayOriginal.size() != arrayNew.size()) { return false; } else if (arrayOriginal.size() == 0) { return true; } else { for (int i = 0; i < arrayOriginal.size(); i++) { - JsonElement valueOrig = arrayOriginal.get(i); - JsonElement valueNew = arrayNew.get(i); - if (valueOrig instanceof JsonObject && valueNew instanceof JsonObject) { - if (!isEqual((JsonObject) valueOrig, (JsonObject) valueNew)) { + JsonNode valueOrig = arrayOriginal.get(i); + JsonNode valueNew = arrayNew.get(i); + if (valueOrig.isObject() && valueNew.isObject()) { + if (!isEqual((ObjectNode) valueOrig, (ObjectNode) valueNew)) { return false; } - } else if (valueOrig instanceof JsonPrimitive && valueNew instanceof JsonPrimitive) { - if (!isEqual((JsonPrimitive) valueOrig, (JsonPrimitive) valueNew)) { + } else if (valueOrig.isValueNode() && valueNew.isValueNode()) { + if (!isEqual(valueOrig, valueNew)) { return false; } + } else { + return false; } } } @@ -121,28 +147,21 @@ public class JSONUtil { return true; } - private static boolean isEqual(JsonPrimitive primitiveOrig, JsonPrimitive primitiveNew) { - if (primitiveOrig == null && primitiveNew == null) { + private static boolean isEqual(JsonNode nodeOrig, JsonNode nodeNew) { + if (nodeOrig == null && nodeNew == null) { return true; - } else if (primitiveOrig == null || primitiveNew == null) { + } else if (nodeOrig == null || nodeNew == null) { return false; } else { - if (primitiveOrig.isString() && primitiveNew.isString()) { - if (!primitiveOrig.getAsString().equals(primitiveNew.getAsString())) { - return false; - } - } else if (primitiveOrig.isBoolean() && primitiveNew.isBoolean()) { - if ((Boolean.valueOf(primitiveOrig.getAsBoolean()).compareTo(primitiveNew.getAsBoolean()) != 0)) { - return false; - } - } else if (primitiveOrig.isNumber() && primitiveNew.isNumber()) { - if (Double.valueOf(primitiveOrig.getAsDouble()).compareTo(primitiveNew.getAsDouble()) != 0) { - return false; - } + if (nodeOrig.isTextual() && nodeNew.isTextual()) { + return nodeOrig.asText().equals(nodeNew.asText()); + } else if (nodeOrig.isBoolean() && nodeNew.isBoolean()) { + return nodeOrig.asBoolean() == nodeNew.asBoolean(); + } else if (nodeOrig.isNumber() && nodeNew.isNumber()) { + return nodeOrig.asDouble() == nodeNew.asDouble(); } else { - return primitiveOrig.isJsonNull() && primitiveNew.isJsonNull(); + return nodeOrig.isNull() && nodeNew.isNull(); } } - return true; } } diff --git a/airavata-api/src/main/java/org/apache/airavata/config/JpaConfig.java b/airavata-api/src/main/java/org/apache/airavata/config/JpaConfig.java index ca07715e90..736c29fcca 100644 --- a/airavata-api/src/main/java/org/apache/airavata/config/JpaConfig.java +++ b/airavata-api/src/main/java/org/apache/airavata/config/JpaConfig.java @@ -19,11 +19,12 @@ */ package org.apache.airavata.config; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import javax.sql.DataSource; import org.apache.airavata.common.utils.JPAUtils; -import org.apache.commons.dbcp2.BasicDataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; @@ -97,14 +98,18 @@ public class JpaConfig { throw new IllegalStateException( "Database configuration for registry is missing or invalid. Check airavata.properties for database.registry.url"); } - BasicDataSource dataSource = new BasicDataSource(); - dataSource.setDriverClassName(db.driver); - dataSource.setUrl(db.url); - dataSource.setUsername(db.user); - dataSource.setPassword(db.password); - dataSource.setValidationQuery(properties.database.validationQuery); - dataSource.setTestOnBorrow(true); - return dataSource; + HikariConfig config = new HikariConfig(); + config.setDriverClassName(db.driver); + config.setJdbcUrl(db.url); + config.setUsername(db.user); + config.setPassword(db.password); + config.setConnectionTestQuery(properties.database.validationQuery); + config.setMinimumIdle(2); + config.setMaximumPoolSize(10); + config.setConnectionTimeout(30000); + config.setIdleTimeout(600000); + config.setMaxLifetime(1800000); + return new HikariDataSource(config); } @Bean(name = "expCatalogEntityManagerFactory") diff --git a/airavata-api/src/main/java/org/apache/airavata/helix/impl/task/parsing/models/ParsingTaskInputs.java b/airavata-api/src/main/java/org/apache/airavata/helix/impl/task/parsing/models/ParsingTaskInputs.java index f7f9f2bd4c..abae1b2968 100644 --- a/airavata-api/src/main/java/org/apache/airavata/helix/impl/task/parsing/models/ParsingTaskInputs.java +++ b/airavata-api/src/main/java/org/apache/airavata/helix/impl/task/parsing/models/ParsingTaskInputs.java @@ -19,7 +19,7 @@ */ package org.apache.airavata.helix.impl.task.parsing.models; -import com.google.gson.Gson; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.ArrayList; import java.util.List; import org.apache.airavata.helix.task.api.TaskParamType; @@ -42,12 +42,22 @@ public class ParsingTaskInputs implements TaskParamType { @Override public String serialize() { - return new Gson().toJson(this); + try { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.writeValueAsString(this); + } catch (Exception e) { + throw new RuntimeException("Failed to serialize ParsingTaskInputs", e); + } } @Override public void deserialize(String content) { - ParsingTaskInputs parsingTaskInputs = new Gson().fromJson(content, ParsingTaskInputs.class); - this.inputs = parsingTaskInputs.getInputs(); + try { + ObjectMapper objectMapper = new ObjectMapper(); + ParsingTaskInputs parsingTaskInputs = objectMapper.readValue(content, ParsingTaskInputs.class); + this.inputs = parsingTaskInputs.getInputs(); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize ParsingTaskInputs", e); + } } } diff --git a/airavata-api/src/main/java/org/apache/airavata/helix/impl/task/parsing/models/ParsingTaskOutputs.java b/airavata-api/src/main/java/org/apache/airavata/helix/impl/task/parsing/models/ParsingTaskOutputs.java index c9d14ba6a6..17050d4172 100644 --- a/airavata-api/src/main/java/org/apache/airavata/helix/impl/task/parsing/models/ParsingTaskOutputs.java +++ b/airavata-api/src/main/java/org/apache/airavata/helix/impl/task/parsing/models/ParsingTaskOutputs.java @@ -19,7 +19,7 @@ */ package org.apache.airavata.helix.impl.task.parsing.models; -import com.google.gson.Gson; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.ArrayList; import java.util.List; import org.apache.airavata.helix.task.api.TaskParamType; @@ -41,12 +41,22 @@ public class ParsingTaskOutputs implements TaskParamType { @Override public String serialize() { - return new Gson().toJson(this); + try { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.writeValueAsString(this); + } catch (Exception e) { + throw new RuntimeException("Failed to serialize ParsingTaskOutputs", e); + } } @Override public void deserialize(String content) { - ParsingTaskOutputs parsingTaskOutputs = new Gson().fromJson(content, ParsingTaskOutputs.class); - this.outputs = parsingTaskOutputs.getOutputs(); + try { + ObjectMapper objectMapper = new ObjectMapper(); + ParsingTaskOutputs parsingTaskOutputs = objectMapper.readValue(content, ParsingTaskOutputs.class); + this.outputs = parsingTaskOutputs.getOutputs(); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize ParsingTaskOutputs", e); + } } } diff --git a/airavata-api/src/main/java/org/apache/airavata/metascheduler/metadata/analyzer/DataInterpreterService.java b/airavata-api/src/main/java/org/apache/airavata/metascheduler/metadata/analyzer/DataInterpreterService.java index ae26f9d71c..c60d89ce1f 100644 --- a/airavata-api/src/main/java/org/apache/airavata/metascheduler/metadata/analyzer/DataInterpreterService.java +++ b/airavata-api/src/main/java/org/apache/airavata/metascheduler/metadata/analyzer/DataInterpreterService.java @@ -30,11 +30,10 @@ import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; -import org.quartz.SchedulerFactory; import org.quartz.SimpleScheduleBuilder; import org.quartz.Trigger; import org.quartz.TriggerBuilder; -import org.quartz.impl.StdSchedulerFactory; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -69,8 +68,9 @@ public class DataInterpreterService implements IServer { @Override public void start() throws Exception { jobTriggerMap.clear(); - SchedulerFactory schedulerFactory = new StdSchedulerFactory(); - scheduler = schedulerFactory.getScheduler(); + SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); + schedulerFactoryBean.afterPropertiesSet(); + scheduler = schedulerFactoryBean.getScheduler(); final int parallelJobs = properties.services.parser.scanningParallelJobs; final double scanningInterval = properties.services.parser.scanningInterval; diff --git a/airavata-api/src/main/java/org/apache/airavata/metascheduler/process/scheduling/engine/rescheduler/ProcessReschedulingService.java b/airavata-api/src/main/java/org/apache/airavata/metascheduler/process/scheduling/engine/rescheduler/ProcessReschedulingService.java index 281167636e..6e504795cd 100644 --- a/airavata-api/src/main/java/org/apache/airavata/metascheduler/process/scheduling/engine/rescheduler/ProcessReschedulingService.java +++ b/airavata-api/src/main/java/org/apache/airavata/metascheduler/process/scheduling/engine/rescheduler/ProcessReschedulingService.java @@ -29,11 +29,10 @@ import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; -import org.quartz.SchedulerFactory; import org.quartz.SimpleScheduleBuilder; import org.quartz.Trigger; import org.quartz.TriggerBuilder; -import org.quartz.impl.StdSchedulerFactory; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @@ -72,8 +71,9 @@ public class ProcessReschedulingService implements IServer { public void start() throws Exception { jobTriggerMap.clear(); - SchedulerFactory schedulerFactory = new StdSchedulerFactory(); - scheduler = schedulerFactory.getScheduler(); + SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); + schedulerFactoryBean.afterPropertiesSet(); + scheduler = schedulerFactoryBean.getScheduler(); final int parallelJobs = properties.services.scheduler.clusterScanningParallelJobs; final double scanningInterval = properties.services.scheduler.jobScanningInterval; diff --git a/airavata-api/src/main/java/org/apache/airavata/monitor/cluster/ClusterStatusMonitorJobScheduler.java b/airavata-api/src/main/java/org/apache/airavata/monitor/cluster/ClusterStatusMonitorJobScheduler.java index 006d49d98d..4d9e3b810a 100644 --- a/airavata-api/src/main/java/org/apache/airavata/monitor/cluster/ClusterStatusMonitorJobScheduler.java +++ b/airavata-api/src/main/java/org/apache/airavata/monitor/cluster/ClusterStatusMonitorJobScheduler.java @@ -28,7 +28,7 @@ import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.Trigger; -import org.quartz.impl.StdSchedulerFactory; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,9 +38,11 @@ public class ClusterStatusMonitorJobScheduler { Scheduler scheduler; private AiravataServerProperties properties; - public ClusterStatusMonitorJobScheduler(AiravataServerProperties properties) throws SchedulerException { + public ClusterStatusMonitorJobScheduler(AiravataServerProperties properties) throws Exception { this.properties = properties; - scheduler = StdSchedulerFactory.getDefaultScheduler(); + SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); + schedulerFactoryBean.afterPropertiesSet(); + scheduler = schedulerFactoryBean.getScheduler(); scheduler.start(); } @@ -62,7 +64,7 @@ public class ClusterStatusMonitorJobScheduler { scheduler.scheduleJob(job, trigger); } - public static void main(String[] args) throws SchedulerException { + public static void main(String[] args) throws Exception { // Note: Properties should be loaded from Spring context in real usage // For main method, this is a placeholder ClusterStatusMonitorJobScheduler jobScheduler = new ClusterStatusMonitorJobScheduler(null); diff --git a/airavata-api/src/main/java/org/apache/airavata/monitor/compute/ComputationalResourceMonitoringService.java b/airavata-api/src/main/java/org/apache/airavata/monitor/compute/ComputationalResourceMonitoringService.java index 22793ed50d..d153ae1124 100644 --- a/airavata-api/src/main/java/org/apache/airavata/monitor/compute/ComputationalResourceMonitoringService.java +++ b/airavata-api/src/main/java/org/apache/airavata/monitor/compute/ComputationalResourceMonitoringService.java @@ -30,11 +30,10 @@ import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; -import org.quartz.SchedulerFactory; import org.quartz.SimpleScheduleBuilder; import org.quartz.Trigger; import org.quartz.TriggerBuilder; -import org.quartz.impl.StdSchedulerFactory; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -70,8 +69,9 @@ public class ComputationalResourceMonitoringService implements IServer { public void start() throws Exception { jobTriggerMap.clear(); - SchedulerFactory schedulerFactory = new StdSchedulerFactory(); - scheduler = schedulerFactory.getScheduler(); + SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); + schedulerFactoryBean.afterPropertiesSet(); + scheduler = schedulerFactoryBean.getScheduler(); // Note: These properties are not in AiravataServerProperties yet, using defaults // TODO: Add these to AiravataServerProperties if needed diff --git a/airavata-api/src/main/java/org/apache/airavata/monitor/realtime/parser/RealtimeJobStatusParser.java b/airavata-api/src/main/java/org/apache/airavata/monitor/realtime/parser/RealtimeJobStatusParser.java index f420f517ad..b32b6002ef 100644 --- a/airavata-api/src/main/java/org/apache/airavata/monitor/realtime/parser/RealtimeJobStatusParser.java +++ b/airavata-api/src/main/java/org/apache/airavata/monitor/realtime/parser/RealtimeJobStatusParser.java @@ -19,8 +19,9 @@ */ package org.apache.airavata.monitor.realtime.parser; -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.List; import java.util.Map; import java.util.Optional; @@ -63,7 +64,8 @@ public class RealtimeJobStatusParser { public JobStatusResult parse(String rawMessage, String publisherId, RegistryService registryService) { try { - Map asMap = new Gson().fromJson(rawMessage, Map.class); + ObjectMapper objectMapper = new ObjectMapper(); + Map<String, Object> asMap = objectMapper.readValue(rawMessage, new TypeReference<Map<String, Object>>() {}); if (asMap.containsKey("jobName") && asMap.containsKey("status")) { String jobName = (String) asMap.get("jobName"); String status = (String) asMap.get("status"); @@ -115,7 +117,7 @@ public class RealtimeJobStatusParser { logger.error("Data structure of message {} is not correct", rawMessage); return null; } - } catch (JsonSyntaxException e) { + } catch (JsonProcessingException e) { logger.error("Failed to parse raw data {} to type Map", rawMessage, e); return null; } diff --git a/airavata-api/src/main/java/org/apache/airavata/registry/utils/migration/MappingToolRunner.java b/airavata-api/src/main/java/org/apache/airavata/registry/utils/migration/MappingToolRunner.java index bc073feed3..81c9727fd7 100644 --- a/airavata-api/src/main/java/org/apache/airavata/registry/utils/migration/MappingToolRunner.java +++ b/airavata-api/src/main/java/org/apache/airavata/registry/utils/migration/MappingToolRunner.java @@ -48,7 +48,7 @@ public class MappingToolRunner { dbInitConfig.getUser(), dbInitConfig.getPassword(), dbInitConfig.getValidationQuery())); - jdbcConfiguration.setConnectionDriverName("org.apache.commons.dbcp2.BasicDataSource"); + jdbcConfiguration.setConnectionDriverName("com.zaxxer.hikari.HikariDataSource"); Options options = new Options(); options.put("sqlFile", outputFile); diff --git a/airavata-api/src/main/java/org/apache/airavata/security/interceptor/SecurityCheck.java b/airavata-api/src/main/java/org/apache/airavata/security/interceptor/SecurityCheck.java index b527ea5020..8d7df2b6f0 100644 --- a/airavata-api/src/main/java/org/apache/airavata/security/interceptor/SecurityCheck.java +++ b/airavata-api/src/main/java/org/apache/airavata/security/interceptor/SecurityCheck.java @@ -19,7 +19,6 @@ */ package org.apache.airavata.security.interceptor; -import com.google.inject.BindingAnnotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -27,8 +26,8 @@ import java.lang.annotation.Target; /** * This is just the definition of the annotation used to mark the API methods to be intercepted. + * Methods annotated with this will be intercepted by Spring AOP for security checks. */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) -@BindingAnnotation public @interface SecurityCheck {} diff --git a/airavata-api/src/main/java/org/apache/airavata/security/interceptor/SecurityModule.java b/airavata-api/src/main/java/org/apache/airavata/security/interceptor/SecurityModule.java index cbad2cafc1..2e143b7b39 100644 --- a/airavata-api/src/main/java/org/apache/airavata/security/interceptor/SecurityModule.java +++ b/airavata-api/src/main/java/org/apache/airavata/security/interceptor/SecurityModule.java @@ -11,40 +11,88 @@ * 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 +* software 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.airavata.security.interceptor; -import com.google.inject.AbstractModule; -import com.google.inject.matcher.Matchers; -import org.apache.airavata.config.AiravataServerProperties; -import org.apache.airavata.security.AiravataSecurityManager; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; /** - * This does the plumbing work of integrating the interceptor with Guice framework for the methods to be - * intercepted upon their invocation. + * Spring AOP aspect for security interception. + * Replaces the Guice-based SecurityModule with Spring AOP. + * Methods annotated with @SecurityCheck will be intercepted by this aspect. */ -public class SecurityModule extends AbstractModule { +@Aspect +@Component +public class SecurityModule { private static final Logger logger = LoggerFactory.getLogger(SecurityModule.class); - private final AiravataSecurityManager securityManager; - private final AiravataServerProperties properties; + private final SecurityInterceptor securityInterceptor; - public SecurityModule(AiravataSecurityManager securityManager, AiravataServerProperties properties) { - this.securityManager = securityManager; - this.properties = properties; + public SecurityModule(SecurityInterceptor securityInterceptor) { + this.securityInterceptor = securityInterceptor; + logger.info("Security AOP aspect initialized"); } - public void configure() { - logger.info("Security module reached..."); - SecurityInterceptor interceptor = new SecurityInterceptor(securityManager, properties); + @Pointcut("@annotation(org.apache.airavata.security.interceptor.SecurityCheck)") + public void securityCheckPointcut() { + // Pointcut definition for methods annotated with @SecurityCheck + } + + @Around("securityCheckPointcut()") + public Object intercept(ProceedingJoinPoint joinPoint) throws Throwable { + // Delegate to SecurityInterceptor which implements MethodInterceptor + return securityInterceptor.invoke(new MethodInvocationAdapter(joinPoint)); + } + + /** + * Adapter to convert ProceedingJoinPoint to MethodInvocation for SecurityInterceptor. + */ + private static class MethodInvocationAdapter implements org.aopalliance.intercept.MethodInvocation { + private final ProceedingJoinPoint joinPoint; + private final java.lang.reflect.Method method; + + public MethodInvocationAdapter(ProceedingJoinPoint joinPoint) { + this.joinPoint = joinPoint; + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + this.method = signature.getMethod(); + } + + @Override + public Object proceed() throws Throwable { + return joinPoint.proceed(); + } + + @Override + public Object getThis() { + return joinPoint.getThis(); + } + + @Override + public java.lang.reflect.Method getMethod() { + return method; + } + + @Override + public Object[] getArguments() { + return joinPoint.getArgs(); + } - bindInterceptor(Matchers.any(), Matchers.annotatedWith(SecurityCheck.class), interceptor); + @Override + public java.lang.reflect.AccessibleObject getStaticPart() { + // Return the method as the static part - this is required by the interface + // but not actually used by SecurityInterceptor + return method; + } } } diff --git a/airavata-api/src/main/java/org/apache/airavata/security/userstore/JDBCUserStore.java b/airavata-api/src/main/java/org/apache/airavata/security/userstore/JDBCUserStore.java index 4ef3e59830..063775e5e4 100644 --- a/airavata-api/src/main/java/org/apache/airavata/security/userstore/JDBCUserStore.java +++ b/airavata-api/src/main/java/org/apache/airavata/security/userstore/JDBCUserStore.java @@ -11,8 +11,7 @@ * 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 +* software 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. @@ -24,46 +23,53 @@ import org.apache.airavata.common.exception.ApplicationSettingsException; import org.apache.airavata.common.utils.DBUtil; import org.apache.airavata.security.UserStoreException; import org.apache.airavata.security.util.PasswordDigester; -import org.apache.shiro.authc.AuthenticationException; -import org.apache.shiro.authc.AuthenticationInfo; -import org.apache.shiro.authc.AuthenticationToken; -import org.apache.shiro.authc.UsernamePasswordToken; -import org.apache.shiro.realm.jdbc.JdbcRealm; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * The JDBC user store implementation. + * Migrated from Apache Shiro to Spring Security JDBC authentication. */ public class JDBCUserStore extends AbstractJDBCUserStore { protected static Logger log = LoggerFactory.getLogger(JDBCUserStore.class); - private JdbcRealm jdbcRealm; - + private DaoAuthenticationProvider authenticationProvider; private PasswordDigester passwordDigester; + private DataSource dataSource; public JDBCUserStore() { - jdbcRealm = new JdbcRealm(); + // Constructor } @Override public boolean authenticate(String userName, Object credentials) throws UserStoreException { - AuthenticationToken authenticationToken = - new UsernamePasswordToken(userName, passwordDigester.getPasswordHashValue((String) credentials)); - - AuthenticationInfo authenticationInfo; try { - - authenticationInfo = jdbcRealm.getAuthenticationInfo(authenticationToken); - return authenticationInfo != null; - - } catch (AuthenticationException e) { - log.debug(e.getLocalizedMessage(), e); + String password = passwordDigester.getPasswordHashValue((String) credentials); + UsernamePasswordAuthenticationToken authRequest = + new UsernamePasswordAuthenticationToken(userName, password); + + Authentication authentication = authenticationProvider.authenticate(authRequest); + return authentication != null && authentication.isAuthenticated(); + } catch (BadCredentialsException e) { + log.debug("JDBC authentication failed for user: {}", userName, e); return false; + } catch (Exception e) { + log.error("Error during JDBC authentication", e); + throw new UserStoreException("Error during JDBC authentication", e); } } @@ -147,26 +153,96 @@ public class JDBCUserStore extends AbstractJDBCUserStore { } protected void initializeDatabaseLookup(String passwordColumn, String userTable, String userNameColumn) - throws ApplicationSettingsException { - DBUtil dbUtil = new DBUtil(getDatabaseURL(), getDatabaseUserName(), getDatabasePassword(), getDatabaseDriver()); - DataSource dataSource = dbUtil.getDataSource(); - jdbcRealm.setDataSource(dataSource); + throws ApplicationSettingsException, UserStoreException { + try { + DBUtil dbUtil = new DBUtil(getDatabaseURL(), getDatabaseUserName(), getDatabasePassword(), getDatabaseDriver()); + dataSource = dbUtil.getDataSource(); - StringBuilder stringBuilder = new StringBuilder(); + // Create user details service + UserDetailsService userDetailsService = new JdbcUserDetailsService( + dataSource, userTable, userNameColumn, passwordColumn); - stringBuilder - .append("SELECT ") - .append(passwordColumn) - .append(" FROM ") - .append(userTable) - .append(" WHERE ") - .append(userNameColumn) - .append(" = ?"); + // Create password encoder adapter + PasswordEncoder passwordEncoder = new PasswordDigesterEncoder(passwordDigester); - jdbcRealm.setAuthenticationQuery(stringBuilder.toString()); + // Create authentication provider + // Note: Using deprecated API for compatibility - Spring Security API has changed + authenticationProvider = new DaoAuthenticationProvider(); + authenticationProvider.setUserDetailsService(userDetailsService); + authenticationProvider.setPasswordEncoder(passwordEncoder); + } catch (Exception e) { + throw new UserStoreException("Failed to initialize JDBC authentication", e); + } } public PasswordDigester getPasswordDigester() { return passwordDigester; } + + /** + * UserDetailsService implementation for JDBC authentication + */ + private static class JdbcUserDetailsService implements UserDetailsService { + private final JdbcTemplate jdbcTemplate; + private final String userTable; + private final String userNameColumn; + private final String passwordColumn; + + public JdbcUserDetailsService(DataSource dataSource, String userTable, + String userNameColumn, String passwordColumn) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + this.userTable = userTable; + this.userNameColumn = userNameColumn; + this.passwordColumn = passwordColumn; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + String sql = String.format("SELECT %s FROM %s WHERE %s = ?", + passwordColumn, userTable, userNameColumn); + + try { + String password = jdbcTemplate.queryForObject(sql, String.class, username); + if (password == null) { + throw new UsernameNotFoundException("User not found: " + username); + } + return User.withUsername(username) + .password(password) + .authorities("ROLE_USER") + .build(); + } catch (org.springframework.dao.EmptyResultDataAccessException e) { + throw new UsernameNotFoundException("User not found: " + username, e); + } + } + } + + /** + * Password encoder adapter for PasswordDigester + */ + private static class PasswordDigesterEncoder implements PasswordEncoder { + private final PasswordDigester passwordDigester; + + public PasswordDigesterEncoder(PasswordDigester passwordDigester) { + this.passwordDigester = passwordDigester; + } + + @Override + public String encode(CharSequence rawPassword) { + try { + return passwordDigester.getPasswordHashValue(rawPassword.toString()); + } catch (UserStoreException e) { + throw new RuntimeException("Failed to encode password", e); + } + } + + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + try { + String hashed = passwordDigester.getPasswordHashValue(rawPassword.toString()); + return hashed.equals(encodedPassword); + } catch (UserStoreException e) { + throw new RuntimeException("Failed to match password", e); + } + } + } } diff --git a/airavata-api/src/main/java/org/apache/airavata/security/userstore/LDAPUserStore.java b/airavata-api/src/main/java/org/apache/airavata/security/userstore/LDAPUserStore.java index f285ddbaa0..f948335351 100644 --- a/airavata-api/src/main/java/org/apache/airavata/security/userstore/LDAPUserStore.java +++ b/airavata-api/src/main/java/org/apache/airavata/security/userstore/LDAPUserStore.java @@ -11,8 +11,7 @@ * 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 +* software 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. @@ -22,43 +21,48 @@ package org.apache.airavata.security.userstore; import org.apache.airavata.security.UserStore; import org.apache.airavata.security.UserStoreException; import org.apache.airavata.security.util.PasswordDigester; -import org.apache.shiro.authc.AuthenticationException; -import org.apache.shiro.authc.AuthenticationInfo; -import org.apache.shiro.authc.AuthenticationToken; -import org.apache.shiro.authc.UsernamePasswordToken; -import org.apache.shiro.realm.ldap.JndiLdapContextFactory; -import org.apache.shiro.realm.ldap.JndiLdapRealm; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.ldap.authentication.PasswordComparisonAuthenticator; +import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; +import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * A user store which talks to LDAP server. User credentials and user information are stored in a LDAP server. + * Migrated from Apache Shiro to Spring Security LDAP. */ public class LDAPUserStore implements UserStore { - private JndiLdapRealm ldapRealm; + private LdapAuthenticationProvider ldapAuthenticationProvider; + private LdapContextSource contextSource; protected static Logger log = LoggerFactory.getLogger(LDAPUserStore.class); private PasswordDigester passwordDigester; public boolean authenticate(String userName, Object credentials) throws UserStoreException { - - AuthenticationToken authenticationToken = - new UsernamePasswordToken(userName, passwordDigester.getPasswordHashValue((String) credentials)); - - AuthenticationInfo authenticationInfo; try { - authenticationInfo = ldapRealm.getAuthenticationInfo(authenticationToken); - } catch (AuthenticationException e) { - log.warn(e.getLocalizedMessage(), e); + String password = passwordDigester.getPasswordHashValue((String) credentials); + UsernamePasswordAuthenticationToken authRequest = + new UsernamePasswordAuthenticationToken(userName, password); + + Authentication authentication = ldapAuthenticationProvider.authenticate(authRequest); + return authentication != null && authentication.isAuthenticated(); + } catch (BadCredentialsException e) { + log.warn("LDAP authentication failed for user: {}", userName, e); return false; + } catch (Exception e) { + log.error("Error during LDAP authentication", e); + throw new UserStoreException("Error during LDAP authentication", e); } - - return authenticationInfo != null; } @Override @@ -123,19 +127,76 @@ public class LDAPUserStore implements UserStore { } protected void initializeLDAP( - String ldapUrl, String systemUser, String systemUserPassword, String userNameTemplate) { + String ldapUrl, String systemUser, String systemUserPassword, String userNameTemplate) throws UserStoreException { + + try { + // Create LDAP context source + contextSource = new LdapContextSource(); + contextSource.setUrl(ldapUrl); + contextSource.setUserDn(systemUser); + contextSource.setPassword(systemUserPassword); + contextSource.afterPropertiesSet(); + } catch (Exception e) { + throw new UserStoreException("Failed to initialize LDAP context", e); + } + + // Create user search - convert Shiro template format to Spring LDAP format + // Shiro: uid={0},ou=system -> Spring: (uid={0}) with base DN ou=system + String searchBase = ""; + String searchFilter = userNameTemplate; + if (userNameTemplate.contains(",")) { + int commaIndex = userNameTemplate.indexOf(","); + searchBase = userNameTemplate.substring(commaIndex + 1); + searchFilter = userNameTemplate.substring(0, commaIndex); + // Convert format: uid={0} -> (uid={0}) + if (!searchFilter.startsWith("(")) { + searchFilter = "(" + searchFilter + ")"; + } + } - JndiLdapContextFactory jndiLdapContextFactory = new JndiLdapContextFactory(); + FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch( + searchBase, searchFilter, contextSource); - jndiLdapContextFactory.setUrl(ldapUrl); - jndiLdapContextFactory.setSystemUsername(systemUser); - jndiLdapContextFactory.setSystemPassword(systemUserPassword); + // Create authenticator + PasswordComparisonAuthenticator authenticator = new PasswordComparisonAuthenticator(contextSource); + authenticator.setUserSearch(userSearch); + authenticator.setPasswordEncoder(new PasswordDigesterEncoder(passwordDigester)); - ldapRealm = new JndiLdapRealm(); + // Create authorities populator (empty for now - can be extended) + DefaultLdapAuthoritiesPopulator authoritiesPopulator = + new DefaultLdapAuthoritiesPopulator(contextSource, ""); - ldapRealm.setContextFactory(jndiLdapContextFactory); - ldapRealm.setUserDnTemplate(userNameTemplate); + // Create authentication provider + ldapAuthenticationProvider = new LdapAuthenticationProvider(authenticator, authoritiesPopulator); + } + + /** + * Password encoder adapter for PasswordDigester + */ + private static class PasswordDigesterEncoder implements org.springframework.security.crypto.password.PasswordEncoder { + private final PasswordDigester passwordDigester; - ldapRealm.init(); + public PasswordDigesterEncoder(PasswordDigester passwordDigester) { + this.passwordDigester = passwordDigester; + } + + @Override + public String encode(CharSequence rawPassword) { + try { + return passwordDigester.getPasswordHashValue(rawPassword.toString()); + } catch (UserStoreException e) { + throw new RuntimeException("Failed to encode password", e); + } + } + + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + try { + String hashed = passwordDigester.getPasswordHashValue(rawPassword.toString()); + return hashed.equals(encodedPassword); + } catch (UserStoreException e) { + throw new RuntimeException("Failed to match password", e); + } + } } } diff --git a/pom.xml b/pom.xml index 1a91ac20c1..64d5aac6c2 100644 --- a/pom.xml +++ b/pom.xml @@ -384,6 +384,17 @@ under the License. <artifactId>dozer-core</artifactId> <version>7.0.0</version> </dependency> + <!-- MapStruct for object mapping (replacing Dozer) --> + <dependency> + <groupId>org.mapstruct</groupId> + <artifactId>mapstruct</artifactId> + <version>1.5.5.Final</version> + </dependency> + <dependency> + <groupId>org.mapstruct</groupId> + <artifactId>mapstruct-processor</artifactId> + <version>1.5.5.Final</version> + </dependency> <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId>
