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