added authentication

This commit is contained in:
Lucio Lelii 2025-05-02 17:17:27 +02:00
parent 13a61aea03
commit a105d2c9e9
16 changed files with 363 additions and 31 deletions

27
pom.xml
View File

@ -36,19 +36,32 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- <dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency> -->
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>24.0.1</version>
</dependency>
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency> -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- oppure jjwt-gson -->
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- PostgreSQL database -->
<dependency>
<groupId>org.postgresql</groupId>
@ -89,7 +102,7 @@
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,52 @@
package it.cnr.isti.workflow.manager;
import java.util.Date;
import javax.crypto.SecretKey;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import it.cnr.isti.workflow.manager.model.entities.LoginEntity;
@Component
public class JwtUtil {
SecretKey secretKey = Keys.hmacShaKeyFor("la-tua-chiave-segreta-di-almeno-32-byte!".getBytes());
public String generateToken(String username) {
String jwt = Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 1 giorno
.signWith(secretKey, SignatureAlgorithm.HS256) // Usa una SecretKey (HMAC)
.compact();
return jwt;
}
public String extractUsername(String token) {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token, LoginEntity userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private boolean isTokenExpired(String token) {
Date expiration = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody()
.getExpiration();
return expiration.before(new Date());
}
}

View File

@ -16,7 +16,6 @@ import it.cnr.isti.workflow.manager.model.ExecutionStep;
import it.cnr.isti.workflow.manager.model.types.NodeDefinition;
import it.cnr.isti.workflow.manager.model.ui.Connection;
import it.cnr.isti.workflow.manager.model.ui.Flow;
import it.cnr.isti.workflow.manager.model.ui.IOModel;
import it.cnr.isti.workflow.manager.model.ui.Node;
import it.cnr.isti.workflow.manager.repositories.NodeDefinitionRepository;

View File

@ -13,6 +13,7 @@ import org.springframework.util.ResourceUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import it.cnr.isti.workflow.manager.executors.ai.AIModel;
import it.cnr.isti.workflow.manager.model.entities.LoginEntity;
import it.cnr.isti.workflow.manager.model.types.IOType;
import it.cnr.isti.workflow.manager.model.types.Key;
import it.cnr.isti.workflow.manager.model.types.NodeDefinition;
@ -21,6 +22,7 @@ import it.cnr.isti.workflow.manager.model.types.ParameterType;
import it.cnr.isti.workflow.manager.model.types.Translator;
import it.cnr.isti.workflow.manager.model.ui.Flow;
import it.cnr.isti.workflow.manager.model.ui.FlowEntity;
import it.cnr.isti.workflow.manager.repositories.AuthRepository;
import it.cnr.isti.workflow.manager.repositories.FlowRepository;
import it.cnr.isti.workflow.manager.repositories.NodeDefinitionRepository;
@ -36,7 +38,7 @@ public class WorkflowManagerApplication {
@Bean
@ConditionalOnProperty(prefix = "app", name = "db.init.enabled", havingValue = "true")
CommandLineRunner init(NodeDefinitionRepository repository, FlowRepository flowRepository) {
CommandLineRunner init(NodeDefinitionRepository repository, FlowRepository flowRepository, AuthRepository authRepository) {
return args -> {
JSONObject rPars = new JSONObject().put("options",
@ -69,6 +71,8 @@ public class WorkflowManagerApplication {
loadNodeDefinitionAndSave(repository);
loadFlowsAndSave(flowRepository);
insertUser(authRepository);
};
}
@ -101,4 +105,9 @@ public class WorkflowManagerApplication {
}
}
private void insertUser(AuthRepository repository) {
LoginEntity le = new LoginEntity("lucio", "lucio");
repository.save(le);
}
}

View File

@ -0,0 +1,56 @@
package it.cnr.isti.workflow.manager.configurations;
import java.io.IOException;
import java.util.Collections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import it.cnr.isti.workflow.manager.JwtUtil;
import it.cnr.isti.workflow.manager.model.entities.LoginEntity;
import it.cnr.isti.workflow.manager.repositories.AuthRepository;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private AuthRepository authRepository;
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String jwt = authHeader.substring(7);
String username = jwtUtil.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
LoginEntity userDetails = authRepository.findById(username).orElse(null);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails, null, Collections.emptyList());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
}
filterChain.doFilter(request, response);
}
}

View File

@ -1,22 +1,40 @@
package it.cnr.isti.workflow.manager.configurations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//import org.springframework.security.config.annotation.web.builders.HttpSecurity;
//import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
/*
@Autowired
private JwtAuthenticationFilter jwtFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt());
return http.build();
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
*/
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}

View File

@ -19,7 +19,6 @@ public class WebConfig implements WebMvcConfigurer {
registry.addMapping("/**")
.allowedOrigins(origins)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
.allowedHeaders("*");
}
}

View File

@ -0,0 +1,59 @@
package it.cnr.isti.workflow.manager.controllers;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import it.cnr.isti.workflow.manager.JwtUtil;
import it.cnr.isti.workflow.manager.model.AuthRequest;
import it.cnr.isti.workflow.manager.model.ChangePasswordRequest;
import it.cnr.isti.workflow.manager.services.AuthService;
import java.util.Collections;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@RestController
@RequestMapping("/auth")
public class AuthController {
private static final Logger logger = org.slf4j.LoggerFactory.getLogger(AuthController.class);
@Autowired
private JwtUtil jwtUtil;
@Autowired
private AuthService authService;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody AuthRequest request) {
logger.info("Login attempt for user: {}", request.getUsername());
if (!authService.validateUser(request.getUsername(), request.getPassword())) {
return ResponseEntity.status(401).body("Invalid credentials");
}
String jwt = jwtUtil.generateToken(request.getUsername());
return ResponseEntity.ok(Collections.singletonMap("token", jwt));
}
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody AuthRequest request) {
if (authService.registerUser(request.getUsername(), request.getPassword())) {
return ResponseEntity.ok().build();
}
return ResponseEntity.badRequest().body("the user already exists");
}
@PostMapping("/change-password")
public ResponseEntity<?> changePassword(@RequestBody ChangePasswordRequest request) {
if (authService.changePassword(request.getUsername(), request.getOldPassword(), request.getNewPassword())) {
return ResponseEntity.ok().build();
}
return ResponseEntity.badRequest().body("Change password failed");
}
}

View File

