diff --git a/smp-angular/src/app/user/user-controller.ts b/smp-angular/src/app/user/user-controller.ts index 48e7ef542756ee63971ff87e59fb0b0fe04c7258..eb60cff81a33c318584f6b35f9a20d0258965db4 100644 --- a/smp-angular/src/app/user/user-controller.ts +++ b/smp-angular/src/app/user/user-controller.ts @@ -12,9 +12,15 @@ import {CertificateRo} from "./certificate-ro.model"; export class UserController implements SearchTableController { - nullCert = this.newCertificateRo(); + nullCert:CertificateRo; + + + compareUserProperties = ["username","password","emailAddress","active","role","certificate"]; + compareCertProperties = ["certificateId","subject","issuer","serialNumber","crlUrl","validFrom","validTo"]; + constructor(protected http: HttpClient, protected lookups: GlobalLookups, public dialog: MatDialog) { + this.nullCert = this.newCertificateRo(); } public showDetails(row: any) { @@ -81,31 +87,41 @@ export class UserController implements SearchTableController { } isCertificateChanged(oldCert, newCert): boolean { - if (!this.isNotNull(oldCert) && !this.isNotNull(newCert)) { + if (this.isNull(oldCert) && this.isNull(newCert)) { + console.log("both null return false! "); return false; } - if (!this.isNotNull(oldCert)) { + if (this.isNull(oldCert)) { oldCert = this.nullCert; } - if (!this.isNotNull(newCert)) { + if (this.isNull(newCert)) { newCert = this.nullCert; } - return this.isRecordChanged(oldCert, newCert); - } + return this.propertyChanged(oldCert, newCert, this.compareCertProperties); + } isRecordChanged(oldModel, newModel): boolean { + return this.propertyChanged(oldModel, newModel, this.compareUserProperties); + } + + propertyChanged(oldModel, newModel, arrayProperties): boolean { + + + let propSize = arrayProperties.length; + for (let i = 0; i < propSize; i++) { - for (var property in oldModel) { - if (property === 'certificate') { - if (this.isCertificateChanged(newModel[property], oldModel[property])) { + let property = arrayProperties[i]; + if (property === 'certificate') { + if (this.isCertificateChanged(oldModel[property], newModel[property])) { return true; // Property has changed } } else { const isEqual = this.isEqual(newModel[property], oldModel[property]); if (!isEqual) { + console.log("property "+property+" is changed! "); return true; // Property has changed } } @@ -122,8 +138,8 @@ export class UserController implements SearchTableController { return (!str || 0 === str.length); } - isNotNull(obj): boolean { - return typeof obj != 'undefined' && obj + isNull(obj): boolean { + return !obj } 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 55333aa40a3738ed5bc392617d07f0813629cc8e..12a771bafe04c3fa5bc5d866f35c9f0b8b957c7e 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 @@ -157,7 +157,7 @@ <tr> <td> <button mat-raised-button color="primary" [mat-dialog-close]="true" (click)="submitForm()" - [disabled]="!userForm.valid || this.current && this.current.certificate && this.current.certificate.invalid"> + [disabled]="!userForm.valid "> <mat-icon>check_circle</mat-icon> <span>OK</span> </button> 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 6c3fe712a760179e5127364106d0ef818513d84d..983157c0ad7f1027cc95eb88d65d97724b0d43af 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 @@ -68,10 +68,8 @@ export class UserDetailsDialogComponent { const validTo = control.get('validTo'); const issuer = control.get('issuer'); const serialNumber = control.get('serialNumber'); - const isValid = control.get('isCertificateValid'); return certificateToggle && subject && validFrom && validTo && issuer && serialNumber && certificateToggle.value - && isValid && !(subject.value && validFrom.value && validTo.value && issuer.value && serialNumber.value) ? {certificateDetailsRequired: true} : null; }; @@ -129,9 +127,10 @@ export class UserDetailsDialogComponent { this.current = this.editMode ? { ...data.row, + password: '', // ensures the user password is cleared before editing confirmation: '', - certificate: data.row.certificate || this.newCertificateRo() + certificate: data.row.certificate? {...data.row.certificate} : this.newCertificateRo() } : { active: true, username: '', @@ -208,6 +207,10 @@ export class UserDetailsDialogComponent { this.userForm.controls['certificateId'].setValue(this.current.certificate.certificateId); this.userForm.controls['isCertificateValid'].setValue(!this.current.certificate.invalid); + + this.certificateValidationMessage =this.current.certificate.invalidReason; + this.isCertificateInvalid= this.current.certificate.invalid; + // if edit mode and user is given - toggle is disabled // username should not be changed.! if (this.editMode && bUserPasswordAuthentication) { @@ -229,7 +232,8 @@ export class UserDetailsDialogComponent { 'validTo': res.validTo, 'issuer': res.issuer, 'serialNumber': res.serialNumber, - 'certificateId': res.certificateId + 'certificateId': res.certificateId, + 'isCertificateValid': !res.invalid }); this.certificateValidationMessage = res.invalidReason; this.isCertificateInvalid = res.invalid; @@ -329,6 +333,8 @@ export class UserDetailsDialogComponent { this.current.certificate.serialNumber = this.userForm.controls['serialNumber'].value; this.current.certificate.validFrom = this.userForm.controls['validFrom'].value; this.current.certificate.validTo = this.userForm.controls['validTo'].value; + this.current.certificate.invalid = this.isCertificateInvalid; + this.current.certificate.invalidReason = this.certificateValidationMessage; } else { this.current.certificate = null; } diff --git a/smp-angular/src/app/user/user.component.css b/smp-angular/src/app/user/user.component.css index 1e2c4567531ce54920b3da930aeb44ed2c39f44e..59f0dc732eb4567f498a4f8d1f0ced45fc390789 100644 --- a/smp-angular/src/app/user/user.component.css +++ b/smp-angular/src/app/user/user.component.css @@ -11,3 +11,24 @@ #hiddenButtonId { position: fixed; } +/deep/ .invalidCertificate { + text-decoration: line-through !important; + font-weight: bold; + color:red; +} + +/deep/ .deleted { + text-decoration: line-through !important; + font-weight: bold; +} +/deep/ .table-row-new { + + color: darkgreen !important; + font-weight: bold; +} +/deep/ .table-row-updated { + font-weight: bold; +} +/deep/ .table-row { + font-weight: normal; +} diff --git a/smp-angular/src/app/user/user.component.html b/smp-angular/src/app/user/user.component.html index 159a84fd03a3119bae4304f0918c7b218203f217..164e42ec6b31449a85d29ee607188a2862788f2e 100644 --- a/smp-angular/src/app/user/user.component.html +++ b/smp-angular/src/app/user/user.component.html @@ -14,6 +14,12 @@ <ng-template #roleCellTemplate let-value="value" ngx-datatable-cell-template>{{getRoleLabel(value)}}</ng-template> + <ng-template #certificateTemplate let-row="row" ngx-datatable-cell-template> + <span [class]='certCssClass(row)' + matTooltip="{{row.certificate?.invalidReason}}" + >{{row.certificate?.certificateId}}</span> + </ng-template> + <ng-template #additionalToolButtons > <span style="width: 2px;background-color: deepskyblue;"> </span> diff --git a/smp-angular/src/app/user/user.component.ts b/smp-angular/src/app/user/user.component.ts index d8ee84c7501c20271e79e34e0daad7e7cb237d4b..54a15810aeed69df947f1bcc14551808eb0d1546 100644 --- a/smp-angular/src/app/user/user.component.ts +++ b/smp-angular/src/app/user/user.component.ts @@ -8,6 +8,7 @@ import {SearchTableComponent} from "../common/search-table/search-table.componen import {SecurityService} from "../security/security.service"; import {GlobalLookups} from "../common/global-lookups"; import {TruststoreEditDialogComponent} from "./truststore-edit-dialog/truststore-edit-dialog.component"; +import {SearchTableEntityStatus} from "../common/search-table/search-table-entity-status.model"; @Component({ templateUrl:'./user.component.html', @@ -19,6 +20,7 @@ export class UserComponent implements OnInit { @ViewChild('rowExtensionAction') rowExtensionAction: TemplateRef<any>; @ViewChild('rowActions') rowActions: TemplateRef<any>; @ViewChild('searchTable') searchTable: SearchTableComponent; + @ViewChild('certificateTemplate') certificateTemplate: TemplateRef<any>; columnPicker: ColumnPicker = new ColumnPicker(); userController: UserController; @@ -42,7 +44,7 @@ export class UserComponent implements OnInit { }, { name: 'Certificate', - prop: 'certificate.certificateId', + cellTemplate: this.certificateTemplate, canAutoResize: true }, { @@ -62,6 +64,21 @@ export class UserComponent implements OnInit { } } + certCssClass(row) { + + if (row.certificate && row.certificate.invalid) { + return 'invalidCertificate'; + } else if (row.status === SearchTableEntityStatus.NEW) { + return 'table-row-new'; + } else if (row.status === SearchTableEntityStatus.UPDATED) { + return 'table-row-updated'; + } else if (row.status === SearchTableEntityStatus.REMOVED) { + return 'deleted'; + }else { + return 'table-row'; + } + } + details(row: any) { this.userController.showDetails(row); } 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 61772e949a11a38bba3321073f5770365e021711..b09b32e022d28760a21092faa949b3dae3a9a836 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 @@ -17,6 +17,7 @@ public class CertificateRO extends BaseRO { private String subject; private String issuer; private String serialNumber; + private String crlUrl; private String encodedValue; private String blueCoatHeader; private boolean isInvalid; @@ -107,6 +108,14 @@ public class CertificateRO extends BaseRO { this.blueCoatHeader = blueCoatHeader; } + public String getCrlUrl() { + return crlUrl; + } + + public void setCrlUrl(String crlUrl) { + this.crlUrl = crlUrl; + } + public boolean isInvalid() { return isInvalid; } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ui/UITruststoreService.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ui/UITruststoreService.java index caa8333761f25c52f80c733983834d453003bc14..db0b62f62aa3947fa3fbe9e29d49262a030f6ee2 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ui/UITruststoreService.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ui/UITruststoreService.java @@ -4,6 +4,7 @@ import eu.europa.ec.edelivery.smp.data.ui.CertificateRO; import eu.europa.ec.edelivery.smp.exceptions.CertificateNotTrustedException; import eu.europa.ec.edelivery.smp.logging.SMPLogger; import eu.europa.ec.edelivery.smp.logging.SMPLoggerFactory; +import eu.europa.ec.edelivery.smp.logging.SMPMessageCode; import eu.europa.ec.edelivery.smp.services.CRLVerifierService; import eu.europa.ec.edelivery.smp.services.ConfigurationService; import eu.europa.ec.edelivery.smp.utils.X509CertificateUtils; @@ -13,6 +14,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.convert.ConversionService; +import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; @@ -20,19 +22,25 @@ import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; import javax.naming.ldap.Rdn; import java.io.*; -import java.nio.file.Paths; import java.security.*; import java.security.cert.Certificate; import java.security.cert.*; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.*; import static java.util.Collections.list; +import static java.util.Locale.US; @Service public class UITruststoreService { private static final SMPLogger LOG = SMPLoggerFactory.getLogger(UITruststoreService.class); + private static final ThreadLocal<DateFormat> dateFormatLocal = ThreadLocal.withInitial(() -> { + return new SimpleDateFormat("MMM d hh:mm:ss yyyy zzz", US); + }); + @Autowired private ConfigurationService configurationService; @@ -158,14 +166,14 @@ public class UITruststoreService { return cro; } - public void checkFullCertificateValidity(X509Certificate cert) throws CertificateException{ + public void checkFullCertificateValidity(X509Certificate cert) throws CertificateException { // test if certificate is valid cert.checkValidity(); // check if certificate or its issuer is on trusted list // check only issuer because using bluecoat Client-cert we do not have whole chain. // if the truststore is empty then truststore validation is ignored // backward compatibility - if ( !normalizedTrustedList.isEmpty() && !(isSubjectOnTrustedList(cert.getSubjectX500Principal().getName()) + if (!normalizedTrustedList.isEmpty() && !(isSubjectOnTrustedList(cert.getSubjectX500Principal().getName()) || isSubjectOnTrustedList(cert.getIssuerDN().getName()))) { throw new CertificateNotTrustedException("Certificate is not trusted!"); @@ -174,10 +182,50 @@ public class UITruststoreService { crlVerifierService.verifyCertificateCRLs(cert); } + public void checkFullCertificateValidity(CertificateRO cert) throws CertificateException { + // trust data in database + Date currentDate = Calendar.getInstance().getTime(); + if (cert.getValidFrom() != null && currentDate.before(cert.getValidFrom())) { + throw new CertificateNotYetValidException("Certificate: " + cert.getCertificateId() + " is valid from: " + + dateFormatLocal.get().format(cert.getValidFrom()) + "."); + + } + if (cert.getValidTo() != null && currentDate.after(cert.getValidTo())) { + throw new CertificateExpiredException("Certificate: " + cert.getCertificateId() + " was valid to: " + + dateFormatLocal.get().format(cert.getValidTo()) + "."); + } + // if trusted list is not empty and exists issuer or subject then validate + if (!normalizedTrustedList.isEmpty() && ( + !StringUtils.isBlank(cert.getIssuer()) || !StringUtils.isBlank(cert.getSubject()))) { + + if (!isSubjectOnTrustedList(cert.getIssuer()) && !isSubjectOnTrustedList(cert.getSubject())) { + throw new CertificateNotTrustedException("Certificate is not trusted!"); + } + + } + + // Check crl list + String url = cert.getCrlUrl(); + if (!StringUtils.isBlank(url) && !StringUtils.isBlank(cert.getSerialNumber())) { + try { + crlVerifierService.verifyCertificateCRLs(cert.getSerialNumber(), url); + } catch (CertificateRevokedException ex) { + String msg = "Certificate: '" + cert.getCertificateId() + "'" + + " is revoked!"; + LOG.securityWarn(SMPMessageCode.SEC_USER_CERT_INVALID, cert.getCertificateId(), msg); + throw new AuthenticationServiceException(msg); + } catch (Throwable th) { + String msg = "Error occurred while validating CRL for certificate!"; + LOG.error(SMPLogger.SECURITY_MARKER, msg + "Err: " + ExceptionUtils.getRootCauseMessage(th), th); + throw new AuthenticationServiceException(msg); + } + } + } + boolean isTruststoreChanged() { File file = getTruststoreFile(); return !Objects.equals(lastUpdateTrustStoreFile, file) || - file!=null && file.lastModified() != lastUpdateTrustoreFileTime; + file != null && file.lastModified() != lastUpdateTrustoreFileTime; } public File getTruststoreFile() { @@ -186,7 +234,7 @@ public class UITruststoreService { private KeyStore loadTruststore(File truststoreFile) { - if (truststoreFile==null) { + if (truststoreFile == null) { LOG.error("Truststore file is not configured! Update SMP configuration!"); return null; } 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 8d2a70de65d9ee6761ea359f467bdb868dae43d5..bdcfdc1ff5874ec39eaebefd57885473930019f9 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 @@ -22,6 +22,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.io.StringWriter; +import java.security.cert.CertificateException; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.List; @@ -37,6 +38,10 @@ public class UIUserService extends UIServiceBase<DBUser, UserRO> { @Autowired private ConversionService conversionService; + @Autowired + private UITruststoreService truststoreService; + + @Override protected BaseDao<DBUser> getDatabaseDao() { return userDao; @@ -55,10 +60,26 @@ public class UIUserService extends UIServiceBase<DBUser, UserRO> { @Transactional public ServiceResult<UserRO> getTableList(int page, int pageSize, String sortField, String sortOrder, Object filter) { ServiceResult<UserRO> resUsers = super.getTableList(page, pageSize, sortField, sortOrder, filter); - resUsers.getServiceEntities().forEach(usr -> usr.setPassword(null)); + resUsers.getServiceEntities().forEach(this::updateUserStatus); return resUsers; } + protected void updateUserStatus(UserRO user){ + // never return password even if is hashed... + user.setPassword(null); + if (user.getCertificate()!=null && !StringUtils.isBlank(user.getCertificate().getCertificateId())){ + // validate certificate + try { + truststoreService.checkFullCertificateValidity(user.getCertificate()); + } catch (CertificateException e) { + LOG.warn("Set invalid cert status: " + user.getCertificate().getCertificateId() + " reason: " +e.getMessage()); + user.getCertificate().setInvalid(true); + user.getCertificate().setInvalidReason(e.getMessage()); + } + } + + } + @Transactional public void updateUserList(List<UserRO> lst, LocalDateTime passwordChange) { for (UserRO userRO : lst) {