Skip to content

Commit

Permalink
Added new entity MailStatementProfile to store setup of fetching stat…
Browse files Browse the repository at this point in the history
…ements from mail

Updated Manage Transaction Dashboard to show mail statement profile
Adapted action 'Fetch from Mail' to select MailStatementProfile instead of providing all the details
Minor changes in TransactionReaderCallback to consider sourceId while looking for existing transaction
  • Loading branch information
Jayeshecs committed Jan 12, 2020
1 parent 42cd83a commit bad5c2d
Show file tree
Hide file tree
Showing 13 changed files with 681 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- increase length of password column from 40 to 1024
ALTER TABLE "statements"."MailConnectionProfile" ALTER COLUMN "password" VARCHAR(1024);
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

CREATE TABLE "statements"."MailStatementProfile"("id" BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 0) NOT NULL,"fileNamePattern" VARCHAR(128) NOT NULL, "fromAddress" VARCHAR(255), "subjectWords" VARCHAR(255) NOT NULL,"folderName" VARCHAR(255) NOT NULL,"readerId" BIGINT NOT NULL, "sourceId" BIGINT NOT NULL, "mailConnectionProfileId" BIGINT NOT NULL,"description" VARCHAR(4000),"name" VARCHAR(40) NOT NULL,"version" TIMESTAMP NOT NULL,CONSTRAINT "MailStatementProfile_PK" PRIMARY KEY("id"),CONSTRAINT "MailStatementProfile_name_UNQ" UNIQUE("name"));
ALTER TABLE "statements"."MailStatementProfile" ALTER COLUMN "id" RESTART WITH 0;

CREATE INDEX "MailStatementProfile_N51" ON "statements"."MailStatementProfile"("sourceId");
CREATE INDEX "MailStatementProfile_N50" ON "statements"."MailStatementProfile"("readerId");
CREATE INDEX "MailStatementProfile_N49" ON "statements"."MailStatementProfile"("mailConnectionProfileId");

ALTER TABLE "statements"."MailStatementProfile" ADD CONSTRAINT "MailStatementProfile_FK3" FOREIGN KEY("mailConnectionProfileId") REFERENCES "statements"."MailConnectionProfile"("id");
ALTER TABLE "statements"."MailStatementProfile" ADD CONSTRAINT "MailStatementProfile_FK2" FOREIGN KEY("readerId") REFERENCES "statements"."StatementReader"("id");
ALTER TABLE "statements"."MailStatementProfile" ADD CONSTRAINT "MailStatementProfile_FK1" FOREIGN KEY("sourceId") REFERENCES "statements"."StatementSource"("id");
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/**
*
*/
package domainapp.modules.base.service;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.inject.Inject;

import org.apache.isis.applib.annotation.DomainService;
import org.apache.isis.applib.annotation.NatureOfService;
import org.apache.isis.applib.annotation.Programmatic;
import org.apache.isis.applib.services.config.ConfigurationService;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