@ -46,6 +46,7 @@ public class GeminiModel implements AIModel {
// Deserializzazione della risposta JSON in un oggetto GeminiResponse
GeminiResponse response = objectMapper.readValue(result, GeminiResponse.class);
// Estrazione del testo dal primo candidato
String candidateText = response.getCandidates().get(0).getContent().getParts().get(0).getText();

View File

@ -0,0 +1,20 @@
package it.cnr.isti.workflow.manager.model;
public class AuthRequest {
private String username;
private String password;
// Getters e Setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
boolean isValid() {
return username.contains(" ") && username != null && !username.isEmpty() && username.length()>5 &&
password != null && username.contains(" ") && password.length()>5 && !password.isEmpty();
}
}

View File

@ -0,0 +1,18 @@
package it.cnr.isti.workflow.manager.model;
public class ChangePasswordRequest {
private String username;
private String oldPassword;
private String newPassword;
// Getters e Setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getOldPassword() { return oldPassword; }
public void setOldPassword(String oldPassword) { this.oldPassword = oldPassword; }
public String getNewPassword() { return newPassword; }
public void setNewPassword(String newPassword) { this.newPassword = newPassword; }
}

View File

@ -6,6 +6,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
@ -78,16 +79,25 @@ public class ExecutionStep implements InputReadyListener {
if (areAllInputsReady())
new Thread(() -> {
this.context.getStepsUnderExecution().add(this.id);
log.info("starting executing of step {}", this.nodeDefinition.getType());
try {
Map<String, Object> realInputs = prepareInputs();
//log.debug("input translation for step {} is {}", id, realInputs);
Map<String, Object> returned = executor.execute(this.runtimeParameters, null, realInputs);
this.context.getNodeResult().put(this.id, returned);
for (InputReadyListener nextStep : nextSteps)
for (Map.Entry<String, Object> entry : returned.entrySet())
nextStep.inputReady(!nodeDefinition.getExecutorToOutputTranslationMappings().isEmpty()
? nodeDefinition.getExecutorToOutputTranslationMappings().get(entry.getKey())
: entry.getKey(), entry.getValue());
//mapping executor output to current node output
Map<String, Object> mappedReturn = returned.entrySet().stream().collect(Collectors.toMap(
e -> { return !nodeDefinition.getExecutorToOutputTranslationMappings().isEmpty()
? nodeDefinition.getExecutorToOutputTranslationMappings().get(e.getKey())
: e.getKey();
},
e -> e.getValue()));
context.nodeResult.put(this.id, mappedReturn);
//sendig output to next steps
nextSteps.forEach(n -> mappedReturn.forEach((k, v) -> n.inputReady(k,v)));
} catch (ExecutionException e) {
log.error("error during execution of step {}: {}", id, e.getMessage());
this.context.getErrors().add(String.format("[%S] %s", this.id, e.getMessage()));
@ -95,7 +105,9 @@ public class ExecutionStep implements InputReadyListener {
return;
} finally {
this.context.getStepsUnderExecution().remove(this.id);
}
log.info("finished executing of step {}", this.nodeDefinition.getType());
}
}).start();
else
log.debug("not all inputs are ready for step {}", id);

View File

@ -0,0 +1,17 @@
package it.cnr.isti.workflow.manager.model.entities;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginEntity {
@Id
private String username;
private String password;
}

View File

@ -0,0 +1,12 @@
package it.cnr.isti.workflow.manager.repositories;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import it.cnr.isti.workflow.manager.model.entities.LoginEntity;
@Repository
public interface AuthRepository extends JpaRepository<LoginEntity, String> {
}

View File

@ -0,0 +1,47 @@
package it.cnr.isti.workflow.manager.services;
import org.keycloak.adapters.jaas.AbstractKeycloakLoginModule.Auth;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import it.cnr.isti.workflow.manager.model.entities.LoginEntity;
import it.cnr.isti.workflow.manager.repositories.AuthRepository;
@Service
public class AuthService {
private static final Logger logger = org.slf4j.LoggerFactory.getLogger(AuthService.class);
@Autowired
AuthRepository authRepository;
public boolean validateUser(String username, String password) {
LoginEntity le = authRepository.findById(username).orElse(null);
if (le == null) {
return false; // User not found
}
return le.getPassword().equals(password); // Validate password
}
public boolean registerUser(String username, String password) {
if (authRepository.existsById(username)) {
return false; // User already exists
}
authRepository.save(new LoginEntity(username, password));
return true;
}
public boolean changePassword(String username, String oldPassword, String newPassword) {
return authRepository.findById(username)
.filter(user -> user.getPassword().equals(oldPassword))
.map(user -> {
user.setPassword(newPassword);
authRepository.save(user);
return true;
})
.orElse(false);
}
}

View File

@ -256,7 +256,7 @@
"inputToExecutorTranslationMappings": { },
"inputTranslators": {
"prompt": {
"translation": "Act as a **Senior Software Engineer** specializing in **model-driven development**. Generate clean, maintainable code that:\n1. Implements all validated requirements (from CSV) \n2. Matches the structure of the provided class diagram \n3. Follows the interaction flows from the sequence diagram \n\n**Input:** \n1. requirements (ID, Corrected Requirement, Customer Quote):\n\n${{corrected-requirements}}\n\n2. class diagram (PlantUML) :\n\n${{class-diagram}}\n\n3. sequence diagram (PlantUML) :\n\n\n\n\n**Output Rules:** \n- **Language**: Python\n- **Architecture**: \n - 1:1 mapping of PlantUML classes to code \n - Sequence diagram steps → method logic \n- **Quality**: \n - Type hints (Python)\n - Docstrings referencing requirement IDs (e.g., `# F1`) \n - Appropriate error handling \n- **Format**:\n - Output only executable code, do not add any text, besides comments\n\n**Example Output (Python Calculator):** \n\nfrom enum import Enum, auto\n\nclass OperationType(Enum):\n \"\"\"Supported operations (matches class diagram <<enum>>)\"\"\"\n ADD = auto() # F3: Basic arithmetic\n SUBTRACT = auto() # F3\n MULTIPLY = auto() # F3\n DIVIDE = auto() # F3\n\nclass Calculator:\n \"\"\"Core calculator logic (from class diagram)\"\"\"\n def __init__(self):\n self.current_input = 0 # F1: Digit entry\n self.stored_value = None # For multi-step operations\n self.selected_op = None # F3: Operation selection\n \n def calculate(self) -> float:\n \"\"\"Execute the current operation (sequence diagram '=' press)\"\"\"\n if not self.selected_op:\n return self.current_input\n \n # Sequence diagram compute step\n match self.selected_op:\n case OperationType.ADD:\n result = self.stored_value + self.current_input\n case OperationType.SUBTRACT:\n result = self.stored_value - self.current_input\n # ... other operations (F3)\n \n self.current_input = result\n return result\n\nclass InputHandler:\n \"\"\"Manages user input flow (sequence diagram interactions)\"\"\"\n def __init__(self, calculator: Calculator, display: Display):\n self.calc = calculator\n self.display = display\n \n def handle_digit(self, digit: int): # F1\n \"\"\"Process digit press (sequence diagram step 1)\"\"\"\n self.calc.current_input = self.calc.current_input * 10 + digit\n self.display.update(str(self.calc.current_input))\n\n def handle_operator(self, op: OperationType): # F3\n \"\"\"Process +,-,*,/ press (sequence diagram step 2)\"\"\"\n self.calc.stored_value = self.calc.current_input\n self.calc.selected_op = op\n self.calc.current_input = 0\n"
"translation": "Act as a **Senior Software Engineer** specializing in **model-driven development**. Generate clean, maintainable code that:\n1. Implements all validated requirements (from CSV) \n2. Matches the structure of the provided class diagram \n3. Follows the interaction flows from the sequence diagram \n\n**Input:** \n1. requirements (ID, Corrected Requirement, Customer Quote):\n\n${{corrected-requirements}}\n\n2. class diagram (PlantUML) :\n\n${{class-diagram}}\n\n3. sequence diagram (PlantUML) :\n\n${{sequence-diagram}}\n\n**Output Rules:** \n- **Language**: Python\n- **Architecture**: \n - 1:1 mapping of PlantUML classes to code \n - Sequence diagram steps → method logic \n- **Quality**: \n - Type hints (Python)\n - Docstrings referencing requirement IDs (e.g., `# F1`) \n - Appropriate error handling \n- **Format**:\n - Output only executable code, do not add any text, besides comments\n\n**Example Output (Python Calculator):** \n\nfrom enum import Enum, auto\n\nclass OperationType(Enum):\n \"\"\"Supported operations (matches class diagram <<enum>>)\"\"\"\n ADD = auto() # F3: Basic arithmetic\n SUBTRACT = auto() # F3\n MULTIPLY = auto() # F3\n DIVIDE = auto() # F3\n\nclass Calculator:\n \"\"\"Core calculator logic (from class diagram)\"\"\"\n def __init__(self):\n self.current_input = 0 # F1: Digit entry\n self.stored_value = None # For multi-step operations\n self.selected_op = None # F3: Operation selection\n \n def calculate(self) -> float:\n \"\"\"Execute the current operation (sequence diagram '=' press)\"\"\"\n if not self.selected_op:\n return self.current_input\n \n # Sequence diagram compute step\n match self.selected_op:\n case OperationType.ADD:\n result = self.stored_value + self.current_input\n case OperationType.SUBTRACT:\n result = self.stored_value - self.current_input\n # ... other operations (F3)\n \n self.current_input = result\n return result\n\nclass InputHandler:\n \"\"\"Manages user input flow (sequence diagram interactions)\"\"\"\n def __init__(self, calculator: Calculator, display: Display):\n self.calc = calculator\n self.display = display\n \n def handle_digit(self, digit: int): # F1\n \"\"\"Process digit press (sequence diagram step 1)\"\"\"\n self.calc.current_input = self.calc.current_input * 10 + digit\n self.display.update(str(self.calc.current_input))\n\n def handle_operator(self, op: OperationType): # F3\n \"\"\"Process +,-,*,/ press (sequence diagram step 2)\"\"\"\n self.calc.stored_value = self.calc.current_input\n self.calc.selected_op = op\n self.calc.current_input = 0\n"
}
}
}