From 54c1d47e55b9ee2d008a999ee24007219fc32a99 Mon Sep 17 00:00:00 2001
From: RIHTARSIC Joze <joze.rihtarsic@ext.ec.europa.eu>
Date: Mon, 2 Sep 2024 15:43:27 +0200
Subject: [PATCH] [EDELIVERY-13839] Reset password policy validation fix

---
 smp-angular/src/app/common/global-lookups.ts  | 43 ++++++++++---------
 .../reset-credential.component.ts             | 31 +++++++++++--
 .../smp/services/CredentialService.java       |  7 ++-
 3 files changed, 55 insertions(+), 26 deletions(-)

diff --git a/smp-angular/src/app/common/global-lookups.ts b/smp-angular/src/app/common/global-lookups.ts
index fc8f8e5e8..177ed421a 100644
--- a/smp-angular/src/app/common/global-lookups.ts
+++ b/smp-angular/src/app/common/global-lookups.ts
@@ -13,6 +13,7 @@ import {SecurityEventService} from "../security/security-event.service";
 import {DateAdapter} from "@angular/material/core";
 import {NgxMatDateAdapter} from "@angular-material-components/datetime-picker";
 import {DomainRo} from "./model/domain-ro.model";
+import {Subject} from "rxjs";
 
 /**
  * Purpose of object is to fetch lookups as domains and users
@@ -20,6 +21,9 @@ import {DomainRo} from "./model/domain-ro.model";
 
 @Injectable()
 export class GlobalLookups {
+  // global data observers. The components will subscribe to these Subject to get
+  // data updates.
+  private smpInfoUpdateSubject: Subject<SmpInfo> = new Subject<SmpInfo>();
 
   domainObserver: Observable<SearchTableResult>
   userObserver: Observable<SearchTableResult>
@@ -71,15 +75,6 @@ export class GlobalLookups {
     this.refreshDomainLookup(domainUrl);
   }
 
-  public refreshDomainLookupForLoggedUser() {
-    let domainUrl = SmpConstants.REST_PUBLIC_DOMAIN;
-    // for authenticated admin use internal url which returns more data!
-    if (this.securityService.isCurrentUserSystemAdmin()) {
-      domainUrl = SmpConstants.REST_INTERNAL_DOMAIN_MANAGE_DEPRECATED;
-    }
-    this.refreshDomainLookup(domainUrl);
-  }
-
   public refreshDomainLookup(domainUrl: string) {
     let params: HttpParams = new HttpParams()
       .set('page', '-1')
@@ -96,16 +91,18 @@ export class GlobalLookups {
     });
   }
 
-
   public refreshApplicationInfo() {
 
     this.http.get<SmpInfo>(SmpConstants.REST_PUBLIC_APPLICATION_INFO)
-      .subscribe((res: SmpInfo) => {
+      .subscribe({
+        next: (res: SmpInfo): void => {
           this.cachedApplicationInfo = res;
-        }, error => {
-          console.log("getSmpInfo:" + error);
+          this.smpInfoUpdateSubject.next(res);
+        },
+        error: (err: any): void => {
+          console.log("getSmpInfo:" + err);
         }
-      );
+      });
 
   }
 
@@ -116,12 +113,13 @@ export class GlobalLookups {
       console.log("Refresh application configuration is authenticated " + isAuthenticated)
       if (isAuthenticated) {
         this.http.get<SmpConfig>(SmpConstants.REST_PUBLIC_APPLICATION_CONFIG)
-          .subscribe((res: SmpConfig) => {
-              this.cachedApplicationConfig = res;
-            }, error => {
-              console.log("getSmpConfig:" + error);
-            }
-          );
+          .subscribe({next: (res :SmpConfig):void => {
+          this.cachedApplicationConfig = res;
+        },
+        error: (err: any)=> {
+          console.log("getSmpConfig:" + err);
+        }
+      });
       }
     });
   }
@@ -143,7 +141,6 @@ export class GlobalLookups {
       let sub: Subscription = this.userObserver.subscribe((users: SearchTableResult) => {
         this.cachedServiceGroupOwnerList = users.serviceEntities.map(serviceEntity => {
           return {...serviceEntity}
-
         });
         sub.unsubscribe();
       }, (error: any) => {
@@ -160,4 +157,8 @@ export class GlobalLookups {
     this.cachedApplicationConfig = null;
     this.cachedDomainList = [];
   }
+
+  public onSmpInfoUpdateEvent(): Observable<SmpInfo> {
+    return this.smpInfoUpdateSubject.asObservable();
+  }
 }
diff --git a/smp-angular/src/app/security/reset-credential/reset-credential.component.ts b/smp-angular/src/app/security/reset-credential/reset-credential.component.ts
index 0cc719943..0956a149b 100644
--- a/smp-angular/src/app/security/reset-credential/reset-credential.component.ts
+++ b/smp-angular/src/app/security/reset-credential/reset-credential.component.ts
@@ -2,8 +2,11 @@
 import {ActivatedRoute, Router} from '@angular/router';
 import {FormGroup, UntypedFormControl, Validators} from "@angular/forms";
 import {GlobalLookups} from "../../common/global-lookups";
-import {equal} from "../../common/dialogs/password-change-dialog/password-change-dialog.component";
+import {
+  equal
+} from "../../common/dialogs/password-change-dialog/password-change-dialog.component";
 import {SecurityService} from "../security.service";
+import {SmpInfo} from "../../app-info/smp-info.model";
 
 @Component({
   templateUrl: './reset-credential.component.html',
@@ -26,18 +29,38 @@ export class ResetCredentialComponent implements OnInit {
 
   ngOnInit() {
     this.initForm();
+    this.lookups.onSmpInfoUpdateEvent().subscribe((data: SmpInfo): void => {
+      this.resetPasswordValidator(data);
+    })
     this.resetToken = this.activatedRoute.snapshot.params['resetToken'];
   }
 
+  /**
+   * Reset password validator from the application info data object
+   * @param data - SmpInfo
+   */
+  private resetPasswordValidator(data: SmpInfo): void {
+    this.resetForm.controls['new-password'].setValidators([Validators.required, Validators.pattern(data.passwordValidationRegExp)]);
+  }
+
   private initForm() {
 
-    let newPasswdFormControl: UntypedFormControl = new UntypedFormControl({value: null, readonly: false},
+    let newPasswdFormControl: UntypedFormControl = new UntypedFormControl({
+        value: null,
+        readonly: false
+      },
       [Validators.required, Validators.pattern(this.passwordValidationRegExp)]);
-    let confirmNewPasswdFormControl: UntypedFormControl = new UntypedFormControl({value: null, readonly: false},
+    let confirmNewPasswdFormControl: UntypedFormControl = new UntypedFormControl({
+        value: null,
+        readonly: false
+      },
       [Validators.required, equal(newPasswdFormControl, true)]);
 
     this.resetForm = new FormGroup({
-      'resetUsername': new UntypedFormControl({value: null, readonly: true}, null),
+      'resetUsername': new UntypedFormControl({
+        value: null,
+        readonly: true
+      }, null),
       'new-password': newPasswdFormControl,
       'confirm-new-password': confirmNewPasswdFormControl
     });
diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/CredentialService.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/CredentialService.java
index bc7908249..d3c85c44d 100644
--- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/CredentialService.java
+++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/CredentialService.java
@@ -56,6 +56,7 @@ import java.text.SimpleDateFormat;
 import java.time.OffsetDateTime;
 import java.time.temporal.ChronoUnit;
 import java.util.*;
+import java.util.regex.Pattern;
 
 import static java.util.Locale.US;
 
@@ -354,12 +355,16 @@ public class CredentialService {
             return;
         }
         DBCredential dbCredential = optCredential.get();
-
         if (!resetToken.equals(dbCredential.getResetToken())) {
             LOG.warn("User [{}] reset token does not match the active reset token! The request is ignored", username);
             return;
         }
 
+        Pattern pattern = configurationService.getPasswordPolicyRexExp();
+        if (pattern != null && !pattern.matcher(newPassword).matches()) {
+            throw new SMPRuntimeException(ErrorCode.INVALID_REQUEST, "PasswordChange", configurationService.getPasswordPolicyValidationMessage());
+        }
+
         OffsetDateTime now = OffsetDateTime.now();
         dbCredential.setValue(BCrypt.hashpw(newPassword, BCrypt.gensalt()));
         dbCredential.setResetToken(null);
-- 
GitLab