/**
* @author jayeshecs
* Ref: https://stackoverflow.com/questions/24338108/java-encrypt-string-with-existing-public-key-file
*/
@DomainService(
nature = NatureOfService.DOMAIN
)
public class EncryptionService {

private static final String ALGO_RSA = "RSA";
private PublicKey publicKey;
private PrivateKey privateKey;

public EncryptionService() {
// DO NOTHING
}

@PostConstruct
public void init() {
String keyLocation = configurationService.getProperty("base.kl", System.getProperty("user.home") + "/.ssh");
File keyLocationFile = new File(keyLocation);
if (!keyLocationFile.exists()) {
throw new IllegalArgumentException("Location '" + keyLocation + "' does not exists");
}
if (!keyLocationFile.isDirectory()) {
throw new IllegalArgumentException("Location '" + keyLocation + "' must be a valid directory");
}
if (!keyLocationFile.canWrite() || !keyLocationFile.canExecute() || !keyLocationFile.canRead()) {
throw new IllegalArgumentException("Location '" + keyLocation + "' must have read/write/execute permission");
}
File file = new File(keyLocationFile, ".dmbse"); // key file name

if (!file.exists()) {
KeyPair keyPair = generateKeyPair();
saveGeneratedKeys(file, keyPair);
}

List<String> encodedKeys = loadEncodedKeys(file);
publicKey = loadPublicKeyFromEncodedKeys(encodedKeys);
privateKey = loadPrivateKeyFromEncodedKeys(encodedKeys);
}

@Programmatic
private PublicKey loadPublicKeyFromEncodedKeys(List<String> encodedKeys) {
try {
/* Generate public key. */
X509EncodedKeySpec ks = new X509EncodedKeySpec(Base64.getDecoder().decode(encodedKeys.get(0).getBytes()));
KeyFactory kf = KeyFactory.getInstance(ALGO_RSA);
PublicKey pub = kf.generatePublic(ks);
return pub;
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new IllegalStateException("Error occurred while loading public key from encoded keys", e);
}
}

@Programmatic
private PrivateKey loadPrivateKeyFromEncodedKeys(List<String> encodedKeys) {
try {
/* Generate private key. */
PKCS8EncodedKeySpec ks = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(encodedKeys.get(1).getBytes()));
KeyFactory kf = KeyFactory.getInstance(ALGO_RSA);
PrivateKey privateKey = kf.generatePrivate(ks);
return privateKey;
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new IllegalStateException("Error occurred while loading private key from encoded keys", e);
}
}

@Programmatic
private List<String> loadEncodedKeys(File file) {
try {
byte[] bytes = Files.readAllBytes(file.toPath());
byte[] decodedBytes = Base64.getDecoder().decode(bytes);
Gson gson = new GsonBuilder().create();
@SuppressWarnings("unchecked")
List<String> encodedKeys = gson.fromJson(new String(decodedBytes), List.class);
return encodedKeys;
} catch (IOException e) {
throw new IllegalStateException("Error occurred while loading encoded keys from file - " + file);
}
}

/**
* @param file
* @param publicKey
* @param privateKey
*/
@Programmatic
private void saveGeneratedKeys(File file, KeyPair keyPair) {
// encode key pair
String encodedKeyPair = encodeKeyPair(keyPair);
// save encodedKeyPair
saveEncodedKeyPair(file, encodedKeyPair);
}

/**
* @param keyPair
* @return
*/
@Programmatic
private String encodeKeyPair(KeyPair keyPair) {
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
// get encoded keys
String encodedPublicKey = new String(Base64.getEncoder().encode(publicKey.getEncoded()));
String encodedPrivateKey = new String(Base64.getEncoder().encode(privateKey.getEncoded()));
// prepare list of encoded keys
List<String> keys = new ArrayList<String>();
keys.add(encodedPublicKey);
keys.add(encodedPrivateKey);
// convert keys list to JSON
Gson gson = new GsonBuilder().create();
String json = gson.toJson(keys);
// decode json string using Base64
String encodedKeyPair = Base64.getEncoder().encodeToString(json.getBytes());
return encodedKeyPair;
}

/**
* @param file
* @param encodedKeyPair
*/
@Programmatic
private void saveEncodedKeyPair(File file, String encodedKeyPair) {
try {
Files.write(file.toPath(), encodedKeyPair.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
} catch (IOException e) {
throw new IllegalStateException("Error occurred while saving generated key pair");
}
}

/**
* @return
*/
@Programmatic
private KeyPair generateKeyPair() {
try {
KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance(ALGO_RSA);
keyGenerator.initialize(2048);
KeyPair keyPair = keyGenerator.generateKeyPair();
return keyPair;
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Error occurred while generating key pair");
}
}

@PreDestroy
public void destroy() {
privateKey = null;
publicKey = null;
}

@Programmatic
public String encrypt(String content) {
try {
Cipher instance = Cipher.getInstance(ALGO_RSA);
instance.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encrypted = instance.doFinal(content.getBytes());
byte[] encoded = Base64.getEncoder().encode(encrypted);
return new String(encoded);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException | InvalidKeyException e) {
throw new IllegalStateException("Error occurred while encrypting given content using public key", e);
}
}

public String decrypt(String encryptedContent) {
try {
Cipher instance = Cipher.getInstance(ALGO_RSA);
instance.init(Cipher.DECRYPT_MODE, privateKey);
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedContent.getBytes());
byte[] decrypted = instance.doFinal(encryptedBytes);
return new String(decrypted);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException | InvalidKeyException e) {
throw new IllegalStateException("Error occurred while decrypting given content using private key", e);
}
}

