diff --git a/smp-angular/src/app/app.component.html b/smp-angular/src/app/app.component.html index ebb1cdde949786dc81e246f964bf41bd9defdc4f..afc31f99138a397a8d9e66d9b17cd20751a88db5 100644 --- a/smp-angular/src/app/app.component.html +++ b/smp-angular/src/app/app.component.html @@ -1,4 +1,5 @@ <alert #alertMessage ></alert> +<spinner [show]="showSpinner" [size]="150"></spinner> <div style="display:flex; flex-direction: column;height: 100%"> <window-toolbar #windowToolbar/> <mat-drawer-container id="smp-window" [hasBackdrop]="false" style="flex: 1 1 auto;"> diff --git a/smp-angular/src/app/app.component.ts b/smp-angular/src/app/app.component.ts index 36da13e9659461511a11a7d8da112905b13ca766..dd6f64b5d557c7ad908de5dd8308b486728b7af5 100644 --- a/smp-angular/src/app/app.component.ts +++ b/smp-angular/src/app/app.component.ts @@ -11,6 +11,7 @@ import {ToolbarComponent} from "./window/toolbar/toolbar.component"; import {ThemeService} from "./common/theme-service/theme.service"; import {UserController} from "./common/services/user-controller"; import {TranslateService} from "@ngx-translate/core"; +import {WindowSpinnerService} from "./common/services/window-spinner.service"; @Component({ @@ -31,6 +32,7 @@ export class AppComponent { constructor( private alertService: AlertMessageService, private securityService: SecurityService, + private windowSpinnerService: WindowSpinnerService, private router: Router, private http: HttpClient, private dialog: MatDialog, @@ -95,4 +97,7 @@ export class AppComponent { this.alertService.setKeepAfterNavigationChange(scrollTop > 0) } + get showSpinner(): boolean { + return this.windowSpinnerService.showSpinner + } } diff --git a/smp-angular/src/app/app.module.ts b/smp-angular/src/app/app.module.ts index 4456a5531890d13590fb4b43257121086f318a38..d075ee2c8c27845337c2f8c3acb1faa5dcabcca8 100644 --- a/smp-angular/src/app/app.module.ts +++ b/smp-angular/src/app/app.module.ts @@ -165,6 +165,7 @@ import { DocumentPropertyDialogComponent } from "./common/dialogs/document-property-dialog/document-property-dialog.component"; import {NgxTranslateModule} from "./translate/translate.module"; +import {WindowSpinnerService} from "./common/services/window-spinner.service"; @NgModule({ @@ -328,6 +329,7 @@ import {NgxTranslateModule} from "./translate/translate.module"; ThemeService, UserDetailsService, UserService, + WindowSpinnerService, { provide: ExtendedHttpClient, useFactory: extendedHttpClientCreator, diff --git a/smp-angular/src/app/common/panels/document-properties-panel/document-properties-panel.component.scss b/smp-angular/src/app/common/panels/document-properties-panel/document-properties-panel.component.scss index 9f1ad60fe59831908890ef23303b065d3471e78d..626a02625bda49fbeb176e78dccb0f02e781909a 100644 --- a/smp-angular/src/app/common/panels/document-properties-panel/document-properties-panel.component.scss +++ b/smp-angular/src/app/common/panels/document-properties-panel/document-properties-panel.component.scss @@ -12,7 +12,7 @@ bottom: 0; left: 0; right: 0; - verflow-y: scroll; + overflow-y: scroll; } #property-table-container table { diff --git a/smp-angular/src/app/common/services/window-spinner.service.ts b/smp-angular/src/app/common/services/window-spinner.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..7e9a5280621765840745eb21c9fecdee203c6106 --- /dev/null +++ b/smp-angular/src/app/common/services/window-spinner.service.ts @@ -0,0 +1,14 @@ +import {Injectable} from "@angular/core"; + +@Injectable() +export class WindowSpinnerService { + private _showSpinner: boolean = false; + + get showSpinner(): boolean { + return this._showSpinner; + } + + set showSpinner(value: boolean) { + this._showSpinner = value; + } +} diff --git a/smp-angular/src/app/security/reset-credential/reset-credential.component.ts b/smp-angular/src/app/security/reset-credential/reset-credential.component.ts index 0956a149b1b4da4649bdbeb688dbb7a99ae35d97..9c08e3158f1d2bd64241ec9bd580466d5085d89f 100644 --- a/smp-angular/src/app/security/reset-credential/reset-credential.component.ts +++ b/smp-angular/src/app/security/reset-credential/reset-credential.component.ts @@ -33,6 +33,9 @@ export class ResetCredentialComponent implements OnInit { this.resetPasswordValidator(data); }) this.resetToken = this.activatedRoute.snapshot.params['resetToken']; + + // check if reset token is valid + this.securityService.validateCredentialReset(this.resetToken); } /** diff --git a/smp-angular/src/app/security/security.service.ts b/smp-angular/src/app/security/security.service.ts index ea25f79bd6cb271619d31cbb846dbfaebccddc43..4e8d97adba684e3ee75bdc559bd8157eb8b45385 100644 --- a/smp-angular/src/app/security/security.service.ts +++ b/smp-angular/src/app/security/security.service.ts @@ -14,6 +14,7 @@ import { import {MatDialog} from "@angular/material/dialog"; import {Router} from "@angular/router"; import {TranslateService} from "@ngx-translate/core"; +import {WindowSpinnerService} from "../common/services/window-spinner.service"; @Injectable() export class SecurityService { @@ -26,7 +27,8 @@ export class SecurityService { private securityEventService: SecurityEventService, private dialog: MatDialog, private router: Router, - private translateService: TranslateService + private translateService: TranslateService, + private windowSpinnerService: WindowSpinnerService ) { this.securityEventService.onLogoutSuccessEvent().subscribe(() => { this.dialog.closeAll(); @@ -56,6 +58,7 @@ export class SecurityService { } requestCredentialReset(userid: string) { + this.windowSpinnerService.showSpinner = true; let headers: HttpHeaders = new HttpHeaders({'Content-Type': 'application/json'}); return this.http.post<User>(SmpConstants.REST_PUBLIC_SECURITY_RESET_CREDENTIALS_REQUEST, JSON.stringify({ @@ -65,8 +68,10 @@ export class SecurityService { {headers}) .subscribe({ complete: () => { + this.windowSpinnerService.showSpinner = false; }, // completeHandler error: (error: any) => { + this.windowSpinnerService.showSpinner = false; this.alertService.error(error) }, // errorHandler next: async () => { @@ -77,8 +82,31 @@ export class SecurityService { }); } + validateCredentialReset(token: string) { + let headers: HttpHeaders = new HttpHeaders({'Content-Type': 'application/json'}); + this.windowSpinnerService.showSpinner = true; + return this.http.post<User>(SmpConstants.REST_PUBLIC_SECURITY_RESET_CREDENTIALS_VALIDATE, + JSON.stringify({ + credentialType: 'USERNAME_PASSWORD', + resetToken: token, + }), + {headers}) + .subscribe({ + complete: () => { + this.windowSpinnerService.showSpinner = false; + }, // completeHandler + error: (error: any) => { + this.windowSpinnerService.showSpinner = false; + this.router.navigate(['/search']); + this.alertService.error(error); + } + // errorHandler + }); + } + credentialReset(userid: string, token: string, newPassword: string) { let headers: HttpHeaders = new HttpHeaders({'Content-Type': 'application/json'}); + this.windowSpinnerService.showSpinner = true; return this.http.post<User>(SmpConstants.REST_PUBLIC_SECURITY_RESET_CREDENTIALS, JSON.stringify({ credentialName: userid, @@ -89,12 +117,14 @@ export class SecurityService { {headers}) .subscribe({ complete: () => { + this.windowSpinnerService.showSpinner = false; this.router.navigate(['/login']); - }, // completeHandler + }, // completeHandler error: (error: any) => { - this.alertService.error(error); + this.windowSpinnerService.showSpinner = false; this.router.navigate(['/login']); - }, // errorHandler + this.alertService.error(error); + }, // errorHandler next: async () => { this.alertService.success(await lastValueFrom(this.translateService.get("reset.credentials.success.password.reset")), true, -1); } diff --git a/smp-angular/src/app/smp.constants.ts b/smp-angular/src/app/smp.constants.ts index 1cd984cfc84483aeb05915432bf6824e265bb58a..1581c54ebc485d3abf2ae5f48cb8008cb533efb8 100644 --- a/smp-angular/src/app/smp.constants.ts +++ b/smp-angular/src/app/smp.constants.ts @@ -164,8 +164,10 @@ export class SmpConstants { 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_RESET_CREDENTIALS_REQUEST = SmpConstants.REST_PUBLIC_SECURITY + 'request-reset-credential'; + public static readonly REST_PUBLIC_SECURITY_RESET_CREDENTIALS_VALIDATE = SmpConstants.REST_PUBLIC_SECURITY + 'validate-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-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/CredentialDao.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/CredentialDao.java index d709fdb81e43e7a9c2b7225fd2326884dc3f9a60..befbf646294f12b4279584a046543eea687bfca7 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/CredentialDao.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/CredentialDao.java @@ -23,7 +23,6 @@ import eu.europa.ec.edelivery.smp.data.enums.CredentialTargetType; import eu.europa.ec.edelivery.smp.data.enums.CredentialType; import eu.europa.ec.edelivery.smp.data.model.DBUserDeleteValidation; import eu.europa.ec.edelivery.smp.data.model.user.DBCredential; -import eu.europa.ec.edelivery.smp.data.model.user.DBUser; import eu.europa.ec.edelivery.smp.exceptions.SMPRuntimeException; import eu.europa.ec.edelivery.smp.logging.SMPLogger; import eu.europa.ec.edelivery.smp.logging.SMPLoggerFactory; @@ -78,15 +77,16 @@ public class CredentialDao extends BaseDao<DBCredential> { } /** - * Method finds user by username.If user does not exist + * Method finds credentials by username. If credential does not exist * Optional with isPresent - false is returned. * * @param username - * @return returns Optional DBUser for username + * @return returns Optional DBCredential for username */ public Optional<DBCredential> findUsernamePasswordCredentialForUsernameAndUI(String username) { // check if blank if (StringUtils.isBlank(username)) { + LOG.debug("Username is blank! Return empty optional"); return Optional.empty(); } try { @@ -97,12 +97,41 @@ public class CredentialDao extends BaseDao<DBCredential> { return Optional.of(query.getSingleResult()); } catch (NoResultException e) { + LOG.debug("No results: Return empty optional"); return Optional.empty(); } catch (NonUniqueResultException e) { throw new SMPRuntimeException(ILLEGAL_STATE_USERNAME_MULTIPLE_ENTRY, username); } } + /** + * Method finds credential entity by reset token. If credential does not exist + * Optional with isPresent - false is returned. + * + * @param resetTokenIdentifier + * @return returns Optional DBCredential for reset Token Identifier + * @throws SMPRuntimeException if more than one credential is found! + */ + public Optional<DBCredential> findUCredentialForUsernamePasswordTypeAndResetToken(String resetTokenIdentifier) { + // check if blank + if (StringUtils.isBlank(resetTokenIdentifier)) { + return Optional.empty(); + } + try { + TypedQuery<DBCredential> query = memEManager.createNamedQuery(QUERY_CREDENTIALS_BY_TYPE_RESET_TOKEN, DBCredential.class); + query.setParameter(PARAM_CREDENTIAL_RESET_TOKEN, StringUtils.trim(resetTokenIdentifier)); + query.setParameter(PARAM_CREDENTIAL_TYPE, CredentialType.USERNAME_PASSWORD); + query.setParameter(PARAM_CREDENTIAL_TARGET, CredentialTargetType.UI); + + return Optional.of(query.getSingleResult()); + } catch (NoResultException e) { + LOG.debug("No results: Return empty optional for reset token: [{}]", resetTokenIdentifier); + return Optional.empty(); + } catch (NonUniqueResultException e) { + throw new SMPRuntimeException(ILLEGAL_STATE_USERNAME_MULTIPLE_ENTRY, resetTokenIdentifier); + } + } + /** * Method finds username/password credential for user id.If user does not exist * an empty Optional is returned. If there are more than one credential the SMPRuntimeException is thrown @@ -114,6 +143,7 @@ public class CredentialDao extends BaseDao<DBCredential> { public Optional<DBCredential> findUsernamePasswordCredentialForUserIdAndUI(Long userId) { // check if blank if (userId == null) { + LOG.debug("User ID is null! Return empty optional"); return Optional.empty(); } List<DBCredential> list = findUserCredentialForByUserIdTypeAndTarget(userId, @@ -121,6 +151,7 @@ public class CredentialDao extends BaseDao<DBCredential> { CredentialTargetType.UI); if (list.isEmpty()) { + LOG.debug("No results: Return empty optional for user ID: [{}]", userId); return Optional.empty(); } else if (list.size() > 1) { throw new SMPRuntimeException(ILLEGAL_STATE_USERNAME_MULTIPLE_ENTRY, userId); @@ -138,6 +169,7 @@ public class CredentialDao extends BaseDao<DBCredential> { public Optional<DBCredential> findAccessTokenCredentialForAPI(String accessToken) { // check if blank if (StringUtils.isBlank(accessToken)) { + LOG.debug("Access token is blank! Return empty optional"); return Optional.empty(); } try { @@ -148,6 +180,7 @@ public class CredentialDao extends BaseDao<DBCredential> { return Optional.of(query.getSingleResult()); } catch (NoResultException e) { + LOG.debug("No results: Return empty optional for access token: [{}]", accessToken); return Optional.empty(); } catch (NonUniqueResultException e) { throw new SMPRuntimeException(ILLEGAL_STATE_USERNAME_MULTIPLE_ENTRY, accessToken); @@ -171,29 +204,6 @@ public class CredentialDao extends BaseDao<DBCredential> { return query.getResultList(); } - /** - * Method finds user by user authentication token identifier. If user identity token not exist - * Optional with isPresent - false is returned. - * - * @param tokeIdentifier - * @return returns Optional DBUser for username - */ - public Optional<DBUser> findUserByAuthenticationToken(String tokeIdentifier) { - // check if blank - if (StringUtils.isBlank(tokeIdentifier)) { - return Optional.empty(); - } - try { - TypedQuery<DBUser> query = memEManager.createNamedQuery("DBUser.getUserByPatId", DBUser.class); - query.setParameter("patId", tokeIdentifier.trim()); - return Optional.of(query.getSingleResult()); - } catch (NoResultException e) { - return Optional.empty(); - } catch (NonUniqueResultException e) { - throw new SMPRuntimeException(ILLEGAL_STATE_USERNAME_MULTIPLE_ENTRY, tokeIdentifier); - } - } - /** * Get users with credentials which are about to expire, and they were not yet notified in alertInterval period * @param credentialType - the credential type to send alert @@ -290,6 +300,7 @@ public class CredentialDao extends BaseDao<DBCredential> { query.setParameter(PARAM_CERTIFICATE_IDENTIFIER, certificateId); return Optional.of(query.getSingleResult()); } catch (NoResultException e) { + LOG.debug("No results: Return empty optional for certificate ID: [{}]", certificateId); return Optional.empty(); } catch (NonUniqueResultException e) { throw new SMPRuntimeException(ILLEGAL_STATE_CERT_ID_MULTIPLE_ENTRY, certificateId); diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/QueryNames.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/QueryNames.java index 12ef52e08969945dd2283de8df03cada89e15714..5b77e0bfa65e78a49bf3c67a275d9d3de33da7c4 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/QueryNames.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/dao/QueryNames.java @@ -23,6 +23,8 @@ public class QueryNames { public static final String QUERY_CREDENTIAL_BY_CREDENTIAL_NAME_TYPE_TARGET = "DBCredential.getUserByCredentialNameTypeAndTarget"; public static final String QUERY_CREDENTIALS_BY_CI_USERNAME_CREDENTIAL_TYPE_TARGET = "DBCredential.getUserByUsernameCredentialTypeAndTarget"; public static final String QUERY_CREDENTIALS_BY_USERID_CREDENTIAL_TYPE_TARGET = "DBCredential.getUserByUserIdCredentialTypeAndTarget"; + public static final String QUERY_CREDENTIALS_BY_TYPE_RESET_TOKEN = "DBCredential.getCredentialTypeAndResetToken"; + public static final String QUERY_CREDENTIAL_ALL = "DBCredential.getAll"; public static final String QUERY_CREDENTIAL_BY_CERTIFICATE_ID = "DBCredential.getCredentialByCertificateId"; @@ -204,10 +206,7 @@ public class QueryNames { public static final String PARAM_CREDENTIAL_NAME = "credential_name"; public static final String PARAM_CREDENTIAL_TYPE = "credential_type"; public static final String PARAM_CREDENTIAL_TARGET = "credential_target"; - - - - + public static final String PARAM_CREDENTIAL_RESET_TOKEN = "reset_token"; private QueryNames() { } 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 9b7fb456088dafaf30bfc5ea9a23473d324c10c1..41db69e01bca7763cdbe5fcad4207ac7501b0ab6 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 @@ -37,11 +37,15 @@ import static eu.europa.ec.edelivery.smp.data.dao.QueryNames.*; @Table(name = "SMP_CREDENTIAL", indexes = { @Index(name = "SMP_CRD_USER_NAME_TYPE_IDX", columnList = "CREDENTIAL_NAME, CREDENTIAL_TYPE, CREDENTIAL_TARGET", unique = true), + @Index(name = "SMP_CRD_USER_NAME_RESET_IDX", columnList = "RESET_TOKEN, CREDENTIAL_TYPE, CREDENTIAL_TARGET", unique = true) }) @org.hibernate.annotations.Table(appliesTo = "SMP_CREDENTIAL", comment = "Credentials for the users") @NamedQuery(name = QUERY_CREDENTIAL_ALL, query = "SELECT u FROM DBCredential u") @NamedQuery(name = QUERY_CREDENTIALS_BY_CI_USERNAME_CREDENTIAL_TYPE_TARGET, query = "SELECT c FROM DBCredential c " + "WHERE upper(c.user.username) = upper(:username) and c.credentialType = :credential_type and c.credentialTarget = :credential_target") +@NamedQuery(name = QUERY_CREDENTIALS_BY_TYPE_RESET_TOKEN, query = "SELECT c FROM DBCredential c " + + "WHERE c.credentialType = :credential_type and c.credentialTarget = :credential_target and c.resetToken=:reset_token") + @NamedQuery(name = QUERY_CREDENTIALS_BY_USERID_CREDENTIAL_TYPE_TARGET, query = "SELECT c FROM DBCredential c " + "WHERE c.user.id = :user_id and c.credentialType = :credential_type and c.credentialTarget = :credential_target order by c.id") // case-insensitive search @@ -64,18 +68,16 @@ import static eu.europa.ec.edelivery.smp.data.dao.QueryNames.*; " AND (c.expireAlertOn IS NULL " + " OR c.expireAlertOn <= c.expireOn " + " OR c.expireAlertOn < :lastSendAlertDate )") +// native queries to validate if user is owner of the credential +@NamedNativeQuery(name = "DBCredentialDeleteValidation.validateUsersForOwnership", + resultSetMapping = "DBCredentialDeleteValidationMapping", + query = "SELECT S.ID as ID, S.USERNAME as USERNAME, " + + " C.CERTIFICATE_ID as certificateId, COUNT(S.ID) as ownedCount FROM " + + " SMP_USER S LEFT JOIN SMP_CERTIFICATE C ON (S.ID=C.ID) " + + " INNER JOIN SMP_RESOURCE_MEMBER SG ON (S.ID = SG.FK_USER_ID) " + + " WHERE S.ID IN (:idList)" + + " GROUP BY S.ID, S.USERNAME, C.CERTIFICATE_ID") - -@NamedNativeQueries({ - @NamedNativeQuery(name = "DBCredentialDeleteValidation.validateUsersForOwnership", - resultSetMapping = "DBCredentialDeleteValidationMapping", - query = "SELECT S.ID as ID, S.USERNAME as USERNAME, " + - " C.CERTIFICATE_ID as certificateId, COUNT(S.ID) as ownedCount FROM " + - " SMP_USER S LEFT JOIN SMP_CERTIFICATE C ON (S.ID=C.ID) " + - " INNER JOIN SMP_RESOURCE_MEMBER SG ON (S.ID = SG.FK_USER_ID) " + - " WHERE S.ID IN (:idList)" + - " GROUP BY S.ID, S.USERNAME, C.CERTIFICATE_ID"), -}) @SqlResultSetMapping(name = "DBCredentialDeleteValidationMapping", classes = { @ConstructorResult(targetClass = DBUserDeleteValidation.class, columns = {@ColumnResult(name = "id", type = Long.class), 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 index 66b5f074be349a0dbcf97c7a6cea59e04f860ca3..d3ff4c66bb864993d3510f461322431296e9151d 100644 --- 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 @@ -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,9 +22,12 @@ import eu.europa.ec.edelivery.smp.data.enums.CredentialType; import java.util.StringJoiner; +import static eu.europa.ec.edelivery.smp.utils.PropertyUtils.getMaskedData; + /** * Credential request reset object + * * @author Joze Rihtarsic * @since 5.1 */ @@ -74,8 +77,10 @@ public class CredentialResetRO extends BaseRO { public String toString() { return new StringJoiner(", ", CredentialResetRO.class.getSimpleName() + "[", "]") .add("credentialName='" + credentialName + "'") - .add("credentialValue='" + credentialValue + "'") .add("credentialType='" + credentialType + "'") + .add("credentialValue='" + getMaskedData(credentialValue) + "'") + .add("resetToken='" + getMaskedData(credentialValue) + "'") .toString(); } + } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/exceptions/ErrorCode.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/exceptions/ErrorCode.java index 02bb0fc1dcea2bd65cb3ef76e6f40bd4177ed3be..77f5a3dd9f385e5195f72187b6aa6e0238262ca8 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/exceptions/ErrorCode.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/exceptions/ErrorCode.java @@ -32,6 +32,7 @@ public enum ErrorCode { UNAUTHORIZED(401, "SMP:003",ErrorBusinessCode.UNAUTHORIZED, "User not authorized!"), UNAUTHORIZED_INVALID_USER_IDENTIFIER(401, "SMP:004",ErrorBusinessCode.UNAUTHORIZED, "Invalid user identifier! User not authorized."), UNAUTHORIZED_INVALID_IDENTIFIER(401, "SMP:005",ErrorBusinessCode.UNAUTHORIZED, "Invalid entity identifier! User not authorized to access the entity data"), + UNAUTHORIZED_INVALID_RESET_TOKEN(401, "SMP:006",ErrorBusinessCode.UNAUTHORIZED, "Reset token; Invalid or not active reset token!"), INVALID_ENCODING (500, "SMP:100",ErrorBusinessCode.TECHNICAL, "Unsupported or invalid encoding for %s!"), SML_INVALID_IDENTIFIER (400,"SMP:101",ErrorBusinessCode.FORMAT_ERROR,"Malformed identifier, scheme and id should be delimited by double colon: %s "), @@ -80,6 +81,7 @@ public enum ErrorCode { JAXB_INITIALIZATION (500,"SMP:511",ErrorBusinessCode.TECHNICAL, "Could not create Unmarshaller for class [%s]!"), XML_PARSE_EXCEPTION (500,"SMP:512",ErrorBusinessCode.TECHNICAL, "Error occurred while parsing input stream for [%s]. Error: %s!"), INVALID_REQUEST(400,"SMP:513",ErrorBusinessCode.TECHNICAL, "Invalid request [%s]. Error: %s!"), + INVALID_REQUEST_NO_DETAILS(400,"SMP:513",ErrorBusinessCode.TECHNICAL, "Invalid request"), INTERNAL_ERROR (500,"SMP:514",ErrorBusinessCode.TECHNICAL, "Internal error [%s]. Error: %s!"), CERTIFICATE_ERROR (500,"SMP:515",ErrorBusinessCode.TECHNICAL, "Certificate error [%s]. Error: %s!"), CONFIGURATION_ERROR (500,"SMP:516",ErrorBusinessCode.TECHNICAL, "Configuration error: [%s]!"), @@ -112,6 +114,9 @@ public enum ErrorCode { return messageTemplate; } public String getMessage(Object ... args) { + if (args == null || args.length == 0) { + return messageTemplate; + } return String.format(messageTemplate, args); } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/logging/SMPMessageCode.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/logging/SMPMessageCode.java index 533ab1b82e1ec0818758e116cd01c2875d87a9fe..c121a3c28b5c36f571c279cd19ab57d1008f8576 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/logging/SMPMessageCode.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/logging/SMPMessageCode.java @@ -70,6 +70,8 @@ public enum SMPMessageCode implements MessageCode { SEC_USER_SUSPENDED("SEC-008", "User [{}] is temporarily suspended."), SEC_INVALID_TOKEN("SEC-009", "User [{}] has invalid token value for token id: [{}]."), SEC_TRUSTSTORE_CERT_INVALID("SEC-010", "Truststore certificate with alias [{}] is invalid: [{}]."), + SEC_RESET_TOKEN_NOT_EXISTS("SEC-011", "Reset token [{}] for type [{}] not exists."), + SEC_RESET_TOKEN_INVALID("SEC-012", "Reset token for credential [{}] for type [{}] is invalid."), ; String code; 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 d3c85c44d4929e42d5caf51e83263b1bbf454ca7..cb02726b6f6494a9973bf56a7fd23bdbb4de0af7 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 @@ -23,6 +23,7 @@ import eu.europa.ec.edelivery.security.utils.SecurityUtils; import eu.europa.ec.edelivery.smp.auth.SMPAuthenticationToken; import eu.europa.ec.edelivery.smp.auth.SMPUserDetails; import eu.europa.ec.edelivery.smp.auth.UILoginAuthenticationToken; +import eu.europa.ec.edelivery.smp.config.SMPEnvironmentProperties; import eu.europa.ec.edelivery.smp.data.dao.CredentialDao; import eu.europa.ec.edelivery.smp.data.dao.UserDao; import eu.europa.ec.edelivery.smp.data.enums.CredentialType; @@ -71,7 +72,10 @@ import static java.util.Locale.US; 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 UNAUTHORIZED_INVALID_RESET_TOKEN = new BadCredentialsException(ErrorCode.UNAUTHORIZED_INVALID_RESET_TOKEN.getMessage()); protected static final BadCredentialsException SUSPENDED_CREDENTIALS_EXCEPTION = new BadCredentialsException(ErrorCode.UNAUTHORIZED_CREDENTIAL_SUSPENDED.getMessage()); + protected static final int RESET_TOKEN_LENGTH = 64; + final UserDao userDao; final CredentialDao credentialDao; final ConversionService conversionService; @@ -135,7 +139,7 @@ public class CredentialService { LOG.securityWarn(SMPMessageCode.SEC_INVALID_USER_CREDENTIALS, username, credential.getName(), credential.getCredentialType(), credential.getCredentialTarget()); loginAttemptFailedAndThrowError(credential, true, startTime); } - LOG.debug("authenticateByUsernamePassword: reset failed attempts for user token [{}]", username); + LOG.debug("authenticateByUsernamePassword: clear failed attempts for user token [{}]", username); credential.setSequentialLoginFailureCount(0); credential.setLastFailedLoginAttempt(null); } catch (IllegalArgumentException ex) { @@ -344,15 +348,22 @@ public class CredentialService { generateResetTokenAndSubmitMail(dbCredential); } + /** + * Method validates if the reset token is valid (exists and is not expired) + * and updates the password. + * + * @param username of the user + * @param resetToken active reset token + * @param newPassword new password + * @throws AuthenticationServiceException if the reset token is invalid + */ @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; + throw UNAUTHORIZED_INVALID_RESET_TOKEN; } DBCredential dbCredential = optCredential.get(); if (!resetToken.equals(dbCredential.getResetToken())) { @@ -367,22 +378,27 @@ public class CredentialService { 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())); + + dbCredential.setResetToken(null); + dbCredential.setResetExpireOn(null); + // submit mail with reset token alertService.alertCredentialChanged(dbCredential); } + /** - * Method gets credentials for active user and validates if not expired reset token exists . + * Method gets User password credential entity for active user and validates + * resent token exists and if active. * - * @param username - * @return + * @param username of the user + * @return Optional of DBCredential: if the user has active credentials and active else empty is returned */ private Optional<DBCredential> getActiveCredentialsForUsernameToReset(String username, boolean toGenerateResetToken) { // retrieve user Optional credentials by username @@ -401,19 +417,36 @@ public class CredentialService { // 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(); + LOG.warn("User [{}] has already active reset token. Generate new reset token!", username); } // 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!"); + LOG.securityWarn(SMPMessageCode.SEC_RESET_TOKEN_INVALID, dbCredential.getName(), CredentialType.USERNAME_PASSWORD); + throw UNAUTHORIZED_INVALID_RESET_TOKEN; } return optCredential; } + public void validatePasswordResetToken(String resetToken){ + Optional<DBCredential> optCredential = credentialDao.findUCredentialForUsernamePasswordTypeAndResetToken(resetToken); + if (!optCredential.isPresent()) { + LOG.securityWarn(SMPMessageCode.SEC_RESET_TOKEN_NOT_EXISTS, resetToken, CredentialType.USERNAME_PASSWORD); + throw UNAUTHORIZED_INVALID_RESET_TOKEN; + } + DBCredential dbCredential = optCredential.get(); + if (!hasValidResetToken(dbCredential)) { + LOG.securityWarn(SMPMessageCode.SEC_RESET_TOKEN_INVALID, dbCredential.getName(), CredentialType.USERNAME_PASSWORD); + throw UNAUTHORIZED_INVALID_RESET_TOKEN; + } + } + + /** + * Method validates if the user has valid reset token. The token is valid if it is not empty + * and the expiry date is after the current date. + * @param dbCredential + * @return true if the reset token is valid, else false + */ private boolean hasValidResetToken(DBCredential dbCredential) { return StringUtils.isNotBlank(dbCredential.getResetToken()) && dbCredential.getResetExpireOn() != null @@ -427,7 +460,8 @@ public class CredentialService { * @param dbCredential credential for which the reset token is generated. */ private void generateResetTokenAndSubmitMail(DBCredential dbCredential) { - dbCredential.setResetToken(UUID.randomUUID().toString()); + boolean isDevMode = SMPEnvironmentProperties.getInstance().isSMPStartupInDevMode(); + dbCredential.setResetToken(SecurityUtils.generateAuthenticationTokenIdentifier(isDevMode, RESET_TOKEN_LENGTH)); dbCredential.setResetExpireOn(OffsetDateTime.now().plusMinutes(configurationService.getCredentialsResetPolicyValidMinutes())); // submit mail with reset token dbCredential.getUser().getEmailAddress(); 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 d1531230fb4ef668b34741817f371db5e7c03adf..77887976fea0d623dc8f8763ae2043721bdb66d6 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 @@ -261,11 +261,14 @@ public class UIUserService extends UIServiceBase<DBUser, UserRO> { dbCredential.setActiveFrom(currentTime); dbCredential.setExpireOn(adminUpdate ? null : currentTime.plusDays(configurationService.getPasswordPolicyValidDays())); + // clear reset token if exists + dbCredential.setResetToken(null); + dbCredential.setResetExpireOn(null); // clear failed attempts dbCredential.setLastFailedLoginAttempt(null); dbCredential.setSequentialLoginFailureCount(null); - // if the credentials are not managed by the session , e.g. new - the persist it + // if the credentials are not managed by the session , e.g. "new" then persist it if (dbCredential.getId() == null) { credentialDao.persist(dbCredential); } diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/PropertyUtils.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/PropertyUtils.java index 34d545f8b0e6a3e2f9302e27e47622e52778d9c1..d8ca9d339538b3d0ceb5baae69bf5c5b965e678a 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/PropertyUtils.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/utils/PropertyUtils.java @@ -208,6 +208,10 @@ public class PropertyUtils { * @return masked value for sensitive properties. Else it returns value! */ public static String getMaskedData(String property, String value) { - return isSensitiveData(property) ? MASKED_VALUE : value; + return isSensitiveData(property) ? getMaskedData(value) : value; + } + + public static String getMaskedData(String value) { + return isNotBlank(value) ? MASKED_VALUE : "Null/Empty/Blank"; } } 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 34c2208df9f7529ef6f6e8779344fda0f26b1045..ef65571b9dad35753b0143a427647eb613da80a7 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 @@ -20,6 +20,7 @@ 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.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.CredentialService; @@ -89,10 +90,33 @@ public class SMPAuthenticationService { LOG.info("resetUsernamePassword [{}]", username); // retrieve user Optional credentials by username long startTime = Calendar.getInstance().getTimeInMillis(); - credentialService.resetUsernamePassword(username, resetToken, newPassword); + try { + credentialService.resetUsernamePassword(username, resetToken, newPassword); + } catch (SMPRuntimeException | AuthenticationException e) { + // delay response to prevent timing attack + credentialService.delayResponse(CredentialType.USERNAME_PASSWORD, startTime); + throw e; + } credentialService.delayResponse(CredentialType.USERNAME_PASSWORD, startTime); } + /** + * Method validates reset token exists and is valid! If not exception is thrown. + * @param resetToken + * @throws AuthenticationException if token is not exists or is not valid + */ + public void validateUsernamePasswordResetToken(String resetToken) { + long startTime = Calendar.getInstance().getTimeInMillis(); + try { + credentialService.validatePasswordResetToken(resetToken); + } catch (SMPRuntimeException | AuthenticationException e) { + // delay response to prevent timing attack + credentialService.delayResponse(CredentialType.USERNAME_PASSWORD, startTime); + throw e; + } + credentialService.validatePasswordResetToken(resetToken); + } + 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 30624687921ff44265270682a92c6579d70a3642..747e827a7021572576cec8d4d9417d7fd9454eb0 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 @@ -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,11 +28,13 @@ 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.exceptions.ErrorCode; 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.ui.UIUserService; import eu.europa.ec.edelivery.smp.utils.SMPCookieWriter; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.annotation.Secured; import org.springframework.security.authentication.BadCredentialsException; @@ -108,7 +110,7 @@ public class AuthenticationController { * * @param requestResetRO - the request object containing the credential name and type */ - @PostMapping(value = ResourceConstants.PATH_ACTION_RESET_CREDENTIAL_REQUEST ) + @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) { @@ -116,7 +118,7 @@ public class AuthenticationController { } else { LOG.warn("Invalid or null credential type [{}] not supported for reset!", requestResetRO.getCredentialType()); - throw new IllegalArgumentException("Invalid request!"); + throw new IllegalArgumentException(ErrorCode.INVALID_REQUEST_NO_DETAILS.getMessage()); } } @@ -126,20 +128,41 @@ public class AuthenticationController { * * @param resetRO - the reset object containing the credential name, type, reset token and new credential value */ - @PostMapping(value = ResourceConstants.PATH_ACTION_RESET_CREDENTIAL ) + @PostMapping(value = ResourceConstants.PATH_ACTION_RESET_CREDENTIAL) public void resetCredentials(@RequestBody CredentialResetRO resetRO) { - LOG.debug("credentialResetRO [{}]", resetRO.getCredentialName()); - if (resetRO.getCredentialType() == CredentialType.USERNAME_PASSWORD) { + LOG.debug("resetCredentials [{}]", resetRO); + if (resetRO == null + || resetRO.getResetToken() == null + || StringUtils.isBlank(resetRO.getResetToken()) + || StringUtils.isBlank(resetRO.getCredentialName()) + || resetRO.getCredentialType() != CredentialType.USERNAME_PASSWORD) { + LOG.warn("Invalid or incomplete reset token!"); + throw new IllegalArgumentException(ErrorCode.INVALID_REQUEST_NO_DETAILS.getMessage()); + } + authenticationService.resetUsernamePassword(resetRO.getCredentialName(), + resetRO.getResetToken(), + resetRO.getCredentialValue()); - 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!"); + } + /** + * Method validates if the reset token is valid (exists and is not expired) + * for the given credential type. + * + * @param resetRO - the reset object containing the credential name, type, reset token and new credential value + * Return 200 if token is valid, 401 if token is invalid + */ + @PostMapping(value = ResourceConstants.PATH_ACTION_VALIDATE_RESET_TOKEN) + public void validateResetToken(@RequestBody CredentialResetRO resetRO) { + LOG.debug("validateResetToken [{}]", resetRO); + if (resetRO == null + || StringUtils.isBlank(resetRO.getResetToken()) + || resetRO.getCredentialType() != CredentialType.USERNAME_PASSWORD) { + LOG.warn("Invalid or null reset token or invalid reset token type!"); + throw new IllegalArgumentException(ErrorCode.INVALID_REQUEST_NO_DETAILS.getMessage()); } + + authenticationService.validateUsernamePasswordResetToken(resetRO.getResetToken()); } @DeleteMapping(value = ResourceConstants.PATH_ACTION_AUTHENTICATION) 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 9aebb52a5e98f27c5edf235517912d2f8e35a6e5..6c94d2e7507d049d850f4d0635dd60d13eb983dc 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 @@ -73,6 +73,7 @@ public class ResourceConstants { public static final String PATH_ACTION_UPDATE_RESOURCE_TYPES = "update-resource-types"; public static final String PATH_ACTION_UPDATE_SML_DATA = "update-sml-integration-data"; public static final String PATH_ACTION_RESET_CREDENTIAL_REQUEST = "request-reset-credential"; + public static final String PATH_ACTION_VALIDATE_RESET_TOKEN = "validate-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_GENERATE_DNS_QUERY = "generate-dns-query"; 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 421be51f4488369b97b2d55b72acc73cddd08320..2b82adba38fd9a546778ad24572f81c8c26605f4 100644 --- a/smp-webapp/src/main/smp-setup/database-scripts/mysql5innodb.ddl +++ b/smp-webapp/src/main/smp-setup/database-scripts/mysql5innodb.ddl @@ -587,6 +587,9 @@ alter table SMP_CREDENTIAL add constraint SMP_CRD_USER_NAME_TYPE_IDX unique (CREDENTIAL_NAME, CREDENTIAL_TYPE, CREDENTIAL_TARGET); + alter table SMP_CREDENTIAL + add constraint SMP_CRD_USER_NAME_RESET_IDX unique (RESET_TOKEN, CREDENTIAL_TYPE, CREDENTIAL_TARGET); + alter table SMP_DOCUMENT_PROPERTY add constraint SMP_DOC_PROP_IDX unique (FK_DOCUMENT_ID, PROPERTY_NAME); create index SMP_DOCVER_DOCUMENT_IDX on SMP_DOCUMENT_VERSION (FK_DOCUMENT_ID); 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 c3460e7dd0c793b4acd46f763e89fca37b602e59..953bb1be081432cc584862d50ad970fbebfd56f4 100644 --- a/smp-webapp/src/main/smp-setup/database-scripts/oracle10g.ddl +++ b/smp-webapp/src/main/smp-setup/database-scripts/oracle10g.ddl @@ -859,6 +859,9 @@ create sequence SMP_USER_SEQ start with 1 increment by 1; alter table SMP_CREDENTIAL add constraint SMP_CRD_USER_NAME_TYPE_IDX unique (CREDENTIAL_NAME, CREDENTIAL_TYPE, CREDENTIAL_TARGET); + alter table SMP_CREDENTIAL + add constraint SMP_CRD_USER_NAME_RESET_IDX unique (RESET_TOKEN, CREDENTIAL_TYPE, CREDENTIAL_TARGET); + alter table SMP_DOCUMENT_PROPERTY add constraint SMP_DOC_PROP_IDX unique (FK_DOCUMENT_ID, PROPERTY_NAME); create index SMP_DOCVER_DOCUMENT_IDX on SMP_DOCUMENT_VERSION (FK_DOCUMENT_ID);