From 4c059c5859d2b597626c4b434b6a4027ac535e7c Mon Sep 17 00:00:00 2001
From: TINCU Sebastian-Ion <Sebastian-Ion.TINCU@ext.ec.europa.eu>
Date: Thu, 18 Oct 2018 03:54:26 +0200
Subject: [PATCH] EDELIVERY-3687 SMP UI Add/Edit user

Rename validUntil to validTo as present in the back-end.
Add support for fingerprints when reading the uploaded certificate.
Add Jackson 2 support for JSR310 dates and configure a Jackson 2
HttpMessageConverter.
---
 smp-angular/src/app/smp.constants.ts          |  2 +
 .../src/app/user/certificate-ro.model.ts      |  2 +-
 .../src/app/user/certificate.service.ts       |  3 +-
 .../user-details-dialog.component.css         |  2 +-
 .../user-details-dialog.component.html        |  6 +--
 .../user-details-dialog.component.ts          | 28 ++++++--------
 smp-parent-pom/pom.xml                        |  7 +++-
 smp-server-library/pom.xml                    |  4 ++
 .../edelivery/smp/data/ui/CertificateRO.java  | 22 +++++------
 .../edelivery/smp/exceptions/ErrorCode.java   |  3 +-
 .../smp/services/ui/UIUserService.java        | 37 +++++++++++++++----
 .../edelivery/smp/config/SmpWebAppConfig.java | 37 ++++++++++++++++++-
 12 files changed, 107 insertions(+), 46 deletions(-)

diff --git a/smp-angular/src/app/smp.constants.ts b/smp-angular/src/app/smp.constants.ts
index f78f13a5f..1a036d8d5 100644
--- a/smp-angular/src/app/smp.constants.ts
+++ b/smp-angular/src/app/smp.constants.ts
@@ -4,4 +4,6 @@ export class SmpConstants {
   public static readonly REST_USER = 'rest/user';
   public static readonly REST_SEARCH = 'rest/search';
   public static readonly REST_EDIT = 'rest/servicegroup';
+  public static readonly REST_CERTIFICATE = `${SmpConstants.REST_USER}/certdata`;
+
 }
