Code development platform for open source projects from the European Union institutions :large_blue_circle: EU Login authentication by SMS will be completely phased out by mid-2025. To see alternatives please check here

Skip to content
Snippets Groups Projects
Commit bed2be14 authored by Joze RIHTARSIC's avatar Joze RIHTARSIC
Browse files

add user table certificate validation and show errors on UI

parent f3aef01b
No related branches found
No related tags found
No related merge requests found
......@@ -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
}
......
......@@ -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>
......
......@@ -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;
}
......
......@@ -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;
}
......@@ -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;">&nbsp;</span>
......
......@@ -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);
}
......
......@@ -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;
}
......
......@@ -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;
}
......
......@@ -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) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment