diff --git a/docs/WebServices-Methods.md b/docs/WebServices-Methods.md
index d82741fc0..ef556b7c3 100644
--- a/docs/WebServices-Methods.md
+++ b/docs/WebServices-Methods.md
@@ -25,6 +25,7 @@ The following `v3` methods are published using the service:
- [prepareActivation](#method-prepareactivation)
- [createActivation](#method-createactivation)
- [updateActivationOtp](#method-updateactivationotp)
+ - [updateActivationName](#method-updateactivationname)
- [commitActivation](#method-commitactivation)
- [getActivationStatus](#method-getactivationstatus)
- [removeActivation](#method-removeactivation)
@@ -519,6 +520,35 @@ REST endpoint: `POST /rest/v3/activation/otp/update`
| `String` | `activationId` | An identifier of an activation |
| `boolean` | `updated` | Flag indicating that OTP has been updated |
+### Method 'updateActivationName'
+
+Update activation name for activation with given ID.
+No allowed for activation in status `CREATED`, `REMOVED`, or `BLOCKED`.
+
+After successful, activation name is updated.
+
+#### Request
+
+REST endpoint: `POST /rest/v3/activation/name/update`
+
+`UpdateActivationNameRequest`
+
+| Type | Name | Description |
+|----------|------------------|---------------------------------------------------------------------------------------------------|
+| `String` | `activationId` | An identifier of an activation. |
+| `String` | `externalUserId` | User ID of user who changes the activation. Use null value if activation owner caused the change. |
+| `String` | `activationName` | A new value of activation name. |
+
+#### Response
+
+`UpdateActivationNameResponse`
+
+| Type | Name | Description |
+|--------------------|--------------------|---------------------------------|
+| `String` | `activationId` | An identifier of an activation |
+| `String` | `activationName` | A new value of activation name. |
+| `ActivationStatus` | `activationStatus` | An activation status. |
+
### Method 'commitActivation'
Commit activation with given ID. Only non-expired activations in PENDING_COMMIT state can be committed.
@@ -1166,14 +1196,15 @@ REST endpoint: `POST /rest/v3/activation/history`
`ActivationHistoryResponse.Item`
-| Type | Name | Description |
-|------|------|-------------|
-| `Long` | `id` | Change ID |
-| `String` | `activationId` | An identifier of an activation |
-| `ActivationStatus` | `activationStatus` | An activation status at the moment of a signature verification |
-| `String` | `eventReason` | Reason why this activation history record was created (default: null) |
-| `String` | `externalUserId` | User ID of user who modified the activation. Null value is used if activation owner caused the change. |
-| `DateTime` | `timestampCreated` | Timestamp when the record was created |
+| Type | Name | Description |
+|--------------------|--------------------|--------------------------------------------------------------------------------------------------------|
+| `Long` | `id` | Change ID |
+| `String` | `activationId` | An identifier of an activation |
+| `ActivationStatus` | `activationStatus` | An activation status at the moment of a signature verification |
+| `String` | `eventReason` | Reason why this activation history record was created (default: null) |
+| `String` | `externalUserId` | User ID of user who modified the activation. Null value is used if activation owner caused the change. |
+| `String` | `activationName` | Activation name. |
+| `DateTime` | `timestampCreated` | Timestamp when the record was created |
## Integration management
diff --git a/docs/db/changelog/changesets/powerauth-java-server/1.6.x/20231103-add-activation-name-history.xml b/docs/db/changelog/changesets/powerauth-java-server/1.6.x/20231103-add-activation-name-history.xml
new file mode 100644
index 000000000..984806c2c
--- /dev/null
+++ b/docs/db/changelog/changesets/powerauth-java-server/1.6.x/20231103-add-activation-name-history.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+ Add activation_name column to pa_activation_history
+
+
+
+
+
diff --git a/docs/db/changelog/changesets/powerauth-java-server/1.6.x/db.changelog-version.xml b/docs/db/changelog/changesets/powerauth-java-server/1.6.x/db.changelog-version.xml
new file mode 100644
index 000000000..a9d46c614
--- /dev/null
+++ b/docs/db/changelog/changesets/powerauth-java-server/1.6.x/db.changelog-version.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/docs/images/arch_db_structure.png b/docs/images/arch_db_structure.png
index de65c78dc..9d7ab18a4 100644
Binary files a/docs/images/arch_db_structure.png and b/docs/images/arch_db_structure.png differ
diff --git a/pom.xml b/pom.xml
index a69e56bfa..7fab884c4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -101,6 +101,7 @@
1.10.0
7.4
+ 3.15.3
@@ -158,6 +159,13 @@
pom
import
+
+
+ nl.jqno.equalsverifier
+ equalsverifier
+ ${equalsverifier.version}
+ test
+
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/PowerAuthClient.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/PowerAuthClient.java
index 8b8f0820b..77e26fe12 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/PowerAuthClient.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/PowerAuthClient.java
@@ -213,6 +213,26 @@ PrepareActivationResponse prepareActivation(String activationCode, String applic
*/
CreateActivationResponse createActivation(CreateActivationRequest request, MultiValueMap queryParams, MultiValueMap httpHeaders) throws PowerAuthClientException;
+ /**
+ * Update the activation name directly, using the updateActivationName method of the PowerAuth Server interface.
+ *
+ * @param request Update activation name request.
+ * @return Update activation name response.
+ * @throws PowerAuthClientException In case REST API call fails.
+ */
+ UpdateActivationNameResponse updateActivationName(UpdateActivationNameRequest request) throws PowerAuthClientException;
+
+ /**
+ * Update the activation name directly, using the updateActivationName method of the PowerAuth Server interface.
+ *
+ * @param request Update activation request.
+ * @param queryParams HTTP query parameters.
+ * @param httpHeaders HTTP headers.
+ * @return Update activation name response.
+ * @throws PowerAuthClientException In case REST API call fails.
+ */
+ UpdateActivationNameResponse updateActivationName(UpdateActivationNameRequest request, MultiValueMap queryParams, MultiValueMap httpHeaders) throws PowerAuthClientException;
+
/**
* Call the createActivation method of the PowerAuth 3.0 Server interface.
*
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/entity/ActivationHistoryItem.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/entity/ActivationHistoryItem.java
index b8250433e..8502ebeb8 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/entity/ActivationHistoryItem.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/entity/ActivationHistoryItem.java
@@ -35,6 +35,7 @@ public class ActivationHistoryItem {
private ActivationStatus activationStatus;
private String eventReason;
private String externalUserId;
+ private String activationName;
private Date timestampCreated;
private Long version;
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/UpdateActivationNameRequest.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/UpdateActivationNameRequest.java
new file mode 100644
index 000000000..b42ebbc65
--- /dev/null
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/UpdateActivationNameRequest.java
@@ -0,0 +1,41 @@
+/*
+ * PowerAuth Server and related software components
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.wultra.security.powerauth.client.model.request;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+/**
+ * Model class representing request for updating activation.
+ *
+ * @author Lubos Racansky, lubos.racansky@wultra.com
+ */
+@Data
+public class UpdateActivationNameRequest {
+
+ @NotBlank
+ private String activationId;
+
+ @NotBlank
+ private String activationName;
+
+ @NotBlank
+ private String externalUserId;
+
+}
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/UpdateActivationNameResponse.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/UpdateActivationNameResponse.java
new file mode 100644
index 000000000..8cc45b029
--- /dev/null
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/UpdateActivationNameResponse.java
@@ -0,0 +1,43 @@
+/*
+ * PowerAuth Server and related software components
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.wultra.security.powerauth.client.model.response;
+
+import com.wultra.security.powerauth.client.model.enumeration.ActivationStatus;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * Model class representing response with updated activation.
+ *
+ * @author Lubos Racansky, lubos.racansky@wultra.com
+ */
+@Data
+public class UpdateActivationNameResponse {
+
+ @NotBlank
+ private String activationId;
+
+ @NotBlank
+ private String activationName;
+
+ @NotNull
+ private ActivationStatus activationStatus;
+
+}
diff --git a/powerauth-java-server/pom.xml b/powerauth-java-server/pom.xml
index 0a5317482..32938d8c8 100644
--- a/powerauth-java-server/pom.xml
+++ b/powerauth-java-server/pom.xml
@@ -185,6 +185,11 @@
h2
test
+
+
+ nl.jqno.equalsverifier
+ equalsverifier
+
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/controller/RESTControllerAdvice.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/controller/RESTControllerAdvice.java
index 066e68313..edc2038e3 100644
--- a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/controller/RESTControllerAdvice.java
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/controller/RESTControllerAdvice.java
@@ -23,15 +23,21 @@
import io.getlime.security.powerauth.app.server.service.exceptions.ActivationRecoveryException;
import io.getlime.security.powerauth.app.server.service.exceptions.GenericServiceException;
import io.getlime.security.powerauth.app.server.service.exceptions.TelemetryReportException;
+import io.getlime.security.powerauth.app.server.service.model.ServiceError;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
+import java.util.Comparator;
+import java.util.stream.Collectors;
+
/**
* Class used for handling RESTful service errors.
*
@@ -95,6 +101,30 @@ public class RESTControllerAdvice {
return new ObjectResponse<>("ERROR", error);
}
+ /**
+ * Resolver for validation xception.
+ *
+ * @param ex Exception.
+ * @return Activation recovery error.
+ */
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ @ExceptionHandler
+ public @ResponseBody ObjectResponse returnActivationRecoveryError(final MethodArgumentNotValidException ex) {
+ logger.error("Error occurred while processing the request: {}", ex.getMessage());
+ logger.debug("Exception details:", ex);
+
+ final String message = ex.getBindingResult().getFieldErrors().stream()
+ .sorted(Comparator.comparing(FieldError::getField))
+ .map(it -> String.join(" - ", it.getField(), it.getDefaultMessage()))
+ .collect(Collectors.joining(", "));
+
+ final PowerAuthError error = new PowerAuthError();
+ error.setCode(ServiceError.INVALID_REQUEST);
+ error.setMessage(message);
+ error.setLocalizedMessage(message);
+ return new ObjectResponse<>("ERROR", error);
+ }
+
/**
* Resolver for HTTP request message errors.
* @param ex Exception for HTTP message not readable.
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/controller/api/PowerAuthController.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/controller/api/PowerAuthController.java
index 52b020d09..40ac6143b 100644
--- a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/controller/api/PowerAuthController.java
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/controller/api/PowerAuthController.java
@@ -24,8 +24,12 @@
import io.getlime.core.rest.model.base.response.Response;
import io.getlime.security.powerauth.app.server.service.PowerAuthService;
import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
/**
* Class implementing the RESTful controller for PowerAuth service.
@@ -171,6 +175,17 @@ public ObjectResponse getActivationListForUser
return new ObjectResponse<>("OK", powerAuthService.getActivationListForUser(request.getRequestObject()));
}
+ /**
+ * Update the activation name.
+ *
+ * @param request This is an {@link ObjectRequest} that contains a {@link UpdateActivationNameRequest}.
+ * @return This endpoint returns an {@link ObjectResponse} that contains a {@link UpdateActivationNameResponse}.
+ * @throws Exception In case the service throws an exception, it will be propagated and should be handled by the caller.
+ */
+ @PostMapping("/activation/name/update")
+ public ObjectResponse updateActivation(@Valid @RequestBody ObjectRequest request) throws Exception {
+ return new ObjectResponse<>(powerAuthService.updateActivationName(request.getRequestObject()));
+ }
/**
* Call {@link PowerAuthService#lookupActivations(LookupActivationsRequest)} method and
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/database/model/AdditionalInformation.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/database/model/AdditionalInformation.java
index f5f58cfd2..05e3bc21d 100644
--- a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/database/model/AdditionalInformation.java
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/database/model/AdditionalInformation.java
@@ -80,6 +80,11 @@ public static class Reason {
*/
public static final String ACTIVATION_VERSION_CHANGED = "ACTIVATION_VERSION_CHANGED";
+ /**
+ * Logged when the activation name has been updated.
+ */
+ public static final String ACTIVATION_NAME_UPDATED = "ACTIVATION_NAME_UPDATED";
+
}
private AdditionalInformation() {
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/database/model/entity/ActivationHistoryEntity.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/database/model/entity/ActivationHistoryEntity.java
index 65e72ec5b..8afc5a091 100644
--- a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/database/model/entity/ActivationHistoryEntity.java
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/database/model/entity/ActivationHistoryEntity.java
@@ -20,6 +20,8 @@
import io.getlime.security.powerauth.app.server.database.model.converter.ActivationStatusConverter;
import io.getlime.security.powerauth.app.server.database.model.enumeration.ActivationStatus;
import jakarta.persistence.*;
+import lombok.ToString;
+import org.springframework.data.util.ProxyUtils;
import java.io.Serial;
import java.io.Serializable;
@@ -33,6 +35,7 @@
*/
@Entity
@Table(name = "pa_activation_history")
+@ToString
public class ActivationHistoryEntity implements Serializable {
@Serial
@@ -67,34 +70,8 @@ public class ActivationHistoryEntity implements Serializable {
@Column(name = "activation_version")
private Integer activationVersion;
- /**
- * No-arg constructor.
- */
- public ActivationHistoryEntity() {
- }
-
- /**
- * Constructor with all properties.
- *
- * @param id Signature audit item record ID.
- * @param activation Associated activation, or null of no related activation was found.
- * @param activationStatus Activation status at the time of signature computation attempt.
- * @param timestampCreated Created timestamp.
- * @param activationVersion Activation version.
- */
- public ActivationHistoryEntity(
- final Long id,
- final ActivationRecordEntity activation,
- final ActivationStatus activationStatus,
- final Date timestampCreated,
- final Integer activationVersion) {
-
- this.id = id;
- this.activation = activation;
- this.activationStatus = activationStatus;
- this.timestampCreated = timestampCreated;
- this.activationVersion = activationVersion;
- }
+ @Column(name = "activation_name")
+ private String activationName;
/**
* Get record ID.
@@ -216,54 +193,36 @@ public void setActivationVersion(Integer version) {
this.activationVersion = version;
}
- @Override
- public int hashCode() {
- int hash = 7;
- hash = 23 * hash + Objects.hashCode(this.activation);
- hash = 23 * hash + Objects.hashCode(this.activationStatus);
- hash = 23 * hash + Objects.hashCode(this.eventReason);
- hash = 23 * hash + Objects.hashCode(this.externalUserId);
- hash = 23 * hash + Objects.hashCode(this.timestampCreated);
- hash = 23 * hash + Objects.hashCode(this.activationVersion);
- return hash;
+ public String getActivationName() {
+ return activationName;
+ }
+
+ public void setActivationName(final String activationName) {
+ this.activationName = activationName;
}
@Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
+ public boolean equals(final Object o) {
+ if (null == o) {
return false;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
- final ActivationHistoryEntity other = (ActivationHistoryEntity) obj;
- if (!Objects.equals(this.activation, other.activation)) {
- return false;
- }
- if (!Objects.equals(this.activationStatus, other.activationStatus)) {
- return false;
- }
- if (!Objects.equals(this.eventReason, other.eventReason)) {
- return false;
- }
- if (!Objects.equals(this.externalUserId, other.externalUserId)) {
- return false;
- }
- if (!Objects.equals(this.timestampCreated, other.timestampCreated)) {
+ } else if (this == o) {
+ return true;
+ } else if (!this.getClass().equals(ProxyUtils.getUserClass(o))) {
return false;
+ } else {
+ final ActivationHistoryEntity that = (ActivationHistoryEntity) o;
+ return Objects.equals(getActivationId(), that.getActivationId()) && Objects.equals(getTimestampCreated(), that.getTimestampCreated());
}
- return Objects.equals(this.activationVersion, other.activationVersion);
}
@Override
- public String toString() {
- return "ActivationHistoryEntity{" +
- "id=" + id + ", activation=" + activation + ", activationStatus=" + activationStatus +
- ", eventReason=" + eventReason + ", externalUserId=" + externalUserId + ", timestampCreated=" + timestampCreated +
- ", version=" + activationVersion +'}';
+ public int hashCode() {
+ return Objects.hash(getActivationId(), timestampCreated);
+ }
+
+ // TODO (racansky, 2023-11-08) remove when activation equals and hashCode implemented correctly
+ private String getActivationId() {
+ return getActivation() == null ? null : getActivation().getActivationId();
}
}
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/PowerAuthService.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/PowerAuthService.java
index 2cfc963ca..b8926d979 100644
--- a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/PowerAuthService.java
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/PowerAuthService.java
@@ -1981,4 +1981,22 @@ public void removeOperationTemplate(OperationTemplateDeleteRequest request) thro
throw new GenericServiceException(ServiceError.UNKNOWN_ERROR, ex.getMessage(), ex.getLocalizedMessage());
}
}
+
+ @Transactional
+ public UpdateActivationNameResponse updateActivationName(final UpdateActivationNameRequest request) throws GenericServiceException {
+ try {
+ final String activationId = request.getActivationId();
+ logger.info("UpdateActivationRequest call received, activation ID: {}", activationId);
+ logger.debug("Updating activation: {}", request);
+ final UpdateActivationNameResponse response = behavior.getActivationServiceBehavior().updateActivationName(request);
+ logger.info("UpdateActivationRequest succeeded, activation ID: {}", activationId);
+ return response;
+ } catch (RuntimeException ex) {
+ logger.error("Runtime exception or error occurred, transaction will be rolled back", ex);
+ throw ex;
+ } catch (Exception ex) {
+ logger.error("Unknown error occurred", ex);
+ throw new GenericServiceException(ServiceError.UNKNOWN_ERROR, ex.getMessage(), ex.getLocalizedMessage());
+ }
+ }
}
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/ActivationHistoryServiceBehavior.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/ActivationHistoryServiceBehavior.java
index a109af41e..3f09eaa63 100644
--- a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/ActivationHistoryServiceBehavior.java
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/ActivationHistoryServiceBehavior.java
@@ -22,6 +22,7 @@
import com.wultra.security.powerauth.client.model.entity.ActivationHistoryItem;
import com.wultra.security.powerauth.client.model.response.ActivationHistoryResponse;
import io.getlime.security.powerauth.app.server.converter.ActivationStatusConverter;
+import io.getlime.security.powerauth.app.server.database.model.AdditionalInformation;
import io.getlime.security.powerauth.app.server.database.model.enumeration.ActivationStatus;
import io.getlime.security.powerauth.app.server.database.model.entity.ActivationHistoryEntity;
import io.getlime.security.powerauth.app.server.database.model.entity.ActivationRecordEntity;
@@ -96,6 +97,8 @@ public void saveActivationAndLogChange(ActivationRecordEntity activation, String
activationHistoryEntity.setExternalUserId(externalUserId);
activationHistoryEntity.setTimestampCreated(changeTimestamp);
activationHistoryEntity.setActivationVersion(activation.getVersion());
+ activationHistoryEntity.setActivationName(activation.getActivationName());
+
activation.getActivationHistory().add(activationHistoryEntity);
// ActivationHistoryEntity is persisted together with activation using Cascade.ALL on ActivationEntity
activationRepository.save(activation);
@@ -129,6 +132,7 @@ public ActivationHistoryResponse getActivationHistory(String activationId, Date
item.setVersion(Long.valueOf(activationVersion));
}
item.setExternalUserId(activationHistoryEntity.getExternalUserId());
+ item.setActivationName(activationHistoryEntity.getActivationName());
item.setTimestampCreated(activationHistoryEntity.getTimestampCreated());
response.getItems().add(item);
@@ -169,15 +173,15 @@ private void logAuditItem(ActivationRecordEntity activation, String externalUser
// Build audit log message
final AuditDetail auditDetail = auditDetailBuilder.build();
- switch (activation.getActivationStatus()) {
- case CREATED ->
- audit.log(AuditLevel.INFO, "Created activation with ID: {}", auditDetail, activation.getActivationId());
- case PENDING_COMMIT, BLOCKED, ACTIVE ->
- audit.log(AuditLevel.INFO, "Activation ID: {} is now {}", auditDetail, activation.getActivationId(), activation.getActivationStatus());
- default ->
- audit.log(AuditLevel.INFO, "Removing activation with ID: {}", auditDetail, activation.getActivationId());
+ if (AdditionalInformation.Reason.ACTIVATION_NAME_UPDATED.equals(historyEventReason)) {
+ audit.log(AuditLevel.INFO, "Updated activation with ID: {}", auditDetail, activation.getActivationId());
+ } else {
+ switch (activation.getActivationStatus()) {
+ case CREATED -> audit.log(AuditLevel.INFO, "Created activation with ID: {}", auditDetail, activation.getActivationId());
+ case PENDING_COMMIT, BLOCKED, ACTIVE -> audit.log(AuditLevel.INFO, "Activation ID: {} is now {}", auditDetail, activation.getActivationId(), activation.getActivationStatus());
+ default -> audit.log(AuditLevel.INFO, "Removing activation with ID: {}", auditDetail, activation.getActivationId());
+ }
}
-
}
}
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/ActivationServiceBehavior.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/ActivationServiceBehavior.java
index 2c1c7aa4d..80d4a811c 100644
--- a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/ActivationServiceBehavior.java
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/ActivationServiceBehavior.java
@@ -21,6 +21,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wultra.security.powerauth.client.model.entity.Activation;
import com.wultra.security.powerauth.client.model.request.RecoveryCodeActivationRequest;
+import com.wultra.security.powerauth.client.model.request.UpdateActivationNameRequest;
import com.wultra.security.powerauth.client.model.response.*;
import io.getlime.security.powerauth.app.server.configuration.PowerAuthServiceConfiguration;
import io.getlime.security.powerauth.app.server.converter.ActivationOtpValidationConverter;
@@ -1325,6 +1326,45 @@ public CommitActivationResponse commitActivation(String activationId, String ext
return response;
}
+ /**
+ * Update name of the given activation.
+ *
+ * @param request Update request.
+ * @return Response with updated activation
+ * @throws GenericServiceException In case invalid data is provided or activation is not found, in invalid state or already expired.
+ */
+ public UpdateActivationNameResponse updateActivationName(final UpdateActivationNameRequest request) throws GenericServiceException {
+ final String activationId = request.getActivationId();
+ final ActivationRepository activationRepository = repositoryCatalogue.getActivationRepository();
+ final ActivationRecordEntity activation = activationRepository.findActivationWithLock(activationId);
+ if (activation == null) {
+ logger.info("Activation ID: {} does not exist.", activationId);
+ // Rollback is not required, error occurs before writing to database
+ throw localizationProvider.buildExceptionForCode(ServiceError.ACTIVATION_NOT_FOUND);
+ }
+
+ final List notAllowedStatuses = List.of(ActivationStatus.CREATED, ActivationStatus.REMOVED, ActivationStatus.BLOCKED);
+ final ActivationStatus activationStatus = activation.getActivationStatus();
+ if (notAllowedStatuses.contains(activationStatus)) {
+ logger.info("Activation is in not allowed status {} to update, activation ID: {}", activationStatus, activationId);
+ // Rollback is not required, error occurs before writing to database
+ throw localizationProvider.buildExceptionForCode(ServiceError.ACTIVATION_INCORRECT_STATE);
+ }
+
+ final Date timestamp = new Date();
+
+ activation.setActivationName(request.getActivationName());
+ activation.setTimestampLastChange(timestamp);
+
+ activationHistoryServiceBehavior.saveActivationAndLogChange(activation, request.getExternalUserId(), AdditionalInformation.Reason.ACTIVATION_NAME_UPDATED);
+
+ final UpdateActivationNameResponse response = new UpdateActivationNameResponse();
+ response.setActivationId(activationId);
+ response.setActivationName(activation.getActivationName());
+ response.setActivationStatus(activationStatusConverter.convert(activationStatus));
+ return response;
+ }
+
/**
* Update activation OTP for given activation ID.
*
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/model/ServiceError.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/model/ServiceError.java
index d2fddb952..7c2d1965f 100644
--- a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/model/ServiceError.java
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/model/ServiceError.java
@@ -28,7 +28,7 @@
*
* @author Petr Dvorak, petr@wultra.com
*/
-public class ServiceError {
+public final class ServiceError {
/**
* Unknown error occurred.
diff --git a/powerauth-java-server/src/test/java/io/getlime/security/powerauth/app/server/controller/api/PowerAuthControllerTest.java b/powerauth-java-server/src/test/java/io/getlime/security/powerauth/app/server/controller/api/PowerAuthControllerTest.java
new file mode 100644
index 000000000..a35f4f85b
--- /dev/null
+++ b/powerauth-java-server/src/test/java/io/getlime/security/powerauth/app/server/controller/api/PowerAuthControllerTest.java
@@ -0,0 +1,126 @@
+/*
+ * PowerAuth Server and related software components
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package io.getlime.security.powerauth.app.server.controller.api;
+
+import com.wultra.core.audit.base.database.DatabaseAudit;
+import io.getlime.security.powerauth.app.server.database.model.entity.ActivationHistoryEntity;
+import io.getlime.security.powerauth.app.server.database.model.entity.ActivationRecordEntity;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.Tuple;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Test for {@link PowerAuthController}.
+ *
+ * @author Lubos Racansky, lubos.racansky@wultra.com
+ */
+@SpringBootTest
+@AutoConfigureMockMvc
+@Sql
+@Transactional
+class PowerAuthControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private EntityManager entityManager;
+
+ @Autowired
+ private DatabaseAudit databaseAudit;
+
+ @Test
+ void testUpdateActivation() throws Exception {
+ mockMvc.perform(post("/rest/v3/activation/name/update")
+ .content("""
+ {
+ "requestObject": {
+ "activationId": "e43a5dec-afea-4a10-a80b-b2183399f16b",
+ "activationName": "my iPhone",
+ "externalUserId": "joe-1"
+ }
+ }
+ """)
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value("OK"))
+ .andExpect(jsonPath("$.responseObject.activationName").value("my iPhone"));
+
+ final ActivationRecordEntity activation = entityManager.find(ActivationRecordEntity.class, "e43a5dec-afea-4a10-a80b-b2183399f16b");
+ assertEquals("my iPhone", activation.getActivationName());
+
+ final List historyEntries = entityManager.createQuery("select h from ActivationHistoryEntity h where h.activation = :activation", ActivationHistoryEntity.class)
+ .setParameter("activation", activation)
+ .getResultList();
+ assertEquals(1, historyEntries.size());
+
+ final ActivationHistoryEntity historyEntry = historyEntries.iterator().next();
+ assertEquals("my iPhone", historyEntry.getActivationName());
+ assertEquals("ACTIVATION_NAME_UPDATED", historyEntry.getEventReason());
+ assertEquals("joe-1", historyEntry.getExternalUserId());
+
+ databaseAudit.flush();
+ final String expectedAuditMessage = "Updated activation with ID: e43a5dec-afea-4a10-a80b-b2183399f16b";
+ @SuppressWarnings("unchecked")
+ final List auditEntries = entityManager.createNativeQuery("select * from audit_log where message = :message", Tuple.class)
+ .setParameter("message", expectedAuditMessage)
+ .getResultList();
+ assertEquals(1, auditEntries.size());
+
+ final String param = auditEntries.get(0).get("param").toString();
+ assertThat(param, containsString("\"activationId\":\"e43a5dec-afea-4a10-a80b-b2183399f16b\""));
+ assertThat(param, containsString("\"activationName\":\"my iPhone\""));
+ assertThat(param, containsString("\"reason\":\"ACTIVATION_NAME_UPDATED\""));
+ }
+
+ @Test
+ void testUpdateActivation_badRequest() throws Exception {
+ final String expectedErrorMessage = "requestObject.activationId - must not be blank, requestObject.activationName - must not be blank, requestObject.externalUserId - must not be blank";
+
+ mockMvc.perform(post("/rest/v3/activation/name/update")
+ .content("""
+ {
+ "requestObject": {}
+ }
+ """)
+ .contentType(MediaType.APPLICATION_JSON)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.status").value("ERROR"))
+ .andExpect(jsonPath("$.responseObject.code").value("ERR0024"))
+ .andExpect(jsonPath("$.responseObject.message").value(expectedErrorMessage))
+ .andExpect(jsonPath("$.responseObject.localizedMessage").value(expectedErrorMessage));
+ }
+}
diff --git a/powerauth-java-server/src/test/java/io/getlime/security/powerauth/app/server/database/model/entity/ActivationHistoryEntityTest.java b/powerauth-java-server/src/test/java/io/getlime/security/powerauth/app/server/database/model/entity/ActivationHistoryEntityTest.java
new file mode 100644
index 000000000..1c5c97f29
--- /dev/null
+++ b/powerauth-java-server/src/test/java/io/getlime/security/powerauth/app/server/database/model/entity/ActivationHistoryEntityTest.java
@@ -0,0 +1,62 @@
+/*
+ * PowerAuth Server and related software components
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package io.getlime.security.powerauth.app.server.database.model.entity;
+
+import lombok.extern.slf4j.Slf4j;
+import nl.jqno.equalsverifier.EqualsVerifier;
+import nl.jqno.equalsverifier.Warning;
+import org.junit.jupiter.api.Test;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Test for {@link ActivationHistoryEntity}.
+ *
+ * @author Lubos Racansky, lubos.racansky@wultra.com
+ */
+@Slf4j
+class ActivationHistoryEntityTest {
+
+ @Test
+ void testEqualsContract() {
+ final ApplicationEntity application1 = new ApplicationEntity();
+ application1.setId("app1");
+ final ApplicationEntity application2 = new ApplicationEntity();
+ application2.setId("app2");
+
+ final ApplicationVersionEntity applicationVersion1 = new ApplicationVersionEntity();
+ applicationVersion1.setId("v1");
+ final ApplicationVersionEntity applicationVersion2 = new ApplicationVersionEntity();
+ applicationVersion2.setId("v2");
+
+ final ActivationHistoryEntity activationHistory1 = new ActivationHistoryEntity();
+ activationHistory1.setTimestampCreated(new Date(1));
+ final ActivationHistoryEntity activationHistory2 = new ActivationHistoryEntity();
+ activationHistory2.setTimestampCreated(new Date(2));
+
+ EqualsVerifier.forClass(ActivationHistoryEntity.class)
+ .withOnlyTheseFields("activation", "timestampCreated")
+ // TODO (racansky, 2023-11-09) equals and hashCode is using getActivation().getActivationId() but still getting false positive; https://jqno.nl/equalsverifier/manual/jpa-entities/
+ .suppress(Warning.JPA_GETTER)
+ .withPrefabValues(ApplicationEntity.class, application1, application2)
+ .withPrefabValues(ApplicationVersionEntity.class, applicationVersion1, applicationVersion2)
+ .withPrefabValues(List.class, List.of(activationHistory1), List.of(activationHistory2))
+ .verify();
+ }
+}
diff --git a/powerauth-java-server/src/test/resources/io/getlime/security/powerauth/app/server/controller/api/PowerAuthControllerTest.sql b/powerauth-java-server/src/test/resources/io/getlime/security/powerauth/app/server/controller/api/PowerAuthControllerTest.sql
new file mode 100644
index 000000000..bf3b2bf4b
--- /dev/null
+++ b/powerauth-java-server/src/test/resources/io/getlime/security/powerauth/app/server/controller/api/PowerAuthControllerTest.sql
@@ -0,0 +1,8 @@
+INSERT INTO pa_application (id, name, roles) VALUES
+ (21, 'PA_Tests', '[ "ROLE3", "ROLE4" ]');
+
+INSERT INTO pa_master_keypair (id, application_id, master_key_private_base64, master_key_public_base64, name, timestamp_created)
+VALUES (21, 21, 'KdcJHQAT/BBF+26uBGNhGC0GQ93ncTx7V6kusNA8AdE=', 'BP8ZZ0LjiwRCQPob3NFwF9pPDLhxCjnPNmENzayEeeGCiDdk0gl3UzUhYk9ntMg18LZdhpvYnprZ8mk/71WlQqo=', 'PA_Tests Default Keypair', '2022-06-07 09:13:27.599000');
+
+INSERT INTO pa_activation (activation_id, application_id, user_id, activation_name, activation_code, activation_status, activation_otp, activation_otp_validation, blocked_reason, counter, ctr_data, device_public_key_base64, extras, platform, device_info, flags, failed_attempts, max_failed_attempts, server_private_key_base64, server_private_key_encryption, server_public_key_base64, timestamp_activation_expire, timestamp_created, timestamp_last_used, timestamp_last_change, master_keypair_id, version) VALUES
+ ('e43a5dec-afea-4a10-a80b-b2183399f16b', 21, 'TestUserV3_d8c2e122-b12a-47f1-bca7-e04637bffd14', 'test v3', 'PXSNR-E2B46-7TY3G-TMR2Q', 3, null, 0, null, 0, 'D5XibWWPCv+nOOfcdfnUGQ==', 'BF3Sc/vqg8Zk70Y8rbT45xzAIxblGoWgLqknCHuNj7f6QFBNi2UnLbG7yMqf2eWShhyBJdu9zqx7DG2qzlqhbBE=', null, 'unknown', 'backend-tests', '[ ]', 0, 1, 'PUz/He8+RFoOPS1NG6Gw3TDXIQ/DnS1skNBOQWzXX60=', 0, 'BPHJ4N90NUuLDq92FJUPcaKZOMad1KH2HrwQEN9DB5ST5fiJU4baYF1VlK1JHglnnN1miL3/Qb6IyW3YSMBySYM=', '2023-04-03 14:04:06.015000', '2023-04-03 13:59:06.015000', '2023-04-03 13:59:16.293000', '2023-04-03 13:59:16.343000', 21, 3);
diff --git a/powerauth-java-server/src/test/resources/schema.sql b/powerauth-java-server/src/test/resources/schema.sql
new file mode 100644
index 000000000..3db62afeb
--- /dev/null
+++ b/powerauth-java-server/src/test/resources/schema.sql
@@ -0,0 +1,36 @@
+-- Scheduler lock table - https://github.com/lukas-krecan/ShedLock#configure-lockprovider
+CREATE TABLE IF NOT EXISTS shedlock
+(
+ name VARCHAR(64) NOT NULL,
+ lock_until TIMESTAMP NOT NULL,
+ locked_at TIMESTAMP NOT NULL,
+ locked_by VARCHAR(255) NOT NULL,
+ PRIMARY KEY (name)
+);
+
+
+-- Create audit log table - https://github.com/wultra/lime-java-core#wultra-auditing-library
+CREATE TABLE IF NOT EXISTS audit_log
+(
+ audit_log_id VARCHAR(36) PRIMARY KEY,
+ application_name VARCHAR(256) NOT NULL,
+ audit_level VARCHAR(32) NOT NULL,
+ audit_type VARCHAR(256),
+ timestamp_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ message TEXT NOT NULL,
+ exception_message TEXT,
+ stack_trace TEXT,
+ param TEXT,
+ calling_class VARCHAR(256) NOT NULL,
+ thread_name VARCHAR(256) NOT NULL,
+ version VARCHAR(256),
+ build_time TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS audit_param
+(
+ audit_log_id VARCHAR(36),
+ timestamp_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ param_key VARCHAR(256),
+ param_value VARCHAR(4000)
+);
diff --git a/powerauth-rest-client-spring/src/main/java/com/wultra/security/powerauth/rest/client/PowerAuthRestClient.java b/powerauth-rest-client-spring/src/main/java/com/wultra/security/powerauth/rest/client/PowerAuthRestClient.java
index ed0451d38..06435d2fd 100644
--- a/powerauth-rest-client-spring/src/main/java/com/wultra/security/powerauth/rest/client/PowerAuthRestClient.java
+++ b/powerauth-rest-client-spring/src/main/java/com/wultra/security/powerauth/rest/client/PowerAuthRestClient.java
@@ -293,6 +293,16 @@ public CreateActivationResponse createActivation(String userId, Date timestampAc
return createActivation(request, EMPTY_MULTI_MAP, EMPTY_MULTI_MAP);
}
+ @Override
+ public UpdateActivationNameResponse updateActivationName(UpdateActivationNameRequest request) throws PowerAuthClientException {
+ return updateActivationName(request, EMPTY_MULTI_MAP, EMPTY_MULTI_MAP);
+ }
+
+ @Override
+ public UpdateActivationNameResponse updateActivationName(UpdateActivationNameRequest request, MultiValueMap queryParams, MultiValueMap httpHeaders) throws PowerAuthClientException {
+ return callV3RestApi("/activation/name/update", request, queryParams, httpHeaders, UpdateActivationNameResponse.class);
+ }
+
@Override
public UpdateActivationOtpResponse updateActivationOtp(String activationId, String externalUserId, String activationOtp) throws PowerAuthClientException {
final UpdateActivationOtpRequest request = new UpdateActivationOtpRequest();