Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
Zorin95670 committed Oct 22, 2024
1 parent d9f5d5d commit dbbc5f6
Show file tree
Hide file tree
Showing 11 changed files with 414 additions and 17 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dependencies {
implementation 'commons-lang:commons-lang:2.6'
implementation 'commons-beanutils:commons-beanutils:1.9.4'
implementation 'com.github.erosb:json-sKema:0.18.0'
implementation 'com.hubspot.jinjava:jinjava:2.7.0'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql:42.7.4'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
9 changes: 9 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

* Add api endpoints:
* For AI configuration actions on proxy:
* `GET /api/ai/proxy/configuration`, to send configuration on the proxy.
* `GET /api/ai/proxy/descriptions`, to get all configurations descriptions on the proxy.
* For AI configurations:
* `GET /api/ai/configurations`, to get all AI configurations.
* `POST /api/ai/configurations`, to get create a AI configuration.
* `GET /api/ai/configurations/[CONFIGURATION_ID]`, to get an AI configuration.
* `PUT /api/ai/configurations/[CONFIGURATION_ID]`, to update an AI configuration.
* `DELETE /api/ai/configurations/[CONFIGURATION_ID]`, to delete an AI configuration.
* For AI secrets:
* `GET /api/ai/secrets`, to get all AI secret keys.
* `POST /api/ai/secrets`, to create an AI secret.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import com.ditrit.letomodelizerapi.model.permission.EntityPermission;
import com.ditrit.letomodelizerapi.persistence.model.User;
import com.ditrit.letomodelizerapi.service.AIConfigurationService;
import com.ditrit.letomodelizerapi.service.AISecretService;
import com.ditrit.letomodelizerapi.service.AIService;
import com.ditrit.letomodelizerapi.service.UserPermissionService;
import com.ditrit.letomodelizerapi.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -50,14 +52,25 @@
public class AIConfigurationController implements DefaultController {

/**
* Service to manage ai configuration.
* Service to manage AI configuration.
*/
private final AIConfigurationService aiConfigurationService;

/**
* Service to manage AI secrets.
*/
private final AISecretService aiSecretService;

/**
* Service to manage AI configuration.
*/
private final AIService aiService;

/**
* Service to manage user.
*/
private final UserService userService;

/**
* Service to manage user permissions.
*/
Expand Down Expand Up @@ -143,6 +156,10 @@ public Response createConfiguration(final @Context HttpServletRequest request,
aiConfigurationRecord.key());
var aiConfiguration = aiConfigurationService.create(aiConfigurationRecord);

var configuration = aiSecretService.generateConfiguration(aiConfiguration);

aiService.sendConfiguration(configuration);

return Response.status(HttpStatus.CREATED.value())
.entity(new BeanMapper<>(AIConfigurationDTO.class).apply(aiConfiguration))
.build();
Expand Down Expand Up @@ -172,7 +189,11 @@ public Response updateConfiguration(final @Context HttpServletRequest request,
userPermissionService.checkPermission(user, "id", EntityPermission.AI_CONFIGURATION, ActionPermission.UPDATE);

log.info("[{}] Received PUT request to update configuration {}", user.getLogin(), id.toString());

var aiConfiguration = aiConfigurationService.update(id, aiConfigurationRecord);
var configuration = aiSecretService.generateConfiguration(aiConfiguration);

aiService.sendConfiguration(configuration);

return Response.status(HttpStatus.OK.value())
.entity(new BeanMapper<>(AIConfigurationDTO.class).apply(aiConfiguration))
Expand Down Expand Up @@ -200,6 +221,14 @@ public Response deleteConfiguration(final @Context HttpServletRequest request,
userPermissionService.checkPermission(user, "id", EntityPermission.AI_CONFIGURATION, ActionPermission.DELETE);

log.info("[{}] Received DELETE request to delete configuration {}", user.getLogin(), id);
var aiConfiguration = aiConfigurationService.findById(id);

aiConfiguration.setValue(null);

var configuration = aiSecretService.generateConfiguration(aiConfiguration);

aiService.sendConfiguration(configuration);

aiConfigurationService.delete(id);

return Response.noContent().build();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ditrit.letomodelizerapi.controller;


import com.ditrit.letomodelizerapi.config.Constants;
import com.ditrit.letomodelizerapi.controller.model.QueryFilter;
import com.ditrit.letomodelizerapi.model.BeanMapper;
import com.ditrit.letomodelizerapi.model.ai.AIConversationDTO;
Expand All @@ -12,6 +13,7 @@
import com.ditrit.letomodelizerapi.model.permission.ActionPermission;
import com.ditrit.letomodelizerapi.model.permission.EntityPermission;
import com.ditrit.letomodelizerapi.persistence.model.User;
import com.ditrit.letomodelizerapi.service.AISecretService;
import com.ditrit.letomodelizerapi.service.AIService;
import com.ditrit.letomodelizerapi.service.UserPermissionService;
import com.ditrit.letomodelizerapi.service.UserService;
Expand Down Expand Up @@ -69,6 +71,11 @@ public class AIController implements DefaultController {
*/
private AIService aiService;

/**
* Service to manage AI request.
*/
private AISecretService aiSecretService;

/**
* Handles a POST request to generate files with an Artificial Intelligence (AI) based on the provided
* request details.
Expand Down Expand Up @@ -287,4 +294,54 @@ public Response findAllMessages(final @Context HttpServletRequest request,

return Response.status(this.getStatus(resources)).entity(resources).build();
}

/**
* Sends the generated configuration to the AI proxy.
* <p>
* This method handles GET requests to the endpoint {@code /proxy/configuration}.
* It uses the {@code aiSecretService} to generate a configuration that is then sent to the AI proxy.
*
* @param request the {@link HttpServletRequest} containing the current HTTP request information, used to retrieve
* the session details.
* @return a {@link Response} with a 204 (No Content) status, indicating the configuration was successfully sent
* to the AI proxy.
*/
@GET
@Path("/proxy/configuration")
public Response sendConfigurationToProxy(final @Context HttpServletRequest request) {
HttpSession session = request.getSession();
log.info("[{}] Received GET request to send configuration to proxy",
session.getAttribute(Constants.DEFAULT_USER_PROPERTY));

var configuration = aiSecretService.generateConfiguration();

aiService.sendConfiguration(configuration);

return Response.noContent().build();
}

/**
* Retrieves configuration descriptions from the AI proxy.
* <p>
* This method handles GET requests to the endpoint {@code /proxy/descriptions}.
* If the user has permission, the method retrieves and returns the configuration descriptions from the AI proxy.
*
* @param request the {@link HttpServletRequest} containing the current HTTP request information, used to retrieve
* the session details and user information.
* @return a {@link Response} containing the configuration descriptions in the response body with a 200 (OK) status.
* If the user lacks permission, an appropriate error response will be returned.
*/
@GET
@Path("/proxy/descriptions")
public Response retrieveConfigurationDescriptions(final @Context HttpServletRequest request) {
HttpSession session = request.getSession();
User user = userService.getFromSession(session);
userPermissionService.checkPermission(user, "id", EntityPermission.AI_SECRET, ActionPermission.ACCESS);

log.info("[{}] Received GET request to get configuration descriptions from proxy", user.getLogin());

var descriptions = aiService.getConfigurationDescriptions();

return Response.ok(descriptions).build();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ditrit.letomodelizerapi.service;

import com.ditrit.letomodelizerapi.model.ai.AISecretRecord;
import com.ditrit.letomodelizerapi.persistence.model.AIConfiguration;
import com.ditrit.letomodelizerapi.persistence.model.AISecret;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand Down Expand Up @@ -54,4 +55,18 @@ public interface AISecretService {
* @param id the ID of the AISecret entity to delete.
*/
void delete(UUID id);

/**
* Retrieve all configurations, apply secrets in configuration values and return encrypted configuration for AI
* proxy.
* @return Encrypted configuration.
*/
byte[] generateConfiguration();

/**
* Apply secret in provided configuration value and return encrypted configuration for AI proxy.
* @param configuration Provided configuration.
* @return Encrypted configuration.
*/
byte[] generateConfiguration(AIConfiguration configuration);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
import com.ditrit.letomodelizerapi.model.ai.AISecretRecord;
import com.ditrit.letomodelizerapi.model.error.ApiException;
import com.ditrit.letomodelizerapi.model.error.ErrorType;
import com.ditrit.letomodelizerapi.persistence.model.AIConfiguration;
import com.ditrit.letomodelizerapi.persistence.model.AISecret;
import com.ditrit.letomodelizerapi.persistence.repository.AIConfigurationRepository;
import com.ditrit.letomodelizerapi.persistence.repository.AISecretRepository;
import com.ditrit.letomodelizerapi.persistence.specification.SpecificationHelper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.hubspot.jinjava.Jinjava;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -24,6 +28,8 @@
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

Expand Down Expand Up @@ -58,11 +64,23 @@ public class AISecretServiceImpl implements AISecretService {
*/
private final AISecretRepository aiSecretRepository;

/**
* The AIConfigurationRepository instance is injected by Spring's dependency injection mechanism.
* This repository is used for performing database operations related to AIConfiguration entities,
* such as querying, saving, and updating access control data.
*/
private final AIConfigurationRepository aiConfigurationRepository;

/**
* The key to encrypt or decrypt secret value.
*/
private final String secretEncryptionKey;

/**
* The key to encrypt or decrypt configuration.
*/
private final String configurationEncryptionKey;

/**
* Size of IV.
*/
Expand All @@ -81,13 +99,19 @@ public class AISecretServiceImpl implements AISecretService {
* Constructor for AISecretServiceImpl.
*
* @param aiSecretRepository Repository to manage AISecret.
* @param aiConfigurationRepository Repository to manage AIConfiguration.
* @param secretEncryptionKey the key to encrypt or decrypt secret value.
* @param configurationEncryptionKey the key to encrypt or decrypt configuration.
*/
@Autowired
public AISecretServiceImpl(final AISecretRepository aiSecretRepository,
@Value("${ai.secrets.encryption.key}") final String secretEncryptionKey) {
final AIConfigurationRepository aiConfigurationRepository,
@Value("${ai.secrets.encryption.key}") final String secretEncryptionKey,
@Value("${ai.configuration.encryption.key}") final String configurationEncryptionKey) {
this.aiSecretRepository = aiSecretRepository;
this.aiConfigurationRepository = aiConfigurationRepository;
this.secretEncryptionKey = secretEncryptionKey;
this.configurationEncryptionKey = configurationEncryptionKey;
}

/**
Expand All @@ -96,10 +120,11 @@ public AISecretServiceImpl(final AISecretRepository aiSecretRepository,
* and uses AES encryption to secure the plain text. The resulting byte array contains both the IV and the
* encrypted text.
*
* @param key key to encrypt the text.
* @param plainText the plain text to be encrypted.
* @return a byte array containing the IV and the encrypted text.
*/
private byte[] encrypt(final String plainText) {
private byte[] encrypt(final String key, final String plainText) {
try {
byte[] clean = plainText.getBytes();

Expand All @@ -111,7 +136,7 @@ private byte[] encrypt(final String plainText) {

// Hashing key
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.update(secretEncryptionKey.getBytes(StandardCharsets.UTF_8));
digest.update(key.getBytes(StandardCharsets.UTF_8));
byte[] keyBytes = new byte[KEY_SIZE];
System.arraycopy(digest.digest(), 0, keyBytes, 0, keyBytes.length);
SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "AES");
Expand All @@ -137,10 +162,11 @@ private byte[] encrypt(final String plainText) {
* This method extracts the IV, hashes the provided secret key using SHA-256, and uses AES decryption
* to convert the encrypted bytes back into plain text.
*
* @param key key to decrypt the text.
* @param encryptedIvTextBytes a byte array containing the IV and the encrypted text.
* @return the decrypted plain text.
*/
private String decrypt(final byte[] encryptedIvTextBytes) {
private String decrypt(final String key, final byte[] encryptedIvTextBytes) {
try {
// Extract IV
byte[] iv = new byte[IV_SIZE];
Expand All @@ -155,7 +181,7 @@ private String decrypt(final byte[] encryptedIvTextBytes) {
// Hash key
byte[] keyBytes = new byte[KEY_SIZE];
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(secretEncryptionKey.getBytes(StandardCharsets.UTF_8));
md.update(key.getBytes(StandardCharsets.UTF_8));
System.arraycopy(md.digest(), 0, keyBytes, 0, keyBytes.length);
SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "AES");

Expand Down Expand Up @@ -227,7 +253,7 @@ public AISecret create(final AISecretRecord aiSecretRecord) {

var aiSecret = new AISecret();
aiSecret.setKey(aiSecretRecord.key());
aiSecret.setValue(encrypt(aiSecretRecord.value()));
aiSecret.setValue(encrypt(secretEncryptionKey, aiSecretRecord.value()));

aiSecret = aiSecretRepository.save(aiSecret);

Expand All @@ -241,7 +267,7 @@ public AISecret create(final AISecretRecord aiSecretRecord) {
public AISecret update(final UUID id, final AISecretRecord aiSecretRecord) {
AISecret aiSecret = findById(id);

aiSecret.setValue(encrypt(aiSecretRecord.value()));
aiSecret.setValue(encrypt(secretEncryptionKey, aiSecretRecord.value()));

aiSecret = aiSecretRepository.save(aiSecret);

Expand All @@ -257,4 +283,57 @@ public void delete(final UUID id) {

aiSecretRepository.delete(aiSecret);
}

@Override
public byte[] generateConfiguration() {
return generateConfiguration(aiConfigurationRepository.findAll());
}

@Override
public byte[] generateConfiguration(final AIConfiguration configuration) {
return generateConfiguration(List.of(configuration));
}

/**
* Generates an encrypted configuration file using a list of AI configurations.
* <p>
* This method uses the Jinjava templating engine to process configuration values and replace
* placeholders with decrypted secret values. It collects all secrets from the {@code aiSecretRepository},
* decrypts them, and stores them in a context map. Then, it iterates over the provided configurations,
* applies the secret replacements, and constructs a JSON object containing the processed configuration values.
* Finally, the generated configuration is encrypted before being returned as a byte array.
*
* @param configurations a list of {@link AIConfiguration} objects that define the keys, values, and optional
* handlers for the configuration. The values may contain placeholders for secrets that will
* be replaced using Jinjava.
* @return a byte array representing the encrypted configuration, ready to be sent to the AI proxy.
* @throws ApiException if encryption fails or any other unexpected error occurs during processing.
*/
public byte[] generateConfiguration(final List<AIConfiguration> configurations) {
var jinjava = new Jinjava();
var secrets = new HashMap<String, String>();
var context = new HashMap<String, Object>();
var json = JsonNodeFactory.instance.objectNode();

aiSecretRepository.findAll().forEach(secret -> secrets.put(secret.getKey(),
decrypt(secretEncryptionKey, secret.getValue())));

context.put("secrets", secrets);

configurations.forEach(configuration -> {
String key = null;

if (configuration.getHandler() == null) {
key = configuration.getKey();
} else {
key = String.format("%s.%s", configuration.getHandler(), configuration.getKey());
}

json.put(key, jinjava.render(configuration.getValue(), context));
});

System.out.println(json.toPrettyString());

return encrypt(configurationEncryptionKey, json.toString());
}
}
Loading

0 comments on commit dbbc5f6

Please sign in to comment.