diff --git a/changelog.txt b/changelog.txt index f728169b67df87917638ebe67519618a598f70b2..6504a0cad0b39437c22ae6775f530373fb972855 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,10 @@ eDelivery SMP 5.1 - Added the HTTP parameter 'Resource-Owner' as alternative to ServiceGroup-Owner +- added new properties: + smp.instance.name: The SMP instance name + smp.credentials.reset_request.url: The URL to reset the user password + smp.credentials.reset_request.url.validMinutes: The time in minutes the reset request is valid + eDelivery SMP 5.0 - removed: bdmsl.participant.multidomain.enabled - environment properties have now 'smp.' prefix diff --git a/domismp-tests/domismp-docker/compose/domismp-tomcat-mysql/docker-compose.yml b/domismp-tests/domismp-docker/compose/domismp-tomcat-mysql/docker-compose.yml index eb1cd7d7636f311d90863201473a7cc372a4c82a..503603ba78ec4dd2e343d25d9d5beceaf17e1d1d 100644 --- a/domismp-tests/domismp-docker/compose/domismp-tomcat-mysql/docker-compose.yml +++ b/domismp-tests/domismp-docker/compose/domismp-tomcat-mysql/docker-compose.yml @@ -29,9 +29,9 @@ services: volumes: - ./properties/db-scripts:/tmp/custom-data/ - ./properties/keystores:/tmp/keystores/ -# ports: + ports: # - "3908:3306" -# - "8982:8080" + - "8982:8080" # - "6902:6901" # - "8953:53" # - "5005:5005" @@ -43,11 +43,11 @@ services: - ./eulogin/init-data:/resources/ecas-mock-server # Map this folder to host to be able to change runtime data for manual testing! # - ./eulogin/ecas-mock-server:/data/ecas-mock-server -# ports: -# - "7102:7102" + ports: + - "7102:7102" mail-service: image: inbucket/inbucket:3.0.0 hostname: mail-server.smp.local -# ports: -# - "9005:9000" + ports: + - "9005:9000" diff --git a/domismp-tests/domismp-docker/functions/common.functions b/domismp-tests/domismp-docker/functions/common.functions index 9d618f277161db80994c17c0f97298c884035b13..ebaab5ec57df2c791030701e866cbb508a3f297d 100644 --- a/domismp-tests/domismp-docker/functions/common.functions +++ b/domismp-tests/domismp-docker/functions/common.functions @@ -4,6 +4,13 @@ # running Docker images for the integration tests. ################################################################ +FUNCTION_FOLDER="$(cd -P $( dirname "${BASH_SOURCE[0]}" ) && pwd)" +SMP_PROJECT_FOLDER=$(readlink -e "${FUNCTION_FOLDER}/../../..") +SMP_ARTEFACTS="${SMP_PROJECT_FOLDER}/smp-webapp/target" +SMP_SPRINGBOOT_ARTEFACTS="${SMP_PROJECT_FOLDER}/smp-springboot/target" +SMP_PLUGIN_EXAMPLE="${SMP_PROJECT_FOLDER}/smp-examples/smp-spi-payload-validation-example/target" +SMP_ARTEFACTS_CLEAR="false" + ################################################################ # Function exports the DomiSMP build specific artefact names. exportBuildArtefactNames() { @@ -34,6 +41,7 @@ function exportImageNames() { function initializeCommonVariables() { echo "initialize common variables" exportImageNames + discoverApplicationVersion BUILD_KEY=$(echo "${bamboo_buildResultKey:-test}" | tr '[:upper:]' '[:lower:]') PLAN_PREFIX="${COMPOSE_PROJECT_NAME}"-${BUILD_KEY} diff --git a/domismp-tests/domismp-docker/images/build-docker-images.sh b/domismp-tests/domismp-docker/images/build-docker-images.sh index 9405a317867c7e7e8e711ce1cda3b4d40285a4a3..ac6a3cb0653b18d75034dd4f45a632d1bae228a2 100755 --- a/domismp-tests/domismp-docker/images/build-docker-images.sh +++ b/domismp-tests/domismp-docker/images/build-docker-images.sh @@ -31,7 +31,6 @@ ORA_VERSION="11.2.0.2" ORA_EDITION="xe" ORA_SERVICE="xe" -SMP_VERSION= ORACLE_ARTEFACTS="/CEF/repo" SMP_PROJECT_FOLDER=$(readlink -e "${WORKDIR}/../../..") @@ -61,8 +60,6 @@ while getopts v:o:a:s:c:p: option; do esac done -# discover SMP version -discoverApplicationVersion echo "*****************************************************************" echo "* SMP artefact folders: [$SMP_ARTEFACTS], (Clear folder after build: [$SMP_ARTEFACTS_CLEAR] )" diff --git a/domismp-tests/domismp-docker/images/domismp-tomcat-mysql/build.sh b/domismp-tests/domismp-docker/images/domismp-tomcat-mysql/build.sh index 57aaae68e0339839ef3e528fa21c3944eb98c450..a8cabf183a21b9f8ea88a9a2173502a3999185d7 100755 --- a/domismp-tests/domismp-docker/images/domismp-tomcat-mysql/build.sh +++ b/domismp-tests/domismp-docker/images/domismp-tomcat-mysql/build.sh @@ -4,12 +4,16 @@ # first it copies external resources to resources folder # then it builds the image using docker-compose.build.yml # and finally it cleans the external resources +WORKING_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${WORKING_DIR}" +e +source "${WORKING_DIR}/../../functions/common.functions" +initializeCommonVariables + : "${SMP_PROJECT_FOLDER:?Need to set SMP project folder non-empty!}" : "${SMP_VERSION:?Need to set SMP version non-empty!}" : "${SMP_ARTEFACTS:?Need to set SMP_ARTEFACTS non-empty!}" -WORKING_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "${WORKING_DIR}" copyExternalImageResources() { echo "Copy test project resources ..." diff --git a/pom.xml b/pom.xml index f8a3ce98b9efcb4a37517864ee5a0247f998568d..7eacbe9a0bc65d406bb356b141c713d3c3ed5e2f 100644 --- a/pom.xml +++ b/pom.xml @@ -83,7 +83,6 @@ See the Licence for the specific language governing permissions and limitations <cxf-xjc-runtime.version>3.3.2</cxf-xjc-runtime.version> <cxf.version>3.5.7</cxf.version> <ehcache.version>2.10.9.2</ehcache.version> - <freemarker.version>2.3.32</freemarker.version> <h2.version>2.2.224</h2.version> <hamcrest-junit.version>2.0.0.0</hamcrest-junit.version> <hamcrest.version>2.2</hamcrest.version> @@ -256,11 +255,6 @@ See the Licence for the specific language governing permissions and limitations <artifactId>httpclient</artifactId> <version>${httpclient.version}</version> </dependency> - <dependency> - <groupId>org.freemarker</groupId> - <artifactId>freemarker</artifactId> - <version>${freemarker.version}</version> - </dependency> <dependency> <groupId>org.hibernate.javax.persistence</groupId> <artifactId>hibernate-jpa-2.1-api</artifactId> diff --git a/smp-angular/src/app/app-info/smp-info.model.ts b/smp-angular/src/app/app-info/smp-info.model.ts index f6818319c8bd32ee64abf6cb79a2be5c93d113b9..e323183eefeba0f13ec893c6d57e737ee757916d 100644 --- a/smp-angular/src/app/app-info/smp-info.model.ts +++ b/smp-angular/src/app/app-info/smp-info.model.ts @@ -4,4 +4,7 @@ export interface SmpInfo { authTypes?: string[]; ssoAuthenticationLabel?: string; ssoAuthenticationURI?: string; + passwordValidationRegExp?: string; + passwordValidationRegExpMessage?: string; + } diff --git a/smp-angular/src/app/app.component.ts b/smp-angular/src/app/app.component.ts index 21402f2d79faf6f50d8db513be1dd5fdd901562d..76f0c1f77b595bbce4b3a0bf00073ec0f87ac160 100644 --- a/smp-angular/src/app/app.component.ts +++ b/smp-angular/src/app/app.component.ts @@ -99,7 +99,7 @@ export class AppComponent { onDrawerContentScroll(scrollEvent: any){ let scrollTop = scrollEvent.srcElement.scrollTop; - this.alertService.setSticky(scrollTop > 0) + this.alertService.setKeepAfterNavigationChange(scrollTop > 0) } } diff --git a/smp-angular/src/app/app.module.ts b/smp-angular/src/app/app.module.ts index 28e510d58ebbd3c8edab03acb799743c289526a6..3a62f776bc78d83770585bc8e30a9fbac962a989 100644 --- a/smp-angular/src/app/app.module.ts +++ b/smp-angular/src/app/app.module.ts @@ -141,6 +141,7 @@ import { } from "./system-settings/admin-properties/property-details-dialog/property-details-dialog.component"; import {UserAlertsComponent} from "./user-settings/user-alerts/user-alerts.component"; import {SmpEditorComponent} from "./common/components/smp-editor/smp-editor.component"; +import {ResetCredentialComponent} from "./security/reset-credential/reset-credential.component"; @NgModule({ @@ -198,6 +199,7 @@ import {SmpEditorComponent} from "./common/components/smp-editor/smp-editor.comp PasswordChangeDialogComponent, PropertyComponent, PropertyDetailsDialogComponent, + ResetCredentialComponent, ResourceDetailsDialogComponent, ResourceDetailsPanelComponent, ResourceDialogComponent, diff --git a/smp-angular/src/app/app.routes.ts b/smp-angular/src/app/app.routes.ts index 4fa4c22fb38bff1188bc6814fd96aa51a69b21fc..904ef73261435712e1bd3e1321d4b3f6ee674235 100644 --- a/smp-angular/src/app/app.routes.ts +++ b/smp-angular/src/app/app.routes.ts @@ -21,6 +21,7 @@ import {authorizeChildSystemAdminGuard} from "./guards/authorize-child-system-ad import {activateChildResourceGuard} from "./guards/activate-child-document.guard"; import {UserAlertsComponent} from "./user-settings/user-alerts/user-alerts.component"; import {AdminAlertsComponent} from "./system-settings/admin-alerts/admin-alerts.component"; +import {ResetCredentialComponent} from "./security/reset-credential/reset-credential.component"; const appRoutes: Routes = [ @@ -28,6 +29,7 @@ const appRoutes: Routes = [ {path: '', component: ResourceSearchComponent}, {path: 'search', redirectTo: ''}, {path: 'login', component: LoginComponent}, + {path: 'reset-credential/:resetToken', component: ResetCredentialComponent}, { path: 'edit', canActivateChild: [authenticationGuard], diff --git a/smp-angular/src/app/common/alert-message/alert-message.component.html b/smp-angular/src/app/common/alert-message/alert-message.component.html index f16cb684cf8c90f2f19e10def611fd26cadae4b0..87883c0bf9833afb25c4ea68b1ff378968713f91 100644 --- a/smp-angular/src/app/common/alert-message/alert-message.component.html +++ b/smp-angular/src/app/common/alert-message/alert-message.component.html @@ -1,6 +1,8 @@ <div #alertMessage class="mat-elevation-z8" *ngIf="message" [ngClass]="{ 'alert-message': message, 'alert-success': message.type === 'success', + 'alert-info': message.type === 'info', + 'alert-warning': message.type === 'warning', 'alert-error': message.type === 'error'}" id="alertmessage_id"> <span class="closebtn" (click)="clearAlert(true)">×</span> diff --git a/smp-angular/src/app/common/alert-message/alert-message.component.ts b/smp-angular/src/app/common/alert-message/alert-message.component.ts index 3c750bea7877d484a3abacadff5f9b994a5952be..35f3e179a773b31f7dbb3d88a92915e12233d3e6 100644 --- a/smp-angular/src/app/common/alert-message/alert-message.component.ts +++ b/smp-angular/src/app/common/alert-message/alert-message.component.ts @@ -2,16 +2,21 @@ import {Component, OnDestroy, OnInit} from '@angular/core'; import {AlertMessageService} from './alert-message.service'; import {Subscription} from "rxjs"; + +/** + * This component is used to display alert messages/notifications on the top of the page in an overlay (growl). + * In case of success messages, the message will be displayed for a certain amount of time and it will automatically disappear + * unless sticky flat is set to true. + * + * The messages can be of different types: success, error, info, warning. + */ @Component({ selector: 'alert', templateUrl: './alert-message.component.html', styleUrls: ['./alert-message.component.css'] }) - export class AlertMessageComponent implements OnInit, OnDestroy { - readonly successTimeout: number = 3000; - message: any; private subscription: Subscription; @@ -20,23 +25,25 @@ export class AlertMessageComponent implements OnInit, OnDestroy { } ngOnInit() { - this.subscription = this.alertService.getMessage().subscribe(message => { this.showMessage(message); }); + this.subscription = this.alertService.getMessage().subscribe(message => { + this.showMessage(message); + }); } ngOnDestroy(): void { this.subscription.unsubscribe(); } - clearAlert(force = false):void { + clearAlert(force = false): void { this.alertService.clearAlert(force); } showMessage(message: any) { this.message = message; - if (message && message.type && message.type === 'success') { + if (message && message.timeoutInSeconds && message.timeoutInSeconds > 0) { setTimeout(() => { this.clearAlert(); - }, this.successTimeout); + }, this.message.timeoutInSeconds * 1000); } } } diff --git a/smp-angular/src/app/common/alert-message/alert-message.service.ts b/smp-angular/src/app/common/alert-message/alert-message.service.ts index 335493add72145eaabc22c5d01ccd4ac5628d629..09e66f237c487b9c52ec8dcb887efb15c5e7d754 100644 --- a/smp-angular/src/app/common/alert-message/alert-message.service.ts +++ b/smp-angular/src/app/common/alert-message/alert-message.service.ts @@ -1,30 +1,40 @@ import {Injectable} from '@angular/core'; import {NavigationEnd, NavigationStart, Router} from '@angular/router'; import {Observable, Subject} from 'rxjs'; +import {HttpErrorResponse} from "@angular/common/http"; + +/** + * AlertMessageRO is the object that will be used to display the message in the SMP alert component in overlay. + * These messages are not the same to SMP alerts, which are used to display alert the page. + * It contains the type of the message (success, error, info, warning), the text of the message and the timeout in seconds. + */ +export interface AlertMessageRO { + type: string, + text: string, + timeoutInSeconds?: number +} @Injectable() export class AlertMessageService { - private subject = new Subject<any>(); - - private previousRoute = ''; - private sticky = false; + // the default timeout duration + readonly DEFAULT_TIMEOUT: number = 3; - private message: { type: string, text: string }; + private subject = new Subject<any>(); + private previousRoute:string = ''; + private keepAfterNavigationChange:boolean = false; + private message: AlertMessageRO; - //TODO move the logic in the ngInit block - constructor (private router: Router) { + constructor(private router: Router) { // clear alert message on route change router.events.subscribe(event => { if (event instanceof NavigationStart) { if (this.isRouteChanged(event.url)) { this.clearAlert(); - } else { - console.log('Alert after when navigating from [' + this.previousRoute + '] to [' + event.url + ']'); } } else if (event instanceof NavigationEnd) { this.previousRoute = event.url; - if (this.sticky) { + if (this.keepAfterNavigationChange) { this.displayCurrentMessage(); this.reset(); } @@ -32,56 +42,102 @@ export class AlertMessageService { }); } - private reset () { - this.sticky = false; + private reset() { + this.keepAfterNavigationChange = false; this.message = null; } - getPath (url: string): string { + getPath(url: string): string { const parser = document.createElement('a'); parser.href = url; return parser.pathname; } - isRouteChanged (currentRoute: string): boolean { + isRouteChanged(currentRoute: string): boolean { let previousRoutePath = this.getPath(this.previousRoute); let currentRoutePath = this.getPath(currentRoute); return previousRoutePath !== currentRoutePath; } - clearAlert (force = false): void { - if (!force && this.sticky) { + clearAlert(force = false): void { + if (!force && this.keepAfterNavigationChange) { return; } this.subject.next(null); } - setSticky (sticky: boolean) { - this.sticky = sticky; + setKeepAfterNavigationChange(keepAfterNavigation: boolean) { + this.keepAfterNavigationChange = keepAfterNavigation; } - displayCurrentMessage () { + displayCurrentMessage() { this.subject.next(this.message); } - success (message: string, keepAfterNavigationChange = false) { - this.setSticky(keepAfterNavigationChange); - this.message = {type: 'success', text: message}; - this.displayCurrentMessage(); + /** + * Extract the message from the object return it as a string. The object can be a string or an HttpErrorResponse + * + * @param messageObject + */ + getObjectMessage(messageObject: any): string { + let message = 'An error occurred'; + if (typeof messageObject === 'string') { + return messageObject; + } + if (messageObject instanceof HttpErrorResponse) { + return this.getHttpErrorResponseMessage(messageObject); + } } - error (message: string, keepAfterNavigationChange = false) { - this.setSticky(keepAfterNavigationChange); - this.message = {type: 'error', text: message}; + /** + * Extract the message from the HttpErrorResponse and return it as a string + * @param httpErrorResponse + */ + getHttpErrorResponseMessage(httpErrorResponse: HttpErrorResponse): string { + let message: string; + if (httpErrorResponse.error) { + if (httpErrorResponse.error.errorDescription) { + message = httpErrorResponse.error.errorDescription; + } else if (httpErrorResponse.error.message) { + message = httpErrorResponse.error.message; + } + } else { + message = httpErrorResponse.statusText + ': ' + httpErrorResponse.message; + } + return message; + } + + /** + * Show a message of a given type + * @param messageObject The message to display + * @param type The type of the message (success, error, info, warning) + * @param keepAfterNavigationChange If true, the message will be displayed after a navigation changed + */ + showMessage(messageObject: any, type: string, keepAfterNavigationChange:boolean = false, timeoutInSeconds: number = null) { + this.setKeepAfterNavigationChange(keepAfterNavigationChange); + this.message = { + type: type, + text: this.getObjectMessage(messageObject), + // if the timeoutInSeconds is not set, we use the default timeout for success messages + timeoutInSeconds: !timeoutInSeconds && type =="success" ?this.DEFAULT_TIMEOUT: timeoutInSeconds + }; this.displayCurrentMessage(); } - exception (message: string, error: any, keepAfterNavigationChange = false): void { - const errMsg = error.message || (error.json ? error.json().message : error ); + success(message: string, keepAfterNavigationChange = false, timeoutInSeconds: number = null) { + this.showMessage(message, 'success', keepAfterNavigationChange, timeoutInSeconds); + } + + error(message: any, keepAfterNavigationChange = false, timeoutInSeconds: number = null) { + this.showMessage(message, 'error', keepAfterNavigationChange, timeoutInSeconds); + } + + exception(message: string, error: any, keepAfterNavigationChange = false): void { + const errMsg = error.message || (error.json ? error.json().message : error); this.error(message + ' \n' + errMsg, keepAfterNavigationChange); } - getMessage (): Observable<any> { + getMessage(): Observable<any> { return this.subject.asObservable(); } } diff --git a/smp-angular/src/app/login/login.component.css b/smp-angular/src/app/login/login.component.css index bb1da45b6e5c3d2d6c64b0911b4149e3e5346221..a55eafa0c9e28f96d33ac2bc638964dce938cfdf 100644 --- a/smp-angular/src/app/login/login.component.css +++ b/smp-angular/src/app/login/login.component.css @@ -3,4 +3,7 @@ a:visited { text-decoration: none; } a:hover { text-decoration: none; } a:active { text-decoration: none; } - +.form-control { + margin: 10px; + padding: 0; +} diff --git a/smp-angular/src/app/login/login.component.html b/smp-angular/src/app/login/login.component.html index 632241d25f489baf026fb09449fde8c045feaaee..22adfe456c3da5b5c2d9bd3ddc8cdc1da2c97f8c 100644 --- a/smp-angular/src/app/login/login.component.html +++ b/smp-angular/src/app/login/login.component.html @@ -1,7 +1,8 @@ <div id="page" class="login-page" [style]="'justify-content:center; align-items:center; height:100%'"> <div fxLayout="row" [style]="'justify-content:center; align-items:center; height:100%'"> - <mat-card *ngIf="isUserAuthSSOEnabled() == true" fxFlex="400px" [style]="'width:400px;height:300px;margin:10px'"> - <mat-card-title class="title-panel" >SSO Login: {{lookups.cachedApplicationInfo.ssoAuthenticationLabel}}</mat-card-title> + <mat-card *ngIf="isUserAuthSSOEnabled() == true" fxFlex="400px" [style]="'width:400px;height:240px;margin:10px'"> + <mat-card-title class="title-panel">SSO Login: {{ lookups.cachedApplicationInfo.ssoAuthenticationLabel }} + </mat-card-title> <mat-card-content style="align-items: center;justify-content: center;display: flex;height: 200px;"> <a mat-raised-button color="primary" href="{{lookups.cachedApplicationInfo.ssoAuthenticationURI}}" [style]="'width:150px'"> @@ -10,32 +11,62 @@ </a> </mat-card-content> </mat-card> - <mat-card *ngIf="isUserAuthPasswdEnabled() == true" fxFlex="400px" [style]="'width:400px;height:300px;margin:10px;'"> + <mat-card *ngIf="isUserAuthPasswdEnabled() == true"> <mat-card-title class="title-panel">SMP Login</mat-card-title> - <mat-card-content style="align-items: center;justify-content: center;display: flex;flex-direction:column; height: 200px;"> - <form name="loginForm" #loginForm="ngForm" (ngSubmit)="login()"> - <mat-form-field style="width: 100%"> - <mat-label>Username</mat-label> - <input matInput placeholder="Username" name="username" [(ngModel)]="model.username" - #username="ngModel" - id="username_id" - auto-focus-directive - required> - </mat-form-field> - <mat-form-field style="width: 100%"> - <mat-label>Password</mat-label> - <input type="password" matInput placeholder="Password" name="password" [(ngModel)]="model.password" - #password="ngModel" required id="password_id"> - </mat-form-field> - - <button mat-raised-button color="primary" [disabled]="!loginForm.form.valid" id="loginbutton_id" - [style]="'width:150px'"> - <mat-icon>input</mat-icon> - <span> Login</span> - </button> - </form> + <mat-card-content style="width:380px; height: 260px;margin: 15px"> + <mat-tab-group> + <mat-tab label="Login"> + <ng-container *ngTemplateOutlet="loginFormContainer"></ng-container> + </mat-tab> + <mat-tab label="Request password reset"> + <ng-container *ngTemplateOutlet="requestResetContainer"></ng-container> + </mat-tab> + </mat-tab-group> </mat-card-content> </mat-card> </div> <footer></footer> </div> + + +<ng-template #loginFormContainer> + <div [formGroup]="loginForm" class="form-control"> + <mat-form-field style="width: 100%"> + <mat-label>Username</mat-label> + <input matInput id="username_id" + formControlName="username" + auto-focus-directive + required> + </mat-form-field> + <mat-form-field style="width: 100%"> + <mat-label>Password</mat-label> + <input matInput id="password_id" type="password" + formControlName="password" required> + </mat-form-field> + <button mat-raised-button color="primary" id="loginbutton_id" [disabled]="!loginForm.valid" + (click)="login()" + [style]="'width:250px'"> + <mat-icon>input</mat-icon> + <span> Login</span> + </button> + </div> +</ng-template> + +<ng-template #requestResetContainer> + <div [formGroup]="resetForm" class="form-control"> + <mat-form-field style="width: 100%"> + <mat-label>Username</mat-label> + <input matInput id="reset_username_id" + formControlName="resetUsername" + auto-focus-directive + required> + </mat-form-field> + <button mat-raised-button color="primary" id="resetbutton_id" [disabled]="!resetForm.valid" + [style]="'width:250px'" + (click)="requestCredentialReset()" + > + <mat-icon>input</mat-icon> + <span> Request reset password</span> + </button> + </div> +</ng-template> diff --git a/smp-angular/src/app/login/login.component.ts b/smp-angular/src/app/login/login.component.ts index 9755a5313c6464a6761cf85b1b0ff41973a6a786..8d654d359f03a5b6a3a7e800424ffb62d26814a5 100644 --- a/smp-angular/src/app/login/login.component.ts +++ b/smp-angular/src/app/login/login.component.ts @@ -1,5 +1,5 @@ import {Component, OnDestroy, OnInit} from '@angular/core'; -import {Router, ActivatedRoute} from '@angular/router'; +import {ActivatedRoute, Router} from '@angular/router'; import {SecurityService} from '../security/security.service'; import {AlertMessageService} from '../common/alert-message/alert-message.service'; import {SecurityEventService} from '../security/security-event.service'; @@ -7,12 +7,15 @@ import {User} from '../security/user.model'; import {MatDialog} from '@angular/material/dialog'; import {DefaultPasswordDialogComponent} from 'app/security/default-password-dialog/default-password-dialog.component'; import {Subscription} from 'rxjs'; -import {ExpiredPasswordDialogComponent} from '../common/dialogs/expired-password-dialog/expired-password-dialog.component'; +import { + ExpiredPasswordDialogComponent +} from '../common/dialogs/expired-password-dialog/expired-password-dialog.component'; import {GlobalLookups} from "../common/global-lookups"; import {PasswordChangeDialogComponent} from "../common/dialogs/password-change-dialog/password-change-dialog.component"; import {InformationDialogComponent} from "../common/dialogs/information-dialog/information-dialog.component"; import {formatDate} from "@angular/common"; import {EntityStatus} from "../common/enums/entity-status.enum"; +import {FormControl, FormGroup, Validators} from "@angular/forms"; @Component({ templateUrl: './login.component.html', @@ -20,7 +23,8 @@ import {EntityStatus} from "../common/enums/entity-status.enum"; }) export class LoginComponent implements OnInit, OnDestroy { - model: any = {}; + loginForm: FormGroup; + resetForm: FormGroup; loading = false; returnUrl: string; sub: Subscription; @@ -35,14 +39,21 @@ export class LoginComponent implements OnInit, OnDestroy { private dialog: MatDialog) { } + ngOnInit() { + this.initForm(); this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; this.sub = this.securityEventService.onLoginSuccessEvent().subscribe( user => { if (user && user.passwordExpired) { if (user.forceChangeExpiredPassword) { - this.dialog.open(PasswordChangeDialogComponent, {data: {user:user,adminUser:false}}).afterClosed().subscribe(res => + this.dialog.open(PasswordChangeDialogComponent, { + data: { + user: user, + adminUser: false + } + }).afterClosed().subscribe(res => this.securityService.finalizeLogout(res) ); } else { @@ -66,7 +77,7 @@ export class LoginComponent implements OnInit, OnDestroy { switch (error.status) { case HTTP_UNAUTHORIZED: message = error.error.errorDescription; - this.model.password = ''; + this.loginForm['password'].setValue(''); break; case HTTP_FORBIDDEN: const forbiddenCode = error.message; @@ -77,7 +88,7 @@ export class LoginComponent implements OnInit, OnDestroy { default: message = error.status + ' The username/password combination you provided are not valid. Please try again or contact your administrator.'; // clear the password - this.model.password = ''; + this.loginForm['password'].setValue(''); break; } break; @@ -93,40 +104,50 @@ export class LoginComponent implements OnInit, OnDestroy { }); } + private initForm() { + this.loginForm = new FormGroup({ + username: new FormControl('', Validators.required), + password: new FormControl('', Validators.required), + }); + + this.resetForm = new FormGroup({ + resetUsername: new FormControl('', Validators.required), + }); + + } + login() { - // clear alerts this.alertService.clearAlert(); - this.securityService.login(this.model.username, this.model.password); + if (this.loginForm.valid) { + this.securityService.login( + this.loginForm.get('username').value, + this.loginForm.get('password').value + ); + } else { + this.alertService.error('Please enter a valid username and password.'); + } + } + + requestCredentialReset() { + this.alertService.clearAlert(); + if (this.resetForm.valid) { + this.securityService.requestCredentialReset( + this.resetForm.get('resetUsername').value + ); + } else { + this.alertService.error('Please enter a valid username.'); + } } showWarningBeforeExpire(user: User) { this.dialog.open(InformationDialogComponent, { data: { title: "Warning! Your password is about to expire", - description: "Your password is about to expire on " + formatDate(user.passwordExpireOn,"longDate","en-US")+"! Please change the password before the expiration date!" + description: "Your password is about to expire on " + formatDate(user.passwordExpireOn, "longDate", "en-US") + "! Please change the password before the expiration date!" } }).afterClosed().subscribe(() => this.router.navigate([this.returnUrl])); } - verifyDefaultLoginUsed() { - const currentUser: User = this.securityService.getCurrentUser(); - if (currentUser.defaultPasswordUsed) { - this.dialog.open(DefaultPasswordDialogComponent); - } - } - - private convertWithMode(config) { - return (config && config.data) - ? { - ...config, - data: { - ...config.data, - mode: config.data.mode || (config.data.edit ? EntityStatus.PERSISTED :EntityStatus.NEW) - } - } - : config; - } - ngOnDestroy(): void { this.sub.unsubscribe(); } diff --git a/smp-angular/src/app/security/reset-credential/reset-credential.component.css b/smp-angular/src/app/security/reset-credential/reset-credential.component.css new file mode 100644 index 0000000000000000000000000000000000000000..a55eafa0c9e28f96d33ac2bc638964dce938cfdf --- /dev/null +++ b/smp-angular/src/app/security/reset-credential/reset-credential.component.css @@ -0,0 +1,9 @@ +a:link { text-decoration: none; } +a:visited { text-decoration: none; } +a:hover { text-decoration: none; } +a:active { text-decoration: none; } + +.form-control { + margin: 10px; + padding: 0; +} diff --git a/smp-angular/src/app/security/reset-credential/reset-credential.component.html b/smp-angular/src/app/security/reset-credential/reset-credential.component.html new file mode 100644 index 0000000000000000000000000000000000000000..6ca111f0e3a0eebfd80e0ccfabb67153987393a0 --- /dev/null +++ b/smp-angular/src/app/security/reset-credential/reset-credential.component.html @@ -0,0 +1,56 @@ +<div id="page" class="login-page" [style]="'justify-content:center; align-items:center; height:100%'"> + <div fxLayout="row" [style]="'justify-content:center; align-items:center; height:100%'"> + <div [formGroup]="resetForm" class="form-control"> + + <div style="display:flex;flex-direction: column;"> + <mat-form-field style="width:100%"> + <mat-label>Enter Username</mat-label> + <input matInput formControlName="resetUsername" id="reset_username_id" required auto-focus-directive> + </mat-form-field> + + <mat-form-field style="width:100%"> + <mat-label>New Password</mat-label> + <input matInput [type]="hideNewPwdFiled ? 'password' : 'text'" + formControlName="new-password" id="np_id" required> + <mat-icon matSuffix + (click)="hideNewPwdFiled = !hideNewPwdFiled">{{ hideNewPwdFiled ? 'visibility_off' : 'visibility' }} + </mat-icon> + <smp-field-error *ngIf="passwordError('new-password', 'error')">New password must not be equal than old + current + password! + </smp-field-error> + <smp-field-error *ngIf="passwordError('new-password', 'pattern')">{{ passwordValidationMessage }} + </smp-field-error> + </mat-form-field> + <mat-form-field style="width:100%"> + <mat-label>Confirm New Password</mat-label> + <input matInput [type]="hideConfPwdFiled ? 'password' : 'text'" + formControlName="confirm-new-password" id="cnp_id" required> + <mat-icon matSuffix + (click)="hideConfPwdFiled = !hideConfPwdFiled">{{ hideConfPwdFiled ? 'visibility_off' : 'visibility' }} + </mat-icon> + <smp-field-error *ngIf="passwordError('confirm-new-password', 'error')">Confirm valued does not match new + password! + </smp-field-error> + </mat-form-field> + + <div class="required-fields">* required fields</div> + </div> + <mat-toolbar-row> + <button id="changeCurrentUserPasswordButton" mat-raised-button color="primary" + (click)="resetPassword()" + [disabled]="!resetForm.valid "> + <mat-icon>check_circle</mat-icon> + <span>Set/change password</span> + </button> + <button id="closeDialogButton" mat-raised-button color="primary" + (click)="cancel()" + mat-dialog-close> + <mat-icon>cancel</mat-icon> + <span>Cancel</span> + </button> + </mat-toolbar-row> + </div> + </div> + <footer></footer> +</div> 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 new file mode 100644 index 0000000000000000000000000000000000000000..0cc71994349c489660301ad59cc7a2e1907c255b --- /dev/null +++ b/smp-angular/src/app/security/reset-credential/reset-credential.component.ts @@ -0,0 +1,72 @@ +import {Component, OnInit} from '@angular/core'; +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 {SecurityService} from "../security.service"; + +@Component({ + templateUrl: './reset-credential.component.html', + styleUrls: ['./reset-credential.component.css'] +}) +export class ResetCredentialComponent implements OnInit { + + resetForm: FormGroup; + resetToken: string + + hideNewPwdFiled: boolean = true; + hideConfPwdFiled: boolean = true; + + constructor(private router: Router, + private activatedRoute: ActivatedRoute, + private lookups: GlobalLookups, + private securityService: SecurityService) { + } + + + ngOnInit() { + this.initForm(); + this.resetToken = this.activatedRoute.snapshot.params['resetToken']; + } + + private initForm() { + + let newPasswdFormControl: UntypedFormControl = new UntypedFormControl({value: null, readonly: false}, + [Validators.required, Validators.pattern(this.passwordValidationRegExp)]); + 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), + 'new-password': newPasswdFormControl, + 'confirm-new-password': confirmNewPasswdFormControl + }); + + this.resetForm.controls['resetUsername'].setValue(""); + this.resetForm.controls['new-password'].setValue(""); + this.resetForm.controls['confirm-new-password'].setValue(""); + } + + public passwordError = (controlName: string, errorName: string) => { + return this.resetForm.controls[controlName].hasError(errorName); + } + + get passwordValidationMessage() { + return this.lookups.cachedApplicationInfo?.passwordValidationRegExpMessage; + } + + + get passwordValidationRegExp() { + return this.lookups.cachedApplicationInfo?.passwordValidationRegExp; + } + + public resetPassword() { + this.securityService.credentialReset(this.resetForm.controls['resetUsername'].value, + this.resetToken, + this.resetForm.controls['new-password'].value,); + } + + public cancel() { + this.router.navigate(['/search']); + } +} diff --git a/smp-angular/src/app/security/security.service.ts b/smp-angular/src/app/security/security.service.ts index e96b7d035082c4cd563ea8bc3a865f4e2c13c115..1357e43d2e8f343e0cc48f7d437b378e88cb533e 100644 --- a/smp-angular/src/app/security/security.service.ts +++ b/smp-angular/src/app/security/security.service.ts @@ -33,13 +33,51 @@ export class SecurityService { password: password }), {headers}) - .subscribe((response: User) => { + .subscribe({ + next: (response: User) => { this.updateUserDetails(response); this.securityEventService.notifyLoginSuccessEvent(response); }, - (error: any) => { + error: (error: any) => { + this.alertService.error(error.error?.errorDescription) this.securityEventService.notifyLoginErrorEvent(error); - }); + } + }); + } + + requestCredentialReset(userid: string) { + let headers: HttpHeaders = new HttpHeaders({'Content-Type': 'application/json'}); + return this.http.post<User>(SmpConstants.REST_PUBLIC_SECURITY_RESET_CREDENTIALS_REQUEST, + JSON.stringify({ + credentialName: userid, + credentialType: 'USERNAME_PASSWORD', + }), + {headers}) + .subscribe({ + complete: () => { }, // completeHandler + error: (error: any) => {this.alertService.error(error) }, // errorHandler + next: () => { this.alertService.success("A confirmation email has been sent to your registered email address for user ["+userid+"]. " + + "Please follow the instructions in the email to complete the account reset process. " + + "If you did not receive mail try later or contact administrator ", true, -1); + this.router.navigate(['/search']);} + }); + } + + credentialReset(userid: string, token: string, newPassword: string) { + let headers: HttpHeaders = new HttpHeaders({'Content-Type': 'application/json'}); + return this.http.post<User>(SmpConstants.REST_PUBLIC_SECURITY_RESET_CREDENTIALS, + JSON.stringify({ + credentialName: userid, + credentialValue: newPassword, + credentialType: 'USERNAME_PASSWORD', + resetToken: token, + }), + {headers}) + .subscribe({ + complete: () => { this.router.navigate(['/login']); }, // completeHandler + error: (error: any) => {this.alertService.error(error);this.router.navigate(['/login']); }, // errorHandler + next: () => { this.alertService.success("Password has been reset successfully. Please login with new password", true, -1);} + }); } refreshLoggedUserFromServer() { diff --git a/smp-angular/src/app/smp.constants.ts b/smp-angular/src/app/smp.constants.ts index 4b5364f18acba3c8067b030220f859bad397b1bb..23e75fb4bd6a59c1cd3d31be991de18acd08f2bc 100644 --- a/smp-angular/src/app/smp.constants.ts +++ b/smp-angular/src/app/smp.constants.ts @@ -149,7 +149,10 @@ export class SmpConstants { // public authentication services public static readonly REST_PUBLIC_SECURITY = SmpConstants.REST_PUBLIC + 'security/'; - public static readonly REST_PUBLIC_SECURITY_AUTHENTICATION = SmpConstants.REST_PUBLIC_SECURITY + 'authentication'; + public static readonly REST_PUBLIC_SECURITY_AUTHENTICATION = SmpConstants.REST_PUBLIC_SECURITY + 'authentication' + public static readonly REST_PUBLIC_SECURITY_RESET_CREDENTIALS_REQUEST = SmpConstants.REST_PUBLIC_SECURITY + 'request-reset-credential'; + public static readonly REST_PUBLIC_SECURITY_RESET_CREDENTIALS = SmpConstants.REST_PUBLIC_SECURITY + 'reset-credential'; + public static readonly REST_PUBLIC_SECURITY_USER = SmpConstants.REST_PUBLIC_SECURITY + 'user'; //------------------------------ diff --git a/smp-angular/src/index.html b/smp-angular/src/index.html index 3e795761c8c839b6c21c0062fe5013d55df1065d..f85af98deaed4b3574f8813d6c0c9d9fb3f87bf2 100644 --- a/smp-angular/src/index.html +++ b/smp-angular/src/index.html @@ -11,7 +11,6 @@ <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> <link href="assets/stylesheets/angularMaterial2Icons.css" rel="stylesheet"> - <link href="assets/stylesheets/codeMirror.css" rel="stylesheet"> </head> <body class="mat-app-background"> <app-root>Loading...</app-root> diff --git a/smp-server-library/pom.xml b/smp-server-library/pom.xml index 59a9b583d9effe34e32ea465b25779752c09ceae..a0702d784661dfc71322c4f920d6d062273b6cc3 100644 --- a/smp-server-library/pom.xml +++ b/smp-server-library/pom.xml @@ -80,10 +80,6 @@ <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> </dependency> - <dependency> - <groupId>org.freemarker</groupId> - <artifactId>freemarker</artifactId> - </dependency> <dependency> <groupId>com.sun.mail</groupId> <artifactId>javax.mail</artifactId> diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/config/ServicesBeansConfiguration.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/config/ServicesBeansConfiguration.java index b96effa60e6af288675fdc120ec0e3f780c89aad..360849ee0e1100c64a4c741506276c0017442d0d 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/config/ServicesBeansConfiguration.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/config/ServicesBeansConfiguration.java @@ -8,9 +8,9 @@ * versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: - * + * * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * + * * Unless required by applicable law or agreed to in writing, software distributed under the Licence is * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Licence for the specific language governing permissions and limitations under the Licence. @@ -18,12 +18,10 @@ */ package eu.europa.ec.edelivery.smp.config; -import freemarker.cache.ClassTemplateLoader; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.mail.javamail.JavaMailSenderImpl; -import org.springframework.ui.freemarker.FreeMarkerConfigurationFactoryBean; @Configuration @ComponentScan(basePackages = { @@ -40,11 +38,4 @@ public class ServicesBeansConfiguration { return new JavaMailSenderImpl(); } - @Bean - public FreeMarkerConfigurationFactoryBean freeMarkerConfigurationFactoryBean() { - final FreeMarkerConfigurationFactoryBean freeMarkerConfigurationFactoryBean = - new FreeMarkerConfigurationFactoryBean(); - freeMarkerConfigurationFactoryBean.setPreTemplateLoaders(new ClassTemplateLoader(ServicesBeansConfiguration.class, "/alert-mail-templates")); - return freeMarkerConfigurationFactoryBean; - } } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/config/enums/SMPPropertyEnum.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/config/enums/SMPPropertyEnum.java index beefba86ee3d740251b61f3ce61ba09c2b5cbc30..7a8bec80e9b843ce110d499a62e295374be661a7 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/config/enums/SMPPropertyEnum.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/config/enums/SMPPropertyEnum.java @@ -383,10 +383,20 @@ public enum SMPPropertyEnum { OPTIONAL, NOT_ENCRYPTED, NO_RESTART_NEEDED, INTEGER), SMP_ALERT_MAIL_FROM("smp.alert.mail.from", "test@alert-send-mail.eu", "Alert send mail", OPTIONAL, NOT_ENCRYPTED, NO_RESTART_NEEDED, EMAIL), + + + SMP_INSTANCE_NAME("smp.instance.name", "Test DomiSMP Instance", "The name of the SMP instance", + OPTIONAL, NOT_ENCRYPTED, NO_RESTART_NEEDED, STRING), + + CREDENTIALS_RESET_URL("smp.credentials.reset_request.url", null, "If null then the reset url is created using the the proxy headers.", + OPTIONAL, NOT_ENCRYPTED, NO_RESTART_NEEDED, URL), + + CREDENTIALS_RESET_POLICY_VALID_DAYS("smp.credentials.reset_request.url.validMinutes", "90", "Number of minutes token is valid", + OPTIONAL, NOT_ENCRYPTED, NO_RESTART_NEEDED, INTEGER), + // deprecated properties CLIENT_CERT_HEADER_ENABLED_DEPRECATED("authentication.blueCoat.enabled", "false", "Property was replaced by property: smp.automation.authentication.external.tls.clientCert.enabled", OPTIONAL, NOT_ENCRYPTED, NO_RESTART_NEEDED, BOOLEAN), - PARTC_EBCOREPARTYID_CONCATENATE("identifiersBehaviour.ParticipantIdentifierScheme.ebCoreId.concatenate", "false", "Concatenate ebCore party id in XML responses <ParticipantIdentifier>urn:oasis:names:tc:ebcore:partyid-type:unregistered:test-ebcore-id</ParticipantIdentifier>", OPTIONAL, NOT_ENCRYPTED, NO_RESTART_NEEDED, BOOLEAN), @@ -394,16 +404,15 @@ public enum SMPPropertyEnum { ; - String property; - String defValue; - String desc; - Pattern valuePattern; - String errorValueMessage; - - boolean isEncrypted; - boolean isMandatory; - boolean restartNeeded; - SMPPropertyTypeEnum propertyType; + private final String property; + private final String defValue; + private final String desc; + private final Pattern valuePattern; + private final String errorValueMessage; + private final boolean isEncrypted; + private final boolean isMandatory; + private final boolean restartNeeded; + private final SMPPropertyTypeEnum propertyType; SMPPropertyEnum(String property, String defValue, String desc, boolean isMandatory, boolean isEncrypted, boolean restartNeeded, SMPPropertyTypeEnum propertyType, String valuePattern, String errorValueMessage) { diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/BaseDao.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/BaseDao.java index faf5a831d4b16a302c7292ff5610f3485f88f962..ef9a8bc00f46b757b80f331f1e0a57a1fbed404f 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/BaseDao.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/BaseDao.java @@ -24,6 +24,7 @@ import eu.europa.ec.edelivery.smp.exceptions.SMPTestIsALiveException; import eu.europa.ec.edelivery.smp.logging.SMPLogger; import eu.europa.ec.edelivery.smp.logging.SMPLoggerFactory; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.core.GenericTypeResolver; import org.springframework.transaction.annotation.Transactional; @@ -190,7 +191,6 @@ public abstract class BaseDao<E extends BaseEntity> { } } - // set order by if (searchParams != null) { List<Predicate> lstPredicate = createPredicates(searchParams, om, cb); @@ -250,7 +250,8 @@ public abstract class BaseDao<E extends BaseEntity> { cls.getMethod("set" + fieldName, m.getReturnType()); } catch (NoSuchMethodException | SecurityException ex) { // method does not have setter // ignore other methods - LOG.error("Field '" + fieldName + "' does not have a setter!", ex); + LOG.warn("Can not set value [{}] to entity [{}]! Search parameter is ignored!. Root cause: [{}]", + fieldName, om.getModel(), ExceptionUtils.getRootCauseMessage(ex)); continue; } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/model/user/DBCredential.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/model/user/DBCredential.java index 401a280a0ad378a1f55643d8b40c5affcb4f7ff4..26676e46d4ed69133a0d0993e97ab7b3d77595fb 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/model/user/DBCredential.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/model/user/DBCredential.java @@ -130,18 +130,20 @@ public class DBCredential extends BaseEntity { @Column(name = "LAST_FAILED_LOGIN_ON") @ColumnDescription(comment = "Last failed login attempt") private OffsetDateTime lastFailedLoginAttempt; - @Enumerated(EnumType.STRING) @Column(name = "CREDENTIAL_TYPE", nullable = false) @ColumnDescription(comment = "Credential type: USERNAME, ACCESS_TOKEN, CERTIFICATE, CAS") private CredentialType credentialType = CredentialType.USERNAME_PASSWORD; - - @Enumerated(EnumType.STRING) @Column(name = "CREDENTIAL_TARGET", nullable = false) @ColumnDescription(comment = "Credential target UI, API") private CredentialTargetType credentialTarget = CredentialTargetType.UI; - + @Column(name = "RESET_TOKEN", length = CommonColumnsLengths.MAX_PASSWORD_LENGTH) + @ColumnDescription(comment = "Reset token for credential reset") + private String resetToken; + @Column(name = "RESET_EXPIRE_ON") + @ColumnDescription(comment = "Date time when reset token will expire") + private OffsetDateTime resetExpireOn; @OneToOne(mappedBy = "credential", cascade = CascadeType.ALL, @@ -262,6 +264,22 @@ public class DBCredential extends BaseEntity { this.credentialTarget = credentialTarget; } + public String getResetToken() { + return resetToken; + } + + public void setResetToken(String resetToken) { + this.resetToken = resetToken; + } + + public OffsetDateTime getResetExpireOn() { + return resetExpireOn; + } + + public void setResetExpireOn(OffsetDateTime resetExpireOn) { + this.resetExpireOn = resetExpireOn; + } + public DBCertificate getCertificate() { return certificate; } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/CredentialRequestResetRO.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/CredentialRequestResetRO.java new file mode 100644 index 0000000000000000000000000000000000000000..1082b4f441def5d43e80a8f8fac08568c0792ffb --- /dev/null +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/CredentialRequestResetRO.java @@ -0,0 +1,62 @@ +/*- + * #START_LICENSE# + * smp-server-library + * %% + * Copyright (C) 2017 - 2023 European Commission | eDelivery | DomiSMP + * %% + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European Commission - subsequent + * versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence is + * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and limitations under the Licence. + * #END_LICENSE# + */ +package eu.europa.ec.edelivery.smp.data.ui; + +import eu.europa.ec.edelivery.smp.data.enums.CredentialType; + +import java.util.StringJoiner; + + +/** + * Credential request reset object + * + * @author Joze Rihtarsic + * @since 5.1 + */ +public class CredentialRequestResetRO extends BaseRO { + + private static final long serialVersionUID = 9008583888835630030L; + + String credentialName; + CredentialType credentialType; + + public String getCredentialName() { + return credentialName; + } + + public void setCredentialName(String credentialName) { + this.credentialName = credentialName; + } + + public CredentialType getCredentialType() { + return credentialType; + } + + public void setCredentialType(CredentialType credentialType) { + this.credentialType = credentialType; + } + + @Override + public String toString() { + return new StringJoiner(", ", CredentialRequestResetRO.class.getSimpleName() + "[", "]") + .add("credentialName=[" + credentialName + "],") + .add("credentialType=[" + credentialType + "]") + .toString(); + } +} diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/CredentialResetRO.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/CredentialResetRO.java new file mode 100644 index 0000000000000000000000000000000000000000..6d85e8a40fc5ccb835ef611567467943c926b654 --- /dev/null +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/CredentialResetRO.java @@ -0,0 +1,81 @@ +/*- + * #START_LICENSE# + * smp-server-library + * %% + * Copyright (C) 2017 - 2023 European Commission | eDelivery | DomiSMP + * %% + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European Commission - subsequent + * versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence is + * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and limitations under the Licence. + * #END_LICENSE# + */ +package eu.europa.ec.edelivery.smp.data.ui; + +import eu.europa.ec.edelivery.smp.data.enums.CredentialType; + +import java.util.StringJoiner; + + +/** + * Credential request reset object + * @author Joze Rihtarsic + * @since 5.1 + */ +public class CredentialResetRO extends BaseRO { + + private static final long serialVersionUID = 9008583888835630030L; + + String credentialName; + String credentialValue; + CredentialType credentialType; + String resetToken; + + + public String getCredentialName() { + return credentialName; + } + + public void setCredentialName(String credentialName) { + this.credentialName = credentialName; + } + + public CredentialType getCredentialType() { + return credentialType; + } + + public void setCredentialType(CredentialType credentialType) { + this.credentialType = credentialType; + } + + public String getCredentialValue() { + return credentialValue; + } + + public void setCredentialValue(String credentialValue) { + this.credentialValue = credentialValue; + } + + public String getResetToken() { + return resetToken; + } + + public void setResetToken(String resetToken) { + this.resetToken = resetToken; + } + + @Override + public String toString() { + return new StringJoiner(", ", CredentialResetRO.class.getSimpleName() + "[", "]") + .add("credentialName='" + credentialName + "'") + .add("credentialValue='" + credentialValue + "'") + .add("credentialType='" + credentialType + "'") + .toString(); + } +} diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/SmpInfoRO.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/SmpInfoRO.java index a04b632e4d39973078be00debc9fcf4457002697..1fcb6fc51f2f0e455ad1c18c4132aaaca4e7ccf5 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/SmpInfoRO.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/SmpInfoRO.java @@ -34,6 +34,10 @@ public class SmpInfoRO implements Serializable { private String ssoAuthenticationLabel; private String ssoAuthenticationURI; private String contextPath; + + private String passwordValidationRegExp; + private String passwordValidationRegExpMessage; + private final List<String> authTypes = new ArrayList<>(); public String getVersion() { @@ -75,4 +79,20 @@ public class SmpInfoRO implements Serializable { public void addAuthTypes(List<String> authTypes) { this.authTypes.addAll(authTypes); } + + public String getPasswordValidationRegExp() { + return passwordValidationRegExp; + } + + public void setPasswordValidationRegExp(String passwordValidationRegExp) { + this.passwordValidationRegExp = passwordValidationRegExp; + } + + public String getPasswordValidationRegExpMessage() { + return passwordValidationRegExpMessage; + } + + public void setPasswordValidationRegExpMessage(String passwordValidationRegExpMessage) { + this.passwordValidationRegExpMessage = passwordValidationRegExpMessage; + } } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/enums/AlertTypeEnum.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/enums/AlertTypeEnum.java index 90f8f350696ff4d5056d20db5b7ceff20c05faae..98e89bb92bb51ca37101af1d839e05f77827650a 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/enums/AlertTypeEnum.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/enums/AlertTypeEnum.java @@ -25,11 +25,13 @@ package eu.europa.ec.edelivery.smp.data.ui.enums; * @since 4.2 */ public enum AlertTypeEnum { - TEST_ALERT("test_mail.ftl"), - CREDENTIAL_IMMINENT_EXPIRATION("credential_imminent_expiration.ftl"), - CREDENTIAL_EXPIRED("credential_expired.ftl"), - CREDENTIAL_SUSPENDED("credential_suspended.ftl"), - CREDENTIAL_VERIFICATION_FAILED("credential_verification_failed.ftl"), + TEST_ALERT("test_mail"), + CREDENTIAL_IMMINENT_EXPIRATION("credential_imminent_expiration"), + CREDENTIAL_EXPIRED("credential_expired"), + CREDENTIAL_SUSPENDED("credential_suspended"), + CREDENTIAL_VERIFICATION_FAILED("credential_verification_failed"), + CREDENTIAL_REQUEST_RESET("credential_request_reset"), + CREDENTIAL_CHANGED("credential_changed"), ; private final String template; diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ConfigurationService.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ConfigurationService.java index d6028ec4c6d6220ef1604f6e3208bf419a9af7b4..edf8c4ba6881d93902e68faf3d11d5c7de7082f2 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ConfigurationService.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ConfigurationService.java @@ -564,6 +564,17 @@ public class ConfigurationService { return configurationDAO.getCachedPropertyValue(ALERT_CERTIFICATE_EXPIRED_MAIL_SUBJECT); } + public String getSMPInstanceName() { + return configurationDAO.getCachedPropertyValue(SMP_INSTANCE_NAME); + } + // ---- + public java.net.URL getCredentialsResetUrl() { + return configurationDAO.getCachedPropertyValue(CREDENTIALS_RESET_URL); + } + + public Integer getCredentialsResetPolicyValidMinutes() { + return configurationDAO.getCachedPropertyValue(CREDENTIALS_RESET_POLICY_VALID_DAYS); + } public Integer getAlertCredentialsBatchSize() { return configurationDAO.getCachedPropertyValue(SMP_ALERT_BATCH_SIZE); 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 a8e2107a6edd798ab4f86bd937b70aa93447b82e..f789b670cfb7570a08876a96d2f3e256ccaf76b2 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 @@ -8,9 +8,9 @@ * versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: - * + * * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * + * * Unless required by applicable law or agreed to in writing, software distributed under the Licence is * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Licence for the specific language governing permissions and limitations under the Licence. @@ -59,13 +59,20 @@ import java.util.*; import static java.util.Locale.US; +/** + * The CredentialService class is a service that provides methods for user authentication and credential management. + * The service is intended for stateful service calls to validate credentials with audit logs, credential reset. + * + * @author Joze Rihtarsic + * @since 5.0 + */ @Service public class CredentialService { protected static final SMPLogger LOG = SMPLoggerFactory.getLogger(CredentialService.class); protected static final BadCredentialsException BAD_CREDENTIALS_EXCEPTION = new BadCredentialsException(ErrorCode.UNAUTHORIZED_INVALID_USERNAME_PASSWORD.getMessage()); protected static final BadCredentialsException SUSPENDED_CREDENTIALS_EXCEPTION = new BadCredentialsException(ErrorCode.UNAUTHORIZED_CREDENTIAL_SUSPENDED.getMessage()); - final UserDao mUserDao; - final CredentialDao mCredentialDao; + final UserDao userDao; + final CredentialDao credentialDao; final ConversionService conversionService; final CRLVerifierService crlVerifierService; final UITruststoreService truststoreService; @@ -78,9 +85,9 @@ public class CredentialService { private static final ThreadLocal<DateFormat> dateFormatLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("MMM d hh:mm:ss yyyy zzz", US)); - public CredentialService(UserDao mUserDao, CredentialDao mCredentialDao, ConversionService conversionService, CRLVerifierService crlVerifierService, UITruststoreService truststoreService, ConfigurationService configurationService, CredentialsAlertService alertService) { - this.mUserDao = mUserDao; - this.mCredentialDao = mCredentialDao; + public CredentialService(UserDao mUserDao, CredentialDao credentialDao, ConversionService conversionService, CRLVerifierService crlVerifierService, UITruststoreService truststoreService, ConfigurationService configurationService, CredentialsAlertService alertService) { + this.userDao = mUserDao; + this.credentialDao = credentialDao; this.conversionService = conversionService; this.crlVerifierService = crlVerifierService; this.truststoreService = truststoreService; @@ -96,9 +103,9 @@ public class CredentialService { LOG.debug("authenticateByUsernamePassword: start [{}]", username); DBCredential credential; try { - Optional<DBCredential> dbCredential = mCredentialDao.findUsernamePasswordCredentialForUsernameAndUI(username); + Optional<DBCredential> dbCredential = credentialDao.findUsernamePasswordCredentialForUsernameAndUI(username); if (!dbCredential.isPresent() || isNotValidCredential(dbCredential.get())) { - LOG.debug("User with username does not exists [{}], continue with next authentication provider"); + LOG.debug("User with username does not exists [{}], continue with next authentication provider", username); LOG.securityWarn(SMPMessageCode.SEC_INVALID_USER_CREDENTIALS, "Username does not exits", username); delayResponse(CredentialType.USERNAME_PASSWORD, startTime); throw BAD_CREDENTIALS_EXCEPTION; @@ -150,7 +157,7 @@ public class CredentialService { DBCredential credential; try { - Optional<DBCredential> dbCredential = mCredentialDao.findAccessTokenCredentialForAPI(authenticationTokenId); + Optional<DBCredential> dbCredential = credentialDao.findAccessTokenCredentialForAPI(authenticationTokenId); if (!dbCredential.isPresent() || isNotValidCredential(dbCredential.get())) { LOG.securityWarn(SMPMessageCode.SEC_USER_NOT_EXISTS, authenticationTokenId); @@ -177,7 +184,7 @@ public class CredentialService { } credential.setSequentialLoginFailureCount(0); credential.setLastFailedLoginAttempt(null); - mCredentialDao.update(credential); + credentialDao.update(credential); } catch (java.lang.IllegalArgumentException ex) { // password is not hashed loginAttemptFailedAndThrowError(credential, true, startTime); @@ -237,7 +244,7 @@ public class CredentialService { } DBCredential credential; try { - Optional<DBCredential> optCredential = mCredentialDao.findUserByCertificateId(certificateIdentifier, true); + Optional<DBCredential> optCredential = credentialDao.findUserByCertificateId(certificateIdentifier, true); if (!optCredential.isPresent() || isNotValidCredential(optCredential.get())) { LOG.securityWarn(SMPMessageCode.SEC_USER_NOT_EXISTS, certificateIdentifier); //https://www.owasp.org/index.php/Authentication_Cheat_Sheet @@ -317,6 +324,111 @@ public class CredentialService { return smpAuthenticationToken; } + /** + * Method retrieves user credentials by username. First it validates if credentials have already active reset token + * and if not it creates new one. + * + * @param username + */ + @Transactional + public void requestResetUsername(String username) { + LOG.debug("requestResetUsername [{}]", username); + // retrieve user Optional credentials by username + Optional<DBCredential> optCredential = getActiveCredentialsForUsernameToReset(username, true); + if (!optCredential.isPresent()) { + LOG.info("Skip generating reset token for username [{}]!", username); + return; + } + DBCredential dbCredential = optCredential.get(); + generateResetTokenAndSubmitMail(dbCredential); + } + + @Transactional + public void resetUsernamePassword(String username, String resetToken, String newPassword) { + // retrieve user Optional credentials by username + LOG.debug("resetUsernamePassword [{}]", username); + // retrieve user Optional credentials by username + Optional<DBCredential> optCredential = getActiveCredentialsForUsernameToReset(username, false); + if (!optCredential.isPresent()) { + LOG.info("Skip generating reset token for username [{}]!", username); + 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; + } + + OffsetDateTime now = OffsetDateTime.now(); + dbCredential.setValue(BCrypt.hashpw(newPassword, BCrypt.gensalt())); + dbCredential.setResetToken(null); + dbCredential.setResetExpireOn(null); + dbCredential.setExpireAlertOn(null); + dbCredential.setSequentialLoginFailureCount(0); + dbCredential.setLastFailedLoginAttempt(null); + dbCredential.setChangedOn(now); + dbCredential.setExpireOn(now.plusDays(configurationService.getPasswordPolicyValidDays())); + // submit mail with reset token + alertService.alertCredentialChanged(dbCredential); + } + + /** + * Method gets credentials for active user and validates if not expired reset token exists . + * + * @param username + * @return + */ + private Optional<DBCredential> getActiveCredentialsForUsernameToReset(String username, boolean toGenerateResetToken) { + // retrieve user Optional credentials by username + Optional<DBCredential> optCredential = credentialDao.findUsernamePasswordCredentialForUsernameAndUI(username); + if (!optCredential.isPresent()) { + LOG.warn("There is not credentials for User [{}]!", username); + return optCredential; + } + DBCredential dbCredential = optCredential.get(); + + if (!dbCredential.getUser().isActive() || !dbCredential.isActive()) { + LOG.info("User [{}] or credentials are not active. Skip reset password request!", username); + return Optional.empty(); + } + + // When toGenerateResetToken check if the user has already active reset token + boolean hasValidResetToken = hasValidResetToken(dbCredential); + if (toGenerateResetToken && hasValidResetToken) { + LOG.info("User [{}] has already active reset token. Skip generating new reset token!", username); + return Optional.empty(); + } + // If action is reset then check if the user has active reset token + if (!toGenerateResetToken && !hasValidResetToken) { + LOG.warn("User [{}] does not have active reset token. The reset token is expired or does not exists!", username); + throw new AuthenticationServiceException("User [" + username + + "] does not have active reset token. Please request new reset token for the user!"); + } + + return optCredential; + } + + private boolean hasValidResetToken(DBCredential dbCredential) { + return StringUtils.isNotBlank(dbCredential.getResetToken()) + && dbCredential.getResetExpireOn() != null + && dbCredential.getResetExpireOn().isAfter(OffsetDateTime.now()); + } + + /** + * Method generates reset token and submit mail with reset token. The method must be invoked from a transactional + * parent method. + * + * @param dbCredential credential for which the reset token is generated. + */ + private void generateResetTokenAndSubmitMail(DBCredential dbCredential) { + dbCredential.setResetToken(UUID.randomUUID().toString()); + dbCredential.setResetExpireOn(OffsetDateTime.now().plusMinutes(configurationService.getCredentialsResetPolicyValidMinutes())); + // submit mail with reset token + dbCredential.getUser().getEmailAddress(); + alertService.alertCredentialRequestReset(dbCredential); + } + /** * Method validates if the certificate contains one of allowed Certificate policy. At the moment it does not validates * the whole chain. Because in some configuration cases does not use the truststore @@ -348,7 +460,7 @@ public class CredentialService { } - protected void delayResponse(CredentialType credentialType, long startTime) { + public void delayResponse(CredentialType credentialType, long startTime) { int delayInMS = getLoginFailDelayInMilliSeconds(credentialType) - (int) (Calendar.getInstance().getTimeInMillis() - startTime); if (delayInMS > 0) { try { @@ -366,7 +478,7 @@ public class CredentialService { CredentialType credentialType = credential.getCredentialType(); credential.setSequentialLoginFailureCount(credential.getSequentialLoginFailureCount() != null ? credential.getSequentialLoginFailureCount() + 1 : 1); credential.setLastFailedLoginAttempt(OffsetDateTime.now()); - mCredentialDao.update(credential); + credentialDao.update(credential); String username = credential.getUser().getUsername(); LOG.securityWarn(SMPMessageCode.SEC_INVALID_USER_CREDENTIALS, username, credential.getName(), @@ -428,7 +540,7 @@ public class CredentialService { LOG.warn("User [{}] for credential [{}:{}] suspension is expired! Clear failed login attempts and last failed login attempt", credential.getName(), credentialType, credential.getName()); credential.setLastFailedLoginAttempt(null); credential.setSequentialLoginFailureCount(0); - mCredentialDao.update(credential); + credentialDao.update(credential); return; } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/CredentialsAlertService.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/CredentialsAlertService.java index 680dd8c44040745d71650200e7766b98263686f1..847d4b3ca7b02add5ee7c9ca21389d6f2c20be7c 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/CredentialsAlertService.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/CredentialsAlertService.java @@ -8,9 +8,9 @@ * versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: - * + * * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * + * * Unless required by applicable law or agreed to in writing, software distributed under the Licence is * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Licence for the specific language governing permissions and limitations under the Licence. @@ -31,21 +31,23 @@ import eu.europa.ec.edelivery.smp.data.ui.enums.AlertStatusEnum; import eu.europa.ec.edelivery.smp.data.ui.enums.AlertTypeEnum; import eu.europa.ec.edelivery.smp.logging.SMPLogger; import eu.europa.ec.edelivery.smp.logging.SMPLoggerFactory; +import eu.europa.ec.edelivery.smp.services.mail.MailDataModel; import eu.europa.ec.edelivery.smp.services.mail.MailService; -import eu.europa.ec.edelivery.smp.services.mail.PropertiesMailModel; -import eu.europa.ec.edelivery.smp.services.mail.prop.CredentialSuspendedProperties; -import eu.europa.ec.edelivery.smp.services.mail.prop.CredentialVerificationFailedProperties; -import eu.europa.ec.edelivery.smp.services.mail.prop.CredentialsExpirationProperties; +import eu.europa.ec.edelivery.smp.services.mail.prop.*; import eu.europa.ec.edelivery.smp.utils.HttpUtils; +import eu.europa.ec.edelivery.smp.utils.SmpUrlBuilder; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; +import java.net.MalformedURLException; +import java.net.URL; import java.time.OffsetDateTime; import java.util.Date; import static eu.europa.ec.edelivery.smp.cron.CronTriggerConfig.TRIGGER_BEAN_CREDENTIAL_ALERTS; +import static java.time.format.DateTimeFormatter.ISO_DATE_TIME; import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME; /** @@ -64,12 +66,14 @@ public class CredentialsAlertService { final UserDao userDao; final CredentialDao credentialDao; final SMPDynamicCronTrigger alertCronTrigger; + final SmpUrlBuilder smpUrlBuilder; public CredentialsAlertService(AlertDao alertDao, MailService mailService, ConfigurationService configurationService, UserDao userDao, CredentialDao credentialDao, + SmpUrlBuilder smpUrlBuilder, @Qualifier(TRIGGER_BEAN_CREDENTIAL_ALERTS) SMPDynamicCronTrigger alertCronTrigger) { this.alertDao = alertDao; this.mailService = mailService; @@ -77,6 +81,7 @@ public class CredentialsAlertService { this.userDao = userDao; this.credentialDao = credentialDao; this.alertCronTrigger = alertCronTrigger; + this.smpUrlBuilder = smpUrlBuilder; } public void alertBeforeCredentialExpire(DBCredential userCredential) { @@ -243,7 +248,7 @@ public class CredentialsAlertService { alert.addProperty(CredentialsExpirationProperties.SERVER_NAME.name(), serverName); alertDao.persistFlushDetach(alert); // submit alerts - submitAlertMail(alert); + submitAlertMail(alert, credential.getUser()); // when alert about to expire - check if the next cron execution is expired // and set date sent tp null to ensure alert submission in next cron execution credentialDao.updateAlertSentForUserCredentials(credential, @@ -259,7 +264,7 @@ public class CredentialsAlertService { Integer failedLoginCount, OffsetDateTime lastFailedLoginDate ) { - LOG.info("Prepare alert for credentials [{}] ", credentialId ); + LOG.info("Prepare alert for credentials [{}] ", credentialId); String serverName = HttpUtils.getServerAddress(); // add alert properties alert.addProperty(CredentialVerificationFailedProperties.CREDENTIAL_TYPE.name(), credentialType.name()); @@ -271,7 +276,7 @@ public class CredentialsAlertService { alert.addProperty(CredentialVerificationFailedProperties.SERVER_NAME.name(), serverName); alertDao.persistFlushDetach(alert); // submit alerts - submitAlertMail(alert); + submitAlertMail(alert, user); } public void alertCredentialSuspended(DBUser user, @@ -294,7 +299,95 @@ public class CredentialsAlertService { alert.addProperty(CredentialSuspendedProperties.SERVER_NAME.name(), serverName); alertDao.persistFlushDetach(alert); // submit alerts - submitAlertMail(alert); + submitAlertMail(alert, user); + } + + /** + * Method generates request reset alert for credentials and submit mail to the user + * + * @param credential credential to reset + */ + public void alertCredentialRequestReset(DBCredential credential) { + + DBUser user = credential.getUser(); + String mailTo = user.getEmailAddress(); + String mailSubject = configurationService.getAlertUserSuspendedSubject(); + AlertLevelEnum alertLevel = AlertLevelEnum.HIGH; + AlertTypeEnum alertType = AlertTypeEnum.CREDENTIAL_REQUEST_RESET; + + DBAlert alert = createAlert(user.getUsername(), mailSubject, mailTo, alertLevel, alertType); + + alertCredentialRequestReset(credential.getResetToken(), + alert, + credential.getCredentialType(), + credential.getName(), + user); + } + + public void alertCredentialRequestReset(String token, + DBAlert alert, + CredentialType credentialType, + String credentialId, + DBUser user) { + + + URL resetUrl = configurationService.getCredentialsResetUrl(); + if (resetUrl == null) { + try { + resetUrl = smpUrlBuilder.buildSMPUriForApplication().toURL(); + LOG.warn("Reset URL is not set! Use default SMP URL [{}]", resetUrl); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + String resetUrlPath = StringUtils.appendIfMissing(resetUrl.toString(), "/", "/") + "ui/#/reset-credential/" + token; + String serverName = HttpUtils.getServerAddress(); + // add alert properties + alert.addProperty(CredentialsResetRequestProperties.CREDENTIAL_TYPE.name(), credentialType.name()); + alert.addProperty(CredentialsResetRequestProperties.CREDENTIAL_ID.name(), credentialId); + alert.addProperty(CredentialsResetRequestProperties.REPORTING_DATETIME.name(), alert.getReportingTime()); + alert.addProperty(CredentialsResetRequestProperties.ALERT_LEVEL.name(), alert.getAlertLevel().name()); + alert.addProperty(CredentialsResetRequestProperties.RESET_URL.name(), resetUrlPath); + alert.addProperty(CredentialsResetRequestProperties.SERVER_NAME.name(), serverName); + alertDao.persistFlushDetach(alert); + // submit alerts + submitAlertMail(alert, user); + } + + + /** + * Method generates request alert for credentials change and submit mail to the user + * + * @param credential credential changed + */ + public void alertCredentialChanged(DBCredential credential) { + + DBUser user = credential.getUser(); + String mailTo = user.getEmailAddress(); + String mailSubject = configurationService.getAlertUserSuspendedSubject(); + AlertLevelEnum alertLevel = AlertLevelEnum.HIGH; + AlertTypeEnum alertType = AlertTypeEnum.CREDENTIAL_CHANGED; + + DBAlert alert = createAlert(user.getUsername(), mailSubject, mailTo, alertLevel, alertType); + + alertCredentialChanged(user, alert, credential.getCredentialType(), credential.getName()); + + } + + public void alertCredentialChanged(DBUser user, DBAlert alert, + CredentialType credentialType, + String credentialId) { + + String serverName = HttpUtils.getServerAddress(); + // add alert properties + alert.addProperty(CredentialsChangedProperties.CREDENTIAL_TYPE.name(), credentialType.name()); + alert.addProperty(CredentialsChangedProperties.CREDENTIAL_ID.name(), credentialId); + alert.addProperty(CredentialsChangedProperties.REPORTING_DATETIME.name(), alert.getReportingTime()); + alert.addProperty(CredentialsChangedProperties.ALERT_LEVEL.name(), alert.getAlertLevel().name()); + alert.addProperty(CredentialsChangedProperties.SERVER_NAME.name(), serverName); + alertDao.persistFlushDetach(alert); + // submit alerts + submitAlertMail(alert, user); } /** @@ -327,7 +420,7 @@ public class CredentialsAlertService { * * @param alert */ - public void submitAlertMail(DBAlert alert) { + public void submitAlertMail(DBAlert alert, DBUser user) { String mailTo = alert.getMailTo(); if (StringUtils.isBlank(mailTo)) { LOG.warn("Can not send mail (empty mail) for alert [{}]!", alert); @@ -335,14 +428,20 @@ public class CredentialsAlertService { return; } + String mailFrom = configurationService.getAlertEmailFrom(); - PropertiesMailModel props = new PropertiesMailModel(alert); + MailDataModel props = new MailDataModel(user.getSmpLocale(), alert); + props.getModel().put(MailDataModel.CommonProperties.SMP_INSTANCE_NAME.name(), + configurationService.getSMPInstanceName()); + props.getModel().put(MailDataModel.CommonProperties.CURRENT_DATETIME.name(), + OffsetDateTime.now().format(ISO_DATE_TIME)); + try { mailService.sendMail(props, mailFrom, alert.getMailTo()); updateAlertStatus(alert, AlertStatusEnum.SUCCESS, null); } catch (Throwable exc) { LOG.error("Can not send mail [{}] for alert [{}]! Error [{}]", - mailTo, alert, ExceptionUtils.getRootCauseMessage(exc)); + mailTo, alert, ExceptionUtils.getRootCauseMessage(exc)); updateAlertStatus(alert, AlertStatusEnum.FAILED, ExceptionUtils.getRootCauseMessage(exc)); } @@ -365,5 +464,4 @@ public class CredentialsAlertService { return nextExecutionDate == null || expireOn == null || expireOn.isBefore(nextExecutionDate.toInstant().atOffset(expireOn.getOffset())); } - } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/PropertiesMailModel.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailDataModel.java similarity index 51% rename from smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/PropertiesMailModel.java rename to smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailDataModel.java index 8cfbca0504e83a0cd2b8e6527fd856e77c26f9f1..1e35ec651c4fb9260a2d6837cb0176586bf9e2c0 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/PropertiesMailModel.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailDataModel.java @@ -8,9 +8,9 @@ * versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: - * + * * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * + * * Unless required by applicable law or agreed to in writing, software distributed under the Licence is * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Licence for the specific language governing permissions and limitations under the Licence. @@ -19,47 +19,42 @@ package eu.europa.ec.edelivery.smp.services.mail; import eu.europa.ec.edelivery.smp.data.model.DBAlert; +import eu.europa.ec.edelivery.smp.data.ui.enums.AlertTypeEnum; -import java.util.Properties; +import java.util.HashMap; +import java.util.Map; -public class PropertiesMailModel implements MailModel<Properties> { - - private final Properties model = new Properties(); - - private final String templatePath; - - private final String subject; - - - public PropertiesMailModel(final String templatePath, final String subject) { - this.templatePath = templatePath; - this.subject = subject; +public class MailDataModel { + public enum CommonProperties { + CURRENT_DATETIME, + SMP_INSTANCE_NAME, } + private final String language; + private final AlertTypeEnum alertType; + Map<String, String> model = new HashMap<>(); - public PropertiesMailModel(final DBAlert alert) { - this.templatePath = alert.getAlertType().getTemplate(); - this.subject = alert.getMailSubject(); - alert.getProperties().forEach((key, prop) -> setProperty(key, prop.getValue()) - ); - } - @Override - public Properties getModel() { - return model; + public MailDataModel(String language, AlertTypeEnum alertType, Map<String, String> model) { + this.language = language; + this.alertType = alertType; + this.model.putAll(model); } + public MailDataModel(String language, final DBAlert alert) { + this.language = language; + this.alertType = alert.getAlertType(); + alert.getProperties().forEach((key, prop) -> this.model.put(key, prop.getValue())); + } - public void setProperty(String key, String value) { - model.setProperty(key, value); + public String getLanguage() { + return language; } - @Override - public String getTemplatePath() { - return templatePath; + public AlertTypeEnum getMailType() { + return alertType; } - @Override - public String getSubject() { - return subject; + public Map<String, String> getModel() { + return model; } } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailService.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailService.java index b7a745a515858f9eb6bcf35f12da55298843b44c..3200c82de8579d57314a3492eebd89d488cadeeb 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailService.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailService.java @@ -8,9 +8,9 @@ * versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: - * + * * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * + * * Unless required by applicable law or agreed to in writing, software distributed under the Licence is * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Licence for the specific language governing permissions and limitations under the Licence. @@ -22,22 +22,16 @@ 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 freemarker.template.Configuration; -import freemarker.template.Template; -import freemarker.template.TemplateException; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.mail.MailException; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Component; -import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; -import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Properties; /** @@ -53,16 +47,16 @@ public class MailService { private static final SMPLogger LOG = SMPLoggerFactory.getLogger(MailService.class); - private final Configuration freemarkerConfig; private final JavaMailSenderImpl javaMailSender; + private final MailTemplateService mailTemplateService; - public MailService(Configuration freemarkerConfig, JavaMailSenderImpl javaMailSender) { - this.freemarkerConfig = freemarkerConfig; + public MailService(JavaMailSenderImpl javaMailSender, MailTemplateService mailTemplateService) { this.javaMailSender = javaMailSender; + this.mailTemplateService = mailTemplateService; } - public <T extends MailModel<Properties>> void sendMail(final T model, final String from, final String to) { + public void sendMail(final MailDataModel model, final String from, final String to) { if (StringUtils.isBlank(to)) { throw new IllegalArgumentException("The 'to' property cannot be null"); } @@ -70,25 +64,27 @@ public class MailService { throw new IllegalArgumentException("The 'from' property cannot be null"); } + MimeMessage message = javaMailSender.createMimeMessage(); try { + MimeMessageHelper helper = getMimeMessageHelper(message); - Template template = freemarkerConfig.getTemplate(model.getTemplatePath()); - final Object mailData = model.getModel(); - String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, mailData); + String subject = mailTemplateService.getMailTitle(model); + String html = mailTemplateService.getMailHtmlContent(model); + // if to contains multiple emails, split them and send as anonymously as BCC if (to.contains(";")) { helper.setBcc(to.split(";")); } else { helper.setTo(to); } helper.setText(html, true); - helper.setSubject(model.getSubject()); + helper.setSubject(subject); helper.setFrom(from); - LOG.info("Send mail to : [{}:{}]",javaMailSender.getHost(),javaMailSender.getPort()); + LOG.info("Send mail to : [{}:{}]", javaMailSender.getHost(), javaMailSender.getPort()); javaMailSender.send(message); - } catch (IOException | MessagingException | TemplateException | MailException e) { + } catch (MessagingException | MailException e) { LOG.error("Exception while sending mail from [{}] to [{}]", from, to, e); throw new SMPRuntimeException(ErrorCode.MAIL_SUBMISSION_ERROR, e, ExceptionUtils.getRootCauseMessage(e)); } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailTemplateService.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailTemplateService.java new file mode 100644 index 0000000000000000000000000000000000000000..f5e3d0c9cfc28f7296b82a188b876bd4de6adc2a --- /dev/null +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailTemplateService.java @@ -0,0 +1,111 @@ +/*- + * #START_LICENSE# + * smp-server-library + * %% + * Copyright (C) 2017 - 2023 European Commission | eDelivery | DomiSMP + * %% + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European Commission - subsequent + * versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence is + * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and limitations under the Licence. + * #END_LICENSE# + */ +package eu.europa.ec.edelivery.smp.services.mail; + +import eu.europa.ec.edelivery.smp.utils.StringNamedSubstitutor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * Mail template for mail content. The class is used to create mail messages from templates and message + * translations. + * + * @author Joze Rihtarsic + * @since 5.1 + */ +@Component +public class MailTemplateService { + private static final String DEFAULT_LANGUAGE = "en"; + private static final String MAIL_TEMPLATE = "/mail-messages/mail-template.htm"; + + private static final String MAIL_HEADER = "MAIL_HEADER"; + private static final String MAIL_FOOTER = "MAIL_FOOTER"; + private static final String MAIL_TITLE = "MAIL_TITLE"; + private static final String MAIL_CONTENT = "MAIL_CONTENT"; + + + public String getMailHtmlContent(MailDataModel model) { + InputStream templateIS = MailTemplateService.class.getResourceAsStream(MAIL_TEMPLATE); + try { + Map<String, String> modelData = new HashMap<>(); + modelData.put(MAIL_HEADER, getMailHeader(model)); + modelData.put(MAIL_FOOTER, getMailFooter(model)); + modelData.put(MAIL_TITLE, getMailTitle(model)); + modelData.put(MAIL_CONTENT, getMailBody(model)); + return StringNamedSubstitutor.resolve(templateIS, modelData); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + public String getMailHeader(MailDataModel model) { + return getMailData(model, "mail.header"); + } + + public String getMailFooter(MailDataModel model) { + return getMailData(model, "mail.footer"); + } + + public String getMailTitle(MailDataModel model) { + return getMailData(model, "mail." + model.getMailType().getTemplate() + ".title"); + } + + public String getMailBody(MailDataModel model) { + return getMailData(model, "mail." + model.getMailType().getTemplate() + ".content"); + } + + public String getMailData(MailDataModel model, String key) { + Properties translations = getMessageTranslations(model.getLanguage()); + String dataTemplate = translations.getProperty(key); + return StringUtils.isBlank(dataTemplate) ? dataTemplate : StringNamedSubstitutor.resolve(dataTemplate, model.getModel()); + } + + public Properties getMessageTranslations(String language) { + + Properties translations = loadTranslations(language); + if (translations != null) { + return translations; + } + return loadTranslations(DEFAULT_LANGUAGE); + } + + @Cacheable("mail-templates-translations") + public Properties loadTranslations(String language) { + String langResource = "/mail-messages/mail-messages_" + language + ".properties"; + InputStream isLanguage = MailTemplateService.class.getResourceAsStream(langResource); + if (isLanguage != null) { + try { + Properties translations = new Properties(); + translations.load(isLanguage); + return translations; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return null; + } +} diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailModel.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/prop/CredentialsChangedProperties.java similarity index 69% rename from smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailModel.java rename to smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/prop/CredentialsChangedProperties.java index 9801b09d2d4b63697fa2d59cb965872b07073a62..668205c9ed727bfa69c5a3423465aa600a3ba44e 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/MailModel.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/prop/CredentialsChangedProperties.java @@ -16,23 +16,12 @@ * See the Licence for the specific language governing permissions and limitations under the Licence. * #END_LICENSE# */ -package eu.europa.ec.edelivery.smp.services.mail; +package eu.europa.ec.edelivery.smp.services.mail.prop; -/** - * Mail model for constitution of the mail content using freemaker template. The class was heavily inspired by Domibus - * mail implementation - * - * @author Thomas Dussart - * @author Joze Rihtarsic - * - * @since 4.2 - */ -public interface MailModel<T> { - - T getModel(); - - String getTemplatePath(); - - String getSubject(); +public enum CredentialsChangedProperties { + CREDENTIAL_TYPE, + CREDENTIAL_ID, + REPORTING_DATETIME, + ALERT_LEVEL, + SERVER_NAME, } - diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/prop/CredentialsResetRequestProperties.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/prop/CredentialsResetRequestProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..1535e1982244ef2d2d4b6eb433053013e7ddce63 --- /dev/null +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/mail/prop/CredentialsResetRequestProperties.java @@ -0,0 +1,28 @@ +/*- + * #START_LICENSE# + * smp-server-library + * %% + * Copyright (C) 2017 - 2023 European Commission | eDelivery | DomiSMP + * %% + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European Commission - subsequent + * versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence is + * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and limitations under the Licence. + * #END_LICENSE# + */ +package eu.europa.ec.edelivery.smp.services.mail.prop; + +public enum CredentialsResetRequestProperties { + CREDENTIAL_TYPE, + CREDENTIAL_ID, + RESET_URL, + REPORTING_DATETIME, + ALERT_LEVEL, + SERVER_NAME, +} 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 3d84a66044b7db087d97efa4e7a7366cebaf6b82..cb8116ae6f2e95c045ef1b9001146d94dec2791d 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 @@ -39,6 +39,7 @@ 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.ConfigurationService; +import eu.europa.ec.edelivery.smp.services.CredentialsAlertService; import eu.europa.ec.edelivery.smp.utils.BCryptPasswordHash; import eu.europa.ec.edelivery.smp.utils.SessionSecurityUtils; import org.apache.commons.lang3.StringUtils; @@ -71,19 +72,21 @@ public class UIUserService extends UIServiceBase<DBUser, UserRO> { CredentialDao credentialDao; private final ConfigurationService configurationService; private final ConversionService conversionService; - + private final CredentialsAlertService alertService; private final UITruststoreService truststoreService; public UIUserService(UserDao userDao, CredentialDao credentialDao, ConfigurationService configurationService, ConversionService conversionService, - UITruststoreService truststoreService) { + UITruststoreService truststoreService, + CredentialsAlertService alertService) { this.userDao = userDao; this.credentialDao = credentialDao; this.configurationService = configurationService; this.conversionService = conversionService; this.truststoreService = truststoreService; + this.alertService = alertService; } @Override @@ -258,6 +261,8 @@ public class UIUserService extends UIServiceBase<DBUser, UserRO> { if (dbCredential.getId() == null) { credentialDao.persist(dbCredential); } + // submit mail with reset token + alertService.alertCredentialChanged(dbCredential); return dbCredential.getUser(); } @@ -492,6 +497,8 @@ public class UIUserService extends UIServiceBase<DBUser, UserRO> { } credentialResultRO.setStatus(EntityROStatus.UPDATED.getStatusNumber()); + alertService.alertCredentialChanged(credential); + return credentialResultRO; } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/SmpUrlBuilder.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/SmpUrlBuilder.java index c3db71cc095a0e81bfcd08c34dc5907eb9319a5b..3d7ac3c1b32f876002bea175bc37697fa9ee88df 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/SmpUrlBuilder.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/SmpUrlBuilder.java @@ -8,9 +8,9 @@ * versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: - * + * * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * + * * Unless required by applicable law or agreed to in writing, software distributed under the Licence is * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Licence for the specific language governing permissions and limitations under the Licence. @@ -33,6 +33,7 @@ import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UrlPathHelper; import javax.servlet.http.HttpServletRequest; +import java.net.URI; /** * This class provides tools to generate SMP's URL in responses. The client can use provided URL for another call to the SMP. @@ -61,7 +62,7 @@ public class SmpUrlBuilder { this.configurationService = configurationService; } - public String buildSMPUrlForApplication() { + public URI buildSMPUriForApplication() { HttpServletRequest req = getCurrentRequest(); HttpForwardedHeaders fh = new HttpForwardedHeaders(req); LOG.debug("Generate response uri with headers data: [{}]", fh); @@ -80,8 +81,11 @@ public class SmpUrlBuilder { } else { LOG.info("Ignore settings header because host is null!"); } - return uriBuilder.build().toUriString(); + return uriBuilder.build().toUri(); + } + public String buildSMPUrlForApplication() { + return buildSMPUriForApplication().toString(); } /** diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/StringNamedSubstitutor.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/StringNamedSubstitutor.java new file mode 100644 index 0000000000000000000000000000000000000000..42f8c4b5e81cdc89ed4654ea1e6ab41d3c2807de --- /dev/null +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/StringNamedSubstitutor.java @@ -0,0 +1,138 @@ +/*- + * #START_LICENSE# + * smp-server-library + * %% + * Copyright (C) 2017 - 2023 European Commission | eDelivery | DomiSMP + * %% + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European Commission - subsequent + * versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence is + * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and limitations under the Licence. + * #END_LICENSE# + */ +package eu.europa.ec.edelivery.smp.utils; + +import org.apache.commons.lang3.StringUtils; + +import java.io.*; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * This class is used to substitute named variables in the string with key value pairs from the map. + * The variables are in the form of ${name} and are case-insensitive and can contain only letters, digits, and chars + * '_' and '.'. + * + * @author Joze Rihtarsic + * @since 4.2 + */ +public class StringNamedSubstitutor { + private static final String START_NAME = "${"; + private static final char END_NAME = '}'; + + /** + * Substitute named variables in the string with key value pairs from the map. + * The variables are in the form of ${name} and are case-insensitive and can contain only letters, digits, _ and . + * + * @param templateIS the InputStream to resolve + * @param config the config to use + * @return the resolved string + */ + public static String resolve(InputStream templateIS, Map<String, String> config) throws IOException { + Map<String, String> lowerCase = config.entrySet().stream() + .collect(Collectors.toMap(e -> StringUtils.lowerCase(e.getKey()), Map.Entry::getValue)); + StringBuilder builder = new StringBuilder(); + + BufferedReader template = new BufferedReader(new InputStreamReader(templateIS)); + int read; + while ((read = template.read()) != -1) { + if (read == START_NAME.charAt(0) && isStartSequence(template)) { + template.skip(START_NAME.length() - 1); + String name = readName(template, END_NAME); + if (name == null) { + builder.append(START_NAME); + } else { + String key = StringUtils.lowerCase(name); + String value = lowerCase.get(key); + if (value != null) { + builder.append(value); + } else { + builder.append(START_NAME).append(name).append(END_NAME); + } + } + } else { + builder.append((char) read); + } + } + + return builder.toString(); + } + + public static boolean isStartSequence(BufferedReader reader) throws IOException { + reader.mark(START_NAME.length()); + int read = reader.read(); + if (read == -1) { + return false; + } + for (int i = 1; i < START_NAME.length(); i++) { + if (read != START_NAME.charAt(i)) { + reader.reset(); + return false; + } + read = reader.read(); + if (read == -1) { + return false; + } + } + reader.reset(); + return true; + } + + /** + * Method reads name until it finds end name char or until the first not letter/digit or + * _ or . character is found. + * + * @param inputStream the string to search index of the character + * @param searchChar the character + * @return the index of the character or -1 if not found + */ + private static String readName(BufferedReader inputStream, char searchChar) throws IOException { + StringBuilder builder = new StringBuilder(); + int read; + while ((read = inputStream.read()) != -1) { + char currChar = (char) read; + if (currChar == searchChar) { + return builder.toString(); + } else if (!Character.isLetterOrDigit(currChar) + && currChar != '_' + && currChar != '.') { + builder.append(currChar); + return builder.toString(); + } + builder.append(currChar); + } + return null; + } + + /** + * Substitute named variables in the string with key value pairs from the map. + * The variables are in the form of ${name} and are case-insensitive and can contain only letters, digits, _ and . + * + * @param string the string to resolve + * @param config the config to use + * @return the resolved string + */ + public static String resolve(String string, Map<String, String> config) { + try { + return resolve(new ByteArrayInputStream(string.getBytes()), config); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/smp-server-library/src/main/resources/alert-mail-templates/credential_expired.ftl b/smp-server-library/src/main/resources/alert-mail-templates/credential_expired.ftl deleted file mode 100644 index 5240d3f2c01f5f02a5c97147e4b77459c4bff2ff..0000000000000000000000000000000000000000 --- a/smp-server-library/src/main/resources/alert-mail-templates/credential_expired.ftl +++ /dev/null @@ -1,115 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml"> -<head> -<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> -<title>DomiSMP</title> -</head> -<body style="margin:0; padding:0; background-color: #f1f1f1;"> -<center> - <table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #f1f1f1;"> - <tr> - <td><!-- MARGIN TOP --> - - <table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #f1f1f1;"> - <tr> - <td> </td> - </tr> - </table> - - <!-- / MARGIN TOP --> - - <table width="540" align="center" border="0" cellspacing="0" cellpadding="0"> - <tr> - <!-- MARGIN LEFT --> - <td width="20" valign="top"> </td> - <!-- / MARGIN LEFT --> - <td width="500" valign="top"><!-- WRAPPER --> - - <table width="500" border="0" cellpadding="0" cellspacing="0"> - <tr> - <td valign="top" style="border:5px solid #4cbdce;"><table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #ffffff;"> - <tr> - <!-- COL LEFT --> - <td width="20" valign="top"> </td> - <!-- / COL LEFT --> - - <!-- CENTER --> - <td width="460" valign="top"><table width="100%" border="0" cellspacing="0" cellpadding="0"> - <tr> - <td height="20" valign="top"> </td> - </tr> - - <!-- TITLE --> - <tr> - <td valign="top" align="left" style=" font-size: 30px; font-family: Arial, Helvetica, sans-serif; color: #000;">DomiSMP<br/></td> - </tr> - <!-- / TITLE --> - - <!-- UNDERLINE --> - <tr> - <td valign="top"><table width="100%" border="0" cellspacing="0" cellpadding="0"> - <tr> - <td width="60" height="10" style="border-bottom:3px solid #4cbdce"></td> - <td width="400" height="5"></td> - </tr> - </table></td> - </tr> - <!-- / UNDERLINE --> - - <!-- TITLE --> - <tr> - <td valign="top" align="left" style=" font-size: 20px; font-family: Arial, Helvetica, sans-serif; color: #000;"><br/> - Credential type: ${CREDENTIAL_TYPE} is expired</td> - </tr> - <!-- / TITLE --> - - <!-- UNDERLINE --> - <tr> - <td valign="top"><table width="100%" border="0" cellspacing="0" cellpadding="0"> - <tr> - <td width="30" height="10" style="border-bottom:3px solid #000000"></td> - <td width="430" height="5"></td> - </tr> - </table></td> - </tr> - <!-- / UNDERLINE --> - - <!-- MAIN CONTENT --> - <tr> - <td valign="top" align="left" style=" font-size: 13px; font-family: Arial, Helvetica, sans-serif; color: #000;"><br/> - <br/> - <p><strong>Credential type:</strong> ${CREDENTIAL_TYPE}</p> - <p><strong>Credential identifier:</strong> ${CREDENTIAL_ID}</p> - <p><strong>Expiration date-time: </strong> ${EXPIRATION_DATETIME}</p> - <p><strong>Reporting date-time:</strong> ${REPORTING_DATETIME}</p> - <p><strong>Alert level:</strong> ${ALERT_LEVEL}</p> - <p><strong>Server name:</strong> ${SERVER_NAME}</p> - </td> - </tr> - <!-- / MAIN CONTENT --> - - <tr> - <td height="20" valign="top"> </td> - </tr> - </table></td> - <!-- / CENTER --> - <!-- COL RIGHT --> - <td width="20" valign="top"> </td> - <!-- / COL RIGHT --> - </tr> - </table></td> - </tr> - </table> - - <!-- / WRAPPER --></td> - <!-- MARGIN RIGHT --> - <td width="20" valign="top"></td> - <!-- / MARGIN RIGHT --> - </tr> - </table> - </td> - </tr> - </table> -</center> -</body> -</html> diff --git a/smp-server-library/src/main/resources/alert-mail-templates/credential_imminent_expiration.ftl b/smp-server-library/src/main/resources/alert-mail-templates/credential_imminent_expiration.ftl deleted file mode 100644 index 16783fddd3c417964a013b56eb2907203cdbb377..0000000000000000000000000000000000000000 --- a/smp-server-library/src/main/resources/alert-mail-templates/credential_imminent_expiration.ftl +++ /dev/null @@ -1,116 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml"> -<head> -<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> -<title>DomiSMP</title> -</head> -<body style="margin:0; padding:0; background-color: #f1f1f1;"> -<center> - <table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #f1f1f1;"> - <tr> - <td><!-- MARGIN TOP --> - - <table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #f1f1f1;"> - <tr> - <td> </td> - </tr> - </table> - - <!-- / MARGIN TOP --> - - <table width="540" align="center" border="0" cellspacing="0" cellpadding="0"> - <tr> - <!-- MARGIN LEFT --> - <td width="20" valign="top"> </td> - <!-- / MARGIN LEFT --> - <td width="500" valign="top"><!-- WRAPPER --> - - <table width="500" border="0" cellpadding="0" cellspacing="0"> - <tr> - <td valign="top" style="border:5px solid #4cbdce;"><table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #ffffff;"> - <tr> - <!-- COL LEFT --> - <td width="20" valign="top"> </td> - <!-- / COL LEFT --> - - <!-- CENTER --> - <td width="460" valign="top"><table width="100%" border="0" cellspacing="0" cellpadding="0"> - <tr> - <td height="20" valign="top"> </td> - </tr> - - <!-- TITLE --> - <tr> - <td valign="top" align="left" style=" font-size: 30px; font-family: Arial, Helvetica, sans-serif; color: #000;">DomiSMP<br/></td> - </tr> - <!-- / TITLE --> - - <!-- UNDERLINE --> - <tr> - <td valign="top"><table width="100%" border="0" cellspacing="0" cellpadding="0"> - <tr> - <td width="60" height="10" style="border-bottom:3px solid #4cbdce"></td> - <td width="400" height="5"></td> - </tr> - </table></td> - </tr> - <!-- / UNDERLINE --> - - <!-- TITLE --> - <tr> - <td valign="top" align="left" style=" font-size: 20px; font-family: Arial, Helvetica, sans-serif; color: #000;"><br/> - Credential type: ${CREDENTIAL_TYPE} imminent expiration</td> - </tr> - <!-- / TITLE --> - - <!-- UNDERLINE --> - <tr> - <td valign="top"><table width="100%" border="0" cellspacing="0" cellpadding="0"> - <tr> - <td width="30" height="10" style="border-bottom:3px solid #000000"></td> - <td width="430" height="5"></td> - </tr> - </table></td> - </tr> - <!-- / UNDERLINE --> - - <!-- MAIN CONTENT --> - <tr> - <td valign="top" align="left" style=" font-size: 13px; font-family: Arial, Helvetica, sans-serif; color: #000;"><br/> - <br/> - <p><strong>Credential type:</strong> ${CREDENTIAL_TYPE}</p> - <p><strong>Credential identifier:</strong> ${CREDENTIAL_ID}</p> - <p><strong>Expiration date-time: </strong> ${EXPIRATION_DATETIME}</p> - <p><strong>Reporting date-time:</strong> ${REPORTING_DATETIME}</p> - <p><strong>Alert level:</strong> ${ALERT_LEVEL}</p> - <p><strong>Server name:</strong> ${SERVER_NAME}</p> - </td> - </tr> - <!-- / MAIN CONTENT --> - - <tr> - <td height="20" valign="top"> </td> - </tr> - </table></td> - <!-- / CENTER --> - <!-- COL RIGHT --> - <td width="20" valign="top"> </td> - <!-- / COL RIGHT --> - </tr> - </table></td> - </tr> - </table> - - <!-- / WRAPPER --></td> - <!-- MARGIN RIGHT --> - <td width="20" valign="top"></td> - <!-- / MARGIN RIGHT --> - </tr> - </table> - - </td> - </tr> - </table> -</center> -</body> -</html> diff --git a/smp-server-library/src/main/resources/alert-mail-templates/credential_suspended.ftl b/smp-server-library/src/main/resources/alert-mail-templates/credential_suspended.ftl deleted file mode 100644 index 514c228bd358df4042ed33d4921efe83f0410224..0000000000000000000000000000000000000000 --- a/smp-server-library/src/main/resources/alert-mail-templates/credential_suspended.ftl +++ /dev/null @@ -1,118 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml"> -<head> -<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> -<title>DomiSMP</title> -</head> -<body style="margin:0; padding:0; background-color: #f1f1f1;"> -<center> - <table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #f1f1f1;"> - <tr> - <td><!-- MARGIN TOP --> - - <table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #f1f1f1;"> - <tr> - <td> </td> - </tr> - </table> - - <!-- / MARGIN TOP --> - - <table width="540" align="center" border="0" cellspacing="0" cellpadding="0"> - <tr> - <!-- MARGIN LEFT --> - <td width="20" valign="top"> </td> - <!-- / MARGIN LEFT --> - <td width="500" valign="top"><!-- WRAPPER --> - - <table width="500" border="0" cellpadding="0" cellspacing="0"> - <tr> - <td valign="top" style="border:5px solid #4cbdce;"><table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #ffffff;"> - <tr> - <!-- COL LEFT --> - <td width="20" valign="top"> </td> - <!-- / COL LEFT --> - - <!-- CENTER --> - <td width="460" valign="top"><table width="100%" border="0" cellspacing="0" cellpadding="0"> - <tr> - <td height="20" valign="top"> </td> - </tr> - - <!-- TITLE --> - <tr> - <td valign="top" align="left" style=" font-size: 30px; font-family: Arial, Helvetica, sans-serif; color: #000;">DomiSMP<br/></td> - </tr> - <!-- / TITLE --> - - <!-- UNDERLINE --> - <tr> - <td valign="top"><table width="100%" border="0" cellspacing="0" cellpadding="0"> - <tr> - <td width="60" height="10" style="border-bottom:3px solid #4cbdce"></td> - <td width="400" height="5"></td> - </tr> - </table></td> - </tr> - <!-- / UNDERLINE --> - - <!-- TITLE --> - <tr> - <td valign="top" align="left" style=" font-size: 20px; font-family: Arial, Helvetica, sans-serif; color: #000;"><br/> - Credential type: ${CREDENTIAL_TYPE} is temporarily suspended</td> - </tr> - <!-- / TITLE --> - - <!-- UNDERLINE --> - <tr> - <td valign="top"><table width="100%" border="0" cellspacing="0" cellpadding="0"> - <tr> - <td width="30" height="10" style="border-bottom:3px solid #000000"></td> - <td width="430" height="5"></td> - </tr> - </table></td> - </tr> - <!-- / UNDERLINE --> - - <!-- MAIN CONTENT --> - <tr> - <td valign="top" align="left" style=" font-size: 13px; font-family: Arial, Helvetica, sans-serif; color: #000;"><br/> - <br/> - <p><strong>Credential type:</strong> ${CREDENTIAL_TYPE}</p> - <p><strong>Credential identifier:</strong> ${CREDENTIAL_ID}</p> - <p><strong>Failed login attempt count:</strong> ${FAILED_LOGIN_ATTEMPT}</p> - <p><strong>Last failed login time:</strong> ${LAST_LOGIN_FAILURE_DATETIME}</p> - <p><strong>Suspended util</strong> ${SUSPENDED_UNTIL_DATETIME}</p> - <p><strong>Reporting time:</strong> ${REPORTING_DATETIME}</p> - <p><strong>Alert level:</strong> ${ALERT_LEVEL}</p> - <p><strong>Server name:</strong> ${SERVER_NAME}</p> - </td> - </tr> - <!-- / MAIN CONTENT --> - - <tr> - <td height="20" valign="top"> </td> - </tr> - </table></td> - <!-- / CENTER --> - <!-- COL RIGHT --> - <td width="20" valign="top"> </td> - <!-- / COL RIGHT --> - </tr> - </table></td> - </tr> - </table> - - <!-- / WRAPPER --></td> - <!-- MARGIN RIGHT --> - <td width="20" valign="top"></td> - <!-- / MARGIN RIGHT --> - </tr> - </table> - - </td> - </tr> - </table> -</center> -</body> -</html> diff --git a/smp-server-library/src/main/resources/alert-mail-templates/credential_verification_failed.ftl b/smp-server-library/src/main/resources/alert-mail-templates/credential_verification_failed.ftl deleted file mode 100644 index 08935d815b1fb04abb84d8c8fb51fc763bac3511..0000000000000000000000000000000000000000 --- a/smp-server-library/src/main/resources/alert-mail-templates/credential_verification_failed.ftl +++ /dev/null @@ -1,117 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml"> -<head> -<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> -<title>DomiSMP</title> -</head> -<body style="margin:0; padding:0; background-color: #f1f1f1;"> -<center> - <table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #f1f1f1;"> - <tr> - <td><!-- MARGIN TOP --> - - <table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #f1f1f1;"> - <tr> - <td> </td> - </tr> - </table> - - <!-- / MARGIN TOP --> - - <table width="540" align="center" border="0" cellspacing="0" cellpadding="0"> - <tr> - <!-- MARGIN LEFT --> - <td width="20" valign="top"> </td> - <!-- / MARGIN LEFT --> - <td width="500" valign="top"><!-- WRAPPER --> - - <table width="500" border="0" cellpadding="0" cellspacing="0"> - <tr> - <td valign="top" style="border:5px solid #4cbdce;"><table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #ffffff;"> - <tr> - <!-- COL LEFT --> - <td width="20" valign="top"> </td> - <!-- / COL LEFT --> - - <!-- CENTER --> - <td width="460" valign="top"><table width="100%" border="0" cellspacing="0" cellpadding="0"> - <tr> - <td height="20" valign="top"> </td> - </tr> - - <!-- TITLE --> - <tr> - <td valign="top" align="left" style=" font-size: 30px; font-family: Arial, Helvetica, sans-serif; color: #000;">DomiSMP<br/></td> - </tr> - <!-- / TITLE --> - - <!-- UNDERLINE --> - <tr> - <td valign="top"><table width="100%" border="0" cellspacing="0" cellpadding="0"> - <tr> - <td width="60" height="10" style="border-bottom:3px solid #4cbdce"></td> - <td width="400" height="5"></td> - </tr> - </table></td> - </tr> - <!-- / UNDERLINE --> - - <!-- TITLE --> - <tr> - <td valign="top" align="left" style=" font-size: 20px; font-family: Arial, Helvetica, sans-serif; color: #000;"><br/> - Credential type: ${CREDENTIAL_TYPE} verification failed!</td> - </tr> - <!-- / TITLE --> - - <!-- UNDERLINE --> - <tr> - <td valign="top"><table width="100%" border="0" cellspacing="0" cellpadding="0"> - <tr> - <td width="30" height="10" style="border-bottom:3px solid #000000"></td> - <td width="430" height="5"></td> - </tr> - </table></td> - </tr> - <!-- / UNDERLINE --> - - <!-- MAIN CONTENT --> - <tr> - <td valign="top" align="left" style=" font-size: 13px; font-family: Arial, Helvetica, sans-serif; color: #000;"><br/> - <br/> - <p><strong>Credential type:</strong> ${CREDENTIAL_TYPE}</p> - <p><strong>Credential identifier:</strong> ${CREDENTIAL_ID}</p> - <p><strong>Failed login attempt count:</strong> ${FAILED_LOGIN_ATTEMPT}</p> - <p><strong>Last failed login time:</strong> ${LAST_LOGIN_FAILURE_DATETIME}</p> - <p><strong>Reporting time:</strong> ${REPORTING_DATETIME}</p> - <p><strong>Alert level:</strong> ${ALERT_LEVEL}</p> - <p><strong>Server name:</strong> ${SERVER_NAME}</p> - </td> - </tr> - <!-- / MAIN CONTENT --> - - <tr> - <td height="20" valign="top"> </td> - </tr> - </table></td> - <!-- / CENTER --> - <!-- COL RIGHT --> - <td width="20" valign="top"> </td> - <!-- / COL RIGHT --> - </tr> - </table></td> - </tr> - </table> - - <!-- / WRAPPER --></td> - <!-- MARGIN RIGHT --> - <td width="20" valign="top"></td> - <!-- / MARGIN RIGHT --> - </tr> - </table> - - </td> - </tr> - </table> -</center> -</body> -</html> diff --git a/smp-server-library/src/main/resources/alert-mail-templates/test_mail.ftl b/smp-server-library/src/main/resources/alert-mail-templates/test_mail.ftl deleted file mode 100644 index 5e9fcbbbda16f8b5af73684e590a2a90349d819e..0000000000000000000000000000000000000000 --- a/smp-server-library/src/main/resources/alert-mail-templates/test_mail.ftl +++ /dev/null @@ -1,113 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml"> -<head> -<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> -<title>SMP test mail</title> -</head> -<body style="margin:0; padding:0; background-color: #f1f1f1;"> -<center> - <table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #f1f1f1;"> - <tr> - <td><!-- MARGIN TOP --> - - <table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #f1f1f1;"> - <tr> - <td> </td> - </tr> - </table> - - <!-- / MARGIN TOP --> - - <table width="540" align="center" border="0" cellspacing="0" cellpadding="0"> - <tr> - <!-- MARGIN LEFT --> - <td width="20" valign="top"> </td> - <!-- / MARGIN LEFT --> - <td width="500" valign="top"><!-- WRAPPER --> - - <table width="500" border="0" cellpadding="0" cellspacing="0"> - <tr> - <td valign="top" style="border:5px solid #4cbdce;"><table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #ffffff;"> - <tr> - <!-- COL LEFT --> - <td width="20" valign="top"> </td> - <!-- / COL LEFT --> - - <!-- CENTER --> - <td width="460" valign="top"><table width="100%" border="0" cellspacing="0" cellpadding="0"> - <tr> - <td height="20" valign="top"> </td> - </tr> - - <!-- TITLE --> - <tr> - <td valign="top" align="left" style=" font-size: 30px; font-family: Arial, Helvetica, sans-serif; color: #000;">SMP<br/></td> - </tr> - <!-- / TITLE --> - - <!-- UNDERLINE --> - <tr> - <td valign="top"><table width="100%" border="0" cellspacing="0" cellpadding="0"> - <tr> - <td width="60" height="10" style="border-bottom:3px solid #4cbdce"></td> - <td width="400" height="5"></td> - </tr> - </table></td> - </tr> - <!-- / UNDERLINE --> - - <!-- TITLE --> - <tr> - <td valign="top" align="left" style=" font-size: 20px; font-family: Arial, Helvetica, sans-serif; color: #000;"><br/> - Test mail submission</td> - </tr> - <!-- / TITLE --> - - <!-- UNDERLINE --> - <tr> - <td valign="top"><table width="100%" border="0" cellspacing="0" cellpadding="0"> - <tr> - <td width="30" height="10" style="border-bottom:3px solid #000000"></td> - <td width="430" height="5"></td> - </tr> - </table></td> - </tr> - <!-- / UNDERLINE --> - - <!-- MAIN CONTENT --> - <tr> - <td valign="top" align="left" style=" font-size: 13px; font-family: Arial, Helvetica, sans-serif; color: #000;"><br/> - <br/> - <p><strong>User:</strong> ${USERNAME}</p> - <p><strong>User mail:</strong> ${USER_MAIL}</p> - <p><strong>Server name:</strong> ${SERVER_NAME}</p> - </td> - </tr> - <!-- / MAIN CONTENT --> - - <tr> - <td height="20" valign="top"> </td> - </tr> - </table></td> - <!-- / CENTER --> - <!-- COL RIGHT --> - <td width="20" valign="top"> </td> - <!-- / COL RIGHT --> - </tr> - </table></td> - </tr> - </table> - - <!-- / WRAPPER --></td> - <!-- MARGIN RIGHT --> - <td width="20" valign="top"></td> - <!-- / MARGIN RIGHT --> - </tr> - </table> - - </td> - </tr> - </table> -</center> -</body> -</html> \ No newline at end of file diff --git a/smp-server-library/src/main/resources/mail-messages/mail-messages_en.properties b/smp-server-library/src/main/resources/mail-messages/mail-messages_en.properties new file mode 100644 index 0000000000000000000000000000000000000000..c25075d8eef1b204c47ece8af950562d0c492049 --- /dev/null +++ b/smp-server-library/src/main/resources/mail-messages/mail-messages_en.properties @@ -0,0 +1,64 @@ +# Email translations. Each email has a header, title, content and footer. The texts are can be HTML formatted. +# The header and footer are common for all emails and should describe instance and purpose of the application. +# The title and content are specific for each email type. + +mail.header=eDelivery DomiSMP sample: ${SMP_INSTANCE_NAME} +mail.footer=DomiSMP instance: ${SMP_INSTANCE_NAME} (${SERVER_NAME}), ${CURRENT_DATETIME} + +# Email texts for password reset +mail.test_mail.title=Test mail: ${CREDENTIAL_TYPE} on DomiSMP ${SMP_INSTANCE_NAME} +mail.test_mail.content=This is a test mail from DomiSMP instance: ${SMP_INSTANCE_NAME} for your user account. + + +# Email texts for password reset +mail.credential_request_reset.title=Request for reset of the Credential type: ${CREDENTIAL_TYPE} on DomiSMP ${SMP_INSTANCE_NAME} +mail.credential_request_reset.content=You're receiving this e-mail because you requested a credential type: ${CREDENTIAL_TYPE} \ + reset on DomiSMP ${SMP_INSTANCE_NAME} for your user account. <br/> Please go to the following page and choose a new password: \ + <br/> <br/> <a href="${RESET_URL}">${RESET_URL}</a> <br/> <br/> The link is valid for a short period of time. If the \ + link has expired, please request a new one. + + +mail.credential_changed.title=Credential type: ${CREDENTIAL_TYPE} on DomiSMP ${SMP_INSTANCE_NAME} changed! +mail.credential_changed.content=You're receiving this e-mail because your credential type: ${CREDENTIAL_TYPE} changed \ + on DomiSMP ${SMP_INSTANCE_NAME}! <p>If you did not update your credential, please inform DomiSMP administrator \ + immediately.</p><br>If you are having trouble accessing your account, reset your password. + +# Email texts for credential expired +mail.credential_expired.title=Credential type: ${CREDENTIAL_TYPE} is expired +mail.credential_expired.content=<p><strong>Credential type:</strong> ${CREDENTIAL_TYPE}</p> \ + <p><strong>Credential identifier:</strong> ${CREDENTIAL_ID}</p> \ + <p><strong>Expiration date-time: </strong> ${EXPIRATION_DATETIME}</p> \ + <p><strong>Reporting date-time:</strong> ${REPORTING_DATETIME}</p> \ + <p><strong>Alert level:</strong> ${ALERT_LEVEL}</p> \ + <p><strong>SMP instance name:</strong> ${SMP_INSTANCE_NAME}</p>. + +# Email texts for credential imminent expiration} +mail.credential_imminent_expiration.title=Credential type: ${CREDENTIAL_TYPE} imminent expiration +mail.credential_imminent_expiration.content=<p><strong>Credential type:</strong> ${CREDENTIAL_TYPE}</p>\ + <p><strong>Credential identifier:</strong> ${CREDENTIAL_ID}</p>\ + <p><strong>Expiration date-time: </strong> ${EXPIRATION_DATETIME}</p>\ + <p><strong>Reporting date-time:</strong> ${REPORTING_DATETIME}</p>\ + <p><strong>Alert level:</strong> ${ALERT_LEVEL}</p>\ + <p><strong>SMP instance name:</strong> ${SMP_INSTANCE_NAME}</p> + +# Email texts for credential suspended +mail.credential_suspended.title=Credential type: ${CREDENTIAL_TYPE} is temporarily suspended +mail.credential_suspended.content=<p><strong>Credential type:</strong> ${CREDENTIAL_TYPE}</p> \ + <p><strong>Credential identifier:</strong> ${CREDENTIAL_ID}</p> \ + <p><strong>Failed login attempt count:</strong> ${FAILED_LOGIN_ATTEMPT}</p> \ + <p><strong>Last failed login time:</strong> ${LAST_LOGIN_FAILURE_DATETIME}</p> \ + <p><strong>Suspended util</strong> ${SUSPENDED_UNTIL_DATETIME}</p> \ + <p><strong>Reporting time:</strong> ${REPORTING_DATETIME}</p> \ + <p><strong>Alert level:</strong> ${ALERT_LEVEL}</p> \ + <p><strong>SMP instance name:</strong> ${SMP_INSTANCE_NAME}</p> + +# Email texts for credential verification failed +mail.credential_verification_failed.title=Credential type: ${CREDENTIAL_TYPE} verification failed! +mail.credential_verification_failed.content=<p><strong>Credential type:</strong> ${CREDENTIAL_TYPE}</p> \ + <p><strong>Credential identifier:</strong> ${CREDENTIAL_ID}</p> \ + <p><strong>Failed login attempt count:</strong> ${FAILED_LOGIN_ATTEMPT}</p> \ + <p><strong>Last failed login time:</strong> ${LAST_LOGIN_FAILURE_DATETIME}</p> \ + <p><strong>Reporting time:</strong> ${REPORTING_DATETIME}</p> \ + <p><strong>Alert level:</strong> ${ALERT_LEVEL}</p> \ + <p><strong>SMP instance name:</strong> ${SMP_INSTANCE_NAME}</p> + diff --git a/smp-server-library/src/main/resources/mail-messages/mail-messages_ro.properties b/smp-server-library/src/main/resources/mail-messages/mail-messages_ro.properties new file mode 100644 index 0000000000000000000000000000000000000000..7db7607c94388c2af38a607df549054f149a9939 --- /dev/null +++ b/smp-server-library/src/main/resources/mail-messages/mail-messages_ro.properties @@ -0,0 +1,63 @@ +# Traduceri prin e-mail. Fiecare e-mail are un antet, titlu, con?inut ?i subsol. Textele pot fi formatate HTML. +# Antetul ?i subsolul sunt comune pentru toate e-mailurile ?i ar trebui s? descrie instan?a ?i scopul aplica?iei. +# Titlul ?i con?inutul sunt specifice fiec?rui tip de e-mail. + +mail.header=eDelivery E?antion DomiSMP: ${SMP_INSTANCE_NAME} +mail.footer=Instan?? DomiSMP: ${SMP_INSTANCE_NAME} (${SERVER_NAME}), ${CURRENT_DATETIME} + +# Texte de e-mail pentru resetarea parolei +mail.test_mail.title=E-mail de testare: ${CREDENTIAL_TYPE} pe DomiSMP ${SMP_INSTANCE_NAME} +mail.test_mail.content=Acesta este un e-mail de test de la instan?a DomiSMP: ${SMP_INSTANCE_NAME} pentru contul dvs. de utilizator. + +# Texte de e-mail pentru resetarea parolei +mail.credential_request_reset.title=Solicitare de resetare a tipului de autentificare: ${CREDENTIAL_TYPE} pe DomiSMP ${SMP_INSTANCE_NAME} +mail.credential_request_reset.content=Primi?i acest e-mail deoarece a?i solicitat un tip de autentificare: ${CREDENTIAL_TYPE} \ + reseta?i pe DomiSMP ${SMP_INSTANCE_NAME} pentru contul dvs. de utilizator. <br/> Accesa?i urm?toarea pagin? ?i alege?i o nou? parol?: \ + <br/> <br/> <a href="${RESET_URL}">${RESET_URL}</a> <br/> <br/> Linkul este valabil pentru o perioad? scurt? de timp. Dac? \ + linkul a expirat, v? rug?m s? solicita?i unul nou. + + +mail.credential_changed.title=Tipul de autentificare: ${CREDENTIAL_TYPE} pe DomiSMP ${SMP_INSTANCE_NAME} schimbat! +mail.credential_changed.content=Primi?i acest e-mail deoarece tipul dvs. de autentificare: ${CREDENTIAL_TYPE} s-a schimbat \ + pe DomiSMP ${SMP_INSTANCE_NAME}! <p>Dac? nu v-a?i actualizat acredit?rile, v? rug?m s? informa?i administratorul DomiSMP \ + imediat.</p><br>Dac? întâmpina?i probleme la accesarea contului, reseta?i-v? parola. + +# Textele de e-mail pentru acredit?ri au expirat +mail.credential_expired.title=Tipul acredit?rii: ${CREDENTIAL_TYPE} a expirat +mail.credential_expired.content=<p><strong>Tipul de autentificare:</strong> ${CREDENTIAL_TYPE}</p> \ + <p><strong>Identificator de acreditare:</strong> ${CREDENTIAL_ID}</p> \ + <p><strong>Data-ora expir?rii: </strong> ${EXPIRATION_DATETIME}</p> \ + <p><strong>Data-ora raport?rii:</strong> ${REPORTING_DATETIME}</p> \ + <p><strong>Nivel alert?:</strong> ${ALERT_LEVEL}</p> \ + <p><strong>Numele instan?ei SMP:</strong> ${SMP_INSTANCE_NAME}</p>. + +# Texte de e-mail pentru expirarea iminent? a acredit?rii} +mail.credential_imminent_expiration.title=Tipul acredit?rii: ${CREDENTIAL_TYPE} expirare iminent? +mail.credential_imminent_expiration.content=<p><strong>Tipul de autentificare:</strong> ${CREDENTIAL_TYPE}</p>\ + <p><strong>Identificator de acreditare:</strong> ${CREDENTIAL_ID}</p>\ + <p><strong>Data-ora expir?rii: </strong> ${EXPIRATION_DATETIME}</p>\ + <p><strong>Data-ora raport?rii:</strong> ${REPORTING_DATETIME}</p>\ + <p><strong>Nivel alert?:</strong> ${ALERT_LEVEL}</p>\ + <p><strong>Numele instan?ei SMP:</strong> ${SMP_INSTANCE_NAME}</p> + +# Texte de e-mail pentru acredit?ri suspendate +mail.credential_suspended.title=Tipul de autentificare: ${CREDENTIAL_TYPE} este suspendat temporar +mail.credential_suspended.content=<p><strong>Tipul de autentificare:</strong> ${CREDENTIAL_TYPE}</p> \ + <p><strong>Identificator de acreditare:</strong> ${CREDENTIAL_ID}</p> \ + <p><strong>Num?r de încerc?ri de conectare e?uate:</strong> ${FAILED_LOGIN_ATTEMPT}</p> \ + <p><strong>Ultima or? de conectare e?uat?:</strong> ${LAST_LOGIN_FAILURE_DATETIME}</p> \ + <p><strong>Util suspendat</strong> ${SUSPENDED_UNTIL_DATETIME}</p> \ + <p><strong>Timp de raportare:</strong> ${REPORTING_DATETIME}</p> \ + <p><strong>Nivel alert?:</strong> ${ALERT_LEVEL}</p> \ + <p><strong>Numele instan?ei SMP:</strong> ${SMP_INSTANCE_NAME}</p> + +# Texte prin e-mail pentru verificarea acredit?rilor e?uate +mail.credential_verification_failed.title=Tipul de autentificare: verificarea ${CREDENTIAL_TYPE} a e?uat! +mail.credential_verification_failed.content=<p><strong>Tipul de autentificare:</strong> ${CREDENTIAL_TYPE}</p> \ + <p><strong>Identificator de autentificare:</strong> ${CREDENTIAL_ID}</p> \ + <p><strong>Num?r de încerc?ri de conectare e?uate:</strong> ${FAILED_LOGIN_ATTEMPT}</p> \ + <p><strong>Ultima or? de conectare e?uat?:</strong> ${LAST_LOGIN_FAILURE_DATETIME}</p> \ + <p><strong>Timp de raportare:</strong> ${REPORTING_DATETIME}</p> \ + <p><strong>Nivel alert?:</strong> ${ALERT_LEVEL}</p> \ + <p><strong>Numele instan?ei SMP:</strong> ${SMP_INSTANCE_NAME}</p> + diff --git a/smp-server-library/src/main/resources/mail-messages/mail-messages_sl.properties b/smp-server-library/src/main/resources/mail-messages/mail-messages_sl.properties new file mode 100644 index 0000000000000000000000000000000000000000..f177e0c5d37b3d3df014267df6473d32c6d64551 --- /dev/null +++ b/smp-server-library/src/main/resources/mail-messages/mail-messages_sl.properties @@ -0,0 +1,64 @@ +# Prevodi e-po?te. +# Vsako e-po?tno sporo?ilo ima glavo, naslov, vsebino in nogo. Besedila so lahko oblikovana v HTML. +# Glava in noga sta skupni za vsa e-po?tna sporo?ila in bi morali opisovati primerek in namen aplikacije. +# Naslov in vsebina sta specifi?na za vsako vrsto e-po?te. + +mail.header=eDelivery DomiSMP: ${SMP_INSTANCE_NAME} +mail.footer=PNaziv SMP streznika: ${SMP_INSTANCE_NAME} (${SERVER_NAME}), ${CURRENT_DATETIME} + +# E-po?tna besedila za ponastavitev gesla +mail.test_mail.title=Testno sporo?ilo: ${CREDENTIAL_TYPE} na DomiSMP ${SMP_INSTANCE_NAME} +mail.test_mail.content=To je testna po?ta DomiSMP sreznika: ${SMP_INSTANCE_NAME} za va? uporabni?ki ra?un. + + +# E-po?tna besedila za ponastavitev gesla +mail.credential_request_reset.title=Zahteva za ponastavitev vrste poverilnice: ${CREDENTIAL_TYPE} na DomiSMP ${SMP_INSTANCE_NAME} +mail.credential_request_reset.content=To e-po?to ste prejeli, ker ste zahtevali vrsto poverilnice: ${CREDENTIAL_TYPE} \ + ponastavite na DomiSMP ${SMP_INSTANCE_NAME} za va? uporabni?ki ra?un. <br/> Pojdite na naslednjo stran in izberite novo geslo: \ + <br/> <br/> <a href="${RESET_URL}">${RESET_URL}</a> <br/> <br/> Povezava je veljavna kratek ?as. ?e \ + povezava je potekla, zahtevajte novo. + + +mail.credential_changed.title=Vrsta poverilnice: ${CREDENTIAL_TYPE} na DomiSMP ${SMP_INSTANCE_NAME} spremenjena! +mail.credential_changed.content=To e-po?to ste prejeli, ker je va?a vrsta poverilnice: ${CREDENTIAL_TYPE} spremenjena \ + na DomiSMP ${SMP_INSTANCE_NAME}! <p>?e niste posodobili svoje poverilnice, obvestite skrbnika DomiSMP \ + takoj.</p><br>?e imate te?ave z dostopom do ra?una, ponastavite geslo. + +# Besedila e-po?te za poverilnico so potekla +mail.credential_expired.title=Vrsta poverilnice: ${CREDENTIAL_TYPE} je potekla +mail.credential_expired.content=<p><strong>Vrsta poverilnice:</strong> ${CREDENTIAL_TYPE}</p> \ + <p><strong>Identifikator poverilnice:</strong> ${CREDENTIAL_ID}</p> \ + <p><strong>Datum-?as poteka: </strong> ${EXPIRATION_DATETIME}</p> \ + <p><strong>Datum in ?as poro?anja:</strong> ${REPORTING_DATETIME}</p> \ + <p><strong>Raven opozorila:</strong> ${ALERT_LEVEL}</p> \ + <p><strong>Naziv SMP streznika::</strong> ${SMP_INSTANCE_NAME}</p>. + +# Besedila e-po?te za poverilnico pred skoraj?njim iztekom} +mail.credential_imminent_expiration.title=Vrsta poverilnice: ${CREDENTIAL_TYPE} skoraj?nji potek +mail.credential_imminent_expiration.content=<p><strong>Vrsta poverilnice:</strong> ${CREDENTIAL_TYPE}</p>\ + <p><strong>Identifikator poverilnice:</strong> ${CREDENTIAL_ID}</p>\ + <p><strong>Datum-?as poteka: </strong> ${EXPIRATION_DATETIME}</p>\ + <p><strong>Datum in ?as poro?anja:</strong> ${REPORTING_DATETIME}</p>\ + <p><strong>Raven opozorila:</strong> ${ALERT_LEVEL}</p>\ + <p><strong>Naziv SMP streznika::</strong> ${SMP_INSTANCE_NAME}</p> + +# Besedila e-po?te za poverilnice so za?asno ustavljena +mail.credential_suspended.title=Vrsta poverilnice: ${CREDENTIAL_TYPE} je za?asno onemogo?ena +mail.credential_suspended.content=<p><strong>Vrsta poverilnice:</strong> ${CREDENTIAL_TYPE}</p> \ + <p><strong>Identifikator poverilnice:</strong> ${CREDENTIAL_ID}</p> \ + <p><strong>?tevilo neuspelih poskusov prijave:</strong> ${FAILED_LOGIN_ATTEMPT}</p> \ + <p><strong>?as zadnje neuspe?ne prijave:</strong> ${LAST_LOGIN_FAILURE_DATETIME}</p> \ + <p><strong>Za?asno ustavljen</strong> ${SUSPENDED_UNTIL_DATETIME}</p> \ + <p><strong>?as poro?anja:</strong> ${REPORTING_DATETIME}</p> \ + <p><strong>Raven opozorila:</strong> ${ALERT_LEVEL}</p> \ + <p><strong>Naziv SMP streznika::</strong> ${SMP_INSTANCE_NAME}</p> + +# E-po?tna besedila za preverjanje poverilnice niso uspela +mail.credential_verification_failed.title=Vrsta poverilnice: ${CREDENTIAL_TYPE} preverjanje ni uspelo! +mail.credential_verification_failed.content=<p><strong>Vrsta poverilnice:</strong> ${CREDENTIAL_TYPE}</p> \ + <p><strong>Identifikator poverilnice:</strong> ${CREDENTIAL_ID}</p> \ + <p><strong>?tevilo neuspelih poskusov prijave:</strong> ${FAILED_LOGIN_ATTEMPT}</p> \ + <p><strong>?as zadnje neuspe?ne prijave:</strong> ${LAST_LOGIN_FAILURE_DATETIME}</p> \ + <p><strong>?as poro?anja:</strong> ${REPORTING_DATETIME}</p> \ + <p><strong>Raven opozorila:</strong> ${ALERT_LEVEL}</p> \ + <p><strong>Naziv SMP streznika::</strong> ${SMP_INSTANCE_NAME}</p> diff --git a/smp-server-library/src/main/resources/mail-messages/mail-template.htm b/smp-server-library/src/main/resources/mail-messages/mail-template.htm new file mode 100644 index 0000000000000000000000000000000000000000..e46c7315084b698e237dcf020ef3692d7300cedb --- /dev/null +++ b/smp-server-library/src/main/resources/mail-messages/mail-template.htm @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <title>${MAIL_HEADER}</title> +</head> +<body style="margin: 0;padding: 0;background-color: #f1f1f1;"> +<div style="display: flex;flex-direction: column;align-items: center;padding: 3em;"> + <div style="padding: 2em;width: 100%;max-width: 1280px;background-color: white;border: 5px solid #4cbdce;"> + <div style="font-size: 30px;font-family: Arial, Helvetica, sans-serif;color: black;"> + ${MAIL_HEADER} + </div> + <hr style="border: 4px solid #4cbdce;border-radius: 4px;max-width: 150px;margin: 0.2em 0 1.5em 0;"> + <div style="font-size: 20px;font-family: Arial, Helvetica, sans-serif;color: #000;"> + ${MAIL_TITLE} + </div> + <hr style="border: 3px solid black;border-radius: 3px;max-width: 120px;margin: 0.2em 0 1.5em 0;"> + <div style="margin-top: 2em;font-size: 14px;font-family: Arial, Helvetica, sans-serif;color: #000;"> + ${MAIL_CONTENT} + </div> + <div style="border-top: 1px solid black;text-align: center;margin: 4em -3em -3em -3em;padding: 0.5em;font-size: 10px;font-family: Arial, Helvetica, sans-serif;color: #000;"> + ${MAIL_FOOTER} + </div> + </div> +</div> + +</body> +</html> diff --git a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/AlertServiceTest.java b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/AlertServiceTest.java index 2493a8b856a3211b5abafe12b7894b2959a070ac..fce94e65088329e9deb01e6df51ea998d493138b 100644 --- a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/AlertServiceTest.java +++ b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/AlertServiceTest.java @@ -8,9 +8,9 @@ * versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: - * + * * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * + * * Unless required by applicable law or agreed to in writing, software distributed under the Licence is * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Licence for the specific language governing permissions and limitations under the Licence. @@ -28,12 +28,13 @@ import eu.europa.ec.edelivery.smp.data.model.user.DBUser; import eu.europa.ec.edelivery.smp.data.ui.enums.AlertLevelEnum; import eu.europa.ec.edelivery.smp.data.ui.enums.AlertStatusEnum; import eu.europa.ec.edelivery.smp.data.ui.enums.AlertTypeEnum; -import eu.europa.ec.edelivery.smp.services.mail.MailModel; +import eu.europa.ec.edelivery.smp.services.mail.MailDataModel; import eu.europa.ec.edelivery.smp.services.mail.MailService; import eu.europa.ec.edelivery.smp.services.mail.prop.CredentialSuspendedProperties; import eu.europa.ec.edelivery.smp.services.mail.prop.CredentialVerificationFailedProperties; import eu.europa.ec.edelivery.smp.services.mail.prop.CredentialsExpirationProperties; import eu.europa.ec.edelivery.smp.testutil.TestDBUtils; +import eu.europa.ec.edelivery.smp.utils.SmpUrlBuilder; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -41,7 +42,6 @@ import org.mockito.Mockito; import java.time.OffsetDateTime; import java.util.Arrays; import java.util.List; -import java.util.Properties; import java.util.stream.Collectors; import static org.junit.Assert.*; @@ -54,10 +54,17 @@ public class AlertServiceTest { ConfigurationService configurationService = Mockito.mock(ConfigurationService.class); UserDao userDao = Mockito.mock(UserDao.class); CredentialDao credentialDao = Mockito.mock(CredentialDao.class); + SmpUrlBuilder smpUrlBuilder = Mockito.mock(SmpUrlBuilder.class); SMPDynamicCronTrigger alertCronTrigger = Mockito.mock(SMPDynamicCronTrigger.class); - CredentialsAlertService testInstance = new CredentialsAlertService(alertDao, mailService, configurationService, userDao, credentialDao, alertCronTrigger); + CredentialsAlertService testInstance = new CredentialsAlertService(alertDao, + mailService, + configurationService, + userDao, + credentialDao, + smpUrlBuilder, + alertCronTrigger); @Test public void testCreateAlert() { @@ -85,8 +92,9 @@ public class AlertServiceTest { public void testSubmitAlertMailNoMail() { DBAlert alert = new DBAlert(); + DBUser user = Mockito.mock(DBUser.class); - testInstance.submitAlertMail(alert); + testInstance.submitAlertMail(alert, user); verify(mailService, Mockito.never()).sendMail(Mockito.any(), Mockito.anyString(), Mockito.anyString()); } @@ -105,7 +113,7 @@ public class AlertServiceTest { doReturn(mailFrom).when(configurationService).getAlertEmailFrom(); AlertTypeEnum alertType = AlertTypeEnum.CREDENTIAL_IMMINENT_EXPIRATION; - List<String> expectedTemplateProperties = Arrays.asList(CredentialsExpirationProperties.values()).stream() + List<String> expectedTemplateProperties = Arrays.stream(CredentialsExpirationProperties.values()) .map(CredentialsExpirationProperties::name).collect(Collectors.toList()); // when testInstance.alertBeforeCredentialExpire(credential); @@ -132,7 +140,7 @@ public class AlertServiceTest { doReturn(alertLevel).when(configurationService).getAlertExpiredPasswordLevel(); doReturn(mailFrom).when(configurationService).getAlertEmailFrom(); AlertTypeEnum alertType = AlertTypeEnum.CREDENTIAL_EXPIRED; - List<String> expectedTemplateProperties = Arrays.asList(CredentialsExpirationProperties.values()).stream() + List<String> expectedTemplateProperties = Arrays.stream(CredentialsExpirationProperties.values()) .map(CredentialsExpirationProperties::name).collect(Collectors.toList()); // when testInstance.alertCredentialExpired(credential); @@ -159,7 +167,7 @@ public class AlertServiceTest { doReturn(mailFrom).when(configurationService).getAlertEmailFrom(); AlertTypeEnum alertType = AlertTypeEnum.CREDENTIAL_IMMINENT_EXPIRATION; - List<String> expectedTemplateProperties = Arrays.asList(CredentialsExpirationProperties.values()).stream() + List<String> expectedTemplateProperties = Arrays.stream(CredentialsExpirationProperties.values()) .map(CredentialsExpirationProperties::name).collect(Collectors.toList()); // when testInstance.alertBeforeCredentialExpire(credential); @@ -185,7 +193,7 @@ public class AlertServiceTest { doReturn(alertLevel).when(configurationService).getAlertExpiredAccessTokenLevel(); doReturn(mailFrom).when(configurationService).getAlertEmailFrom(); AlertTypeEnum alertType = AlertTypeEnum.CREDENTIAL_EXPIRED; - List<String> expectedTemplateProperties = Arrays.asList(CredentialsExpirationProperties.values()).stream() + List<String> expectedTemplateProperties = Arrays.stream(CredentialsExpirationProperties.values()) .map(CredentialsExpirationProperties::name).collect(Collectors.toList()); // when testInstance.alertCredentialExpired(credential); @@ -210,7 +218,7 @@ public class AlertServiceTest { doReturn(alertLevel).when(configurationService).getAlertBeforeExpireCertificateLevel(); doReturn(mailFrom).when(configurationService).getAlertEmailFrom(); AlertTypeEnum alertType = AlertTypeEnum.CREDENTIAL_IMMINENT_EXPIRATION; - List<String> expectedTemplateProperties = Arrays.asList(CredentialsExpirationProperties.values()).stream() + List<String> expectedTemplateProperties = Arrays.stream(CredentialsExpirationProperties.values()) .map(CredentialsExpirationProperties::name).collect(Collectors.toList()); // when testInstance.alertBeforeCredentialExpire(credential); @@ -235,7 +243,7 @@ public class AlertServiceTest { doReturn(alertLevel).when(configurationService).getAlertExpiredCertificateLevel(); doReturn(mailFrom).when(configurationService).getAlertEmailFrom(); AlertTypeEnum alertType = AlertTypeEnum.CREDENTIAL_EXPIRED; - List<String> expectedTemplateProperties = Arrays.asList(CredentialsExpirationProperties.values()).stream() + List<String> expectedTemplateProperties = Arrays.stream(CredentialsExpirationProperties.values()) .map(CredentialsExpirationProperties::name).collect(Collectors.toList()); // when @@ -254,7 +262,10 @@ public class AlertServiceTest { String mailTo = "test.mail@domain.eu"; String mailFrom = "test.mail@domain.eu"; String mailSubject = "mailSubject"; + String language = "en"; AlertTypeEnum template = AlertTypeEnum.CREDENTIAL_IMMINENT_EXPIRATION; + DBUser user = Mockito.mock(DBUser.class); + doReturn(language).when(user).getSmpLocale(); DBAlert alert = new DBAlert(); alert.setAlertType(template); alert.setMailTo(mailTo); @@ -262,9 +273,9 @@ public class AlertServiceTest { alert.addProperty("test", "testValue"); doReturn(mailFrom).when(configurationService).getAlertEmailFrom(); - testInstance.submitAlertMail(alert); + testInstance.submitAlertMail(alert, user); - ArgumentCaptor<MailModel<Properties>> argModel = ArgumentCaptor.forClass(MailModel.class); + ArgumentCaptor<MailDataModel> argModel = ArgumentCaptor.forClass(MailDataModel.class); ArgumentCaptor<String> argMailTo = ArgumentCaptor.forClass(String.class); ArgumentCaptor<String> argFrom = ArgumentCaptor.forClass(String.class); @@ -274,9 +285,9 @@ public class AlertServiceTest { assertEquals(mailTo, argMailTo.getValue()); assertEquals(mailFrom, argFrom.getValue()); - assertEquals(mailSubject, argModel.getValue().getSubject()); - assertEquals(template.getTemplate(), argModel.getValue().getTemplatePath()); - assertEquals(1, argModel.getValue().getModel().size()); + assertEquals(AlertTypeEnum.CREDENTIAL_IMMINENT_EXPIRATION, argModel.getValue().getMailType()); + assertEquals(language, argModel.getValue().getLanguage()); + assertEquals(3, argModel.getValue().getModel().size()); } @Test @@ -297,7 +308,7 @@ public class AlertServiceTest { doReturn(mailFrom).when(configurationService).getAlertEmailFrom(); AlertTypeEnum alertType = AlertTypeEnum.CREDENTIAL_VERIFICATION_FAILED; - List<String> expectedTemplateProperties = Arrays.asList(CredentialVerificationFailedProperties.values()).stream() + List<String> expectedTemplateProperties = Arrays.stream(CredentialVerificationFailedProperties.values()) .map(CredentialVerificationFailedProperties::name).collect(Collectors.toList()); // when @@ -329,7 +340,7 @@ public class AlertServiceTest { doReturn(mailFrom).when(configurationService).getAlertEmailFrom(); //doReturn(123456).when(configurationService).getLoginSuspensionTimeInSeconds(); AlertTypeEnum alertType = AlertTypeEnum.CREDENTIAL_VERIFICATION_FAILED; - List<String> expectedTemplateProperties = Arrays.asList(CredentialVerificationFailedProperties.values()).stream() + List<String> expectedTemplateProperties = Arrays.stream(CredentialVerificationFailedProperties.values()) .map(CredentialVerificationFailedProperties::name).collect(Collectors.toList()); // when @@ -361,7 +372,7 @@ public class AlertServiceTest { doReturn(mailFrom).when(configurationService).getAlertEmailFrom(); doReturn(123456).when(configurationService).getLoginSuspensionTimeInSeconds(); AlertTypeEnum alertType = AlertTypeEnum.CREDENTIAL_SUSPENDED; - List<String> expectedTemplateProperties = Arrays.asList(CredentialSuspendedProperties.values()).stream() + List<String> expectedTemplateProperties = Arrays.stream(CredentialSuspendedProperties.values()) .map(CredentialSuspendedProperties::name).collect(Collectors.toList()); // when @@ -394,7 +405,7 @@ public class AlertServiceTest { doReturn(mailFrom).when(configurationService).getAlertEmailFrom(); doReturn(123456).when(configurationService).getLoginSuspensionTimeInSeconds(); AlertTypeEnum alertType = AlertTypeEnum.CREDENTIAL_SUSPENDED; - List<String> expectedTemplateProperties = Arrays.asList(CredentialSuspendedProperties.values()).stream() + List<String> expectedTemplateProperties = Arrays.stream(CredentialSuspendedProperties.values()) .map(CredentialSuspendedProperties::name).collect(Collectors.toList()); // when @@ -412,7 +423,7 @@ public class AlertServiceTest { public void assertAlertSend(AlertTypeEnum alertType, String mailTo, String mailFrom, String mailSubject, List<String> templateProperties) { - ArgumentCaptor<MailModel<Properties>> argModel = ArgumentCaptor.forClass(MailModel.class); + ArgumentCaptor<MailDataModel> argModel = ArgumentCaptor.forClass(MailDataModel.class); ArgumentCaptor<String> argMailFrom = ArgumentCaptor.forClass(String.class); ArgumentCaptor<String> argMailTo = ArgumentCaptor.forClass(String.class); ArgumentCaptor<DBAlert> argAlert = ArgumentCaptor.forClass(DBAlert.class); @@ -429,16 +440,16 @@ public class AlertServiceTest { assertEquals(mailFrom, argMailFrom.getValue()); - MailModel<Properties> model = argModel.getValue(); - assertEquals(alertType.getTemplate(), model.getTemplatePath()); - assertEquals(mailSubject, model.getSubject()); + MailDataModel model = argModel.getValue(); + assertEquals(alertType, model.getMailType()); + assertEquals("en", model.getLanguage()); // test to contain all properties for (String prop : templateProperties) { - assertTrue(prop, model.getModel().containsKey(prop)); } - assertEquals(templateProperties.size(), model.getModel().size()); + // add two common properties: CURRENT_DATETIME, SMP_INSTANCE_NAME + assertEquals(templateProperties.size() + 2, model.getModel().size()); } diff --git a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/mail/MailServiceTest.java b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/mail/MailServiceTest.java index d2140910022ca3376e10c2fdb7cb0d09f87dab74..6ba282546fb3bf6428359cc74056fa261a3f6d15 100644 --- a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/mail/MailServiceTest.java +++ b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/mail/MailServiceTest.java @@ -30,6 +30,8 @@ import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.test.context.ContextConfiguration; import javax.mail.internet.MimeMessage; +import java.util.HashMap; +import java.util.Map; @ContextConfiguration(classes = {MockAlertBeans.class, MailService.class}) public class MailServiceTest extends AbstractServiceIntegrationTest { @@ -45,12 +47,15 @@ public class MailServiceTest extends AbstractServiceIntegrationTest { public void testSendMail() { Mockito.doNothing().when(mockJavaMailSender).send((MimeMessage) Mockito.any()); + Map<String, String> props = new HashMap<>(); - PropertiesMailModel props = new PropertiesMailModel(AlertTypeEnum.TEST_ALERT.getTemplate(), "testMail"); - props.setProperty(TestMailProperties.SERVER_NAME.name(), "server name"); - props.setProperty(TestMailProperties.USERNAME.name(), "username"); - props.setProperty(TestMailProperties.USER_MAIL.name(), "test@test-receiver-mail.eu"); - testInstance.sendMail(props, "test@test-sender-mail.eu", "test@test-receiver-mail.eu"); + props.put(TestMailProperties.SERVER_NAME.name(), "server name"); + props.put(TestMailProperties.USERNAME.name(), "username"); + props.put(TestMailProperties.USER_MAIL.name(), "test@test-receiver-mail.eu"); + + MailDataModel data = new MailDataModel("en", AlertTypeEnum.TEST_ALERT, props); + + testInstance.sendMail(data, "test@test-sender-mail.eu", "test@test-receiver-mail.eu"); Mockito.verify(mockJavaMailSender, Mockito.times(1)).send((MimeMessage) Mockito.any()); } diff --git a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/mail/MailTemplateTest.java b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/mail/MailTemplateTest.java new file mode 100644 index 0000000000000000000000000000000000000000..1d18d2594e07a6e168e846c2cef806e1b0e4093f --- /dev/null +++ b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/mail/MailTemplateTest.java @@ -0,0 +1,56 @@ +/*- + * #START_LICENSE# + * smp-server-library + * %% + * Copyright (C) 2017 - 2023 European Commission | eDelivery | DomiSMP + * %% + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European Commission - subsequent + * versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence is + * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and limitations under the Licence. + * #END_LICENSE# + */ +package eu.europa.ec.edelivery.smp.services.mail; + +import eu.europa.ec.edelivery.smp.data.ui.enums.AlertTypeEnum; +import eu.europa.ec.edelivery.smp.services.mail.prop.CredentialsExpirationProperties; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MailTemplateTest { + + + MailTemplateService testInstance = new MailTemplateService(); + + @Test + void getMailContent() { + + Map<String, String> props = new HashMap<>(); + props.put(CredentialsExpirationProperties.ALERT_LEVEL.name(), "alert level"); + props.put(CredentialsExpirationProperties.CREDENTIAL_ID.name(), "credential id"); + props.put(CredentialsExpirationProperties.CREDENTIAL_TYPE.name(), "credential name"); + props.put(CredentialsExpirationProperties.EXPIRATION_DATETIME.name(), "expiration date"); + props.put(CredentialsExpirationProperties.REPORTING_DATETIME.name(), "reporting date"); + props.put(CredentialsExpirationProperties.SERVER_NAME.name(), "server name"); + + MailDataModel model = new MailDataModel("en", AlertTypeEnum.CREDENTIAL_EXPIRED, props); + + String result = testInstance.getMailHtmlContent(model); + assertNotNull(result); + assertTrue(result.contains("alert level")); + assertTrue(result.contains("credential id")); + assertTrue(result.contains("credential name")); + + } +} diff --git a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/testutil/TestDBUtils.java b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/testutil/TestDBUtils.java index a3b78db06d54512f54f6517ad48291def5f6c977..13f490aac2f0ee6df79c88662ecc8598d86d6c2c 100644 --- a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/testutil/TestDBUtils.java +++ b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/testutil/TestDBUtils.java @@ -324,6 +324,7 @@ public class TestDBUtils { public static DBUser createDBUserByUsername(String userName) { DBUser dbuser = new DBUser(); dbuser.setUsername(userName); + dbuser.setSmpLocale("en"); dbuser.setEmailAddress(userName + "@test.eu"); dbuser.setActive(true); dbuser.setApplicationRole(ApplicationRoleType.USER); diff --git a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/utils/StringNamedSubstitutorTest.java b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/utils/StringNamedSubstitutorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a0b465c62c1712685f44b0b661006348050e9022 --- /dev/null +++ b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/utils/StringNamedSubstitutorTest.java @@ -0,0 +1,53 @@ +/*- + * #START_LICENSE# + * smp-server-library + * %% + * Copyright (C) 2017 - 2023 European Commission | eDelivery | DomiSMP + * %% + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European Commission - subsequent + * versions of the EUPL (the "Licence"); + * You may not use this work except in compliance with the Licence. + * You may obtain a copy of the Licence at: + * + * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence is + * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Licence for the specific language governing permissions and limitations under the Licence. + * #END_LICENSE# + */ +package eu.europa.ec.edelivery.smp.utils; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class StringNamedSubstitutorTest { + + + @ParameterizedTest + @CsvSource({ + "'The quick ${FOX_COLOR} fox jumps over the ${DOG_MODE} dog', " + + "'FOX_COLOR=red;DOG_MODE=slow', " + + "'The quick red fox jumps over the slow dog'", + "'The quick ${fox_COLOR} fox jumps over the ${dog_MODE} dog', " + + "'FOX_COLOR=red;DOG_MODE=slow', " + + "'The quick red fox jumps over the slow dog'", + "'The quick ${FOX_COLOR} fox jumps over the ${DOG_MODE} dog', " + + "'FOX_COLOR=red', " + + "'The quick red fox jumps over the ${DOG_MODE} dog'", + }) + void resolve(String testString, String values, String expected) { + Map<String, String> mapVal = Stream.of(values.split("\\s*;\\s*")) + .map(s -> s.split("\\s*=\\s*")) + .collect(HashMap::new, (m, v) -> m.put(v[0], v[1]), Map::putAll); + + String result = StringNamedSubstitutor.resolve(testString, mapVal); + assertEquals(expected, result); + } +} diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/auth/SMPAuthenticationService.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/auth/SMPAuthenticationService.java index 0de18387029fcb166d789e169391afaeca40d3f2..4ff89507f36b96b12a04f644f3f16dc7cdf21f45 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/auth/SMPAuthenticationService.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/auth/SMPAuthenticationService.java @@ -8,9 +8,9 @@ * versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: - * + * * [PROJECT_HOME]\license\eupl-1.2\license.txt or https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * + * * Unless required by applicable law or agreed to in writing, software distributed under the Licence is * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Licence for the specific language governing permissions and limitations under the Licence. @@ -19,8 +19,10 @@ package eu.europa.ec.edelivery.smp.auth; import eu.europa.ec.edelivery.smp.config.SMPSecurityConstants; +import eu.europa.ec.edelivery.smp.data.enums.CredentialType; import eu.europa.ec.edelivery.smp.logging.SMPLogger; import eu.europa.ec.edelivery.smp.logging.SMPLoggerFactory; +import eu.europa.ec.edelivery.smp.services.CredentialService; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; @@ -33,6 +35,7 @@ import org.springframework.transaction.annotation.Transactional; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.util.Calendar; import static eu.europa.ec.edelivery.smp.utils.SMPCookieWriter.CSRF_COOKIE_NAME; import static eu.europa.ec.edelivery.smp.utils.SMPCookieWriter.SESSION_COOKIE_NAME; @@ -49,9 +52,12 @@ public class SMPAuthenticationService { private static final SMPLogger LOG = SMPLoggerFactory.getLogger(SMPAuthenticationService.class); private final AuthenticationManager authenticationManager; + private final CredentialService credentialService; - public SMPAuthenticationService(@Qualifier(SMPSecurityConstants.SMP_UI_AUTHENTICATION_MANAGER_BEAN) AuthenticationManager authenticationManager) { + public SMPAuthenticationService(@Qualifier(SMPSecurityConstants.SMP_UI_AUTHENTICATION_MANAGER_BEAN) AuthenticationManager authenticationManager, + CredentialService credentialService) { this.authenticationManager = authenticationManager; + this.credentialService = credentialService; } @Transactional(noRollbackFor = AuthenticationException.class) @@ -63,6 +69,30 @@ public class SMPAuthenticationService { return authentication; } + + /** + * Method retrieves user credentials by username. First it validates if credentials have already active reset token + * and if not it creates new one. + * + * @param username + */ + public void requestResetUsername(String username) { + LOG.info("requestResetUsername [{}]", username); + // retrieve user Optional credentials by username + long startTime = Calendar.getInstance().getTimeInMillis(); + credentialService.requestResetUsername(username); + // delay response to prevent timing attack + credentialService.delayResponse(CredentialType.USERNAME_PASSWORD, startTime); + } + + public void resetUsernamePassword(String username, String resetToken, String newPassword) { + LOG.info("resetUsernamePassword [{}]", username); + // retrieve user Optional credentials by username + long startTime = Calendar.getInstance().getTimeInMillis(); + credentialService.resetUsernamePassword(username, resetToken, newPassword); + credentialService.delayResponse(CredentialType.USERNAME_PASSWORD, startTime); + } + public void logout(HttpServletRequest request, HttpServletResponse response) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth == null) { diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/AuthenticationController.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/AuthenticationController.java index 2232814ab874a664db554fbfe32593b1ed353782..2f44e4ab6e0a7b9a74d2af18971ad1af40ab26ce 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/AuthenticationController.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/AuthenticationController.java @@ -23,6 +23,9 @@ import eu.europa.ec.edelivery.smp.auth.SMPAuthenticationService; import eu.europa.ec.edelivery.smp.auth.SMPAuthorizationService; import eu.europa.ec.edelivery.smp.auth.SMPUserDetails; import eu.europa.ec.edelivery.smp.auth.UILoginAuthenticationToken; +import eu.europa.ec.edelivery.smp.data.enums.CredentialType; +import eu.europa.ec.edelivery.smp.data.ui.CredentialRequestResetRO; +import eu.europa.ec.edelivery.smp.data.ui.CredentialResetRO; import eu.europa.ec.edelivery.smp.data.ui.LoginRO; import eu.europa.ec.edelivery.smp.data.ui.UserRO; import eu.europa.ec.edelivery.smp.logging.SMPLogger; @@ -47,6 +50,9 @@ import static eu.europa.ec.edelivery.smp.data.ui.auth.SMPAuthority.S_AUTHORITY_T import static eu.europa.ec.edelivery.smp.utils.SMPCookieWriter.SESSION_COOKIE_NAME; /** + * The AuthenticationController class is a REST controller that provides endpoints for user authentication actions as + * login, logout, logged user data retrieval, request credential reset. + * * @author Sebastian-Ion TINCU * @since 4.0 */ @@ -81,7 +87,7 @@ public class AuthenticationController { this.csrfTokenRepository = csrfTokenRepository; } - @PostMapping(value = "authentication") + @PostMapping(value = ResourceConstants.PATH_ACTION_AUTHENTICATION) @Transactional(noRollbackFor = BadCredentialsException.class) public UserRO authenticate(@RequestBody LoginRO loginRO, HttpServletRequest request, HttpServletResponse response) { LOG.debug("Authenticating user [{}]", loginRO.getUsername()); @@ -98,7 +104,46 @@ public class AuthenticationController { return authorizationService.getUserData(user.getUser()); } - @DeleteMapping(value = "authentication") + /** + * Request reset of the credentials. The method generates a reset token and sends an email to the user. + * + * @param requestResetRO - the request object containing the credential name and type + */ + @PostMapping(value = ResourceConstants.PATH_ACTION_RESET_CREDENTIAL_REQUEST ) + public void requestResetCredentials(@RequestBody CredentialRequestResetRO requestResetRO) { + LOG.debug("credentialRequestResetRO [{}]", requestResetRO.getCredentialName()); + if (requestResetRO.getCredentialType() == CredentialType.USERNAME_PASSWORD) { + authenticationService.requestResetUsername(requestResetRO.getCredentialName()); + } else { + LOG.warn("Invalid or null credential type [{}] not supported for reset!", + requestResetRO.getCredentialType()); + throw new IllegalArgumentException("Invalid request!"); + + } + } + + /** + * Reset the credentials. The method validates the reset token and updates the credentials. + * + * @param resetRO - the reset object containing the credential name, type, reset token and new credential value + */ + @PostMapping(value = ResourceConstants.PATH_ACTION_RESET_CREDENTIAL ) + public void resetCredentials(@RequestBody CredentialResetRO resetRO) { + LOG.debug("credentialResetRO [{}]", resetRO.getCredentialName()); + if (resetRO.getCredentialType() == CredentialType.USERNAME_PASSWORD) { + + authenticationService.resetUsernamePassword(resetRO.getCredentialName(), + resetRO.getResetToken(), + resetRO.getCredentialValue()); + } else { + LOG.warn("Invalid or null credential type [{}] not supported for reset!", + resetRO.getCredentialType()); + throw new IllegalArgumentException("Invalid request!"); + + } + } + + @DeleteMapping(value = ResourceConstants.PATH_ACTION_AUTHENTICATION) public void logout(HttpServletRequest request, HttpServletResponse response) { LOG.info("Logging out user for the session"); authenticationService.logout(request, response); diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/ResourceConstants.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/ResourceConstants.java index d972175a2a2c9a587209b234a88151fc8e6072bb..69e900415da0f072f0ec599228f2b2b89aef0300 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/ResourceConstants.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/ResourceConstants.java @@ -68,6 +68,9 @@ public class ResourceConstants { public static final String PATH_ACTION_PUT = "put"; public static final String PATH_ACTION_VALIDATE = "validate"; public static final String PATH_ACTION_GENERATE = "generate"; + public static final String PATH_ACTION_RESET_CREDENTIAL_REQUEST = "request-reset-credential"; + public static final String PATH_ACTION_RESET_CREDENTIAL = "reset-credential"; + public static final String PATH_ACTION_AUTHENTICATION = "authentication"; public static final String PATH_ACTION_RETRIEVE = "retrieve"; public static final String PATH_ACTION_SEARCH = "search"; diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/external/ApplicationController.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/external/ApplicationController.java index ae839c0a25d099307c3bce9194b5311d39f43331..4e4e7e3898c49ba2a23e7dd135a7512f49e5fd50 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/external/ApplicationController.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/external/ApplicationController.java @@ -82,6 +82,9 @@ public class ApplicationController { info.setSsoAuthenticationURI(configurationService.getCasSMPLoginRelativePath()); } info.setContextPath(getRootContext()); + // set additional public info + info.setPasswordValidationRegExp(configurationService.getPasswordPolicyRexExpPattern()); + info.setPasswordValidationRegExpMessage(configurationService.getPasswordPolicyValidationMessage()); return info; } diff --git a/smp-webapp/src/main/smp-setup/database-scripts/mysql5innodb.ddl b/smp-webapp/src/main/smp-setup/database-scripts/mysql5innodb.ddl index a1ede38f7eedccbbf899acb07ddea25e0044df4b..7d12a7f04a010a51f8c3e199755756352ccf1411 100644 --- a/smp-webapp/src/main/smp-setup/database-scripts/mysql5innodb.ddl +++ b/smp-webapp/src/main/smp-setup/database-scripts/mysql5innodb.ddl @@ -125,6 +125,8 @@ EXPIRE_ON datetime comment 'Date when password will expire', LAST_FAILED_LOGIN_ON datetime comment 'Last failed login attempt', CREDENTIAL_NAME varchar(256) CHARACTER SET utf8 COLLATE utf8_bin not null comment 'Unique username identifier. The Username must not be null', + RESET_EXPIRE_ON datetime comment 'Date time when reset token will expire', + RESET_TOKEN varchar(256) CHARACTER SET utf8 COLLATE utf8_bin comment 'Reset token for credential reset', LOGIN_FAILURE_COUNT integer comment 'Sequential login failure count', CREDENTIAL_VALUE varchar(256) CHARACTER SET utf8 COLLATE utf8_bin comment 'Credential value - it can be encrypted value', FK_USER_ID bigint not null, @@ -147,6 +149,8 @@ EXPIRE_ON datetime, LAST_FAILED_LOGIN_ON datetime, CREDENTIAL_NAME varchar(256) CHARACTER SET utf8 COLLATE utf8_bin, + RESET_EXPIRE_ON datetime, + RESET_TOKEN varchar(256) CHARACTER SET utf8 COLLATE utf8_bin, LOGIN_FAILURE_COUNT integer, CREDENTIAL_VALUE varchar(256) CHARACTER SET utf8 COLLATE utf8_bin, FK_USER_ID bigint, diff --git a/smp-webapp/src/main/smp-setup/database-scripts/oracle10g.ddl b/smp-webapp/src/main/smp-setup/database-scripts/oracle10g.ddl index 97f8608698e1fe81a22f279af95c0926bec61c79..f9bcd64fd4194234675635ccc69c68dfb45b3545 100644 --- a/smp-webapp/src/main/smp-setup/database-scripts/oracle10g.ddl +++ b/smp-webapp/src/main/smp-setup/database-scripts/oracle10g.ddl @@ -194,6 +194,8 @@ create sequence SMP_USER_SEQ start with 1 increment by 1; EXPIRE_ON timestamp, LAST_FAILED_LOGIN_ON timestamp, CREDENTIAL_NAME varchar2(256 char) not null, + RESET_EXPIRE_ON timestamp, + RESET_TOKEN varchar2(256 char), LOGIN_FAILURE_COUNT number(10,0), CREDENTIAL_VALUE varchar2(256 char), FK_USER_ID number(19,0) not null, @@ -236,6 +238,12 @@ create sequence SMP_USER_SEQ start with 1 increment by 1; comment on column SMP_CREDENTIAL.CREDENTIAL_NAME is 'Unique username identifier. The Username must not be null'; + comment on column SMP_CREDENTIAL.RESET_EXPIRE_ON is + 'Date time when reset token will expire'; + + comment on column SMP_CREDENTIAL.RESET_TOKEN is + 'Reset token for credential reset'; + comment on column SMP_CREDENTIAL.LOGIN_FAILURE_COUNT is 'Sequential login failure count'; @@ -258,6 +266,8 @@ create sequence SMP_USER_SEQ start with 1 increment by 1; EXPIRE_ON timestamp, LAST_FAILED_LOGIN_ON timestamp, CREDENTIAL_NAME varchar2(256 char), + RESET_EXPIRE_ON timestamp, + RESET_TOKEN varchar2(256 char), LOGIN_FAILURE_COUNT number(10,0), CREDENTIAL_VALUE varchar2(256 char), FK_USER_ID number(19,0),