From 0c83817d18b4a47c3e63ddaac0d21c8903419cd2 Mon Sep 17 00:00:00 2001 From: TINCU Sebastian-Ion <Sebastian-Ion.TINCU@ext.ec.europa.eu> Date: Fri, 23 Nov 2018 16:41:53 +0100 Subject: [PATCH] EDELIVERY-3653 SMP: CTL.C8 Administrator accounts Password Management: Add code to reset the password expiration date when a SystemAdministrator changes passwords. Add code to update the password change date when users change their own passwords. Extract common dialog component and add expired password warning dialog component. Add codelyzer dependency back and configure TSLint. --- smp-angular/package-lock.json | 107 ++++++++++++++++- smp-angular/package.json | 3 +- smp-angular/src/app/app.module.ts | 25 ++-- .../cancel-dialog.component.html | 30 +---- .../cancel-dialog/cancel-dialog.component.ts | 3 +- .../dialog.component.css} | 7 +- .../app/common/dialog/dialog.component.html | 32 ++++++ .../common/dialog/dialog.component.spec.ts | 25 ++++ .../src/app/common/dialog/dialog.component.ts | 25 ++++ .../expired-password-dialog.component.html | 4 + .../expired-password-dialog.component.spec.ts | 25 ++++ .../expired-password-dialog.component.ts | 14 +++ .../save-dialog/save-dialog.component.css | 42 ------- .../save-dialog/save-dialog.component.html | 31 +---- .../save-dialog/save-dialog.component.ts | 3 +- smp-angular/src/app/login/login.component.ts | 32 +++--- smp-angular/tslint.json | 11 +- .../conversion/DBUserToUserROConverter.java | 18 ++- .../conversion/UserROToDBUserConverter.java | 19 ++- .../ec/edelivery/smp/data/ui/UserRO.java | 18 ++- .../smp/services/ui/UIUserService.java | 11 +- .../DBUserToUserROConverterTest.java | 108 ++++++++++++++++++ .../UserROToDBUserConverterTest.java | 54 +++++++++ .../ui/UIUserServiceIntegrationTest.java | 13 +-- .../ec/edelivery/smp/ui/UserResource.java | 7 +- 25 files changed, 491 insertions(+), 176 deletions(-) rename smp-angular/src/app/common/{cancel-dialog/cancel-dialog.component.css => dialog/dialog.component.css} (92%) create mode 100644 smp-angular/src/app/common/dialog/dialog.component.html create mode 100644 smp-angular/src/app/common/dialog/dialog.component.spec.ts create mode 100644 smp-angular/src/app/common/dialog/dialog.component.ts create mode 100644 smp-angular/src/app/common/expired-password-dialog/expired-password-dialog.component.html create mode 100644 smp-angular/src/app/common/expired-password-dialog/expired-password-dialog.component.spec.ts create mode 100644 smp-angular/src/app/common/expired-password-dialog/expired-password-dialog.component.ts delete mode 100644 smp-angular/src/app/common/save-dialog/save-dialog.component.css create mode 100644 smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/conversion/DBUserToUserROConverterTest.java create mode 100644 smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/conversion/UserROToDBUserConverterTest.java diff --git a/smp-angular/package-lock.json b/smp-angular/package-lock.json index 6eab6f6b0..48204c253 100644 --- a/smp-angular/package-lock.json +++ b/smp-angular/package-lock.json @@ -891,6 +891,11 @@ "normalize-path": "2.1.1" } }, + "app-root-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.1.0.tgz", + "integrity": "sha1-mL9lmTJ+zqGZMJhm6BQDaP0uZGo=" + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -1898,6 +1903,26 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, + "codelyzer": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-4.5.0.tgz", + "integrity": "sha512-oO6vCkjqsVrEsmh58oNlnJkRXuA30hF8cdNAQV9DytEalDwyOFRvHMnlKFzmOStNerOmPGZU9GAHnBo4tGvtiQ==", + "requires": { + "app-root-path": "2.1.0", + "css-selector-tokenizer": "0.7.1", + "cssauron": "1.4.0", + "semver-dsl": "1.0.1", + "source-map": "0.5.7", + "sprintf-js": "1.1.1" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", + "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=" + } + } + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -2276,12 +2301,35 @@ "nth-check": "1.0.1" } }, + "css-selector-tokenizer": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz", + "integrity": "sha512-xYL0AMZJ4gFzJQsHUKa5jiWWi2vH77WVNg7JYRyewwj6oPh4yb/y6Y9ZCw9dsj/9UauMhtuxR+ogQd//EdEVNA==", + "requires": { + "cssesc": "0.1.0", + "fastparse": "1.1.2", + "regexpu-core": "1.0.0" + } + }, "css-what": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=", "dev": true }, + "cssauron": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz", + "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=", + "requires": { + "through": "2.3.8" + } + }, + "cssesc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", + "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=" + }, "cuint": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", @@ -3383,6 +3431,11 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==" + }, "faye-websocket": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", @@ -7331,6 +7384,11 @@ "integrity": "sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==", "dev": true }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==" + }, "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", @@ -7356,6 +7414,36 @@ "safe-regex": "1.1.0" } }, + "regexpu-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", + "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + "requires": { + "regenerate": "1.4.0", + "regjsgen": "0.2.0", + "regjsparser": "0.1.5" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "http://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=" + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "requires": { + "jsesc": "0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + } + } + }, "relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -7736,8 +7824,15 @@ "semver": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", - "dev": true + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + }, + "semver-dsl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz", + "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=", + "requires": { + "semver": "5.6.0" + } }, "semver-intersect": { "version": "1.4.0", @@ -8156,8 +8251,7 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, "source-map-loader": { "version": "0.2.4", @@ -8585,6 +8679,11 @@ "inherits": "2.0.3" } }, + "through": { + "version": "2.3.8", + "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, "through2": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", diff --git a/smp-angular/package.json b/smp-angular/package.json index b4350405b..a4044ec6f 100644 --- a/smp-angular/package.json +++ b/smp-angular/package.json @@ -19,6 +19,7 @@ "@angular/common": "^6.1.0", "@angular/compiler": "^6.1.0", "@angular/core": "^6.1.0", + "@angular/flex-layout": "^6.0.0-beta.16", "@angular/forms": "^6.1.0", "@angular/http": "^6.1.0", "@angular/material": "^6.4.7", @@ -26,8 +27,8 @@ "@angular/platform-browser-dynamic": "^6.1.0", "@angular/platform-server": "^6.1.0", "@angular/router": "^6.1.0", - "@angular/flex-layout": "^6.0.0-beta.16", "@swimlane/ngx-datatable": "^13.0.0", + "codelyzer": "^4.5.0", "core-js": "^2.5.7", "file-saver": "1.3.3", "hammerjs": "^2.0.8", diff --git a/smp-angular/src/app/app.module.ts b/smp-angular/src/app/app.module.ts index f127a35ee..ba7800171 100644 --- a/smp-angular/src/app/app.module.ts +++ b/smp-angular/src/app/app.module.ts @@ -2,7 +2,7 @@ import {BrowserModule} from '@angular/platform-browser'; import {NgModule} from '@angular/core'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {HttpClient, HttpClientModule} from '@angular/common/http'; -import {FlexLayoutModule} from "@angular/flex-layout"; +import {FlexLayoutModule} from '@angular/flex-layout'; import { MatButtonModule, MatCardModule, @@ -20,7 +20,7 @@ import { MatToolbarModule, MatTooltipModule, } from '@angular/material'; -import "hammerjs"; +import 'hammerjs'; import {NgxDatatableModule} from '@swimlane/ngx-datatable'; @@ -70,13 +70,15 @@ import {DomainDetailsDialogComponent} from './domain/domain-details-dialog/domai import {UserDetailsDialogComponent} from './user/user-details-dialog/user-details-dialog.component'; import {DownloadService} from './download/download.service'; import {CertificateService} from './user/certificate.service'; -import {GlobalLookups} from "./common/global-lookups"; -import {ServiceGroupExtensionWizardDialogComponent} from "./service-group-edit/service-group-extension-wizard-dialog/service-group-extension-wizard-dialog.component"; -import {ServiceMetadataWizardDialogComponent} from "./service-group-edit/service-metadata-wizard-dialog/service-metadata-wizard-dialog.component"; -import {ConfirmationDialogComponent} from "./common/confirmation-dialog/confirmation-dialog.component"; -import {SpinnerComponent} from "./common/spinner/spinner.component"; -import {UserService} from "./user/user.service"; -import {UserDetailsService} from "./user/user-details-dialog/user-details.service"; +import {GlobalLookups} from './common/global-lookups'; +import {ServiceGroupExtensionWizardDialogComponent} from './service-group-edit/service-group-extension-wizard-dialog/service-group-extension-wizard-dialog.component'; +import {ServiceMetadataWizardDialogComponent} from './service-group-edit/service-metadata-wizard-dialog/service-metadata-wizard-dialog.component'; +import {ConfirmationDialogComponent} from './common/confirmation-dialog/confirmation-dialog.component'; +import {SpinnerComponent} from './common/spinner/spinner.component'; +import {UserService} from './user/user.service'; +import {UserDetailsService} from './user/user-details-dialog/user-details.service'; +import { ExpiredPasswordDialogComponent } from './common/expired-password-dialog/expired-password-dialog.component'; +import { DialogComponent } from './common/dialog/dialog.component'; @NgModule({ declarations: [ @@ -110,7 +112,9 @@ import {UserDetailsService} from "./user/user-details-dialog/user-details.servic DomainSelectorComponent, AlertsComponent, SearchTableComponent, - UserDetailsDialogComponent + UserDetailsDialogComponent, + ExpiredPasswordDialogComponent, + DialogComponent, ], entryComponents: [ AppComponent, @@ -124,6 +128,7 @@ import {UserDetailsService} from "./user/user-details-dialog/user-details.servic ConfirmationDialogComponent, SaveDialogComponent, DefaultPasswordDialogComponent, + ExpiredPasswordDialogComponent, ], imports: [ BrowserModule, diff --git a/smp-angular/src/app/common/cancel-dialog/cancel-dialog.component.html b/smp-angular/src/app/common/cancel-dialog/cancel-dialog.component.html index d8bc1fd3c..c01a602f0 100644 --- a/smp-angular/src/app/common/cancel-dialog/cancel-dialog.component.html +++ b/smp-angular/src/app/common/cancel-dialog/cancel-dialog.component.html @@ -1,26 +1,4 @@ -<div style="width: 500px;text-align: center"> - <h1 mat-dialog-title>Do you want to cancel all unsaved operations?</h1> - - <div class="divTable"> - <div class="divTableBody"> - - <div class="divTableRow"> - - <div class="divTableCell"> - <button mat-raised-button color="primary" (click)="dialogRef.close(true)" id="yesbuttondialog_id"> - <mat-icon>check_circle</mat-icon> - <span>Yes</span> - </button> - </div> - - <div class="divTableCell"> - <button mat-raised-button color="primary" (click)="dialogRef.close(false)" id="nobuttondialog_id"> - <mat-icon>cancel</mat-icon> - <span>No</span> - </button> - </div> - - </div> - </div> - </div> -</div> +<smp-dialog [title]="'Do you want to cancel all unsaved operations?'" + [type]="'confirmation'" + [dialogRef]="dialogRef"> +</smp-dialog> diff --git a/smp-angular/src/app/common/cancel-dialog/cancel-dialog.component.ts b/smp-angular/src/app/common/cancel-dialog/cancel-dialog.component.ts index add7e0939..b91918d65 100644 --- a/smp-angular/src/app/common/cancel-dialog/cancel-dialog.component.ts +++ b/smp-angular/src/app/common/cancel-dialog/cancel-dialog.component.ts @@ -2,9 +2,8 @@ import {Component} from '@angular/core'; import {MatDialogRef} from '@angular/material'; @Component({ - selector: 'app-cancel-dialog', + selector: 'smp-cancel-dialog', templateUrl: './cancel-dialog.component.html', - styleUrls: ['./cancel-dialog.component.css'] }) export class CancelDialogComponent { diff --git a/smp-angular/src/app/common/cancel-dialog/cancel-dialog.component.css b/smp-angular/src/app/common/dialog/dialog.component.css similarity index 92% rename from smp-angular/src/app/common/cancel-dialog/cancel-dialog.component.css rename to smp-angular/src/app/common/dialog/dialog.component.css index 5e50dc67f..6286aff86 100644 --- a/smp-angular/src/app/common/cancel-dialog/cancel-dialog.component.css +++ b/smp-angular/src/app/common/dialog/dialog.component.css @@ -1,3 +1,8 @@ +.dialog { + width: 500px; + text-align: center; +} + label:hover, label:active, input:hover + label, input:active + label { color: #3f51b5; } @@ -38,5 +43,3 @@ label:hover, label:active, input:hover + label, input:active + label { .divTableBody { display: table-row-group; } - - diff --git a/smp-angular/src/app/common/dialog/dialog.component.html b/smp-angular/src/app/common/dialog/dialog.component.html new file mode 100644 index 000000000..7b22fd1ec --- /dev/null +++ b/smp-angular/src/app/common/dialog/dialog.component.html @@ -0,0 +1,32 @@ +<div class="dialog"> + <h1 mat-dialog-title>{{title}}</h1> + + <div class="divTable"> + <div class="divTableBody"> + + <div class="divTableRow"> + + <div *ngIf="isConfirmationDialog()" class="divTableCell" > + <button mat-raised-button color="primary" (click)="dialogRef.close(true)" id="yesbuttondialog_id" tabindex="0"> + <mat-icon>check_circle</mat-icon> + <span>Yes</span> + </button> + </div> + + <div *ngIf="isConfirmationDialog()" class="divTableCell"> + <button mat-raised-button color="primary" (click)="dialogRef.close(false)" id="nobuttondialog_id" tabindex="1"> + <mat-icon>cancel</mat-icon> + <span>No</span> + </button> + </div> + + <div *ngIf="isInformationDialog()" class="divTableCell"> + <button mat-raised-button color="primary" (click)="dialogRef.close(true)" id="okbuttondialog_id" tabindex="3"> + <mat-icon></mat-icon> + <span>OK</span> + </button> + </div> + </div> + </div> + </div> +</div> diff --git a/smp-angular/src/app/common/dialog/dialog.component.spec.ts b/smp-angular/src/app/common/dialog/dialog.component.spec.ts new file mode 100644 index 000000000..a6bce8db3 --- /dev/null +++ b/smp-angular/src/app/common/dialog/dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DialogComponent } from './dialog.component'; + +describe('DialogComponent', () => { + let component: DialogComponent; + let fixture: ComponentFixture<DialogComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DialogComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/smp-angular/src/app/common/dialog/dialog.component.ts b/smp-angular/src/app/common/dialog/dialog.component.ts new file mode 100644 index 000000000..6bfee560e --- /dev/null +++ b/smp-angular/src/app/common/dialog/dialog.component.ts @@ -0,0 +1,25 @@ +import { Component, Input } from '@angular/core'; +import { MatDialogRef } from '@angular/material'; + +@Component({ + selector: 'smp-dialog', + templateUrl: './dialog.component.html', + styleUrls: ['./dialog.component.css'] +}) +export class DialogComponent { + + @Input() title: String; + + @Input() type: string; + + @Input() dialogRef: MatDialogRef<any>; + + public isConfirmationDialog() { + return this.type === 'confirmation'; + } + + public isInformationDialog() { + return this.type === 'information'; + } + +} diff --git a/smp-angular/src/app/common/expired-password-dialog/expired-password-dialog.component.html b/smp-angular/src/app/common/expired-password-dialog/expired-password-dialog.component.html new file mode 100644 index 000000000..cd140de69 --- /dev/null +++ b/smp-angular/src/app/common/expired-password-dialog/expired-password-dialog.component.html @@ -0,0 +1,4 @@ +<smp-dialog [title]="'Your password is more than three months old. Please change it as soon as possible!'" + [type]="'information'" + [dialogRef]="dialogRef"> +</smp-dialog> diff --git a/smp-angular/src/app/common/expired-password-dialog/expired-password-dialog.component.spec.ts b/smp-angular/src/app/common/expired-password-dialog/expired-password-dialog.component.spec.ts new file mode 100644 index 000000000..f55b8fd72 --- /dev/null +++ b/smp-angular/src/app/common/expired-password-dialog/expired-password-dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExpiredPasswordDialogComponent } from './expired-password-dialog.component'; + +describe('ExpiredPasswordDialogComponent', () => { + let component: ExpiredPasswordDialogComponent; + let fixture: ComponentFixture<ExpiredPasswordDialogComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ExpiredPasswordDialogComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ExpiredPasswordDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/smp-angular/src/app/common/expired-password-dialog/expired-password-dialog.component.ts b/smp-angular/src/app/common/expired-password-dialog/expired-password-dialog.component.ts new file mode 100644 index 000000000..2d2d5fd1c --- /dev/null +++ b/smp-angular/src/app/common/expired-password-dialog/expired-password-dialog.component.ts @@ -0,0 +1,14 @@ +import { Component, OnInit } from '@angular/core'; +import { MatDialogRef } from '@angular/material'; + +@Component({ + selector: 'smp-expired-password-dialog', + templateUrl: './expired-password-dialog.component.html', +}) +export class ExpiredPasswordDialogComponent { + + constructor( + public dialogRef: MatDialogRef<ExpiredPasswordDialogComponent>, + ) { } + +} diff --git a/smp-angular/src/app/common/save-dialog/save-dialog.component.css b/smp-angular/src/app/common/save-dialog/save-dialog.component.css deleted file mode 100644 index 5e50dc67f..000000000 --- a/smp-angular/src/app/common/save-dialog/save-dialog.component.css +++ /dev/null @@ -1,42 +0,0 @@ -label:hover, label:active, input:hover + label, input:active + label { - color: #3f51b5; -} - -.divTable { - display: table; - width: 100%; -} - -.divTableRow { - display: table-row; -} - -.divTableHeading { - background-color: #EEE; - display: table-header-group; -} - -.divTableCell, .divTableHead { - /*border: 1px solid #999999;*/ - display: table-cell; - padding: 3px 3px; - text-align: center; -} - -.divTableHeading { - background-color: #EEE; - display: table-header-group; - font-weight: bold; -} - -.divTableFoot { - background-color: #EEE; - display: table-footer-group; - font-weight: bold; -} - -.divTableBody { - display: table-row-group; -} - - diff --git a/smp-angular/src/app/common/save-dialog/save-dialog.component.html b/smp-angular/src/app/common/save-dialog/save-dialog.component.html index d174d3e73..c7ff33a66 100644 --- a/smp-angular/src/app/common/save-dialog/save-dialog.component.html +++ b/smp-angular/src/app/common/save-dialog/save-dialog.component.html @@ -1,27 +1,4 @@ -<div style="width: 500px;text-align: center"> - <h1 mat-dialog-title>Do you want to save your changes?</h1> - - <div class="divTable"> - <div class="divTableBody"> - - <div class="divTableRow"> - - <div class="divTableCell"> - <button mat-raised-button color="primary" (click)="dialogRef.close(true)" id="yesbuttondialog_id"> - <mat-icon>check_circle</mat-icon> - <span>Yes</span> - </button> - </div> - - <div class="divTableCell"> - <button mat-raised-button color="primary" (click)="dialogRef.close(false)" id="nobuttondialog_id"> - <mat-icon>cancel</mat-icon> - <span>No</span> - </button> - </div> - - </div> - </div> - </div> -</div> - +<smp-dialog [title]="'Do you want to save your changes?'" + [type]="'confirmation'" + [dialogRef]="dialogRef"> +</smp-dialog> diff --git a/smp-angular/src/app/common/save-dialog/save-dialog.component.ts b/smp-angular/src/app/common/save-dialog/save-dialog.component.ts index f4a76bd36..32ecd098a 100644 --- a/smp-angular/src/app/common/save-dialog/save-dialog.component.ts +++ b/smp-angular/src/app/common/save-dialog/save-dialog.component.ts @@ -2,9 +2,8 @@ import {Component} from '@angular/core'; import {MatDialogRef} from '@angular/material'; @Component({ - selector: 'app-messagefilter-dialog', + selector: 'smp-save-dialog', templateUrl: './save-dialog.component.html', - styleUrls: ['./save-dialog.component.css'] }) export class SaveDialogComponent { diff --git a/smp-angular/src/app/login/login.component.ts b/smp-angular/src/app/login/login.component.ts index b76c44a36..0951c9536 100644 --- a/smp-angular/src/app/login/login.component.ts +++ b/smp-angular/src/app/login/login.component.ts @@ -7,7 +7,8 @@ import {SecurityEventService} from '../security/security-event.service'; import {User} from '../security/user.model'; import {MatDialogRef, MatDialog} from '@angular/material'; import {DefaultPasswordDialogComponent} from 'app/security/default-password-dialog/default-password-dialog.component'; -import {Subscription} from "rxjs"; +import {Subscription} from 'rxjs'; +import {ExpiredPasswordDialogComponent} from '../common/expired-password-dialog/expired-password-dialog.component'; @Component({ moduleId: module.id, @@ -30,24 +31,23 @@ export class LoginComponent implements OnInit, OnDestroy { } ngOnInit() { - // get return url from route parameters or default to '/' this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; this.httpEventService.subscribe((error) => { - console.log("Received forbidden request event") this.securityService.logout(); }); this.sub = this.securityEventService.onLoginSuccessEvent().subscribe( data => { - console.log("Authentication successfull"); - //this.verifyDefaultLoginUsed(); - this.router.navigate([this.returnUrl]); + if (data && data.passwordExpired) { + this.dialog.open(ExpiredPasswordDialogComponent).afterClosed().subscribe(() => this.router.navigate([this.returnUrl])); + } else { + this.router.navigate([this.returnUrl]); + } }); this.securityEventService.onLoginErrorEvent().subscribe( error => { - console.error("Error authenticating:" + error); let message; const HTTP_UNAUTHORIZED = 401; const HTTP_FORBIDDEN = 403; @@ -58,28 +58,27 @@ export class LoginComponent implements OnInit, OnDestroy { switch (error.status) { case HTTP_UNAUTHORIZED: case HTTP_FORBIDDEN: - let forbiddenCode = error.message; - console.log("User forbiden code " + forbiddenCode); + const forbiddenCode = error.message; switch (forbiddenCode) { case USER_INACTIVE: - message = "The user is inactive. Please contact your administrator."; + message = 'The user is inactive. Please contact your administrator.'; break; case USER_SUSPENDED: - message = "The user is suspended. Please try again later or contact your administrator."; + message = 'The user is suspended. Please try again later or contact your administrator.'; break; default: - message = "The username/password combination you provided are not valid. Please try again or contact your administrator."; + message = 'The username/password combination you provided are not valid. Please try again or contact your administrator.'; // clear the password - this.model.password=''; + this.model.password = ''; break; } break; case HTTP_GATEWAY_TIMEOUT: case HTTP_NOTFOUND: - message = "Unable to login. SMP is not running."; + message = 'Unable to login. SMP is not running.'; break; default: - message = "Default error (" + error.status + ") occurred during login."; + message = 'Default error (' + error.status + ') occurred during login.'; break; } this.alertService.error(message); @@ -93,14 +92,13 @@ export class LoginComponent implements OnInit, OnDestroy { } verifyDefaultLoginUsed() { - let currentUser: User = this.securityService.getCurrentUser(); + const currentUser: User = this.securityService.getCurrentUser(); if (currentUser.defaultPasswordUsed) { this.dialog.open(DefaultPasswordDialogComponent); } } ngOnDestroy(): void { - console.log("Destroying login component"); this.sub.unsubscribe(); } } diff --git a/smp-angular/tslint.json b/smp-angular/tslint.json index bb84fcf3c..cd0e0dbce 100644 --- a/smp-angular/tslint.json +++ b/smp-angular/tslint.json @@ -1,7 +1,5 @@ { - "rulesDirectory": [ - "node_modules/codelyzer" - ], + "extends": ["codelyzer"], "rules": { "callable-types": true, "class-name": true, @@ -22,7 +20,7 @@ "label-position": true, "max-line-length": [ true, - 140 + 200 ], "member-access": false, "member-ordering": [ @@ -34,6 +32,7 @@ "no-bitwise": true, "no-console": [ true, + "log", "debug", "info", "time", @@ -98,8 +97,8 @@ "check-type" ], - "directive-selector": [true, "attribute", "app", "camelCase"], - "component-selector": [true, "element", "app", "kebab-case"], + "directive-selector": [true, "attribute", "smp", "camelCase"], + "component-selector": [true, "element", "smp", "kebab-case"], "use-input-property-decorator": true, "use-output-property-decorator": true, "use-host-property-decorator": true, diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/conversion/DBUserToUserROConverter.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/conversion/DBUserToUserROConverter.java index e3d69649b..030d29152 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/conversion/DBUserToUserROConverter.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/conversion/DBUserToUserROConverter.java @@ -3,11 +3,14 @@ package eu.europa.ec.edelivery.smp.conversion; 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.UserRO; +import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; import org.springframework.stereotype.Component; +import java.time.LocalDateTime; + /** * @author Sebastian-Ion TINCU */ @@ -24,7 +27,7 @@ public class DBUserToUserROConverter implements Converter<DBUser, UserRO> { target.setUsername(source.getUsername()); target.setRole(source.getRole()); target.setPassword(source.getPassword()); - target.setPasswordChanged(source.getPasswordChanged()); + target.setPasswordExpired(isPasswordExpired(source)); target.setActive(source.isActive()); target.setId(source.getId()); if (source.getCertificate() != null) { @@ -33,4 +36,17 @@ public class DBUserToUserROConverter implements Converter<DBUser, UserRO> { } return target; } + + private boolean isPasswordExpired(DBUser source) { + return StringUtils.isNotEmpty(source.getPassword()) + && (isPasswordRecentlyReset(source) || isPasswordChangedLongerThanThreeMonthsAgo(source)); + } + + private boolean isPasswordRecentlyReset(DBUser source) { + return source.getPasswordChanged() == null; + } + + private boolean isPasswordChangedLongerThanThreeMonthsAgo(DBUser source) { + return LocalDateTime.now().minusMonths(3).isAfter(source.getPasswordChanged()); + } } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/conversion/UserROToDBUserConverter.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/conversion/UserROToDBUserConverter.java index 7dc34f62a..ebd07559d 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/conversion/UserROToDBUserConverter.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/conversion/UserROToDBUserConverter.java @@ -19,18 +19,17 @@ public class UserROToDBUserConverter implements Converter<UserRO, DBUser> { @Override public DBUser convert(UserRO source) { - DBUser dro = new DBUser(); - dro.setEmailAddress(source.getEmailAddress()); - dro.setUsername(source.getUsername()); - dro.setRole(source.getRole()); - dro.setPassword(source.getPassword()); - dro.setActive(source.isActive()); - dro.setId(source.getId()); - dro.setPasswordChanged(source.getPasswordChanged()); + DBUser target = new DBUser(); + target.setEmailAddress(source.getEmailAddress()); + target.setUsername(source.getUsername()); + target.setRole(source.getRole()); + target.setPassword(source.getPassword()); + target.setActive(source.isActive()); + target.setId(source.getId()); if (source.getCertificate() != null) { DBCertificate certData = conversionService.convert(source.getCertificate(), DBCertificate.class); - dro.setCertificate(certData); + target.setCertificate(certData); } - return dro; + return target; } } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/UserRO.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/UserRO.java index 0204221bf..9b4827f48 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/UserRO.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/UserRO.java @@ -12,23 +12,18 @@ import java.util.List; */ public class UserRO extends BaseRO { + private static final long serialVersionUID = 2821447495333163882L; - - private static final long serialVersionUID = -4971552086560325302L; private String username; private String password; private String emailAddress; private List<String> authorities; - private LocalDateTime passwordChanged; private boolean active = true; private String role; private Long id; private CertificateRO certificate; private int statusPassword = EntityROStatus.PERSISTED.getStatusNumber(); - - public UserRO(){ - - } + private boolean passwordExpired; public Long getId() { return id; @@ -62,12 +57,12 @@ public class UserRO extends BaseRO { this.emailAddress = email; } - public LocalDateTime getPasswordChanged() { - return passwordChanged; + public boolean isPasswordExpired() { + return passwordExpired; } - public void setPasswordChanged(LocalDateTime passwordChanged) { - this.passwordChanged = passwordChanged; + public void setPasswordExpired(boolean passwordExpired) { + this.passwordExpired = passwordExpired; } public boolean isActive() { @@ -93,6 +88,7 @@ public class UserRO extends BaseRO { public void setCertificate(CertificateRO certificate) { this.certificate = certificate; } + public List<String> getAuthorities() { return authorities; } 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 d47773cb2..91ab340fa 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 @@ -75,13 +75,12 @@ public class UIUserService extends UIServiceBase<DBUser, UserRO> { } @Transactional - public void updateUserList(List<UserRO> lst) { - boolean suc = false; + public void updateUserList(List<UserRO> lst, LocalDateTime passwordChange) { for (UserRO userRO : lst) { if (userRO.getStatus() == EntityROStatus.NEW.getStatusNumber()) { DBUser dbUser = convertFromRo(userRO); if (!StringUtils.isBlank(userRO.getPassword())) { - dbUser.setPassword(BCryptPasswordHash.hashPassword(userRO.getPassword())); + dbUser.setPassword(BCryptPasswordHash.hashPassword(userRO.getPassword())); } userDao.persistFlushDetach(dbUser); } else if (userRO.getStatus() == EntityROStatus.UPDATED.getStatusNumber()) { @@ -93,10 +92,10 @@ public class UIUserService extends UIServiceBase<DBUser, UserRO> { if (StringUtils.isBlank(userRO.getUsername()) ){ // if username is empty than clear the password dbUser.setPassword(""); - } - // check for new password - else if (!StringUtils.isBlank(userRO.getPassword())) { + }else if (!StringUtils.isBlank(userRO.getPassword())) { + // check for new password dbUser.setPassword(BCryptPasswordHash.hashPassword(userRO.getPassword())); + dbUser.setPasswordChanged(passwordChange); } // update certificate data if (userRO.getCertificate() == null || StringUtils.isBlank(userRO.getCertificate().getCertificateId())) { diff --git a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/conversion/DBUserToUserROConverterTest.java b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/conversion/DBUserToUserROConverterTest.java new file mode 100644 index 000000000..b4c997865 --- /dev/null +++ b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/conversion/DBUserToUserROConverterTest.java @@ -0,0 +1,108 @@ +package eu.europa.ec.edelivery.smp.conversion; + +import eu.europa.ec.edelivery.smp.data.model.DBCertificate; +import eu.europa.ec.edelivery.smp.data.model.DBUser; +import eu.europa.ec.edelivery.smp.data.ui.UserRO; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.core.convert.ConversionService; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Sebastian-Ion TINCU + */ + +@RunWith(MockitoJUnitRunner.class) +public class DBUserToUserROConverterTest { + + private DBUser source; + + private UserRO target; + + @Mock + private ConversionService conversionService; + + @InjectMocks + private DBUserToUserROConverter converter = new DBUserToUserROConverter(); + + @Test + public void returnsThePasswordAsNotExpiredForCertificateOnlyUsers() { + givenAnExistingCertificateOnlyUser(); + + whenConvertingTheExistingUser(); + + thenThePasswordIsNotMarkedAsExpired("The password should have not been marked as expired when the user has no password"); + } + + @Test + public void returnsThePasswordAsExpiredWhenConvertingAnExistingUserThatHasAPasswordThatHasBeenRecentlyReset() { + givenAnExistingUserHavingAPasswordThatHasJustBeenReset(); + + whenConvertingTheExistingUser(); + + thenThePasswordIsMarkedAsExpired("The passwords should be marked as expired when converting users having passwords that have been reset by SystemAdministrators"); + } + + @Test + public void returnsThePasswordAsNotExpiredWhenConvertingAnExistingUserThatHasAPasswordChangedNoLongerThanThreeMonthsAgo() { + givenAnExistingUserHavingAPasswordThatChangedNoLongerThanThreeMonthsAgo(); + + whenConvertingTheExistingUser(); + + thenThePasswordIsNotMarkedAsExpired("The passwords should not be marked as expired when converting users having password they have changed in the previous 3 months"); + } + + @Test + public void returnsThePasswordAsExpiredWhenConvertingAnExistingUserThatHasAPasswordChangedMoreThanThreeMonthsAgo() { + givenAnExistingUserHavingAPasswordThatChangedMoreThanThreeMonthsAgo(); + + whenConvertingTheExistingUser(); + + thenThePasswordIsMarkedAsExpired("The passwords should be marked as expired when converting users having password they have changed more than 3 months ago"); + } + + private void givenAnExistingCertificateOnlyUser() { + givenAnExistingUser(null, null, new DBCertificate()); + } + + private void givenAnExistingUserHavingAPasswordThatHasJustBeenReset() { + givenAnExistingUser("password", null, null); + } + + private void givenAnExistingUserHavingAPasswordThatChangedNoLongerThanThreeMonthsAgo() { + givenAnExistingUser("password", LocalDateTime.now().minusMonths(2).minusDays(29), null); + } + + private void givenAnExistingUserHavingAPasswordThatChangedMoreThanThreeMonthsAgo() { + givenAnExistingUser("password", LocalDateTime.now().minusMonths(3).minusDays(10), null); + } + + private void givenAnExistingUser(String password, LocalDateTime passwordChange, DBCertificate certificate) { + source = new DBUser(); + source.setCertificate(certificate); + source.setPassword(password); + source.setPasswordChanged(passwordChange); + } + + private void whenConvertingTheExistingUser() { + target = converter.convert(source); + } + + private void thenThePasswordIsMarkedAsExpired(String failureDescription) { + assertThat(target.isPasswordExpired()) + .describedAs(failureDescription) + .isTrue(); + } + + private void thenThePasswordIsNotMarkedAsExpired(String failureDescription) { + assertThat(target.isPasswordExpired()) + .describedAs(failureDescription) + .isFalse(); + } +} \ No newline at end of file diff --git a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/conversion/UserROToDBUserConverterTest.java b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/conversion/UserROToDBUserConverterTest.java new file mode 100644 index 000000000..3ae5846a5 --- /dev/null +++ b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/conversion/UserROToDBUserConverterTest.java @@ -0,0 +1,54 @@ +package eu.europa.ec.edelivery.smp.conversion; + +import eu.europa.ec.edelivery.smp.data.model.DBUser; +import eu.europa.ec.edelivery.smp.data.ui.UserRO; +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.core.convert.ConversionService; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Sebastian-Ion TINCU + */ +@RunWith(MockitoJUnitRunner.class) +public class UserROToDBUserConverterTest { + + private UserRO source; + + private DBUser target; + + @Mock + private ConversionService conversionService; + + @InjectMocks + private UserROToDBUserConverter converter = new UserROToDBUserConverter(); + + @Test + public void doesNotSetPasswordChangedWhenConvertingUser() { + givenUser(); + + whenConvertingTheUserRoSource(); + + thenThePasswordChangeTimeIsNotSet(); + } + + private void givenUser() { + source = new UserRO(); + } + + private void whenConvertingTheUserRoSource() { + target = converter.convert(source); + } + + private void thenThePasswordChangeTimeIsNotSet() { + assertThat(target.getPasswordChanged()) + .describedAs("The last time the password changed should not be set by the converter as it is controlled when the user details are updated " + + "and it depends if it's done by the SystemAdministrators or by the users themselves") + .isNull(); + } +} \ No newline at end of file diff --git a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/ui/UIUserServiceIntegrationTest.java b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/ui/UIUserServiceIntegrationTest.java index 6791adeb2..5d9ce494f 100644 --- a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/ui/UIUserServiceIntegrationTest.java +++ b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/ui/UIUserServiceIntegrationTest.java @@ -95,7 +95,6 @@ public class UIUserServiceIntegrationTest extends AbstractServiceIntegrationTest assertNotNull(res.getServiceEntities().get(0).getUsername()); assertNotNull(res.getServiceEntities().get(0).getEmailAddress()); assertNull(res.getServiceEntities().get(0).getPassword()); // Service list must not return passwords - assertNotNull(res.getServiceEntities().get(0).getPasswordChanged()); assertNotNull(res.getServiceEntities().get(0).getRole()); } @@ -112,7 +111,7 @@ public class UIUserServiceIntegrationTest extends AbstractServiceIntegrationTest //when usr.setPassword(newPassword); usr.setStatus(EntityROStatus.UPDATED.getStatusNumber()); - testInstance.updateUserList(Collections.singletonList(usr)); + testInstance.updateUserList(Collections.singletonList(usr), null); // then DBUser dbuser = userDao.find(usr.getId()); @@ -134,7 +133,7 @@ public class UIUserServiceIntegrationTest extends AbstractServiceIntegrationTest user.setStatus(EntityROStatus.NEW.getStatusNumber()); //when - testInstance.updateUserList(Collections.singletonList(user)); + testInstance.updateUserList(Collections.singletonList(user), null); // then long iCntNew = userDao.getDataListCount(null); @@ -176,7 +175,7 @@ public class UIUserServiceIntegrationTest extends AbstractServiceIntegrationTest user.setStatus(EntityROStatus.NEW.getStatusNumber()); //when - testInstance.updateUserList(Collections.singletonList(user)); + testInstance.updateUserList(Collections.singletonList(user), null); // then long iCntNew = userDao.getDataListCount(null); @@ -222,7 +221,7 @@ public class UIUserServiceIntegrationTest extends AbstractServiceIntegrationTest user.setStatus(EntityROStatus.NEW.getStatusNumber()); //when - testInstance.updateUserList(Collections.singletonList(user)); + testInstance.updateUserList(Collections.singletonList(user), null); // then long iCntNew = userDao.getDataListCount(null); @@ -270,7 +269,7 @@ public class UIUserServiceIntegrationTest extends AbstractServiceIntegrationTest userRO.setCertificate(null); userRO.setStatus(EntityROStatus.UPDATED.getStatusNumber()); - testInstance.updateUserList(Collections.singletonList(userRO)); + testInstance.updateUserList(Collections.singletonList(userRO), null); // then ServiceResult<UserRO> res = testInstance.getTableList(-1,-1,null, null, null); @@ -291,7 +290,7 @@ public class UIUserServiceIntegrationTest extends AbstractServiceIntegrationTest user.setStatus(EntityROStatus.REMOVE.getStatusNumber()); //when - testInstance.updateUserList(Collections.singletonList(user)); + testInstance.updateUserList(Collections.singletonList(user), null); // then long iCntNew = userDao.getDataListCount(null); diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/UserResource.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/UserResource.java index 8fd5eeac0..6d5363676 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/UserResource.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/UserResource.java @@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.security.cert.CertificateException; +import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; @@ -73,7 +74,8 @@ public class UserResource { public UserRO updateCurrentUser(@PathVariable("id") Long id, @RequestBody UserRO user) { LOG.info("Update current user: {}", user); - uiUserService.updateUserList(Arrays.asList(user)); + // Update the user and mark the password as changed at this very instant of time + uiUserService.updateUserList(Arrays.asList(user), LocalDateTime.now()); DBUser updatedUser = uiUserService.findUser(id); UserRO userRO = uiUserService.convertToRo(updatedUser); @@ -85,7 +87,8 @@ public class UserResource { @Secured({SMPAuthority.S_AUTHORITY_TOKEN_SYSTEM_ADMIN}) public void updateUserList(@RequestBody UserRO[] updateEntities ){ LOG.info("Update user list, count: {}", updateEntities.length); - uiUserService.updateUserList(Arrays.asList(updateEntities)); + // Pass the users and mark the passwords of the ones being updated as expired by passing the passwordChange as null + uiUserService.updateUserList(Arrays.asList(updateEntities), null); } @PostMapping("/{id}/certdata") -- GitLab