diff --git a/smp-angular/src/app/app.component.html b/smp-angular/src/app/app.component.html index 78e7522dfdbdf7a3f368d03120ace6a50fb8a61d..6430c83989f01e0b148da751f896f65b9576e652 100644 --- a/smp-angular/src/app/app.component.html +++ b/smp-angular/src/app/app.component.html @@ -92,10 +92,10 @@ <mat-icon>person</mat-icon> <span>{{currentUser}}</span> </button> - <button mat-menu-item id="changePassword_id" (click)="changeCurrentUserPassword()"> + <button *ngIf="isUserAuthPasswdEnabled" mat-menu-item id="changePassword_id" (click)="changeCurrentUserPassword()"> <span>Change password</span> </button> - <button mat-menu-item id="getAccessToken_id" (click)="regenerateCurrentUserAccessToken()"> + <button *ngIf="isWebServiceUserTokenAuthPasswdEnabled" mat-menu-item id="getAccessToken_id" (click)="regenerateCurrentUserAccessToken()"> <span>Generated access token</span> </button> diff --git a/smp-angular/src/app/app.component.ts b/smp-angular/src/app/app.component.ts index c93ff665f835c876d554d4706cb96915fc59972b..f212b3e8e3cd970669b22a00cc9f73e07cca4692 100644 --- a/smp-angular/src/app/app.component.ts +++ b/smp-angular/src/app/app.component.ts @@ -34,6 +34,14 @@ export class AppComponent { this.userController = new UserController(this.http, this.lookups, this.dialog); } + get isWebServiceUserTokenAuthPasswdEnabled(): boolean { + return this.lookups.cachedApplicationConfig?.webServiceAuthTypes?.includes('TOKEN'); + } + + get isUserAuthPasswdEnabled(): boolean { + return this.lookups.cachedApplicationInfo?.authTypes.includes('PASSWORD'); + } + isCurrentUserSystemAdmin(): boolean { return this.securityService.isCurrentUserInRole([Authority.SYSTEM_ADMIN]); } diff --git a/smp-angular/src/app/common/dialogs/access-token-generation-dialog/access-token-generation-dialog.component.html b/smp-angular/src/app/common/dialogs/access-token-generation-dialog/access-token-generation-dialog.component.html index 53b5fa91b71fd11e427ba5af1268dc9f5dd4d32d..0e7a156e3284294bdd8677eeb8229d36395f53d0 100644 --- a/smp-angular/src/app/common/dialogs/access-token-generation-dialog/access-token-generation-dialog.component.html +++ b/smp-angular/src/app/common/dialogs/access-token-generation-dialog/access-token-generation-dialog.component.html @@ -30,7 +30,7 @@ Token will be generated immediately. </mat-label> </mat-card-actions> - <mat-form-field style="width:100%"> + <mat-form-field *ngIf="!securityService.getCurrentUser()?.casAuthenticated" style="width:100%"> <input matInput [placeholder]="getPasswordTitle" [type]="hideCurrPwdFiled ? 'password' : 'text'" formControlName="current-password" required id="cp_id"> <mat-icon matSuffix diff --git a/smp-angular/src/app/common/dialogs/access-token-generation-dialog/access-token-generation-dialog.component.ts b/smp-angular/src/app/common/dialogs/access-token-generation-dialog/access-token-generation-dialog.component.ts index 8ddc1334d90d9d711dc7282d1ec7d05091b6a205..121d836f8b0e490bd0518953ed450dbbea371de5 100644 --- a/smp-angular/src/app/common/dialogs/access-token-generation-dialog/access-token-generation-dialog.component.ts +++ b/smp-angular/src/app/common/dialogs/access-token-generation-dialog/access-token-generation-dialog.component.ts @@ -34,7 +34,7 @@ export class AccessTokenGenerationDialogComponent { @Inject(MAT_DIALOG_DATA) public data: any, private lookups: GlobalLookups, private userDetailsService: UserDetailsService, - private securityService: SecurityService, + public securityService: SecurityService, private fb: FormBuilder ) { dialogRef.disableClose = true;//disable default close operation @@ -48,7 +48,7 @@ export class AccessTokenGenerationDialogComponent { 'username': new FormControl({value: null, readonly: true}, null), 'accessTokenId': new FormControl({value: null, readonly: true}, null), 'accessTokenExpireOn': new FormControl({value: null, readonly: true}, null), - 'current-password': new FormControl({value: null, readonly: false}, [Validators.required]), + 'current-password': new FormControl({value: null, readonly: false}, this.securityService.getCurrentUser().casAuthenticated?null:[Validators.required]), }); this.dialogForm.controls['email'].setValue(this.isEmptyEmailAddress ? "Empty email address!" : this.current.emailAddress); diff --git a/smp-angular/src/app/common/dialogs/password-change-dialog/password-change-dialog.component.html b/smp-angular/src/app/common/dialogs/password-change-dialog/password-change-dialog.component.html index 46e7ab1e30ed41188bcf87ce8003f7e7676a995d..23f2027565200a6d922dda2e28fbcc4f6f584b41 100644 --- a/smp-angular/src/app/common/dialogs/password-change-dialog/password-change-dialog.component.html +++ b/smp-angular/src/app/common/dialogs/password-change-dialog/password-change-dialog.component.html @@ -22,7 +22,7 @@ </mat-card> <mat-card class="password-panel"> <mat-card-content> - <mat-form-field style="width:100%"> + <mat-form-field *ngIf="showCurrentPasswordField" style="width:100%"> <input matInput [placeholder]="getPasswordTitle" [type]="hideCurrPwdFiled ? 'password' : 'text'" formControlName="current-password" required id="cp_id"> <mat-icon matSuffix diff --git a/smp-angular/src/app/common/dialogs/password-change-dialog/password-change-dialog.component.ts b/smp-angular/src/app/common/dialogs/password-change-dialog/password-change-dialog.component.ts index 11c061a39d18acf289d9cccdcc98096126d3de6c..5c3bc59c4f3f441de9caec8fe82098ec24201c05 100644 --- a/smp-angular/src/app/common/dialogs/password-change-dialog/password-change-dialog.component.ts +++ b/smp-angular/src/app/common/dialogs/password-change-dialog/password-change-dialog.component.ts @@ -45,7 +45,8 @@ export class PasswordChangeDialogComponent { this.forceChange = this.current.forceChangeExpiredPassword; - let currentPasswdFormControl: FormControl = new FormControl({value: null, readonly: false}, [Validators.required]); + let currentPasswdFormControl: FormControl = new FormControl({value: null, readonly: false}, + this.securityService.getCurrentUser().casAuthenticated && this.adminUser ? null : [Validators.required]); let newPasswdFormControl: FormControl = new FormControl({value: null, readonly: false}, [Validators.required, Validators.pattern(this.passwordValidationRegExp), equal(currentPasswdFormControl, false)]); let confirmNewPasswdFormControl: FormControl = new FormControl({value: null, readonly: false}, @@ -66,6 +67,10 @@ export class PasswordChangeDialogComponent { this.dialogForm.controls['confirm-new-password'].setValue(''); } + get showCurrentPasswordField():boolean { + return !this.securityService.getCurrentUser()?.casAuthenticated || !this.adminUser + } + public passwordError = (controlName: string, errorName: string) => { return this.dialogForm.controls[controlName].hasError(errorName); } diff --git a/smp-angular/src/app/security/user.model.ts b/smp-angular/src/app/security/user.model.ts index 4a19dc78d21ea108eeeb0b301f5e9336b1b09416..6af54cd47124a3311dce4d4896a51daa33562bb6 100644 --- a/smp-angular/src/app/security/user.model.ts +++ b/smp-angular/src/app/security/user.model.ts @@ -7,6 +7,7 @@ export interface User { accessTokenId?: string; accessTokenExpireOn?: Date; authorities: Array<Authority>; + casAuthenticated?: boolean; defaultPasswordUsed: boolean; forceChangeExpiredPassword?: boolean; showPasswordExpirationWarning?: boolean; diff --git a/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.html b/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.html index 7da5985489d64d889bafcb5168ae8863b79f2c14..6a90d39403a7f1eccb487c0c99a940105d712132 100644 --- a/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.html +++ b/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.html @@ -82,7 +82,7 @@ <input matInput placeholder="Cas identifier" [formControl]="userForm.controls['username']" id="cas-user_id" maxlength="255" disabled> </mat-form-field> - <button mat-flat-button color="primary" style="width: 100%" id="openCASData" + <button mat-flat-button color="primary" style="width: 100%" id="openCASData" [disabled]="!this.current?.casUserDataUrl" (click)="openCurrentCasUserData()"> <span>Open CAS user data</span> </button> diff --git a/smp-docker/compose/tomcat-mysql-smp-sml/docker-compose.yml b/smp-docker/compose/tomcat-mysql-smp-sml/docker-compose.yml index d4bc79f1d1cc3e5eb9cee8769416d6e29e91ad91..6108fc3c4a120ff1a3b11de45d88616a514c11fd 100644 --- a/smp-docker/compose/tomcat-mysql-smp-sml/docker-compose.yml +++ b/smp-docker/compose/tomcat-mysql-smp-sml/docker-compose.yml @@ -33,8 +33,8 @@ services: - "3908:3306" - "8982:8080" - "6902:6901" - - "8953:53" - - "5005:5005" +# - "8953:53" +# - "5005:5005" eulogin-mock-server: image: edelivery-docker.devops.tech.ec.europa.eu/eulogin/mockserver:6.2.7 diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ui/UIPropertyService.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ui/UIPropertyService.java index d12d8ad1dd4c51269c996fa8934d25eb6b896c2f..f7f22d8a777e5020f2193827d01ad44e0ef49787 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ui/UIPropertyService.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/services/ui/UIPropertyService.java @@ -100,8 +100,13 @@ public class UIPropertyService { property.setValuePattern(property.getValuePattern()); if (changedProps.containsKey(property.getProperty())) { - property.setNewValue(changedProps.get(propertyType.getProperty()).getValue()); - property.setUpdateDate(refreshPropertiesTrigger.getNextExecutionDate()); + String newVal = changedProps.get(propertyType.getProperty()).getValue(); + if (!StringUtils.equals(newVal, property.getValue())) { + property.setNewValue(changedProps.get(propertyType.getProperty()).getValue()); + property.setUpdateDate(refreshPropertiesTrigger.getNextExecutionDate()); + } else { + LOG.debug("Property [{}] has newer update time, but it has the same value as the current value!"); + } } return property; } 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 a96d450cba22a96a6373bc4fbed5a5e3d4414cdc..d80cddc129a7ada3d2f4b0f604ecc83d98f79683 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 @@ -106,18 +106,20 @@ public class UIUserService extends UIServiceBase<DBUser, UserRO> { * * @param authorizedUserId which is authorized for update * @param userToUpdateId the user id to be updated + * @param currentPassword authorized password + * @param validatePassword do not validate password if CAS authenticated * @return generated AccessToken. */ @Transactional - public AccessTokenRO generateAccessTokenForUser(Long authorizedUserId, Long userToUpdateId, String currentPassword) { + public AccessTokenRO generateAccessTokenForUser(Long authorizedUserId, Long userToUpdateId, String currentPassword, boolean validateCurrentPassword) { DBUser dbUser = userDao.find(authorizedUserId); if (dbUser == null) { LOG.error("Can not update user password because authorized user with id [{}] does not exist!", authorizedUserId); throw new SMPRuntimeException(ErrorCode.INVALID_REQUEST, "UserId", "Can not find user id!"); } - if (!BCrypt.checkpw(currentPassword, dbUser.getPassword())) { - throw new BadCredentialsException("Password change failed; Invalid current password!"); + if (validateCurrentPassword && !BCrypt.checkpw(currentPassword, dbUser.getPassword())) { + throw new BadCredentialsException("AccessToken generation failed: Invalid current password!"); } boolean adminUpdate = userToUpdateId != null && authorizedUserId != userToUpdateId; DBUser dbUserToUpdate = adminUpdate ? userDao.find(userToUpdateId) : dbUser; @@ -142,11 +144,27 @@ public class UIUserService extends UIServiceBase<DBUser, UserRO> { * Method regenerate access token for user and returns access token * In the database the access token value is saved in format BCryptPasswordHash * - * @param authorizedUserId - authorized user id + * @param authorizedUserId which is authorized for update + * @param userToUpdateId the user id to be updated * @return generated AccessToken. */ @Transactional - public DBUser updateUserPassword(Long authorizedUserId, Long userToUpdateId, String currentPassword, String newPassword) { + public AccessTokenRO generateAccessTokenForUser(Long authorizedUserId, Long userToUpdateId, String currentPassword) { + return generateAccessTokenForUser(authorizedUserId, userToUpdateId, currentPassword, true); + } + + /** + * Method updates the user password + * + * @param authorizedUserId - authorized user id + * @param userToUpdateId - user id to update password user id + * @param authorizationPassword - authorization password + * @param newPassword - new password for the userToUpdateId + * @param validateCurrentPassword - validate authorizationPassword - if CAS authenticated skip this part + * @return generated DBUser. + */ + @Transactional + public DBUser updateUserPassword(Long authorizedUserId, Long userToUpdateId, String authorizationPassword, String newPassword, boolean validateCurrentPassword) { Pattern pattern = configurationService.getPasswordPolicyRexExp(); if (pattern != null && !pattern.matcher(newPassword).matches()) { @@ -158,7 +176,7 @@ public class UIUserService extends UIServiceBase<DBUser, UserRO> { throw new SMPRuntimeException(ErrorCode.INVALID_REQUEST, "UserId", "Can not find user id!"); } - if (!BCrypt.checkpw(currentPassword, dbAuthorizedUser.getPassword())) { + if (validateCurrentPassword && !BCrypt.checkpw(authorizationPassword, dbAuthorizedUser.getPassword())) { throw new BadCredentialsException("Password change failed; Invalid current password!"); } @@ -178,6 +196,20 @@ public class UIUserService extends UIServiceBase<DBUser, UserRO> { return dbUserToUpdate; } + /** + * Method updates the user password + * + * @param authorizedUserId - authorized user id + * @param userToUpdateId - user id to update password user id + * @param authorizationPassword - authorization password + * @param newPassword - new password for the userToUpdateId + * @return generated DBUser. + */ + @Transactional + public DBUser updateUserPassword(Long authorizedUserId, Long userToUpdateId, String authorizationPassword, String newPassword) { + return updateUserPassword(authorizedUserId, userToUpdateId, authorizationPassword, newPassword, true); + } + @Transactional public void updateUserList(List<UserRO> lst, OffsetDateTime passwordChange) { for (UserRO userRO : lst) { diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/auth/SMPAuthorizationService.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/auth/SMPAuthorizationService.java index a741c84c38465af80148be40d6ece5f0ad19b400..0ae7d40ec469cad952a3e7896ccc10705a20893d 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/auth/SMPAuthorizationService.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/auth/SMPAuthorizationService.java @@ -124,6 +124,7 @@ public class SMPAuthorizationService { public UserRO getLoggedUserData() { SMPUserDetails userDetails = getAndValidateUserDetails(); + // refresh data from database! DBUser dbUser = userDao.find(userDetails.getUser().getId()); if (dbUser == null || !dbUser.isActive()) { @@ -132,7 +133,9 @@ public class SMPAuthorizationService { userDetails.getUser().getUsername()); return null; } - return getUserData(dbUser); + UserRO userRO = getUserData(dbUser); + userRO.setCasAuthenticated(userDetails.isCasAuthenticated()); + return userRO; } public UserRO getUserData(DBUser user) { diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/external/UserResource.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/external/UserResource.java index a7d8e4f4433d8dcc6e72c0e0fe150762864476ce..4a280322f9fd54f668df61e6d2be42e7e8ae601e 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/external/UserResource.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/external/UserResource.java @@ -2,6 +2,7 @@ package eu.europa.ec.edelivery.smp.ui.external; 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.data.model.DBUser; import eu.europa.ec.edelivery.smp.data.ui.AccessTokenRO; import eu.europa.ec.edelivery.smp.data.ui.PasswordChangeRO; @@ -9,7 +10,9 @@ import eu.europa.ec.edelivery.smp.data.ui.UserRO; import eu.europa.ec.edelivery.smp.logging.SMPLogger; import eu.europa.ec.edelivery.smp.logging.SMPLoggerFactory; import eu.europa.ec.edelivery.smp.services.ui.UIUserService; +import eu.europa.ec.edelivery.smp.utils.SessionSecurityUtils; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.web.authentication.session.SessionAuthenticationException; import org.springframework.util.MimeTypeUtils; import org.springframework.web.bind.annotation.*; @@ -40,11 +43,16 @@ public class UserResource { @PreAuthorize("@smpAuthorizationService.isCurrentlyLoggedIn(#userId)") @PostMapping(path = "/{user-id}/generate-access-token", produces = MimeTypeUtils.APPLICATION_JSON_VALUE) - public AccessTokenRO generateAccessToken(@PathVariable("user-id") String userId, @RequestBody String password) { + public AccessTokenRO generateAccessToken(@PathVariable("user-id") String userId, @RequestBody(required = false) String password) { Long entityId = decryptEntityId(userId); + SMPUserDetails currentUser = SessionSecurityUtils.getSessionUserDetails(); LOG.info("Generated access token for user:[{}] with id:[{}] ", userId, entityId); + if (currentUser == null) { + throw new SessionAuthenticationException("User session expired!"); + } - return uiUserService.generateAccessTokenForUser(entityId, entityId, password); + // no need to validate password if CAS authenticated + return uiUserService.generateAccessTokenForUser(entityId, entityId, password, !currentUser.isCasAuthenticated()); } @PreAuthorize("@smpAuthorizationService.isCurrentlyLoggedIn(#userId)") @@ -52,6 +60,7 @@ public class UserResource { public boolean changePassword(@PathVariable("user-id") String userId, @RequestBody PasswordChangeRO newPassword, HttpServletRequest request, HttpServletResponse response) { Long entityId = decryptEntityId(userId); LOG.info("Validating the password of the currently logged in user:[{}] with id:[{}] ", userId, entityId); + // when user changing password the current password must be verified even if cas authenticated DBUser result = uiUserService.updateUserPassword(entityId, entityId, newPassword.getCurrentPassword(), newPassword.getNewPassword()); if (result!=null) { LOG.info("Password successfully changed. Logout the user, to be able to login with the new password!"); diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/internal/UserAdminResource.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/internal/UserAdminResource.java index ebaa92d146f1649675c97a1f4f14885da497237d..ca4181ab79c63ced9541904b43a978cdd6100f4a 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/internal/UserAdminResource.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/internal/UserAdminResource.java @@ -12,6 +12,7 @@ import eu.europa.ec.edelivery.smp.services.ui.UIUserService; import eu.europa.ec.edelivery.smp.services.ui.filters.UserFilter; import eu.europa.ec.edelivery.smp.utils.SessionSecurityUtils; import org.springframework.security.access.annotation.Secured; +import org.springframework.security.web.authentication.session.SessionAuthenticationException; import org.springframework.util.MimeTypeUtils; import org.springframework.web.bind.annotation.*; @@ -89,12 +90,18 @@ public class UserAdminResource { public AccessTokenRO generateAccessTokenForUser( @PathVariable("user-id") String userId, @PathVariable("update-user-id") String regenerateForUserId, - @RequestBody String password) { + @RequestBody(required = false) String password) { Long authorizedUserId = decryptEntityId(userId); Long changeUserId = decryptEntityId(regenerateForUserId); LOG.info("Generated access token for user:[{}] with id:[{}] by the system user with id [{}]", userId, regenerateForUserId, authorizedUserId); - return uiUserService.generateAccessTokenForUser(authorizedUserId, changeUserId, password); + SMPUserDetails currentUser = SessionSecurityUtils.getSessionUserDetails(); + if (currentUser == null) { + throw new SessionAuthenticationException("User session expired!"); + } + + // no need to validate password if cas authenticated + return uiUserService.generateAccessTokenForUser(authorizedUserId, changeUserId, password,!currentUser.isCasAuthenticated()); } @@ -102,11 +109,17 @@ public class UserAdminResource { @Secured({SMPAuthority.S_AUTHORITY_TOKEN_SYSTEM_ADMIN}) public UserRO changePassword(@PathVariable("user-id") String userId, @PathVariable("update-user-id") String regenerateForUserId, - @RequestBody PasswordChangeRO newPassword, HttpServletRequest request, HttpServletResponse response) { + @RequestBody PasswordChangeRO newPassword) { Long authorizedUserId = decryptEntityId(userId); Long changeUserId = decryptEntityId(regenerateForUserId); LOG.info("change the password of the currently logged in user:[{}] with id:[{}] ", changeUserId, regenerateForUserId); - DBUser user = uiUserService.updateUserPassword(authorizedUserId, changeUserId, newPassword.getCurrentPassword(), newPassword.getNewPassword()); + + SMPUserDetails currentUser = SessionSecurityUtils.getSessionUserDetails(); + if (currentUser == null) { + throw new SessionAuthenticationException("User session expired!"); + } + + DBUser user = uiUserService.updateUserPassword(authorizedUserId, changeUserId, newPassword.getCurrentPassword(), newPassword.getNewPassword(),!currentUser.isCasAuthenticated()); return authorizationService.sanitize(uiUserService.convertToRo(user)); }