diff --git a/smp-angular/src/app/user/certificate-ro.model.ts b/smp-angular/src/app/user/certificate-ro.model.ts
index bb66f1de2..14b2705c4 100644
--- a/smp-angular/src/app/user/certificate-ro.model.ts
+++ b/smp-angular/src/app/user/certificate-ro.model.ts
@@ -2,7 +2,7 @@ export interface CertificateRo {
   certificateId: string;
   subject: string;
   validFrom: Date;
-  validUntil: Date;
+  validTo: Date;
   issuer: string;
   serialNumber: string;
   fingerprints: string;
diff --git a/smp-angular/src/app/user/certificate.service.ts b/smp-angular/src/app/user/certificate.service.ts
index f758ca4b0..7c969c180 100644
--- a/smp-angular/src/app/user/certificate.service.ts
+++ b/smp-angular/src/app/user/certificate.service.ts
@@ -2,6 +2,7 @@ import {Injectable} from '@angular/core';
 import {Observable} from 'rxjs';
 import {CertificateRo} from './certificate-ro.model';
 import {HttpClient} from '@angular/common/http';
+import {SmpConstants} from "../smp.constants";
 
 @Injectable()
 export class CertificateService {
@@ -9,6 +10,6 @@ export class CertificateService {
   constructor(private http: HttpClient) {}
 
   uploadCertificate$(payload): Observable<CertificateRo> {
-    return this.http.post<CertificateRo>('rest/user/certdata', payload);
+    return this.http.post<CertificateRo>(SmpConstants.REST_CERTIFICATE, payload);
   }
 }
diff --git a/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.css b/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.css
index 4530b75dc..3a1fb9487 100644
--- a/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.css
+++ b/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.css
@@ -11,7 +11,7 @@
   width: 100%;
 }
 
-.certificate-valid-from, .certificate-valid-until {
+.certificate-valid-from, .certificate-valid-to {
   width: 40%;
 }
 
diff --git a/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.html b/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.html
index 96543a7b1..ec24fc45b 100644
--- a/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.html
+++ b/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.html
@@ -54,8 +54,8 @@
         <mat-form-field class="certificate-valid-from">
           <input matInput placeholder="Valid From" [formControl]="userForm.controls['validFrom']">
         </mat-form-field>
-        <mat-form-field class="certificate-valid-until">
-          <input matInput placeholder="Valid To" [formControl]="userForm.controls['validUntil']">
+        <mat-form-field class="certificate-valid-to">
+          <input matInput placeholder="Valid To" [formControl]="userForm.controls['validTo']">
         </mat-form-field>
         <mat-form-field class="certificate-issuer">
           <input matInput placeholder="Issuer" [formControl]="userForm.controls['issuer']">
@@ -65,7 +65,7 @@
         </mat-form-field>
 
         <label class="custom-file-upload">
-          <input #fileInput type="file" id="custom-file-upload" accept=".cer" (change)="uploadCertificate()" [disabled]="!userForm.controls['certificateToggle']?.value">
+          <input #fileInput type="file" id="custom-file-upload" accept=".cer" (change)="uploadCertificate($event)" [disabled]="!userForm.controls['certificateToggle']?.value">
           <button mat-flat-button color="primary" (click)="fileInput.click()" [disabled]="!userForm.controls['certificateToggle']?.value">Import</button>
         </label>
       </div>
diff --git a/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.ts b/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.ts
index b66d87337..173f05fa4 100644
--- a/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.ts
+++ b/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.ts
@@ -1,5 +1,5 @@
 import {Component, Inject, ViewChild} from '@angular/core';
-import {MAT_DIALOG_DATA, MatDialogRef, MatSlideToggle, MatSlideToggleChange} from '@angular/material';
+import {MAT_DIALOG_DATA, MatDialogRef, MatSlideToggleChange} from '@angular/material';
 import {FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators} from '@angular/forms';
 import {UserService} from '../user.service';
 import {Role} from '../../security/role.model';
@@ -52,11 +52,11 @@ export class UserDetailsDialogComponent {
     const certificateToggle = control.get('certificateToggle');
     const subject = control.get('subject');
     const validFrom = control.get('validFrom');
-    const validUntil = control.get('validUntil');
+    const validTo = control.get('validTo');
     const issuer = control.get('issuer');
     const fingerprints = control.get('fingerprints');
-    return certificateToggle && subject && validFrom && validUntil && issuer && fingerprints
-        && certificateToggle.value && !(subject.value && validFrom.value && validUntil.value && issuer.value && fingerprints.value) ? { certificateDetailsRequired: true} : null;
+    return certificateToggle && subject && validFrom && validTo && issuer && fingerprints
+        && certificateToggle.value && !(subject.value && validFrom.value && validTo.value && issuer.value && fingerprints.value) ? { certificateDetailsRequired: true} : null;
   };
 
   constructor(private dialogRef: MatDialogRef<UserDetailsDialogComponent>,
@@ -77,7 +77,7 @@ export class UserDetailsDialogComponent {
         certificate: {
           subject: data.row.subject,
           validFrom: data.row.validFrom,
-          validUntil: data.row.validUntil,
+          validTo: data.row.validTo,
           issuer: data.row.issuer,
           fingerprints: data.row.fingerprints,
         }
@@ -103,7 +103,7 @@ export class UserDetailsDialogComponent {
       'certificateToggle': new FormControl(user && user.certificate && !!user.certificate.subject),
       'subject': new FormControl({ value: user.certificate.subject, disabled: true }, Validators.required),
       'validFrom': new FormControl({ value: user.certificate.validFrom, disabled: true }, Validators.required),
-      'validUntil': new FormControl({ value: user.certificate.validUntil, disabled: true }, Validators.required),
+      'validTo': new FormControl({ value: user.certificate.validTo, disabled: true }, Validators.required),
       'issuer': new FormControl({ value: user.certificate.issuer, disabled: true }, Validators.required),
       'fingerprints': new FormControl({ value: user.certificate.fingerprints, disabled: true }, Validators.required),
     }, {
@@ -122,22 +122,16 @@ export class UserDetailsDialogComponent {
     this.dialogRef.close(true);
   }
 
-  uploadCertificate() {
-    const fi = this.fileInput.nativeElement;
-    const file = fi.files[0];
+  uploadCertificate(event) {
+    const file = event.target.files[0];
 
     const reader = new FileReader();
     reader.onload = (e) => {
-      const arrayBuffer = reader.result;
-      const array = new Uint8Array(arrayBuffer);
-      const binaryString = String.fromCharCode.apply(null, array);
-
-      this.certificateService.uploadCertificate$({content: binaryString})
-        .subscribe((res: CertificateRo) => {
+      this.certificateService.uploadCertificate$(reader.result).subscribe((res: CertificateRo) => {
             this.userForm.patchValue({
               'subject': res.subject,
               'validFrom': this.datePipe.transform(res.validFrom.toString(), this.dateFormat),
-              'validUntil': this.datePipe.transform(res.validUntil.toString(), this.dateFormat),
+              'validTo': this.datePipe.transform(res.validTo.toString(), this.dateFormat),
               'issuer': res.issuer,
               'fingerprints': res.fingerprints
             });
@@ -151,7 +145,7 @@ export class UserDetailsDialogComponent {
       this.alertService.exception('Error reading certificate file ' + file.name, err);
     };
 
-    reader.readAsArrayBuffer(file);
+    reader.readAsBinaryString(file);
   }
 
   onUserToggleChanged({checked}: MatSlideToggleChange) {
diff --git a/smp-parent-pom/pom.xml b/smp-parent-pom/pom.xml
index 63b7d0db3..3b6121fb4 100644
--- a/smp-parent-pom/pom.xml
+++ b/smp-parent-pom/pom.xml
@@ -451,6 +451,11 @@
 				<artifactId>jackson-annotations</artifactId>
 				<version>${jackson.version}</version>
 			</dependency>
+			<dependency>
+				<groupId>com.fasterxml.jackson.datatype</groupId>
+				<artifactId>jackson-datatype-jsr310</artifactId>
+				<version>${jackson.version}</version>
+			</dependency>
 			<!-- End Jackson-->
 			<dependency>
 				<groupId>org.apache.commons</groupId>
@@ -676,7 +681,7 @@
 				<groupId>org.apache.maven.plugins</groupId>
 				<artifactId>maven-compiler-plugin</artifactId>
 				<configuration>
-					<!-- fork needs to be false to compile JAXB resources. Otherwise the 
+					<!-- fork needs to be false to compile JAXB resources. Otherwise the
 						problem with the forward slash in package-info.java on Windows occurs! -->
 					<fork>false</fork>
 					<source>1.8</source>
diff --git a/smp-server-library/pom.xml b/smp-server-library/pom.xml
index bb34c4f79..f5702e941 100644
--- a/smp-server-library/pom.xml
+++ b/smp-server-library/pom.xml
@@ -112,6 +112,10 @@
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-annotations</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.hibernate</groupId>
             <artifactId>hibernate-core</artifactId>
diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/CertificateRO.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/CertificateRO.java
index 37f7a40ac..cd967b91f 100644
--- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/CertificateRO.java
+++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/CertificateRO.java
@@ -1,36 +1,24 @@
 package eu.europa.ec.edelivery.smp.data.ui;
 
-
-
-
-
-import eu.europa.ec.edelivery.smp.data.model.CommonColumnsLengths;
-
-import javax.persistence.Column;
-import javax.persistence.Id;
-import java.math.BigInteger;
 import java.time.LocalDateTime;
 
-
 /**
  * @author Joze Rihtarsic
  * @since 4.1
  */
 public class CertificateRO extends BaseRO {
 
-
-
     private static final long serialVersionUID = -4971552086560325302L;
 
     private String certificateId;
     private String subject;
     private String issuer;
     private String serialNumber;
+    private String fingerprints;
     private LocalDateTime validFrom;
     private LocalDateTime validTo;
 
     public CertificateRO(){
-
     }
 
     public static long getSerialVersionUID() {
@@ -69,6 +57,14 @@ public class CertificateRO extends BaseRO {
         this.serialNumber = serialNumber;
     }
 
+    public String getFingerprints() {
+        return fingerprints;
+    }
+
+    public void setFingerprints(String fingerprints) {
+        this.fingerprints = fingerprints;
+    }
+
     public LocalDateTime getValidFrom() {
         return validFrom;
     }
diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/exceptions/ErrorCode.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/exceptions/ErrorCode.java
index 9872cf97e..a279e25d2 100644
--- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/exceptions/ErrorCode.java
+++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/exceptions/ErrorCode.java
@@ -23,7 +23,8 @@ public enum ErrorCode {
     ILLEGAL_STATE_CERT_ID_MULTIPLE_ENTRY(504,"SMP:122",ErrorBusinessCode.TECHNICAL,"More than one certificate entry (cert. id: '%s') is defined in database!"),
     USER_NOT_EXISTS(400,"SMP:123",ErrorBusinessCode.USER_NOT_FOUND,"User not exists or wrong password!"), // OWASP recommendation\
     USER_IS_NOT_OWNER(400,"SMP:124",ErrorBusinessCode.UNAUTHORIZED,"User %s is not owner of service group (part. id: %s, part. sch.: '%s')!"), // OWASP recommendation
-
+    INVALID_CERTIFICATE_MESSAGE_DIGEST(500, "SMP:125", ErrorBusinessCode.TECHNICAL, "Could not initialize MessageDigest"),
+    INVALID_CERTIFICATE_ENCODING(500, "SMP:126", ErrorBusinessCode.TECHNICAL, "Could not encode certificate"),
 
     // service group error
     ILLEGAL_STATE_SG_MULTIPLE_ENTRY (500,"SMP:130",ErrorBusinessCode.TECHNICAL,"More than one service group ( part. id: %s, part. sch.: '%s') is defined in database!"),
diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ui/UIUserService.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ui/UIUserService.java
index 480b2a003..a16c8d131 100644
--- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ui/UIUserService.java
+++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ui/UIUserService.java
@@ -5,16 +5,15 @@ import eu.europa.ec.edelivery.smp.BCryptPasswordHash;
 import eu.europa.ec.edelivery.smp.data.dao.BaseDao;
 import eu.europa.ec.edelivery.smp.data.dao.UserDao;
 import eu.europa.ec.edelivery.smp.data.model.DBCertificate;
-import eu.europa.ec.edelivery.smp.data.model.DBDomain;
 import eu.europa.ec.edelivery.smp.data.model.DBUser;
 import eu.europa.ec.edelivery.smp.data.ui.CertificateRO;
-import eu.europa.ec.edelivery.smp.data.ui.DomainRO;
 import eu.europa.ec.edelivery.smp.data.ui.ServiceResult;
 import eu.europa.ec.edelivery.smp.data.ui.UserRO;
 import eu.europa.ec.edelivery.smp.data.ui.enums.EntityROStatus;
+import eu.europa.ec.edelivery.smp.exceptions.ErrorCode;
+import eu.europa.ec.edelivery.smp.exceptions.SMPRuntimeException;
 import eu.europa.ec.edelivery.smp.logging.SMPLogger;
 import eu.europa.ec.edelivery.smp.logging.SMPLoggerFactory;
-import eu.europa.ec.edelivery.smp.services.ServiceGroupService;
 import org.apache.commons.beanutils.BeanUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -22,15 +21,16 @@ import org.springframework.security.crypto.bcrypt.BCrypt;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import javax.ejb.Local;
+import javax.xml.bind.DatatypeConverter;
 import java.io.ByteArrayInputStream;
-import java.io.FileInputStream;
 import java.lang.reflect.InvocationTargetException;
 import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateEncodingException;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
-import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.util.List;
@@ -126,21 +126,44 @@ public class UIUserService extends UIServiceBase<DBUser, UserRO> {
             String subject = cert.getSubjectDN().getName();
             String issuer = cert.getIssuerDN().getName();
             String hash = cert.getIssuerDN().getName();
+            String fingerprints = extractFingerprints(cert);
             BigInteger serial = cert.getSerialNumber();
             String certId = getCertificateIdFromCertificate(subject,issuer, serial );
             CertificateRO cro = new CertificateRO();
             cro.setCertificateId(certId);
             cro.setSubject(subject);
             cro.setIssuer(issuer);
+            cro.setFingerprints(fingerprints);
             // set serial as HEX
             cro.setSerialNumber(serial.toString(16));
             cro.setValidFrom(LocalDateTime.ofInstant(cert.getNotBefore().toInstant(), ZoneId.systemDefault()));
             cro.setValidTo(LocalDateTime.ofInstant(cert.getNotAfter().toInstant(), ZoneId.systemDefault()));
 
             return cro;
+    }
 
+    private String extractFingerprints(final X509Certificate certificate) {
+        if (certificate == null)
+            return null;
 
-
+        MessageDigest md = null;
+        try {
+            md = MessageDigest.getInstance("SHA-1");
+        } catch (NoSuchAlgorithmException e) {
+            LOG.warn("Error initializing MessageDigest ", e);
+            throw new SMPRuntimeException(ErrorCode.INVALID_CERTIFICATE_MESSAGE_DIGEST, e);
+        }
+        byte[] der = new byte[0];
+        try {
+            der = certificate.getEncoded();
+        } catch (CertificateEncodingException e) {
+            LOG.warn("Error encoding certificate ", e);
+            throw new SMPRuntimeException(ErrorCode.INVALID_CERTIFICATE_ENCODING, e);
+        }
+        md.update(der);
+        byte[] digest = md.digest();
+        String digestHex = DatatypeConverter.printHexBinary(digest);
+        return digestHex.toLowerCase();
     }
 
     public String getCertificateIdFromCertificate(String subject, String issuer, BigInteger serial ){
diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/config/SmpWebAppConfig.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/config/SmpWebAppConfig.java
index da24d8c4e..e2efdcdeb 100644
--- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/config/SmpWebAppConfig.java
+++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/config/SmpWebAppConfig.java
@@ -13,12 +13,24 @@
 
 package eu.europa.ec.edelivery.smp.config;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
 import eu.europa.ec.edelivery.smp.error.ErrorMappingControllerAdvice;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
-import org.springframework.web.servlet.config.annotation.*;
+import org.springframework.context.annotation.Primary;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
+import java.util.List;
 
 import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE;
 
@@ -35,6 +47,29 @@ import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE;
 @Import({GlobalMethodSecurityConfig.class, ErrorMappingControllerAdvice.class})
 public class SmpWebAppConfig implements WebMvcConfigurer {
 
+    @Override
+    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
+        // There is no clean way of replacing the default Jackson 2 HttpMessageConverter while keeping the other
+        // default converters (@EnableWebMvc disables auto-configuration and most of the default converters are
+        // not available as Spring beans
+        // org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport.addDefaultHttpMessageConverters)
+        converters.removeIf(httpMessageConverter -> httpMessageConverter instanceof MappingJackson2HttpMessageConverter);
+        converters.add(jackson2HttpMessageConverter());
+    }
+
+    @Bean
+    public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() {
+        return new MappingJackson2HttpMessageConverter(objectMapper());
+    }
+
+    @Bean
+    @Primary
+    public ObjectMapper objectMapper() {
+        return Jackson2ObjectMapperBuilder.json()
+                .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+                .build();
+    }
+
     @Override
     public void addViewControllers(ViewControllerRegistry registry) {
         registry.addViewController("/").setViewName("/index.html");
-- 
GitLab