Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #1722: Cannot remove callbacks due to foreign key constraint #1728

Merged
merged 1 commit into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/Database-Structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ Stores callback URLs - per-application endpoints that are notified whenever an a
| retention_period | VARCHAR(64) | - | Minimal duration for which is a completed callback event persisted, stored as a ISO 8601 string. |
| timestamp_last_failure | DATETIME | - | The timestamp of the most recent failed callback event associated with this configuration. |
| failure_count | INTEGER | - | The number of consecutive failed callback events associated with this configuration. |
| enabled | BOOLEAN | - | Indicator specifying whether the Callback URL should be used. |
<!-- end -->

<!-- begin database table pa_token -->
Expand Down
7 changes: 7 additions & 0 deletions docs/PowerAuth-Server-1.9.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ failures:
- `failure_count` to hold the number of consecutive failed callbacks of the same configuration, and
- `timestamp_last_failure` to store the timestamp of the most recent failed callback attempt.

### Add Column Indicating If a Callback Is Enabled

A new column `enabled` has been added to the `pa_application_callback` table to indicate whether a Callback URL is
enabled or disabled. When a new Callback URL is created via the Callback URL Management API, the `enabled` column is set
to `true` (enabled) by default. A Callback URL remains enabled until a delete request is made through the Callback URL
Management API, at which point the `enabled` column is set to `false` (disabled). Disabled Callback URLs will be
excluded from all subsequent queries and operations.

## REST API Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,18 @@
</addColumn>
</changeSet>

<changeSet id="10" logicalFilePath="powerauth-java-server/1.9.x/20240704-callback-event-table.xml" author="Jan Pesek">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="pa_application_callback" columnName="enabled" />
</not>
</preConditions>
<comment>Add enabled column to pa_application_callback table.</comment>
<addColumn tableName="pa_application_callback">
<column name="enabled" type="boolean" defaultValueBoolean="true">
<constraints nullable="false" />
</column>
</addColumn>
</changeSet>

</databaseChangeLog>
Binary file modified docs/images/arch_db_structure.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion docs/sql/mssql/migration_1.8.0_1.9.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,10 @@ GO
-- Changeset powerauth-java-server/1.9.x/20240704-callback-event-table.xml::9::Jan Pesek
-- Add failure_count column to pa_application_callback table.
ALTER TABLE pa_application_callback ADD failure_count int CONSTRAINT DF_pa_application_callback_failure_count DEFAULT 0 NOT NULL;
GO
GO

-- Changeset powerauth-java-server/1.9.x/20240704-callback-event-table.xml::10::Jan Pesek
-- Add enabled column to pa_application_callback table.
ALTER TABLE pa_application_callback ADD enabled bit CONSTRAINT DF_pa_application_callback_enabled DEFAULT 1 NOT NULL;
GO

5 changes: 5 additions & 0 deletions docs/sql/oracle/migration_1.8.0_1.9.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,8 @@ ALTER TABLE pa_application_callback ADD timestamp_last_failure TIMESTAMP(6);
-- Changeset powerauth-java-server/1.9.x/20240704-callback-event-table.xml::9::Jan Pesek
-- Add failure_count column to pa_application_callback table.
ALTER TABLE pa_application_callback ADD failure_count INTEGER DEFAULT 0 NOT NULL;

-- Changeset powerauth-java-server/1.9.x/20240704-callback-event-table.xml::10::Jan Pesek
-- Add enabled column to pa_application_callback table.
ALTER TABLE pa_application_callback ADD enabled BOOLEAN DEFAULT 1 NOT NULL;

5 changes: 5 additions & 0 deletions docs/sql/postgresql/migration_1.8.0_1.9.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,8 @@ ALTER TABLE pa_application_callback ADD timestamp_last_failure TIMESTAMP(6) WITH
-- Changeset powerauth-java-server/1.9.x/20240704-callback-event-table.xml::9::Jan Pesek
-- Add failure_count column to pa_application_callback table.
ALTER TABLE pa_application_callback ADD failure_count INTEGER DEFAULT 0 NOT NULL;

-- Changeset powerauth-java-server/1.9.x/20240704-callback-event-table.xml::10::Jan Pesek
-- Add enabled column to pa_application_callback table.
ALTER TABLE pa_application_callback ADD enabled BOOLEAN DEFAULT TRUE NOT NULL;

Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.SQLRestriction;