@Inject
protected ConfigurationService configurationService;
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public void read(IStatementReaderContext context, File inputFile, Properties con
log.error(String.format("Parsing failed for record : %s", record));
log.error(String.format("Error: %s", e.getMessage() == null ? e.getClass().getName() : e.getMessage()));
context.addErrorCount(1);
e.printStackTrace();
e.printStackTrace(); // TODO: [JP] Find better way to capture stack trace efficiently! No point in logging same trace for all records.
continue ;
}
batch.add(transaction);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
import org.apache.isis.applib.annotation.Editing;
import org.apache.isis.applib.annotation.MemberOrder;
import org.apache.isis.applib.annotation.Property;
import org.apache.isis.applib.annotation.PropertyLayout;
import org.apache.isis.applib.annotation.Publishing;
import org.apache.isis.applib.annotation.Title;
import org.apache.isis.applib.annotation.Where;
import org.apache.isis.schema.utils.jaxbadapters.PersistentEntityAdapter;

import domainapp.modules.addon.dom.Addon;
import domainapp.modules.base.entity.NamedQueryConstants;
import domainapp.modules.base.entity.WithDescription;
import domainapp.modules.base.entity.WithName;
Expand All @@ -41,11 +42,16 @@
name = NamedQueryConstants.QUERY_ALL,
value = "SELECT "
+ "FROM domainapp.modules.rdr.dom.MailConnectionProfile "),
@javax.jdo.annotations.Query(
name = NamedQueryConstants.QUERY_FIND_BY_NAME,
value = "SELECT "
+ "FROM domainapp.modules.rdr.dom.MailConnectionProfile "
+ "WHERE name.indexOf(:name) >= 0 ")
@javax.jdo.annotations.Query(
name = NamedQueryConstants.QUERY_FIND_BY_NAME,
value = "SELECT "
+ "FROM domainapp.modules.rdr.dom.MailConnectionProfile "
+ "WHERE name.indexOf(:name) >= 0 "),
@javax.jdo.annotations.Query(
name = NamedQueryConstants.QUERY_FIND_BY_ID,
value = "SELECT "
+ "FROM domainapp.modules.rdr.dom.MailConnectionProfile "
+ "WHERE id = 0 ")
})
@javax.jdo.annotations.Unique(name="MailConnectionProfile_name_UNQ", members = {"name"})
@DomainObject(
Expand Down Expand Up @@ -102,12 +108,13 @@ public class MailConnectionProfile implements Comparable<MailConnectionProfile>,
@MemberOrder(sequence = "5")
private String username;

@javax.jdo.annotations.Column(allowsNull = "false", length = 40)
@javax.jdo.annotations.Column(allowsNull = "false", length = 1024)
@Property(
editing = Editing.ENABLED,
editing = Editing.DISABLED,
command = CommandReification.ENABLED,
publishing = Publishing.ENABLED
)
@PropertyLayout(hidden = Where.EVERYWHERE)
@Getter @Setter
@MemberOrder(sequence = "6")
private String password;
Expand Down Expand Up @@ -155,7 +162,25 @@ public MailConnectionProfile(final String name, final String description, final
setDebug(debug);
}

public static class CreateEvent extends ActionDomainEvent<MailConnectionProfile> {
public MailConnectionProfile(MailConnectionProfile mailConnectionProfile) {
this(
mailConnectionProfile.getName(),
mailConnectionProfile.getDescription(),
mailConnectionProfile.getHostname(),
mailConnectionProfile.getPort(),
mailConnectionProfile.getUsername(),
mailConnectionProfile.getPassword(),
mailConnectionProfile.getSecure(),
mailConnectionProfile.getStarttls(),
mailConnectionProfile.getDebug()
);
}

public static class CreateEvent extends ActionDomainEvent<MailConnectionProfile> {
private static final long serialVersionUID = 1L;
}

public static class UpdateEvent extends ActionDomainEvent<MailConnectionProfile> {
private static final long serialVersionUID = 1L;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,21 @@
*/
package domainapp.modules.rdr.service;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import javax.mail.Message;
import javax.inject.Inject;

import org.apache.isis.applib.annotation.DomainService;
import org.apache.isis.applib.annotation.NatureOfService;
import org.apache.isis.applib.annotation.Programmatic;

import domainapp.modules.base.entity.NamedQueryConstants;
import domainapp.modules.base.service.AbstractService;
import domainapp.modules.base.service.EncryptionService;
import domainapp.modules.rdr.dom.MailConnectionProfile;
import lombok.extern.slf4j.Slf4j;

/**
* @author jayeshecs
Expand All @@ -23,6 +27,7 @@
nature = NatureOfService.DOMAIN,
repositoryFor = MailConnectionProfile.class
)
@Slf4j
public class MailConnectionProfileService extends AbstractService<MailConnectionProfile> {

public MailConnectionProfileService() {
Expand All @@ -37,14 +42,42 @@ public List<MailConnectionProfile> all() {

@Programmatic
public MailConnectionProfile create(String name, String description, String hostname, String port, String username, String password, Boolean secure, Boolean starttls, Boolean debug) {
password = encryptionService.encrypt(password);
log.info("Encrypted password = " + password);
MailConnectionProfile newMailConnectionProfile = MailConnectionProfile.builder().name(name).description(description).hostname(hostname).port(port).username(username).password(password).secure(secure).starttls(starttls).debug(debug).build();
MailConnectionProfile mailConnectionProfile = repositoryService.persistAndFlush(newMailConnectionProfile);
return mailConnectionProfile;
}

@Programmatic
public void changePassword(MailConnectionProfile mailConnectionProfile, String password) {
password = encryptionService.encrypt(password);
log.info("Encrypted password = " + password);
List<MailConnectionProfile> list = search(NamedQueryConstants.QUERY_FIND_BY_NAME, "name", mailConnectionProfile.getName());
MailConnectionProfile mailConnectionProfilePersisted = null;
if (list != null && !list.isEmpty()) {
Optional<MailConnectionProfile> first = list.stream().filter(mcp -> {
return (mcp.getName().equals(mailConnectionProfile.getName()));
}).findFirst();
if (first.isPresent()) {
mailConnectionProfilePersisted = first.get();
}
}
if (mailConnectionProfilePersisted == null) {
throw new IllegalArgumentException("No mail connection profile found with name - " + mailConnectionProfile.getName());
}
mailConnectionProfilePersisted.setPassword(password);
save(Arrays.asList(mailConnectionProfilePersisted));
}

@Programmatic
public MailConnection getMailConnection(MailConnectionProfile mailConnectionProfile) {
MailConnection connection = new MailConnection(mailConnectionProfile);
MailConnectionProfile mailConnectionProfileCopy = new MailConnectionProfile(mailConnectionProfile);
mailConnectionProfileCopy.setPassword(encryptionService.decrypt(mailConnectionProfile.getPassword()));
MailConnection connection = new MailConnection(mailConnectionProfileCopy);
return connection;
}

@Inject
protected EncryptionService encryptionService;
}

0 comments on commit bad5c2d

Please sign in to comment.