import java.io.Serial;
import java.io.Serializable;
Expand All @@ -41,6 +42,7 @@
@Entity
@Table(name = "pa_application_callback")
@Getter @Setter
@SQLRestriction("enabled = true")
public class CallbackUrlEntity implements Serializable {

@Serial
Expand Down Expand Up @@ -131,6 +133,12 @@ public class CallbackUrlEntity implements Serializable {
@Column(name = "failure_count", nullable = false)
private Integer failureCount;

/**
* Whether the callback is enabled and can be used.
*/
@Column(name = "enabled", nullable = false)
private boolean enabled = true;

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,13 @@ public interface CallbackUrlRepository extends CrudRepository<CallbackUrlEntity,
""")
void resetFailureCount(String id);

@Modifying
@Query("""
UPDATE CallbackUrlEntity c
SET c.enabled = false
WHERE c = :entity
""")
@Override
void delete(CallbackUrlEntity entity);

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,37 @@
package io.getlime.security.powerauth.app.server.service.behavior.tasks;

import com.wultra.security.powerauth.client.model.enumeration.CallbackUrlType;
import com.wultra.security.powerauth.client.model.request.CreateCallbackUrlRequest;
import com.wultra.security.powerauth.client.model.request.GetCallbackUrlListRequest;
import com.wultra.security.powerauth.client.model.request.RemoveCallbackUrlRequest;
import com.wultra.security.powerauth.client.model.request.UpdateCallbackUrlRequest;
import com.wultra.security.powerauth.client.model.response.CreateCallbackUrlResponse;
import com.wultra.security.powerauth.client.model.response.GetCallbackUrlListResponse;
import com.wultra.security.powerauth.client.model.response.RemoveCallbackUrlResponse;
import io.getlime.security.powerauth.app.server.configuration.PowerAuthCallbacksConfiguration;
import io.getlime.security.powerauth.app.server.database.model.entity.ActivationRecordEntity;
import io.getlime.security.powerauth.app.server.database.model.entity.CallbackUrlEntity;
import io.getlime.security.powerauth.app.server.database.model.entity.CallbackUrlEventEntity;
import io.getlime.security.powerauth.app.server.database.model.entity.OperationEntity;
import io.getlime.security.powerauth.app.server.service.callbacks.CallbackUrlEventService;
import io.getlime.security.powerauth.app.server.service.callbacks.model.CallbackUrlConvertor;
import io.getlime.security.powerauth.app.server.service.exceptions.GenericServiceException;
import io.getlime.security.powerauth.app.server.service.model.ServiceError;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;

import java.util.Map;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

/**
* Test for {@link CallbackUrlBehavior}.
Expand All @@ -52,6 +67,36 @@ class CallbackUrlBehaviorTest {
@Autowired
private EntityManager entityManager;

@Autowired
private PowerAuthCallbacksConfiguration powerAuthCallbacksConfiguration;

@MockBean
private CallbackUrlEventService callbackUrlEventService;

@Test
void testCreateCallbackUrl() throws Exception {
final CreateCallbackUrlRequest request = new CreateCallbackUrlRequest();
request.setApplicationId("PA_Tests");
request.setCallbackUrl("http://localhost:8080");
request.setAuthentication(null);
request.setName("callbackName");
request.setType(CallbackUrlType.OPERATION_STATUS_CHANGE);

final CreateCallbackUrlResponse response = tested.createCallbackUrl(request);
assertEquals(powerAuthCallbacksConfiguration.getDefaultRetentionPeriod(), response.getRetentionPeriod());
assertEquals(powerAuthCallbacksConfiguration.getDefaultInitialBackoff(), response.getInitialBackoff());
assertEquals(powerAuthCallbacksConfiguration.getDefaultMaxAttempts(), response.getMaxAttempts());

final CallbackUrlEntity entity = entityManager.find(CallbackUrlEntity.class, response.getId());
assertEquals("PA_Tests", entity.getApplication().getId());
assertEquals("callbackName", entity.getName());
assertNull(entity.getRetentionPeriod());
assertNull(entity.getInitialBackoff());
assertNull(entity.getMaxAttempts());
assertEquals(0, entity.getFailureCount());
assertTrue(entity.isEnabled());
}

@Test
void updateCallbackUrlTest() throws Exception {
final CallbackUrlEntity callbackUrl = entityManager.find(CallbackUrlEntity.class, "cafec169-28a6-490c-a1d5-c012b9e3c044");
Expand Down Expand Up @@ -89,6 +134,19 @@ void updateCallbackUrlInvalidCallbackId() throws Exception {
assertEquals(ServiceError.INVALID_REQUEST, exception.getCode());
}

@Test
void testUpdateCallbackUrl_callbackDisabled() {
final UpdateCallbackUrlRequest request = new UpdateCallbackUrlRequest();
request.setId("c3d5083a-ce9f-467c-af2c-0c950c197bba");
request.setType(CallbackUrlType.ACTIVATION_STATUS_CHANGE);
request.setApplicationId("PA_Tests");
request.setCallbackUrl("http://localhost:8080");
request.setName("new-name");

final GenericServiceException exception = assertThrows(GenericServiceException.class, () -> tested.updateCallbackUrl(request));
assertEquals(ServiceError.INVALID_REQUEST, exception.getCode());
}

@Test
void updateCallbackUrlInvalidApplicationId() {
final CallbackUrlEntity callbackUrl = entityManager.find(CallbackUrlEntity.class, "cafec169-28a6-490c-a1d5-c012b9e3c044");
Expand All @@ -106,4 +164,83 @@ void updateCallbackUrlInvalidApplicationId() {
assertEquals(ServiceError.INVALID_REQUEST, exception.getCode());
}

@Test
void testGetCallbackUrlList() throws Exception {
final GetCallbackUrlListRequest request = new GetCallbackUrlListRequest();
request.setApplicationId("PA_Tests");

final GetCallbackUrlListResponse response = tested.getCallbackUrlList(request);
assertEquals(1, response.getCallbackUrlList().size());
assertEquals("cafec169-28a6-490c-a1d5-c012b9e3c044", response.getCallbackUrlList().get(0).getId());
}

@Sql
@Test
void testNotifyCallbackListenersOnOperationChange() {

when(callbackUrlEventService.obtainMaxAttempts(any()))
.thenReturn(1);
when(callbackUrlEventService.failureThresholdReached(any()))
.thenReturn(false);

final OperationEntity operation = entityManager.find(OperationEntity.class, "07e927af-689a-43ac-bd21-291179801912");
try (var mockedCallbackConvertor = mockStatic(CallbackUrlConvertor.class)) {
tested.notifyCallbackListenersOnOperationChange(operation);
}

final CallbackUrlEntity enabledCallback = entityManager.find(CallbackUrlEntity.class, "cba5f7aa-889e-4846-b97a-b6ba1bd51ad5");
final CallbackUrlEntity disabledCallback = entityManager.find(CallbackUrlEntity.class, "b5446f8f-a994-447e-b637-e7cd171a24b5");
final CallbackUrlEntity activationCallback = entityManager.find(CallbackUrlEntity.class, "be335b28-8474-41a6-82c8-19ff8b7e82d2");
final Map<String, Object> callbackData = Map.of("operationId", "07e927af-689a-43ac-bd21-291179801912", "type", "OPERATION");

verify(callbackUrlEventService)
.createAndSaveEventForProcessing(enabledCallback, callbackData);
verify(callbackUrlEventService, never())
.createAndSaveEventForProcessing(disabledCallback, callbackData);
verify(callbackUrlEventService, never())
.createAndSaveEventForProcessing(activationCallback, callbackData);
}

@Sql
@Test
void testNotifyCallbackListenersOnActivationChange() {
when(callbackUrlEventService.obtainMaxAttempts(any()))
.thenReturn(1);
when(callbackUrlEventService.failureThresholdReached(any()))
.thenReturn(false);

final ActivationRecordEntity activation = entityManager.find(ActivationRecordEntity.class, "e43a5dec-afea-4a10-a80b-b2183399f16b");
try (var mockedCallbackConvertor = mockStatic(CallbackUrlConvertor.class)) {
tested.notifyCallbackListenersOnActivationChange(activation);
}

final CallbackUrlEntity enabledCallback = entityManager.find(CallbackUrlEntity.class, "cba5f7aa-889e-4846-b97a-b6ba1bd51ad5");
final CallbackUrlEntity disabledCallback = entityManager.find(CallbackUrlEntity.class, "b5446f8f-a994-447e-b637-e7cd171a24b5");
final CallbackUrlEntity activationCallback = entityManager.find(CallbackUrlEntity.class, "be335b28-8474-41a6-82c8-19ff8b7e82d2");
final Map<String, Object> callbackData = Map.of("activationId", "e43a5dec-afea-4a10-a80b-b2183399f16b", "type", "ACTIVATION");

verify(callbackUrlEventService)
.createAndSaveEventForProcessing(enabledCallback, callbackData);
verify(callbackUrlEventService, never())
.createAndSaveEventForProcessing(disabledCallback, callbackData);
verify(callbackUrlEventService, never())
.createAndSaveEventForProcessing(activationCallback, callbackData);
}

@Test
void testRemoveCallbackUrl() throws Exception {
final CallbackUrlEntity callbackUrlEntity = entityManager.find(CallbackUrlEntity.class, "cafec169-28a6-490c-a1d5-c012b9e3c044");
assertTrue(callbackUrlEntity.isEnabled());

final CallbackUrlEventEntity callbackUrlEventEntity = entityManager.find(CallbackUrlEventEntity.class, 1);
assertNotNull(callbackUrlEventEntity);
assertEquals(callbackUrlEventEntity.getCallbackUrlEntity().getId(), callbackUrlEntity.getId());

final RemoveCallbackUrlRequest request = new RemoveCallbackUrlRequest();
request.setId(callbackUrlEntity.getId());

final RemoveCallbackUrlResponse response = tested.removeCallbackUrl(request);
entityManager.flush();
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
INSERT INTO pa_application (id, name) VALUES
(21, 'PA_Tests');

INSERT INTO pa_application_callback (id, application_id, name, callback_url, type, failure_count) VALUES
('cafec169-28a6-490c-a1d5-c012b9e3c044', 21, 'test-callback', 'http://localhost:8080', 'ACTIVATION_STATUS_CHANGE', 0);
INSERT INTO pa_application_callback (id, application_id, name, callback_url, type, failure_count, enabled) VALUES
('cafec169-28a6-490c-a1d5-c012b9e3c044', 21, 'test-callback', 'http://localhost:8080', 'ACTIVATION_STATUS_CHANGE', 0, true),
('c3d5083a-ce9f-467c-af2c-0c950c197bba', 21, 'test-callback', 'http://localhost:8080', 'ACTIVATION_STATUS_CHANGE', 0, false);

INSERT INTO pa_application_callback_event (id, application_callback_id, callback_data, status, timestamp_created, attempts, idempotency_key) VALUES
(1, 'cafec169-28a6-490c-a1d5-c012b9e3c044', '{}', 'COMPLETED', '2020-10-04 12:13:27.599000', 1, '729c3cd9-45e7-46b2-bc24-cd638138ccfe');
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
INSERT INTO pa_application (id, name) VALUES
(1, 'PA_Tests');

INSERT INTO pa_master_keypair (id, application_id, master_key_private_base64, master_key_public_base64, name, timestamp_created) VALUES
(1, 1, 'KdcJHQAT/BBF+26uBGNhGC0GQ93ncTx7V6kusNA8AdE=', 'BP8ZZ0LjiwRCQPob3NFwF9pPDLhxCjnPNmENzayEeeGCiDdk0gl3UzUhYk9ntMg18LZdhpvYnprZ8mk/71WlQqo=', 'PA_Tests Default Keypair', '2020-10-04 12: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', 1, 'testUser', 'test v4', 'PXSNR-E2B46-7TY3G-TMR2Q', 3, null, 0, null, 0, 'D5XibWWPCv+nOOfcdfnUGQ==', 'BF3Sc/vqg8Zk70Y8rbT45xzAIxblGoWgLqknCHuNj7f6QFBNi2UnLbG7yMqf2eWShhyBJdu9zqx7DG2qzlqhbBE=', null, 'unknown', 'backend-tests', '[ "test-flag1", "test-flag2", "test-flag3" ]', 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', 1, 3);

INSERT INTO pa_application_callback (id, application_id, name, callback_url, type, failure_count, enabled) VALUES
('cba5f7aa-889e-4846-b97a-b6ba1bd51ad5', 1, 'test-callback-enabled', 'http://localhost:8080', 'ACTIVATION_STATUS_CHANGE', 0, true),
('b5446f8f-a994-447e-b637-e7cd171a24b5', 1, 'test-callback-disabled', 'http://localhost:8080', 'ACTIVATION_STATUS_CHANGE', 0, false),
('be335b28-8474-41a6-82c8-19ff8b7e82d2', 1, 'test-callback-operation', 'http://localhost:8080', 'OPERATION_STATUS_CHANGE', 0, true);
Loading