diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/pom.xml b/components/org.wso2.carbon.identity.notification.push.device.handler/pom.xml index a992db8..ef6e1bb 100644 --- a/components/org.wso2.carbon.identity.notification.push.device.handler/pom.xml +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/pom.xml @@ -51,6 +51,10 @@ org.wso2.carbon.identity.organization.management.core org.wso2.carbon.identity.organization.management.service + + org.wso2.carbon.identity.event.handler.notification + org.wso2.carbon.identity.event.handler.notification + org.wso2.carbon.identity.event.handler.notification org.wso2.carbon.identity.notification.sender.tenant.config @@ -95,6 +99,10 @@ org.wso2.carbon.identity.framework org.wso2.carbon.identity.central.log.mgt + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.configuration.mgt.core + org.wso2.orbit.com.google.firebase firebase-admin @@ -139,12 +147,19 @@ org.json, org.wso2.carbon.identity.central.log.mgt.utils; version="${carbon.identity.framework.imp.pkg.version.range}", + org.wso2.carbon.identity.configuration.mgt.core; version="${carbon.identity.framework.imp.pkg.version.range}", + org.wso2.carbon.identity.configuration.mgt.core.exception; version="${carbon.identity.framework.imp.pkg.version.range}", + org.wso2.carbon.identity.configuration.mgt.core.model; version="${carbon.identity.framework.imp.pkg.version.range}", + org.wso2.carbon.identity.configuration.mgt.core.constant; version="${carbon.identity.framework.imp.pkg.version.range}", org.apache.commons.logging; version="${org.apache.commons.logging.range}", + org.apache.commons.collections, org.osgi.framework; version="${osgi.framework.imp.pkg.version.range}", org.osgi.service.component; version="${osgi.service.component.imp.pkg.version.range}", org.wso2.carbon.identity.base; version="${carbon.identity.framework.imp.pkg.version.range}", org.wso2.carbon.identity.core; version="${carbon.identity.framework.imp.pkg.version.range}", org.wso2.carbon.identity.core.cache; version="${carbon.identity.framework.imp.pkg.version.range}", + org.wso2.carbon.identity.core.context; version="${carbon.identity.framework.imp.pkg.version.range}", + org.wso2.carbon.identity.core.context.model; version="${carbon.identity.framework.imp.pkg.version.range}", org.wso2.carbon.identity.core.util; version="${carbon.identity.framework.imp.pkg.version.range}", org.wso2.carbon.identity.organization.management.service;version="${org.wso2.carbon.identity.organization.management.core.version.range}", org.wso2.carbon.identity.organization.management.service.exception;version="${org.wso2.carbon.identity.organization.management.core.version.range}", @@ -168,6 +183,10 @@ org.wso2.carbon.user.core.common; version="${carbon.kernel.package.import.version.range}", org.wso2.carbon; version="${carbon.kernel.package.import.version.range}", org.wso2.carbon.context; version="${carbon.kernel.package.import.version.range}", + org.wso2.carbon.identity.event; version="${carbon.identity.framework.imp.pkg.version.range}", + org.wso2.carbon.identity.event.event; version="${carbon.identity.framework.imp.pkg.version.range}", + org.wso2.carbon.identity.event.services; version="${carbon.identity.framework.imp.pkg.version.range}", + org.wso2.carbon.identity.governance.service.notification; version="${identity.governance.imp.pkg.version.range}", diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/DeviceHandlerService.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/DeviceHandlerService.java index dbab23e..66674d8 100644 --- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/DeviceHandlerService.java +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/DeviceHandlerService.java @@ -23,6 +23,9 @@ import org.wso2.carbon.identity.notification.push.device.handler.model.RegistrationDiscoveryData; import org.wso2.carbon.identity.notification.push.device.handler.model.RegistrationRequest; +import java.util.Collections; +import java.util.List; + /** * Device Handler interface. */ @@ -60,6 +63,7 @@ Device registerDevice(RegistrationRequest registrationRequest, String tenantDoma * Remove all devices of a registered user. * * @param userId User ID. + * @param tenantDomain Tenant Domain. * @throws PushDeviceHandlerException Push Device Handler Exception. */ void unregisterDeviceByUserId(String userId, String tenantDomain) throws PushDeviceHandlerException; @@ -77,11 +81,25 @@ Device registerDevice(RegistrationRequest registrationRequest, String tenantDoma * Get a device by the user ID. * * @param userId User ID. + * @param tenantDomain Tenant Domain. * @return Device. * @throws PushDeviceHandlerException Push Device Handler Exception. */ Device getDeviceByUserId(String userId, String tenantDomain) throws PushDeviceHandlerException; + /** + * Get devices by the user ID. + * + * @param userId User ID. + * @param tenantDomain Tenant Domain. + * @return Device. + * @throws PushDeviceHandlerException Push Device Handler Exception. + */ + default List getDevicesByUserId(String userId, String tenantDomain) throws PushDeviceHandlerException { + + return Collections.emptyList(); + } + /** * Edit the name of a registered device. * @@ -111,5 +129,4 @@ RegistrationDiscoveryData getRegistrationDiscoveryData(String username, String t * @throws PushDeviceHandlerException Push Device Handler Exception. */ String getPublicKey(String deviceId) throws PushDeviceHandlerException; - } diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/constant/PushDeviceHandlerConstants.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/constant/PushDeviceHandlerConstants.java index 7a6f958..3ce7866 100644 --- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/constant/PushDeviceHandlerConstants.java +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/constant/PushDeviceHandlerConstants.java @@ -31,6 +31,104 @@ public class PushDeviceHandlerConstants { "PushAuthenticator.DeviceRegistrationContext.ValidityPeriod"; public static final int DEFAULT_DEVICE_REGISTRATION_CONTEXT_VALIDITY_PERIOD = 180; + public static final String MAX_DEVICE_LIMIT_PER_USER = "PushAuthenticator.DeviceManagement.MaxDeviceLimitPerUser"; + public static final int DEFAULT_MAX_DEVICE_LIMIT_PER_USER = 10; + public static final int DEFAULT_MIN_DEVICE_LIMIT_PER_USER = 2; + public static final String PUSH_DEVICE_MGT_RESOURCE_TYPE = "DEVICE_MANAGEMENT"; + public static final String PUSH_DEVICE_MGT_RESOURCE_NAME = "PUSH_DEVICE_MANAGEMENT"; + public static final String ATTR_ENABLE_MULTIPLE_DEVICE_ENROLLMENT = "enableMultipleDeviceEnrollment"; + public static final String ATTR_MAX_DEVICE_LIMIT = "maximumDeviceLimit"; + public static final String ATTR_ENABLE_DEVICE_REGISTRATION_NOTIFICATIONS = "enableDeviceRegistrationNotifications"; + public static final String ATTR_DEVICE_REGISTRATION_NOTIFICATION_CHANNELS = + "deviceRegistrationNotificationChannels"; + public static final String NOTIFICATION_CHANNELS_SEPARATOR = ","; + public static final String REGISTRATION_TIME_FORMATTER_PATTERN = "yyyy-MM-dd HH:mm:ss 'UTC'"; + + + + /** + * Constants related to email notifications sent during device management operations. + */ + public static class EmailNotificationConstants { + + public static final String PUSH_DEVICE_REGISTRATION_TEMPLATE = "PushDeviceRegistration"; + + // Placeholders used in the template body + public static final String DEVICE_NAME_PLACEHOLDER = "push-device-name"; + public static final String DEVICE_MODEL_PLACEHOLDER = "push-device-model"; + public static final String REGISTRATION_TIME_PLACEHOLDER = "registration-time"; + public static final String IP_ADDRESS_PLACEHOLDER = "ip-address"; + } + + /** + * Constants related to push notifications sent during device management operations. + */ + public static final class PushNotificationConstants { + + public static final String PUSH_NOTIFICATION_EVENT_NAME = "TRIGGER_PUSH_NOTIFICATION"; + public static final String PUSH_NOTIFICATION_CHANNEL = "PUSH_NOTIFICATION"; + public static final String NOTIFICATION_SCENARIO = "NOTIFICATION_SCENARIO"; + public static final String DEVICE_REGISTRATION_SCENARIO = "DEVICE_REGISTRATION"; + public static final String NOTIFICATION_PROVIDER = "notificationProvider"; + public static final String DEVICE_TOKEN = "deviceToken"; + public static final String DEVICE_ID = "deviceId"; + public static final String DEVICE_HANDLE = "deviceHandle"; + public static final String IP_ADDRESS = "ipAddress"; + + // Placeholders rendered in the push notification template. + public static final String DEVICE_NAME_PLACEHOLDER = "push-device-name"; + public static final String DEVICE_MODEL_PLACEHOLDER = "push-device-model"; + public static final String REGISTRATION_TIME_PLACEHOLDER = "registration-time"; + + /** + * Private constructor to prevent initialization of the class. + */ + private PushNotificationConstants() { + + } + } + + /** + * Constants related to diagnostic logging of device management operations. + */ + public static class LogConstants { + + public static final String PUSH_DEVICE_HANDLER_SERVICE = "push-device-handler-service"; + + /** + * Action identifiers used in diagnostic logs. + */ + public static class ActionIDs { + + public static final String TRIGGER_DEVICE_REGISTRATION_EMAIL_NOTIFICATION = + "trigger-device-registration-email-notification"; + public static final String TRIGGER_DEVICE_REGISTRATION_PUSH_NOTIFICATION = + "trigger-device-registration-push-notification"; + + private ActionIDs() { + + } + } + + /** + * Input parameter keys used in diagnostic logs. + */ + public static class InputKeys { + + public static final String DEVICE_ID = "device-id"; + public static final String USER_ID = "user-id"; + public static final String TENANT_DOMAIN = "tenant-domain"; + + private InputKeys() { + + } + } + + private LogConstants() { + + } + } + /** * Private constructor to prevent initialization of the class. */ @@ -136,6 +234,38 @@ public enum ErrorMessages { ERROR_CODE_FAILED_TO_RESOLVE_PUSH_PROVIDER( "PDH-15013", "Failed to resolve the correct push provider for the request." + ), + ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG( + "PDH-15014", + "Failed to get push device configuration for the tenant." + ), + ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG( + "PDH-15015", + "Failed to add push device configuration for the tenant." + ), + ERROR_CODE_UPDATING_PUSH_DEVICE_CONFIG( + "PDH-15016", + "Failed to update push device configuration for the tenant." + ), + ERROR_CODE_INVALID_DEVICE_LIMIT_CONFIG( + "PDH-15017", + "Maximum device limit can only be updated when multiple device enrollment is enabled." + ), + ERROR_CODE_DEVICE_LIMIT_PER_USER_EXCEEDS( + "PDH-15018", + "Maximum device limit cannot exceed the server-configured device registration per user." + ), + ERROR_CODE_MAX_DEVICE_LIMIT_REACHED( + "PDH-15019", + "Maximum device limit reached for the user: %s." + ), + ERROR_CODE_DEVICE_ID_ALREADY_REGISTERED( + "PDH-15020", + "Device is already registered for the device ID: %s." + ), + ERROR_CODE_INVALID_DEVICE_LIMIT_VALUE( + "PDH-15021", + "Maximum device limit must be a positive value when multiple device enrollment is enabled." ); private final String code; diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAO.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAO.java index d63842c..e652ddf 100644 --- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAO.java +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAO.java @@ -18,9 +18,11 @@ package org.wso2.carbon.identity.notification.push.device.handler.dao; +import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerException; import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerServerException; import org.wso2.carbon.identity.notification.push.device.handler.model.Device; +import java.util.List; import java.util.Optional; /** @@ -33,9 +35,9 @@ public interface DeviceDAO { * * @param device Device to be registered. * @param tenantId Tenant ID. - * @throws PushDeviceHandlerServerException PushDeviceHandlerServerException. + * @throws PushDeviceHandlerException PushDeviceHandlerException. */ - void registerDevice(Device device, int tenantId) throws PushDeviceHandlerServerException; + void registerDevice(Device device, int tenantId) throws PushDeviceHandlerException; /** * Unregister a device. @@ -67,11 +69,22 @@ public interface DeviceDAO { * Get a device by the user ID. * * @param userId User ID. + * @param tenantId Tenant ID. * @return Device. * @throws PushDeviceHandlerServerException Push Device Handler Server Exception. */ Optional getDeviceByUserId(String userId, int tenantId) throws PushDeviceHandlerServerException; + /** + * Get all devices registered for a user. + * + * @param userId User ID. + * @param tenantId Tenant ID. + * @return List of registered devices, empty list if none found. + * @throws PushDeviceHandlerServerException Push Device Handler Server Exception. + */ + List getDevicesByUserId(String userId, int tenantId) throws PushDeviceHandlerServerException; + /** * Get the public key of a device. * diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAOImpl.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAOImpl.java index 8080901..726790b 100644 --- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAOImpl.java +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAOImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -21,14 +21,20 @@ import org.wso2.carbon.database.utils.jdbc.NamedPreparedStatement; import org.wso2.carbon.identity.core.util.IdentityDatabaseUtil; import org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants; +import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerClientException; +import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerException; import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerServerException; import org.wso2.carbon.identity.notification.push.device.handler.model.Device; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.SQLIntegrityConstraintViolationException; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_DEVICE_ID_ALREADY_REGISTERED; import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.SQLQueries.EDIT_DEVICE; import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.SQLQueries.GET_DEVICE_BY_DEVICE_ID; import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.SQLQueries.GET_DEVICE_BY_USER_ID; @@ -42,7 +48,7 @@ public class DeviceDAOImpl implements DeviceDAO { @Override - public void registerDevice(Device device, int tenantId) throws PushDeviceHandlerServerException { + public void registerDevice(Device device, int tenantId) throws PushDeviceHandlerException { Connection connection = IdentityDatabaseUtil.getDBConnection(true); try (NamedPreparedStatement statement = new NamedPreparedStatement(connection, REGISTER_DEVICE)) { @@ -59,7 +65,13 @@ public void registerDevice(Device device, int tenantId) throws PushDeviceHandler IdentityDatabaseUtil.commitTransaction(connection); } catch (SQLException e) { IdentityDatabaseUtil.rollbackTransaction(connection); - throw new PushDeviceHandlerServerException("Error occurred while registering the device.", e); + if (e instanceof SQLIntegrityConstraintViolationException) { + throw new PushDeviceHandlerClientException( + ERROR_CODE_DEVICE_ID_ALREADY_REGISTERED.getCode(), + ERROR_CODE_DEVICE_ID_ALREADY_REGISTERED.getMessage() + device.getDeviceId()); + } + throw new PushDeviceHandlerServerException( + "Error occurred while registering the device.", e); } finally { IdentityDatabaseUtil.closeConnection(connection); } @@ -156,6 +168,39 @@ public Optional getDeviceByUserId(String userId, int tenantId) throws Pu return Optional.ofNullable(device); } + @Override + public List getDevicesByUserId(String userId, int tenantId) + throws PushDeviceHandlerServerException { + + List devices = new ArrayList<>(); + Connection connection = IdentityDatabaseUtil.getDBConnection(true); + try (NamedPreparedStatement statement = + new NamedPreparedStatement(connection, GET_DEVICE_BY_USER_ID)) { + statement.setString(PushDeviceHandlerConstants.ColumnNames.USER_ID, userId); + statement.setInt(PushDeviceHandlerConstants.ColumnNames.TENANT_ID, tenantId); + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + Device device = new Device(); + device.setDeviceId(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.ID)); + device.setUserId(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.USER_ID)); + device.setDeviceName(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.DEVICE_NAME)); + device.setDeviceModel(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.DEVICE_MODEL)); + device.setDeviceToken(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.DEVICE_TOKEN)); + device.setDeviceHandle(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.DEVICE_HANDLE)); + device.setPublicKey(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.PUBLIC_KEY)); + device.setProvider(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.PROVIDER)); + devices.add(device); + } + } + } catch (SQLException e) { + throw new PushDeviceHandlerServerException( + "Error occurred while retrieving devices for user.", e); + } finally { + IdentityDatabaseUtil.closeConnection(connection); + } + return devices; + } + @Override public Optional getPublicKey(String deviceId) throws PushDeviceHandlerServerException { diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/impl/DeviceHandlerServiceImpl.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/impl/DeviceHandlerServiceImpl.java index 320092b..61433d9 100644 --- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/impl/DeviceHandlerServiceImpl.java +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/impl/DeviceHandlerServiceImpl.java @@ -18,17 +18,26 @@ package org.wso2.carbon.identity.notification.push.device.handler.impl; +import org.apache.commons.collections.MapUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.osgi.annotation.bundle.Capability; import org.wso2.carbon.identity.base.IdentityException; +import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; +import org.wso2.carbon.identity.core.context.IdentityContext; +import org.wso2.carbon.identity.core.context.model.Request; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.event.IdentityEventConstants; +import org.wso2.carbon.identity.event.IdentityEventException; +import org.wso2.carbon.identity.event.event.Event; +import org.wso2.carbon.identity.governance.service.notification.NotificationChannels; import org.wso2.carbon.identity.notification.push.common.PushChallengeValidator; import org.wso2.carbon.identity.notification.push.common.exception.PushTokenValidationException; import org.wso2.carbon.identity.notification.push.device.handler.DeviceHandlerService; import org.wso2.carbon.identity.notification.push.device.handler.DeviceRegistrationContextManager; +import org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants; import org.wso2.carbon.identity.notification.push.device.handler.dao.DeviceDAO; import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerClientException; import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerException; @@ -36,10 +45,13 @@ import org.wso2.carbon.identity.notification.push.device.handler.internal.PushDeviceHandlerDataHolder; import org.wso2.carbon.identity.notification.push.device.handler.model.Device; import org.wso2.carbon.identity.notification.push.device.handler.model.DeviceRegistrationContext; +import org.wso2.carbon.identity.notification.push.device.handler.model.DeviceRegistrationNotificationChannelEnum; +import org.wso2.carbon.identity.notification.push.device.handler.model.PushDeviceMgtConfigData; import org.wso2.carbon.identity.notification.push.device.handler.model.RegistrationDiscoveryData; import org.wso2.carbon.identity.notification.push.device.handler.model.RegistrationRequest; import org.wso2.carbon.identity.notification.push.device.handler.model.RegistrationRequestProviderData; import org.wso2.carbon.identity.notification.push.device.handler.utils.DeviceHandlerAuditLogger; +import org.wso2.carbon.identity.notification.push.device.handler.utils.PushDeviceConfigManager; import org.wso2.carbon.identity.notification.push.provider.PushProvider; import org.wso2.carbon.identity.notification.push.provider.exception.PushProviderClientException; import org.wso2.carbon.identity.notification.push.provider.exception.PushProviderException; @@ -52,7 +64,10 @@ import org.wso2.carbon.identity.organization.management.service.util.OrganizationManagementUtil; import org.wso2.carbon.user.api.UserRealm; import org.wso2.carbon.user.api.UserStoreException; +import org.wso2.carbon.user.core.UserCoreConstants; import org.wso2.carbon.user.core.common.AbstractUserStoreManager; +import org.wso2.carbon.user.core.common.User; +import org.wso2.carbon.utils.DiagnosticLog; import org.wso2.carbon.utils.multitenancy.MultitenantUtils; import java.nio.charset.StandardCharsets; @@ -64,13 +79,23 @@ import java.security.SignatureException; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.UUID; +import static org.wso2.carbon.identity.event.handler.notification.NotificationConstants.EmailNotification.ARBITRARY_SEND_TO; +import static org.wso2.carbon.identity.event.handler.notification.NotificationConstants.EmailNotification.EMAIL_TEMPLATE_TYPE; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.DEFAULT_MIN_DEVICE_LIMIT_PER_USER; import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.DEFAULT_PUSH_PROVIDER; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.EmailNotificationConstants.PUSH_DEVICE_REGISTRATION_TEMPLATE; import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_DEVICE_ALREADY_REGISTERED; import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_DEVICE_NOT_FOUND; import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_DEVICE_NOT_FOUND_FOR_USER_ID; @@ -79,12 +104,27 @@ import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_FAILED_TO_RESOLVE_PUSH_PROVIDER; import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_INVALID_EDIT_DEVICE_SCENARIO; import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_INVALID_SIGNATURE; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_MAX_DEVICE_LIMIT_REACHED; import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_PUBLIC_KEY_NOT_FOUND; import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_REGISTRATION_CONTEXT_ALREADY_USED; import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_REGISTRATION_CONTEXT_NOT_FOUND; import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_SIGNATURE_VERIFICATION_FAILED; import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_TOKEN_CLAIM_VERIFICATION_FAILED; import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.HASHING_ALGORITHM; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.LogConstants.ActionIDs.TRIGGER_DEVICE_REGISTRATION_EMAIL_NOTIFICATION; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.LogConstants.ActionIDs.TRIGGER_DEVICE_REGISTRATION_PUSH_NOTIFICATION; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.LogConstants.InputKeys.TENANT_DOMAIN; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.LogConstants.PUSH_DEVICE_HANDLER_SERVICE; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.DEVICE_HANDLE; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.DEVICE_ID; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.DEVICE_REGISTRATION_SCENARIO; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.DEVICE_TOKEN; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.IP_ADDRESS; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.NOTIFICATION_PROVIDER; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.NOTIFICATION_SCENARIO; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.PUSH_NOTIFICATION_CHANNEL; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.PushNotificationConstants.PUSH_NOTIFICATION_EVENT_NAME; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.REGISTRATION_TIME_FORMATTER_PATTERN; import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.SIGNATURE_ALGORITHM; import static org.wso2.carbon.identity.notification.sender.tenant.config.NotificationSenderManagementConstants.PUSH_PUBLISHER_TYPE; @@ -104,6 +144,8 @@ public class DeviceHandlerServiceImpl implements DeviceHandlerService { private DeviceDAO deviceDAO; private DeviceRegistrationContextManager deviceRegistrationContextManager; private static final DeviceHandlerAuditLogger AUDIT_LOGGER = new DeviceHandlerAuditLogger(); + private static final DateTimeFormatter REGISTRATION_TIME_FORMATTER = + DateTimeFormatter.ofPattern(REGISTRATION_TIME_FORMATTER_PATTERN).withZone(ZoneOffset.UTC); /** * Constructor of DeviceHandlerServiceImpl. @@ -145,6 +187,12 @@ public Device registerDevice(RegistrationRequest registrationRequest, String ten String.format(ERROR_CODE_REGISTRATION_CONTEXT_ALREADY_USED.getMessage(), deviceId)); } + AUDIT_LOGGER.printAuditLog( + DeviceHandlerAuditLogger.Operation.REGISTER_DEVICE, + deviceId, + device.getUserId() + ); + return device; } @@ -240,6 +288,15 @@ public Device getDeviceByUserId(String userId, String tenantDomain) throws PushD } } + @Override + public List getDevicesByUserId(String userId, String tenantDomain) throws PushDeviceHandlerException { + + int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); + List devices = deviceDAO.getDevicesByUserId(userId, tenantId); + + return devices != null ? devices : Collections.emptyList(); + } + @Override public void editDevice(String deviceId, String path, String value) throws PushDeviceHandlerException { @@ -367,39 +424,29 @@ private void handleSignatureVerification(RegistrationRequest registrationRequest private Device handleDeviceRegistration(RegistrationRequest registrationRequest, DeviceRegistrationContext context) throws PushDeviceHandlerException { - String userId; String username = context.getUsername(); String tenantDomain = context.getTenantDomain(); int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); - try { - UserRealm userRealm = IdentityTenantUtil.getRealm(tenantDomain, username); - AbstractUserStoreManager userStoreManager = (AbstractUserStoreManager) userRealm.getUserStoreManager(); - userId = userStoreManager.getUserIDFromUserName(MultitenantUtils.getTenantAwareUsername(username)); - if (StringUtils.isBlank(userId)) { - String errorMessage = String.format(ERROR_CODE_FAILED_TO_GET_USER_ID.toString(), username); - throw new PushDeviceHandlerServerException(errorMessage); - } - } catch (UserStoreException | IdentityException e) { - String errorMessage = String.format(ERROR_CODE_FAILED_TO_GET_USER_ID.toString(), username); - throw new PushDeviceHandlerServerException(errorMessage, e); - } - try { - Device existingDevice = getDeviceByUserId(userId, tenantDomain); - if (existingDevice != null) { + PushDeviceMgtConfigData tenantConfig = PushDeviceConfigManager.getPushDeviceConfig(tenantDomain); + User user = resolveUser(username, tenantDomain, tenantConfig); + String userId = user.getUserID(); + + List existingDevices = deviceDAO.getDevicesByUserId(userId, tenantId); + + // if multiple device enrollment is not enabled, check if the user already has a registered device + if (!Boolean.TRUE.equals(tenantConfig.getEnableMultipleDeviceEnrollment())) { + if (!existingDevices.isEmpty()) { throw new PushDeviceHandlerClientException( ERROR_CODE_DEVICE_ALREADY_REGISTERED.getCode(), ERROR_CODE_DEVICE_ALREADY_REGISTERED.toString()); } - } catch (PushDeviceHandlerClientException e) { - // This means there is no existing device registered for the user. - if ((ERROR_CODE_DEVICE_NOT_FOUND_FOR_USER_ID.getCode()).equals(e.getErrorCode())) { - if (LOG.isDebugEnabled()) { - String message = String.format("No existing device registered for the user: %s", userId); - LOG.debug(message); - } - } else { - throw e; + } else { + int maxDeviceLimit = tenantConfig.getMaximumDeviceLimit() != null + ? tenantConfig.getMaximumDeviceLimit() : DEFAULT_MIN_DEVICE_LIMIT_PER_USER; + if (existingDevices.size() >= maxDeviceLimit) { + throw new PushDeviceHandlerClientException(ERROR_CODE_MAX_DEVICE_LIMIT_REACHED.getCode(), + String.format(ERROR_CODE_MAX_DEVICE_LIMIT_REACHED.getMessage(), userId)); } } @@ -421,15 +468,64 @@ private Device handleDeviceRegistration(RegistrationRequest registrationRequest, try { deviceDAO.registerDevice(device, tenantId); context.setRegistered(true); - } catch (PushDeviceHandlerServerException e) { + } catch (PushDeviceHandlerException e) { String errorMessage = String.format(ERROR_CODE_DEVICE_REGISTRATION_FAILED.toString(), registrationRequest.getDeviceId()); throw new PushDeviceHandlerServerException(errorMessage, e); } + // Trigger notifications about the new device registration. + if (Boolean.TRUE.equals(tenantConfig.getEnableDeviceRegistrationNotifications())) { + Set channels = + tenantConfig.getDeviceRegistrationNotificationChannels(); + if (channels != null && channels.contains(DeviceRegistrationNotificationChannelEnum.EMAIL)) { + triggerEmailNotification(device, tenantDomain, user); + } + if (channels != null && channels.contains(DeviceRegistrationNotificationChannelEnum.PUSH_NOTIFICATION)) { + triggerPushNotification(device, existingDevices, user, tenantDomain); + } + } + return device; } + /** + * Resolve the user owning the device registration, along with the claims + * required for the device registration notifications. + * + * @param username Username from the device registration context. + * @param tenantDomain Tenant domain of the user. + * @return Resolved user; never {@code null}. + * @throws PushDeviceHandlerServerException If the user cannot be resolved. + */ + private User resolveUser(String username, String tenantDomain, PushDeviceMgtConfigData tenantConfig) + throws PushDeviceHandlerServerException { + + try { + UserRealm userRealm = IdentityTenantUtil.getRealm(tenantDomain, username); + AbstractUserStoreManager userStoreManager = (AbstractUserStoreManager) userRealm.getUserStoreManager(); + String tenantAwareUsername = MultitenantUtils.getTenantAwareUsername(username); + String userId = userStoreManager.getUserIDFromUserName(tenantAwareUsername); + if (StringUtils.isBlank(userId)) { + String errorMessage = String.format(ERROR_CODE_FAILED_TO_GET_USER_ID.toString(), username); + throw new PushDeviceHandlerServerException(errorMessage); + } + + User user; + if (Boolean.TRUE.equals(tenantConfig.getEnableDeviceRegistrationNotifications())) { + String[] requestedClaims = new String[] {NotificationChannels.EMAIL_CHANNEL.getClaimUri()}; + user = userStoreManager.getUserWithID(userId, requestedClaims, UserCoreConstants.DEFAULT_PROFILE); + } else { + user = new User(userId, tenantAwareUsername, null); + } + + return user; + } catch (UserStoreException | IdentityException e) { + String errorMessage = String.format(ERROR_CODE_FAILED_TO_GET_USER_ID.toString(), username); + throw new PushDeviceHandlerServerException(errorMessage, e); + } + } + /** * Handle the device registration for the push notification providers. * @@ -594,7 +690,7 @@ private String getPushProviderName(RegistrationRequestProviderData providerData) String pushProvider = pushSenders.get(0).getProvider(); if (LOG.isDebugEnabled()) { LOG.debug(String.format("Only one push sender is available: %s. " + - "Using it as the push provider.", pushProvider)); + "Using it as the push provider.", pushProvider)); } return pushProvider; } @@ -609,7 +705,7 @@ private String getPushProviderName(RegistrationRequestProviderData providerData) LOG.debug("Error occurred while retrieving the default push notification provider", e); throw new PushDeviceHandlerServerException( "Error occurred while retrieving the default push notification provider.", e); - } + } } /** @@ -673,4 +769,203 @@ public static PushSenderData buildPushSenderData(PushSenderDTO pushSenderDTO) { pushSenderData.setProviderId(pushSenderDTO.getProviderId()); return pushSenderData; } + + /** + * Trigger an email notification informing the user that a new device has + * been registered for their account. + * + * @param device Newly registered device. + * @param tenantDomain Tenant domain of the user. + * @param user Resolved user, used to obtain the username and email claim. + */ + private void triggerEmailNotification(Device device, String tenantDomain, User user) { + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format( + "Sending device registration email notification for the device ID: %s.", + device.getDeviceId())); + } + + String userId = user.getUserID(); + String email = getEmailAddress(user); + if (StringUtils.isBlank(email)) { + if (LOG.isDebugEnabled()) { + LOG.debug("Email address not found for userId: " + userId + + ". Skipping device registration email notification."); + } + return; + } + + HashMap properties = new HashMap<>(); + + properties.put(IdentityEventConstants.EventProperty.USER_NAME, user.getUsername()); + properties.put(IdentityEventConstants.EventProperty.TENANT_DOMAIN, tenantDomain); + properties.put(IdentityEventConstants.EventProperty.USER_STORE_DOMAIN, user.getUserStoreDomain()); + + // Explicit recipient so the handler does not need to re-resolve claims. + properties.put(ARBITRARY_SEND_TO, email); + + // Tell the notification handler which template to render. + properties.put(EMAIL_TEMPLATE_TYPE, PUSH_DEVICE_REGISTRATION_TEMPLATE); + + // Custom placeholders the template can reference. + properties.put(PushDeviceHandlerConstants.EmailNotificationConstants.DEVICE_NAME_PLACEHOLDER, + device.getDeviceName()); + properties.put(PushDeviceHandlerConstants.EmailNotificationConstants.DEVICE_MODEL_PLACEHOLDER, + device.getDeviceModel()); + properties.put(PushDeviceHandlerConstants.EmailNotificationConstants.REGISTRATION_TIME_PLACEHOLDER, + formatRegistrationTime(System.currentTimeMillis())); + String ipAddress = resolveClientIpAddress(); + if (StringUtils.isNotBlank(ipAddress)) { + properties.put(PushDeviceHandlerConstants.EmailNotificationConstants.IP_ADDRESS_PLACEHOLDER, ipAddress); + } + + try { + Event event = new Event(IdentityEventConstants.Event.TRIGGER_NOTIFICATION, properties); + PushDeviceHandlerDataHolder.getInstance().getIdentityEventService().handleEvent(event); + } catch (IdentityEventException e) { + if (LOG.isDebugEnabled()) { + LOG.debug("Error while triggering device registration email " + + "notification for userId: " + userId, e); + } + + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new DiagnosticLog.DiagnosticLogBuilder( + PUSH_DEVICE_HANDLER_SERVICE, TRIGGER_DEVICE_REGISTRATION_EMAIL_NOTIFICATION); + diagnosticLogBuilder + .resultMessage("Error while triggering the device registration email notification.") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED) + .inputParam(PushDeviceHandlerConstants.LogConstants.InputKeys.DEVICE_ID, device.getDeviceId()) + .inputParam(PushDeviceHandlerConstants.LogConstants.InputKeys.USER_ID, userId) + .inputParam(TENANT_DOMAIN, tenantDomain); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + } + } + + /** + * Get the email address from the resolved user. + * + * @param user Resolved user. + * @return Email address, or {@code null} if not available. + */ + private String getEmailAddress(User user) { + + Map userAttributes = user.getAttributes(); + if (MapUtils.isEmpty(userAttributes)) { + return null; + } + return userAttributes.get(NotificationChannels.EMAIL_CHANNEL.getClaimUri()); + } + + /** + * Trigger push notifications to the user's previously registered devices to + * notify that a new device has been registered for the account. + * + * @param registeredDevice Newly registered device. + * @param existingDevices Previously registered devices of the user. + * @param user Resolved user who owns the devices. + * @param tenantDomain Tenant domain of the user. + */ + private void triggerPushNotification(Device registeredDevice, List existingDevices, User user, + String tenantDomain) { + + if (existingDevices == null || existingDevices.isEmpty()) { + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("No previously registered devices found to notify about " + + "the registration of device ID: %s.", + registeredDevice.getDeviceId())); + } + return; + } + + String registrationTime = formatRegistrationTime(System.currentTimeMillis()); + String ipAddress = resolveClientIpAddress(); + + HashMap properties = new HashMap<>(); + + // Standard identity event properties expected by the handler. + properties.put(IdentityEventConstants.EventProperty.USER_ID, registeredDevice.getUserId()); + properties.put(IdentityEventConstants.EventProperty.USER_NAME, user.getUsername()); + properties.put(IdentityEventConstants.EventProperty.USER_STORE_DOMAIN, user.getUserStoreDomain()); + properties.put(IdentityEventConstants.EventProperty.TENANT_DOMAIN, tenantDomain); + properties.put(IdentityEventConstants.EventProperty.NOTIFICATION_CHANNEL, PUSH_NOTIFICATION_CHANNEL); + properties.put(NOTIFICATION_SCENARIO, DEVICE_REGISTRATION_SCENARIO); + + // Details of the newly registered device rendered in the notification. + properties.put(PushDeviceHandlerConstants.PushNotificationConstants.DEVICE_NAME_PLACEHOLDER, + StringUtils.defaultString(registeredDevice.getDeviceName())); + properties.put(PushDeviceHandlerConstants.PushNotificationConstants.DEVICE_MODEL_PLACEHOLDER, + StringUtils.defaultString(registeredDevice.getDeviceModel())); + properties.put(PushDeviceHandlerConstants.PushNotificationConstants.REGISTRATION_TIME_PLACEHOLDER, + registrationTime); + if (StringUtils.isNotBlank(ipAddress)) { + properties.put(IP_ADDRESS, ipAddress); + } + + for (Device device : existingDevices) { + + // The notification is delivered to the previously registered device. + properties.put(NOTIFICATION_PROVIDER, device.getProvider()); + properties.put(DEVICE_TOKEN, device.getDeviceToken()); + properties.put(DEVICE_ID, device.getDeviceId()); + properties.put(DEVICE_HANDLE, device.getDeviceHandle()); + + try { + Event event = new Event(PUSH_NOTIFICATION_EVENT_NAME, properties); + PushDeviceHandlerDataHolder.getInstance().getIdentityEventService().handleEvent(event); + } catch (IdentityEventException e) { + if (LOG.isDebugEnabled()) { + LOG.debug(String.format( + "Error while triggering the device registration push notification to the device ID: %s.", + device.getDeviceId()), e); + } + + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new DiagnosticLog.DiagnosticLogBuilder( + PUSH_DEVICE_HANDLER_SERVICE, TRIGGER_DEVICE_REGISTRATION_PUSH_NOTIFICATION); + diagnosticLogBuilder + .resultMessage("Error while triggering the device registration push notification.") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED) + .inputParam(PushDeviceHandlerConstants.LogConstants.InputKeys.DEVICE_ID, + device.getDeviceId()) + .inputParam(PushDeviceHandlerConstants.LogConstants.InputKeys.USER_ID, + registeredDevice.getUserId()) + .inputParam(TENANT_DOMAIN, tenantDomain); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + } + } + } + + /** + * Resolve the client IP address of the current request. + * + * @return Client IP address, or {@code null} if it cannot be resolved. + */ + private String resolveClientIpAddress() { + + Request request = IdentityContext.getThreadLocalIdentityContext().getRequest(); + if (request == null || StringUtils.isBlank(request.getIpAddress())) { + return null; + } + + return request.getIpAddress(); + } + + /** + * Format a registration timestamp consistently across notification channels. + * + * @param time Registration time. + * @return Human-readable UTC timestamp. + */ + private String formatRegistrationTime(long time) { + + Instant instant = Instant.ofEpochMilli(time); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(REGISTRATION_TIME_FORMATTER_PATTERN) + .withZone(ZoneOffset.UTC); + return formatter.format(instant); + } } diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/impl/PushDefaultConfigResolverImpl.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/impl/PushDefaultConfigResolverImpl.java new file mode 100644 index 0000000..a10e1ae --- /dev/null +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/impl/PushDefaultConfigResolverImpl.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.notification.push.device.handler.impl; + + +import org.wso2.carbon.identity.configuration.mgt.core.DefaultConfigResolver; +import org.wso2.carbon.identity.configuration.mgt.core.model.Resource; +import org.wso2.carbon.identity.configuration.mgt.core.model.ResourceIdentifier; +import org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants; +import org.wso2.carbon.identity.notification.push.device.handler.utils.PushDeviceConfigManager; + +/** + * Default config resolver for push device management configurations. Serves the default + * device management configs when no resource is available in the configuration store. + */ +public class PushDefaultConfigResolverImpl implements DefaultConfigResolver { + + @Override + public ResourceIdentifier getResourceIdentifier() { + + return new ResourceIdentifier( + PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE, + PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_NAME); + } + + @Override + public Resource getDefaultConfigs( + String resourceTypeName, String resourceName) { + + return PushDeviceConfigManager.getDefaultPushDeviceMgtConfigs(); + } +} diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerDataHolder.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerDataHolder.java index 5c7a206..c93c74a 100644 --- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerDataHolder.java +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerDataHolder.java @@ -18,6 +18,8 @@ package org.wso2.carbon.identity.notification.push.device.handler.internal; +import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager; +import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.notification.push.provider.PushProvider; import org.wso2.carbon.identity.notification.sender.tenant.config.NotificationSenderManagementService; import org.wso2.carbon.identity.organization.management.service.OrganizationManager; @@ -36,6 +38,8 @@ public class PushDeviceHandlerDataHolder { private RealmService realmService; private OrganizationManager organizationManager; private NotificationSenderManagementService notificationSenderManagementService; + private ConfigurationManager configurationManager; + private IdentityEventService identityEventService; private PushDeviceHandlerDataHolder() { @@ -138,4 +142,42 @@ public PushProvider getPushProvider(String providerName) { return pushNotificationProviders.get(providerName); } + + /** + * Get the ConfigurationManager. + * + * @return ConfigurationManager instance. + */ + public ConfigurationManager getConfigurationManager() { + + return configurationManager; + } + + /** + * Set the ConfigurationManager. + * + * @param configurationManager ConfigurationManager instance. + */ + public void setConfigurationManager(ConfigurationManager configurationManager) { + + this.configurationManager = configurationManager; + } + + /** + * Get the IdentityEventService. + * @return + */ + public IdentityEventService getIdentityEventService() { + + return identityEventService; + } + + /** + * Set the identityEventService. + * @param identityEventService + */ + public void setIdentityEventService(IdentityEventService identityEventService) { + + this.identityEventService = identityEventService; + } } diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerServiceComponent.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerServiceComponent.java index 6844fd6..8bcd4fd 100644 --- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerServiceComponent.java +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerServiceComponent.java @@ -27,12 +27,16 @@ import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; +import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager; +import org.wso2.carbon.identity.configuration.mgt.core.DefaultConfigResolver; +import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.notification.push.device.handler.DeviceHandlerService; import org.wso2.carbon.identity.notification.push.device.handler.DeviceRegistrationContextManager; import org.wso2.carbon.identity.notification.push.device.handler.dao.DeviceDAO; import org.wso2.carbon.identity.notification.push.device.handler.dao.DeviceDAOImpl; import org.wso2.carbon.identity.notification.push.device.handler.impl.DeviceHandlerServiceImpl; import org.wso2.carbon.identity.notification.push.device.handler.impl.DeviceRegistrationContextManagerImpl; +import org.wso2.carbon.identity.notification.push.device.handler.impl.PushDefaultConfigResolverImpl; import org.wso2.carbon.identity.notification.push.provider.PushProvider; import org.wso2.carbon.identity.notification.sender.tenant.config.NotificationSenderManagementService; import org.wso2.carbon.identity.organization.management.service.OrganizationManager; @@ -59,6 +63,8 @@ protected void activate(ComponentContext context) { new DeviceHandlerServiceImpl(deviceRegistrationContextManager, deviceDAO); context.getBundleContext().registerService( DeviceHandlerService.class.getName(), deviceHandlerService, null); + context.getBundleContext().registerService( + DefaultConfigResolver.class.getName(), new PushDefaultConfigResolverImpl(), null); } catch (Throwable e) { LOG.error("Error occurred while activating Push Device Handler Service Component", e); } @@ -131,4 +137,36 @@ protected void unsetProvider(PushProvider provider) { PushDeviceHandlerDataHolder.getInstance().removePushProvider(provider.getName()); } + + @Reference( + name = "configuration.manager", + service = ConfigurationManager.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetConfigurationManager") + protected void setConfigurationManager(ConfigurationManager configurationManager) { + + PushDeviceHandlerDataHolder.getInstance().setConfigurationManager(configurationManager); + } + + protected void unsetConfigurationManager(ConfigurationManager configurationManager) { + + PushDeviceHandlerDataHolder.getInstance().setConfigurationManager(null); + } + + @Reference( + name = "EventMgtService", + service = org.wso2.carbon.identity.event.services.IdentityEventService.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetIdentityEventService") + protected void setIdentityEventService(IdentityEventService eventService) { + + PushDeviceHandlerDataHolder.getInstance().setIdentityEventService(eventService); + } + + protected void unsetIdentityEventService(IdentityEventService eventService) { + + PushDeviceHandlerDataHolder.getInstance().setIdentityEventService(null); + } } diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/model/DeviceRegistrationNotificationChannelEnum.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/model/DeviceRegistrationNotificationChannelEnum.java new file mode 100644 index 0000000..ab37dec --- /dev/null +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/model/DeviceRegistrationNotificationChannelEnum.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.notification.push.device.handler.model; + +/** + * Channel used to notify the user when a device registration completes. + */ +public enum DeviceRegistrationNotificationChannelEnum { + + EMAIL, + PUSH_NOTIFICATION +} diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/model/PushDeviceMgtConfigData.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/model/PushDeviceMgtConfigData.java new file mode 100644 index 0000000..42e8d08 --- /dev/null +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/model/PushDeviceMgtConfigData.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2023-2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.notification.push.device.handler.model; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; + +/** + * This class represents the push device management configuration. + */ +public class PushDeviceMgtConfigData { + + private Boolean enableMultipleDeviceEnrollment; + private Integer maximumDeviceLimit; + private Boolean enableDeviceRegistrationNotifications; + private Set deviceRegistrationNotificationChannelEnums; + + /** + * Get whether multiple device enrollment is enabled. + * + * @return Whether multiple device enrollment is enabled. + */ + public Boolean getEnableMultipleDeviceEnrollment() { + + return enableMultipleDeviceEnrollment; + } + + /** + * Set whether multiple device enrollment is enabled. + * + * @param enableMultipleDeviceEnrollment Whether multiple device enrollment + * is enabled. + */ + public void setEnableMultipleDeviceEnrollment(Boolean enableMultipleDeviceEnrollment) { + + this.enableMultipleDeviceEnrollment = enableMultipleDeviceEnrollment; + } + + /** + * Get the maximum device limit. + * + * @return Maximum device limit. + */ + public Integer getMaximumDeviceLimit() { + + return maximumDeviceLimit; + } + + /** + * Set the maximum device limit. + * + * @param maximumDeviceLimit Maximum device limit. + */ + public void setMaximumDeviceLimit(Integer maximumDeviceLimit) { + + this.maximumDeviceLimit = maximumDeviceLimit; + } + + /** + * Get whether device registration notifications are enabled. + * + * @return Whether device registration notifications are enabled. + */ + public Boolean getEnableDeviceRegistrationNotifications() { + + return enableDeviceRegistrationNotifications; + } + + /** + * Set whether device registration notifications are enabled. + * + * @param enableDeviceRegistrationNotifications Whether device registration + * notifications are enabled. + */ + public void setEnableDeviceRegistrationNotifications(Boolean enableDeviceRegistrationNotifications) { + + this.enableDeviceRegistrationNotifications = enableDeviceRegistrationNotifications; + } + + /** + * Get the channels used to deliver device registration notifications. + * + * @return Unmodifiable view of the configured channels, or {@code null} + * when the channels have not been set. + */ + public Set getDeviceRegistrationNotificationChannels() { + + if (deviceRegistrationNotificationChannelEnums == null) { + return null; + } + return Collections.unmodifiableSet(deviceRegistrationNotificationChannelEnums); + } + + /** + * Set the channels used to deliver device registration notifications. + * + * @param deviceRegistrationNotificationChannelEnums Notification channels. + */ + public void setDeviceRegistrationNotificationChannels( + Set deviceRegistrationNotificationChannelEnums) { + + if (deviceRegistrationNotificationChannelEnums == null) { + this.deviceRegistrationNotificationChannelEnums = null; + return; + } + this.deviceRegistrationNotificationChannelEnums = deviceRegistrationNotificationChannelEnums.isEmpty() + ? EnumSet.noneOf(DeviceRegistrationNotificationChannelEnum.class) + : EnumSet.copyOf(deviceRegistrationNotificationChannelEnums); + } +} diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/utils/DeviceHandlerAuditLogger.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/utils/DeviceHandlerAuditLogger.java index b3b2d33..602dc3a 100644 --- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/utils/DeviceHandlerAuditLogger.java +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/utils/DeviceHandlerAuditLogger.java @@ -45,7 +45,7 @@ public class DeviceHandlerAuditLogger { */ public void printAuditLog(Operation operation, String deviceId, String userId) { - JSONObject data = createAuditLogEntry(userId); + JSONObject data = createAuditLogEntry(userId, operation, deviceId); buildAuditLog(operation, deviceId, data); } @@ -72,11 +72,18 @@ private void buildAuditLog(Operation operation, String targetId, JSONObject data * * @return Audit log data. */ - private JSONObject createAuditLogEntry(String userId) { + private JSONObject createAuditLogEntry(String userId, Operation operation, String deviceId) { JSONObject data = new JSONObject(); data.put(LogConstants.END_USER_ID, userId != null ? userId : JSONObject.NULL); - data.put(LogConstants.UNREGISTERED_AT, System.currentTimeMillis()); + if (operation.equals(Operation.REGISTER_DEVICE)) { + data.put(LogConstants.REGISTERED_AT, System.currentTimeMillis()); + } else if (operation.equals(Operation.UPDATE_DEVICE_MGT_CONFIG)) { + data.put(LogConstants.UPDATED_AT, System.currentTimeMillis()); + } else { + data.put(LogConstants.UNREGISTERED_AT, System.currentTimeMillis()); + } + data.put(LogConstants.DEVICE_ID, deviceId != null ? deviceId : JSONObject.NULL); return data; } @@ -128,7 +135,9 @@ private String getInitiatorId() { */ public enum Operation { - UNREGISTER_DEVICE("Unregister-Push-Auth-Device"); + UNREGISTER_DEVICE("Unregister-Push-Auth-Device"), + REGISTER_DEVICE("Register-Push-Auth-Device"), + UPDATE_DEVICE_MGT_CONFIG("Update-Push-Auth-Device-Mgt-Config"); private final String logAction; @@ -150,6 +159,9 @@ private static class LogConstants { public static final String TARGET_TYPE_FIELD = "Push-Auth-Device"; public static final String END_USER_ID = "UserId"; + public static final String DEVICE_ID = "DeviceId"; public static final String UNREGISTERED_AT = "UnregisteredAt"; + public static final String REGISTERED_AT = "RegisteredAt"; + public static final String UPDATED_AT = "UpdatedAt"; } } diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/utils/PushDeviceConfigManager.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/utils/PushDeviceConfigManager.java new file mode 100644 index 0000000..b1bb946 --- /dev/null +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/main/java/org/wso2/carbon/identity/notification/push/device/handler/utils/PushDeviceConfigManager.java @@ -0,0 +1,391 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.notification.push.device.handler.utils; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.context.CarbonContext; +import org.wso2.carbon.identity.base.IdentityException; +import org.wso2.carbon.identity.configuration.mgt.core.constant.ConfigurationConstants; +import org.wso2.carbon.identity.configuration.mgt.core.exception.ConfigurationManagementException; +import org.wso2.carbon.identity.configuration.mgt.core.model.Attribute; +import org.wso2.carbon.identity.configuration.mgt.core.model.Resource; +import org.wso2.carbon.identity.configuration.mgt.core.model.ResourceTypeAdd; +import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants; +import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerClientException; +import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerException; +import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerServerException; +import org.wso2.carbon.identity.notification.push.device.handler.internal.PushDeviceHandlerDataHolder; +import org.wso2.carbon.identity.notification.push.device.handler.model.DeviceRegistrationNotificationChannelEnum; +import org.wso2.carbon.identity.notification.push.device.handler.model.PushDeviceMgtConfigData; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.wso2.carbon.identity.configuration.mgt.core.constant.ConfigurationConstants.ErrorMessages.ERROR_CODE_RESOURCE_DOES_NOT_EXISTS; +import static org.wso2.carbon.identity.configuration.mgt.core.constant.ConfigurationConstants.ErrorMessages.ERROR_CODE_RESOURCE_TYPE_DOES_NOT_EXISTS; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.DEFAULT_MAX_DEVICE_LIMIT_PER_USER; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.DEFAULT_MIN_DEVICE_LIMIT_PER_USER; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_DEVICE_LIMIT_PER_USER_EXCEEDS; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_INVALID_DEVICE_LIMIT_CONFIG; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_INVALID_DEVICE_LIMIT_VALUE; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_UPDATING_PUSH_DEVICE_CONFIG; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.MAX_DEVICE_LIMIT_PER_USER; + +/** + * Push device config manager. + */ +public class PushDeviceConfigManager { + + private static final Log LOG = LogFactory.getLog(PushDeviceConfigManager.class); + private static final DeviceHandlerAuditLogger AUDIT_LOGGER = new DeviceHandlerAuditLogger(); + + /** + * Get the maximum device limit per user. + * + * @return maximum device limit per user. + */ + private static int getMaxDeviceLimitPerUser() { + + String configValue = IdentityUtil.getProperty(MAX_DEVICE_LIMIT_PER_USER); + if (StringUtils.isNotBlank(configValue)) { + try { + return Integer.parseInt(configValue.trim()); + } catch (NumberFormatException e) { + if (LOG.isDebugEnabled()) { + LOG.debug("Invalid value configured for '" + MAX_DEVICE_LIMIT_PER_USER + "': " + + configValue + ". Using default: " + DEFAULT_MAX_DEVICE_LIMIT_PER_USER); + } + } + } + return DEFAULT_MAX_DEVICE_LIMIT_PER_USER; + } + + /** + * Get the push device configuration resource for the given tenant. + * + * @param tenantDomain Tenant domain. + * @return Push device configuration resource. + * @throws PushDeviceHandlerServerException If an error occurs while retrieving the config. + */ + public static PushDeviceMgtConfigData getPushDeviceConfig(String tenantDomain) + throws PushDeviceHandlerServerException { + + Resource resource = getResource(tenantDomain, true); + if (resource == null || resource.getAttributes() == null || resource.getAttributes().isEmpty()) { + resource = getDefaultPushDeviceMgtConfigs(); + } + + return buildDTOFromResource(resource); + } + + /** + * Retrieve the configuration resource for the given tenant domain. + * + * @param tenantDomain Tenant domain. + * @param resolve Whether to resolve the resource from the parent organization hierarchy. + * @return Configuration resource, or null if not found. + * @throws PushDeviceHandlerServerException If an error occurs while retrieving the config. + */ + private static Resource getResource(String tenantDomain, boolean resolve) throws PushDeviceHandlerServerException { + + Resource resource = null; + + try { + resource = PushDeviceHandlerDataHolder.getInstance().getConfigurationManager() + .getResource(PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE, + PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_NAME, resolve); + } catch (ConfigurationManagementException e) { + if (!ERROR_CODE_RESOURCE_TYPE_DOES_NOT_EXISTS.getCode().equals(e.getErrorCode()) + && !ERROR_CODE_RESOURCE_DOES_NOT_EXISTS.getCode().equals(e.getErrorCode())) { + throw new PushDeviceHandlerServerException(ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.getCode(), + ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.getMessage(), e); + } + } + return resource; + } + + /** + * Update the push device configuration for the given tenant. + * + * @param config Push device management configuration to persist. + * @param tenantDomain Tenant domain. + * @throws PushDeviceHandlerException If an error occurs while updating the config. + */ + public static PushDeviceMgtConfigData updatePushDeviceConfig(PushDeviceMgtConfigData config, String tenantDomain) + throws PushDeviceHandlerException { + + boolean multipleDeviceEnrollment = Boolean.TRUE.equals(config.getEnableMultipleDeviceEnrollment()); + Integer maximumDeviceLimit = config.getMaximumDeviceLimit(); + + if (multipleDeviceEnrollment) { + if (maximumDeviceLimit == null || maximumDeviceLimit < 1) { + throw new PushDeviceHandlerClientException(ERROR_CODE_INVALID_DEVICE_LIMIT_VALUE.getCode(), + ERROR_CODE_INVALID_DEVICE_LIMIT_VALUE.getMessage()); + } + int upperBound = getMaxDeviceLimitPerUser(); + if (maximumDeviceLimit > upperBound) { + throw new PushDeviceHandlerClientException(ERROR_CODE_DEVICE_LIMIT_PER_USER_EXCEEDS.getCode(), + ERROR_CODE_DEVICE_LIMIT_PER_USER_EXCEEDS.getMessage() + + String.format(" Configured upper bound: %s", upperBound)); + } + } else if (maximumDeviceLimit != null && maximumDeviceLimit > 1) { + throw new PushDeviceHandlerClientException(ERROR_CODE_INVALID_DEVICE_LIMIT_CONFIG.getCode(), + ERROR_CODE_INVALID_DEVICE_LIMIT_CONFIG.getMessage()); + } + + Resource resource = buildResourceFromConfig(config); + Resource existingResource = getResource(tenantDomain, false); + + if (existingResource != null && existingResource.getAttributes() != null + && !existingResource.getAttributes().isEmpty()) { + replaceResource(resource); + } else { + addResource(resource); + } + + AUDIT_LOGGER.printAuditLog( + DeviceHandlerAuditLogger.Operation.UPDATE_DEVICE_MGT_CONFIG, + tenantDomain, + CarbonContext.getThreadLocalCarbonContext().getUserId() + ); + + return config; + } + + /** + * Replace an existing configuration resource for the given tenant domain. + * + * @param resource Resource to replace. + * @throws PushDeviceHandlerException If an error occurs while replacing the resource. + */ + private static void replaceResource(Resource resource) throws PushDeviceHandlerServerException { + + try { + PushDeviceHandlerDataHolder.getInstance().getConfigurationManager() + .replaceResource(PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE, resource); + } catch (ConfigurationManagementException e) { + throw new PushDeviceHandlerServerException(ERROR_CODE_UPDATING_PUSH_DEVICE_CONFIG.getCode(), + ERROR_CODE_UPDATING_PUSH_DEVICE_CONFIG.getMessage(), e); + } + } + + /** + * Build a configuration resource from the given DTO. + * + * @param config Push device management configuration. + * @return Configuration resource. + */ + private static Resource buildResourceFromConfig(PushDeviceMgtConfigData config) { + + Resource resource = new Resource(); + resource.setResourceName(PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_NAME); + + List attributes = new ArrayList<>(); + attributes.add(new Attribute(PushDeviceHandlerConstants.ATTR_ENABLE_MULTIPLE_DEVICE_ENROLLMENT, + String.valueOf(Boolean.TRUE.equals(config.getEnableMultipleDeviceEnrollment())))); + + int maximumDeviceLimit = config.getMaximumDeviceLimit() != null ? config.getMaximumDeviceLimit() : + DEFAULT_MIN_DEVICE_LIMIT_PER_USER; + attributes.add(new Attribute(PushDeviceHandlerConstants.ATTR_MAX_DEVICE_LIMIT, + String.valueOf(maximumDeviceLimit))); + attributes.add(new Attribute(PushDeviceHandlerConstants.ATTR_ENABLE_DEVICE_REGISTRATION_NOTIFICATIONS, + String.valueOf(Boolean.TRUE.equals(config.getEnableDeviceRegistrationNotifications())))); + attributes.add(new Attribute(PushDeviceHandlerConstants.ATTR_DEVICE_REGISTRATION_NOTIFICATION_CHANNELS, + serializeChannels(config.getDeviceRegistrationNotificationChannels()))); + + resource.setAttributes(attributes); + return resource; + } + + /** + * Build a default configuration resource. + * + * @return Default configuration resource. + */ + public static Resource getDefaultPushDeviceMgtConfigs() { + + Resource resource = new Resource(PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_NAME, + PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE); + + List attributeList = new ArrayList<>(); + attributeList.add(new Attribute(PushDeviceHandlerConstants.ATTR_ENABLE_MULTIPLE_DEVICE_ENROLLMENT, "false")); + attributeList.add(new Attribute(PushDeviceHandlerConstants.ATTR_MAX_DEVICE_LIMIT, "2")); + attributeList.add( + new Attribute(PushDeviceHandlerConstants.ATTR_ENABLE_DEVICE_REGISTRATION_NOTIFICATIONS, "false")); + attributeList.add( + new Attribute(PushDeviceHandlerConstants.ATTR_DEVICE_REGISTRATION_NOTIFICATION_CHANNELS, + DeviceRegistrationNotificationChannelEnum.EMAIL.name())); + + resource.setAttributes(attributeList); + return resource; + } + + /** + * Add a configuration resource for the given tenant domain. + * + * @param resource Resource to add. + * @return Added resource. + * @throws IdentityException If an error occurs while adding the resource. + */ + private static Resource addResource(Resource resource) throws PushDeviceHandlerServerException { + + try { + return PushDeviceHandlerDataHolder.getInstance().getConfigurationManager() + .addResource(PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE, resource); + } catch (ConfigurationManagementException e) { + if (ConfigurationConstants.ErrorMessages.ERROR_CODE_RESOURCE_TYPE_DOES_NOT_EXISTS.getCode() + .equals(e.getErrorCode())) { + + /* If the insert is failing due to the relevant resource-type is not existing in the database, + create the resource-type and retry the configuration addition. */ + createResourceType(); + return addResource(resource); + } + throw new PushDeviceHandlerServerException(ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getCode(), + ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getMessage(), e); + } + } + + /** + * Create the resource type for push authentication settings. + * + * @throws IdentityException If an error occurs while creating the resource + * type. + */ + private static void createResourceType() throws PushDeviceHandlerServerException { + + try { + ResourceTypeAdd resourceTypeAdd = new ResourceTypeAdd(); + resourceTypeAdd.setName(PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE); + resourceTypeAdd.setDescription("Resource Type for " + + PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE); + PushDeviceHandlerDataHolder.getInstance().getConfigurationManager().addResourceType(resourceTypeAdd); + } catch (ConfigurationManagementException e) { + if (LOG.isDebugEnabled()) { + LOG.debug("Error while creating resource type: " + + PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE, e); + } + throw new PushDeviceHandlerServerException(ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getCode(), + ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getMessage(), e); + } + } + + /** + * Build a configuration DTO from the given resource. + * + * @param resource Configuration resource. + * @return Configuration resource or default if resource is empty. + */ + private static PushDeviceMgtConfigData buildDTOFromResource(Resource resource) { + + PushDeviceMgtConfigData config = new PushDeviceMgtConfigData(); + resource.getAttributes().forEach(attribute -> { + if (PushDeviceHandlerConstants.ATTR_ENABLE_MULTIPLE_DEVICE_ENROLLMENT.equals(attribute.getKey())) { + config.setEnableMultipleDeviceEnrollment(Boolean.parseBoolean(attribute.getValue())); + } else if (PushDeviceHandlerConstants.ATTR_MAX_DEVICE_LIMIT.equals(attribute.getKey())) { + config.setMaximumDeviceLimit(parseMaxDeviceLimit(attribute.getValue())); + } else if (PushDeviceHandlerConstants.ATTR_ENABLE_DEVICE_REGISTRATION_NOTIFICATIONS + .equals(attribute.getKey())) { + config.setEnableDeviceRegistrationNotifications(Boolean.parseBoolean(attribute.getValue())); + } else if (PushDeviceHandlerConstants.ATTR_DEVICE_REGISTRATION_NOTIFICATION_CHANNELS + .equals(attribute.getKey())) { + config.setDeviceRegistrationNotificationChannels(parseNotificationChannels(attribute.getValue())); + } + }); + + return config; + } + + /** + * Parse a stored maximum device limit value into an integer. + * + * @param value Stored attribute value. + * @return Parsed maximum device limit, defaulting to 1. + */ + private static int parseMaxDeviceLimit(String value) { + + if (StringUtils.isNotBlank(value)) { + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + if (LOG.isDebugEnabled()) { + LOG.debug("Invalid value stored for '" + PushDeviceHandlerConstants.ATTR_MAX_DEVICE_LIMIT + + "': " + value + ". Using default:" + DEFAULT_MIN_DEVICE_LIMIT_PER_USER + ".", e); + } + } + } + return DEFAULT_MIN_DEVICE_LIMIT_PER_USER; + } + + /** + * Serialize the configured notification channels into a comma-separated string. + * + * @param channels Channels to serialize. + * @return Comma-separated channel names, or an empty string when none are set. + */ + private static String serializeChannels(Set channels) { + + if (channels == null || channels.isEmpty()) { + return ""; + } + return channels.stream() + .map(Enum::name) + .collect(Collectors.joining(PushDeviceHandlerConstants.NOTIFICATION_CHANNELS_SEPARATOR)); + } + + /** + * Parse a stored notification channels value into the corresponding enum set. + * Unknown tokens are skipped and a blank value yields an empty set. + * + * @param value Stored attribute value. + * @return Parsed channels. + */ + private static Set parseNotificationChannels(String value) { + + if (StringUtils.isBlank(value)) { + return Collections.emptySet(); + } + Set channels = + EnumSet.noneOf(DeviceRegistrationNotificationChannelEnum.class); + for (String token : value.split(PushDeviceHandlerConstants.NOTIFICATION_CHANNELS_SEPARATOR)) { + String trimmed = token.trim(); + if (trimmed.isEmpty()) { + continue; + } + try { + channels.add(DeviceRegistrationNotificationChannelEnum.valueOf(trimmed.toUpperCase(Locale.ENGLISH))); + } catch (IllegalArgumentException e) { + if (LOG.isDebugEnabled()) { + LOG.debug("Unknown device registration notification channel: " + token, e); + } + } + } + return channels; + } +} diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/constant/PushDeviceHandlerConstantsTest.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/constant/PushDeviceHandlerConstantsTest.java new file mode 100644 index 0000000..ef5758e --- /dev/null +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/constant/PushDeviceHandlerConstantsTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.notification.push.device.handler.constant; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG; + +/** + * Unit tests for PushDeviceHandlerConstants — covering newly added error codes. + */ +public class PushDeviceHandlerConstantsTest { + + @Test + public void testErrorCodeGettingPushDeviceConfig_HasCorrectCode() { + + Assert.assertEquals(ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.getCode(), "PDH-15014"); + } + + @Test + public void testErrorCodeGettingPushDeviceConfig_HasNonEmptyMessage() { + + Assert.assertNotNull(ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.getMessage()); + Assert.assertFalse(ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.getMessage().isEmpty()); + } + + @Test + public void testErrorCodeAddingPushDeviceConfig_HasCorrectCode() { + + Assert.assertEquals(ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getCode(), "PDH-15015"); + } + + @Test + public void testErrorCodeAddingPushDeviceConfig_HasNonEmptyMessage() { + + Assert.assertNotNull(ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getMessage()); + Assert.assertFalse(ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getMessage().isEmpty()); + } + + @Test + public void testErrorCodes_ToString_ContainsCodeAndMessage() { + + String gettingConfigStr = ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.toString(); + Assert.assertTrue(gettingConfigStr.contains("PDH-15014"), + "toString must include the error code."); + Assert.assertTrue(gettingConfigStr.contains(ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.getMessage()), + "toString must include the error message."); + + String addingConfigStr = ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.toString(); + Assert.assertTrue(addingConfigStr.contains("PDH-15015"), + "toString must include the error code."); + Assert.assertTrue(addingConfigStr.contains(ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getMessage()), + "toString must include the error message."); + } + + @Test + public void testErrorCodes_AreDistinct() { + + Assert.assertNotEquals(ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.getCode(), + ERROR_CODE_ADDING_PUSH_DEVICE_CONFIG.getCode(), + "The two new error codes must have different code values."); + } +} diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAOImplTest.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAOImplTest.java index 8376fbe..0e3fa29 100644 --- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAOImplTest.java +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/dao/DeviceDAOImplTest.java @@ -219,6 +219,133 @@ public void testGetDeviceByUserId() throws Exception { } } + @Test + public void testGetDevicesByUserId() throws Exception { + + String userId = "user"; + int tenantId = 1; + Device device = new Device("id", "did", "name", "model", "token", "handle", "provider", "key"); + + try (MockedStatic mockedDbUtil = Mockito.mockStatic(IdentityDatabaseUtil.class)) { + Connection connection = Mockito.mock(Connection.class); + NamedPreparedStatement statement = Mockito.mock(NamedPreparedStatement.class); + ResultSet resultSet = Mockito.mock(ResultSet.class); + + mockedDbUtil.when(() -> IdentityDatabaseUtil.getDBConnection(true)).thenReturn(connection); + Mockito.when(connection.prepareStatement(Mockito.anyString())).thenReturn(statement); + Mockito.when(statement.executeQuery()).thenReturn(resultSet); + // Two devices then end of result set. + Mockito.when(resultSet.next()).thenReturn(true, true, false); + Mockito.when(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.ID)) + .thenReturn(device.getDeviceId()); + Mockito.when(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.USER_ID)) + .thenReturn(device.getUserId()); + Mockito.when(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.DEVICE_NAME)) + .thenReturn(device.getDeviceName()); + Mockito.when(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.DEVICE_MODEL)) + .thenReturn(device.getDeviceModel()); + Mockito.when(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.DEVICE_TOKEN)) + .thenReturn(device.getDeviceToken()); + Mockito.when(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.DEVICE_HANDLE)) + .thenReturn(device.getDeviceHandle()); + Mockito.when(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.PUBLIC_KEY)) + .thenReturn(device.getPublicKey()); + Mockito.when(resultSet.getString(PushDeviceHandlerConstants.ColumnNames.PROVIDER)) + .thenReturn(device.getProvider()); + + java.util.List result = deviceDAO.getDevicesByUserId(userId, tenantId); + + Assert.assertEquals(result.size(), 2); + Assert.assertEquals(result.get(0).getDeviceId(), device.getDeviceId()); + } + } + + @Test + public void testGetDevicesByUserIdEmpty() throws Exception { + + String userId = "user"; + int tenantId = 1; + + try (MockedStatic mockedDbUtil = Mockito.mockStatic(IdentityDatabaseUtil.class)) { + Connection connection = Mockito.mock(Connection.class); + NamedPreparedStatement statement = Mockito.mock(NamedPreparedStatement.class); + ResultSet resultSet = Mockito.mock(ResultSet.class); + + mockedDbUtil.when(() -> IdentityDatabaseUtil.getDBConnection(true)).thenReturn(connection); + Mockito.when(connection.prepareStatement(Mockito.anyString())).thenReturn(statement); + Mockito.when(statement.executeQuery()).thenReturn(resultSet); + Mockito.when(resultSet.next()).thenReturn(false); + + java.util.List result = deviceDAO.getDevicesByUserId(userId, tenantId); + + Assert.assertTrue(result.isEmpty()); + } + } + + @Test + public void testGetDeviceNotFound() throws Exception { + + String deviceId = "id"; + + try (MockedStatic mockedDbUtil = Mockito.mockStatic(IdentityDatabaseUtil.class)) { + Connection connection = Mockito.mock(Connection.class); + NamedPreparedStatement statement = Mockito.mock(NamedPreparedStatement.class); + ResultSet resultSet = Mockito.mock(ResultSet.class); + + mockedDbUtil.when(() -> IdentityDatabaseUtil.getDBConnection(true)).thenReturn(connection); + Mockito.when(connection.prepareStatement(Mockito.anyString())).thenReturn(statement); + Mockito.when(statement.executeQuery()).thenReturn(resultSet); + Mockito.when(resultSet.next()).thenReturn(false); + + Optional result = deviceDAO.getDevice(deviceId); + + Assert.assertFalse(result.isPresent()); + } + } + + @Test + public void testGetDeviceByUserIdNotFound() throws Exception { + + String userId = "user"; + int tenantId = 1; + + try (MockedStatic mockedDbUtil = Mockito.mockStatic(IdentityDatabaseUtil.class)) { + Connection connection = Mockito.mock(Connection.class); + NamedPreparedStatement statement = Mockito.mock(NamedPreparedStatement.class); + ResultSet resultSet = Mockito.mock(ResultSet.class); + + mockedDbUtil.when(() -> IdentityDatabaseUtil.getDBConnection(true)).thenReturn(connection); + Mockito.when(connection.prepareStatement(Mockito.anyString())).thenReturn(statement); + Mockito.when(statement.executeQuery()).thenReturn(resultSet); + Mockito.when(resultSet.next()).thenReturn(false); + + Optional result = deviceDAO.getDeviceByUserId(userId, tenantId); + + Assert.assertFalse(result.isPresent()); + } + } + + @Test + public void testGetPublicKeyNotFound() throws Exception { + + String deviceId = "id"; + + try (MockedStatic mockedDbUtil = Mockito.mockStatic(IdentityDatabaseUtil.class)) { + Connection connection = Mockito.mock(Connection.class); + NamedPreparedStatement statement = Mockito.mock(NamedPreparedStatement.class); + ResultSet resultSet = Mockito.mock(ResultSet.class); + + mockedDbUtil.when(() -> IdentityDatabaseUtil.getDBConnection(true)).thenReturn(connection); + Mockito.when(connection.prepareStatement(Mockito.anyString())).thenReturn(statement); + Mockito.when(statement.executeQuery()).thenReturn(resultSet); + Mockito.when(resultSet.next()).thenReturn(false); + + Optional result = deviceDAO.getPublicKey(deviceId); + + Assert.assertFalse(result.isPresent()); + } + } + @Test public void testGetPublicKey() throws Exception { diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/impl/DeviceHandlerServiceImplTest.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/impl/DeviceHandlerServiceImplTest.java index ed63812..99bf7f0 100644 --- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/impl/DeviceHandlerServiceImplTest.java +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/impl/DeviceHandlerServiceImplTest.java @@ -29,12 +29,19 @@ import org.testng.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; -import org.testng.annotations.BeforeTest; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.wso2.carbon.context.CarbonContext; import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; +import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager; +import org.wso2.carbon.identity.core.context.IdentityContext; +import org.wso2.carbon.identity.core.context.model.Header; +import org.wso2.carbon.identity.core.context.model.Request; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.event.IdentityEventException; +import org.wso2.carbon.identity.event.services.IdentityEventService; +import org.wso2.carbon.identity.governance.service.notification.NotificationChannels; import org.wso2.carbon.identity.notification.push.device.handler.DeviceRegistrationContextManager; import org.wso2.carbon.identity.notification.push.device.handler.dao.DeviceDAO; import org.wso2.carbon.identity.notification.push.device.handler.exception.PushDeviceHandlerClientException; @@ -60,6 +67,7 @@ import org.wso2.carbon.user.core.UserRealm; import org.wso2.carbon.user.core.UserStoreException; import org.wso2.carbon.user.core.common.AbstractUserStoreManager; +import org.wso2.carbon.user.core.common.User; import org.wso2.carbon.user.core.util.UserCoreUtil; import org.wso2.carbon.utils.multitenancy.MultitenantUtils; @@ -189,7 +197,7 @@ public static void tearDownClass() { } } - @BeforeTest + @BeforeMethod void setUp() { MockitoAnnotations.openMocks(this); @@ -248,6 +256,8 @@ public void testRegisterDevice() PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); mockedPushDeviceHandlerDataHolder.when( PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); NotificationSenderManagementService notificationSenderManagementService = mock(NotificationSenderManagementService.class); @@ -340,6 +350,8 @@ public void testRegisterDeviceWithUserIdBlank() throws UserStoreException { try ( MockedStatic mockedIdentityTenantUtil = Mockito.mockStatic(IdentityTenantUtil.class); + MockedStatic mockedPushDeviceHandlerDataHolder = + Mockito.mockStatic(PushDeviceHandlerDataHolder.class); ) { mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234); @@ -350,6 +362,12 @@ public void testRegisterDeviceWithUserIdBlank() throws UserStoreException { when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn(null); + PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); + mockedPushDeviceHandlerDataHolder.when( + PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); + Assert.assertThrows(PushDeviceHandlerServerException.class, () -> { deviceHandlerService.registerDevice(registrationRequest, "carbon.super"); }); @@ -369,6 +387,8 @@ public void testRegisterDeviceWithExistingDevice() throws UserStoreException, Pu try ( MockedStatic mockedIdentityTenantUtil = Mockito.mockStatic(IdentityTenantUtil.class); + MockedStatic mockedPushDeviceHandlerDataHolder = + Mockito.mockStatic(PushDeviceHandlerDataHolder.class); ) { mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234); @@ -379,8 +399,14 @@ public void testRegisterDeviceWithExistingDevice() throws UserStoreException, Pu when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId"); - Optional device = Optional.of(new Device()); - when(deviceDAO.getDeviceByUserId(anyString(), anyInt())).thenReturn(device); + PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); + mockedPushDeviceHandlerDataHolder.when( + PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); + + when(deviceDAO.getDevicesByUserId(anyString(), anyInt())) + .thenReturn(java.util.Collections.singletonList(new Device())); Assert.assertThrows(PushDeviceHandlerClientException.class, () -> { deviceHandlerService.registerDevice(registrationRequest, "carbon.super"); @@ -466,6 +492,44 @@ public void testGetDeviceByUserIdFailure() throws PushDeviceHandlerException { } } + @Test + public void testGetDevicesByUserId() throws PushDeviceHandlerException { + + try ( + MockedStatic mockedIdentityTenantUtil = + Mockito.mockStatic(IdentityTenantUtil.class); + ) { + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234); + + Device deviceObj = new Device(); + deviceObj.setDeviceId("1234567890"); + List deviceList = new ArrayList<>(); + deviceList.add(deviceObj); + when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(deviceList); + + List result = deviceHandlerService.getDevicesByUserId("testUserId", "carbon.super"); + Assert.assertNotNull(result); + Assert.assertEquals(result.size(), 1); + } + } + + @Test + public void testGetDevicesByUserIdWhenNoDevices() throws PushDeviceHandlerException { + + try ( + MockedStatic mockedIdentityTenantUtil = + Mockito.mockStatic(IdentityTenantUtil.class); + ) { + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234); + when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(new ArrayList<>()); + + // No registered devices is a valid result and must return an empty list, not throw. + List result = deviceHandlerService.getDevicesByUserId("testUserId", "carbon.super"); + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + } + } + @Test public void testUnregisterDevice() throws PushDeviceHandlerException, NotificationSenderManagementException, PushProviderException { @@ -1097,6 +1161,8 @@ public void testRegisterDevicePushProviderClientException() PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); mockedPushDeviceHandlerDataHolder.when( PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); NotificationSenderManagementService notificationSenderManagementService = mock(NotificationSenderManagementService.class); @@ -1131,6 +1197,170 @@ public void testRegisterDevicePushProviderClientException() } } + @Test + public void testRegisterDeviceWithAlreadyRegisteredDeviceId() + throws Exception { + + RegistrationRequest registrationRequest = createRegistrationRequest(); + DeviceRegistrationContext deviceRegistrationContext = createDeviceRegistrationContext(); + when(deviceRegistrationContextManager.getContext(anyString(), anyString())) + .thenReturn(deviceRegistrationContext); + + try ( + MockedStatic mockedIdentityTenantUtil = + Mockito.mockStatic(IdentityTenantUtil.class); + MockedStatic mockedPushDeviceHandlerDataHolder = + Mockito.mockStatic(PushDeviceHandlerDataHolder.class); + ) { + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234); + UserRealm userRealm = mock(UserRealm.class); + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getRealm(anyString(), anyString())) + .thenReturn(userRealm); + AbstractUserStoreManager abstractUserStoreManager = mock(AbstractUserStoreManager.class); + when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); + when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId"); + + PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); + mockedPushDeviceHandlerDataHolder.when( + PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); + + // Multiple device enrollment disabled, so a user that already has a registered device + // must not be allowed to register another one. + org.wso2.carbon.identity.configuration.mgt.core.model.Resource resource = + new org.wso2.carbon.identity.configuration.mgt.core.model.Resource(); + List attributes = new ArrayList<>(); + attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute( + "enableMultipleDeviceEnrollment", "false")); + attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute( + "maximumDeviceLimit", "1")); + resource.setAttributes(attributes); + when(configManager.getResource(anyString(), anyString(), anyBoolean())).thenReturn(resource); + + when(deviceDAO.getDevicesByUserId(anyString(), anyInt())) + .thenReturn(java.util.Collections.singletonList(new Device())); + + Assert.assertThrows(PushDeviceHandlerClientException.class, + () -> deviceHandlerService.registerDevice(registrationRequest, "carbon.super")); + } + } + + @Test + public void testRegisterDeviceWhenMultiDeviceEnabledAndLimitReached() + throws Exception { + + RegistrationRequest registrationRequest = createRegistrationRequest(); + DeviceRegistrationContext deviceRegistrationContext = createDeviceRegistrationContext(); + when(deviceRegistrationContextManager.getContext(anyString(), anyString())) + .thenReturn(deviceRegistrationContext); + + try ( + MockedStatic mockedIdentityTenantUtil = + Mockito.mockStatic(IdentityTenantUtil.class); + MockedStatic mockedPushDeviceHandlerDataHolder = + Mockito.mockStatic(PushDeviceHandlerDataHolder.class); + ) { + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234); + UserRealm userRealm = mock(UserRealm.class); + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getRealm(anyString(), anyString())) + .thenReturn(userRealm); + AbstractUserStoreManager abstractUserStoreManager = mock(AbstractUserStoreManager.class); + when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); + when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId"); + + PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); + mockedPushDeviceHandlerDataHolder.when( + PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); + + org.wso2.carbon.identity.configuration.mgt.core.model.Resource resource = + new org.wso2.carbon.identity.configuration.mgt.core.model.Resource(); + List attributes = new ArrayList<>(); + attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute( + "enableMultipleDeviceEnrollment", "true")); + attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute( + "maximumDeviceLimit", "2")); + resource.setAttributes(attributes); + when(configManager.getResource(anyString(), anyString(), anyBoolean())).thenReturn(resource); + + when(deviceDAO.getDevicesByUserId(anyString(), anyInt())) + .thenReturn(java.util.Arrays.asList(new Device(), new Device())); + + Assert.assertThrows(PushDeviceHandlerClientException.class, + () -> deviceHandlerService.registerDevice(registrationRequest, "carbon.super")); + } + } + + @Test + public void testRegisterDeviceWhenMultiDeviceEnabledAndUnderLimit() + throws Exception { + + RegistrationRequest registrationRequest = createRegistrationRequest(); + DeviceRegistrationContext deviceRegistrationContext = createDeviceRegistrationContext(); + when(deviceRegistrationContextManager.getContext(anyString(), anyString())) + .thenReturn(deviceRegistrationContext); + + try ( + MockedStatic mockedIdentityTenantUtil = + Mockito.mockStatic(IdentityTenantUtil.class); + MockedStatic mockedPushDeviceHandlerDataHolder = + Mockito.mockStatic(PushDeviceHandlerDataHolder.class); + ) { + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234); + UserRealm userRealm = mock(UserRealm.class); + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getRealm(anyString(), anyString())) + .thenReturn(userRealm); + AbstractUserStoreManager abstractUserStoreManager = mock(AbstractUserStoreManager.class); + when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); + when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId"); + + PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); + mockedPushDeviceHandlerDataHolder.when( + PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); + + org.wso2.carbon.identity.configuration.mgt.core.model.Resource resource = + new org.wso2.carbon.identity.configuration.mgt.core.model.Resource(); + List attributes = new ArrayList<>(); + attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute( + "enableMultipleDeviceEnrollment", "true")); + attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute( + "maximumDeviceLimit", "5")); + resource.setAttributes(attributes); + when(configManager.getResource(anyString(), anyString(), anyBoolean())).thenReturn(resource); + + when(deviceDAO.getDevicesByUserId(anyString(), anyInt())) + .thenReturn(java.util.Collections.singletonList(new Device())); + + NotificationSenderManagementService notificationSenderManagementService = + mock(NotificationSenderManagementService.class); + when(pushDeviceHandlerDataHolder.getNotificationSenderManagementService()) + .thenReturn(notificationSenderManagementService); + PushSenderDTO pushSenderDTO = new PushSenderDTO(); + pushSenderDTO.setName("FCM_PushPublisher"); + pushSenderDTO.setProvider("FCM"); + pushSenderDTO.setProviderId("fcm-provider-id"); + List pushSenders = new ArrayList<>(); + pushSenders.add(pushSenderDTO); + when(notificationSenderManagementService.getPushSenders(anyBoolean())) + .thenReturn(pushSenders); + + FCMPushProvider fcmPushProvider = mock(FCMPushProvider.class); + when(pushDeviceHandlerDataHolder.getPushProvider(anyString())).thenReturn(fcmPushProvider); + when(fcmPushProvider.getName()).thenReturn("FCM"); + doNothing().when(fcmPushProvider).registerDevice(any(), any()); + doNothing().when(deviceDAO).registerDevice(any(), anyInt()); + + Device registeredDevice = deviceHandlerService.registerDevice(registrationRequest, "carbon.super"); + + Assert.assertNotNull(registeredDevice); + Assert.assertTrue(deviceRegistrationContext.isRegistered()); + } + } + private DeviceRegistrationContext createDeviceRegistrationContext() { return new DeviceRegistrationContext( @@ -1179,10 +1409,13 @@ public void testRegisterDeviceWithExplicitProviderData() when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId"); when(deviceDAO.getDeviceByUserId(anyString(), anyInt())).thenReturn(Optional.empty()); + when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(new ArrayList<>()); PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); mockedPushDeviceHandlerDataHolder.when( PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); NotificationSenderManagementService notificationSenderManagementService = mock(NotificationSenderManagementService.class); @@ -1244,6 +1477,8 @@ public void testRegisterDeviceWhenNotificationSenderServiceFails() throws Except PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); mockedPushDeviceHandlerDataHolder.when(PushDeviceHandlerDataHolder::getInstance) .thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); NotificationSenderManagementService notificationSenderManagementService = mock(NotificationSenderManagementService.class); @@ -1303,6 +1538,8 @@ public void testRegisterDeviceWhenNoPushSenderFoundForProvider() throws Exceptio PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); mockedPushDeviceHandlerDataHolder.when(PushDeviceHandlerDataHolder::getInstance) .thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); NotificationSenderManagementService notificationSenderManagementService = mock(NotificationSenderManagementService.class); @@ -1360,6 +1597,8 @@ public void testRegisterDeviceWhenPushSenderDoesNotMatchProvider() throws Except PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); mockedPushDeviceHandlerDataHolder.when(PushDeviceHandlerDataHolder::getInstance) .thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); NotificationSenderManagementService notificationSenderManagementService = mock(NotificationSenderManagementService.class); @@ -1424,10 +1663,13 @@ public void testRegisterDeviceWithoutProviderData() when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId"); when(deviceDAO.getDeviceByUserId(anyString(), anyInt())).thenReturn(Optional.empty()); + when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(new ArrayList<>()); PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); mockedPushDeviceHandlerDataHolder.when( PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); NotificationSenderManagementService notificationSenderManagementService = mock(NotificationSenderManagementService.class); @@ -1503,6 +1745,8 @@ public void testRegisterDeviceFailsWithNoProviderAndNoDefault() throws UserStore PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); mockedPushDeviceHandlerDataHolder.when( PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); NotificationSenderManagementService notificationSenderManagementService = mock(NotificationSenderManagementService.class); @@ -1559,6 +1803,8 @@ public void testRegisterDeviceFailsWhenDefaultProviderRetrievalFails() throws Us PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); mockedPushDeviceHandlerDataHolder.when( PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); NotificationSenderManagementService notificationSenderManagementService = mock(NotificationSenderManagementService.class); @@ -1611,10 +1857,13 @@ public void testRegisterDeviceWithBlankProviderNameFallsBackToDefault() when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId"); when(deviceDAO.getDeviceByUserId(anyString(), anyInt())).thenReturn(Optional.empty()); + when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(new ArrayList<>()); PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); mockedPushDeviceHandlerDataHolder.when( PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); NotificationSenderManagementService notificationSenderManagementService = mock(NotificationSenderManagementService.class); @@ -1688,6 +1937,8 @@ public void testRegisterDeviceWithConfigMissingDefaultKeyFallsBackToSenderCheck( PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); mockedPushDeviceHandlerDataHolder.when( PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); NotificationSenderManagementService notificationSenderManagementService = mock(NotificationSenderManagementService.class); @@ -1747,6 +1998,8 @@ public void testRegisterDeviceWithNoProviderAndNoDefaultSetsOnlyAvailableSenderA PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); mockedPushDeviceHandlerDataHolder.when( PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); NotificationSenderManagementService notificationSenderManagementService = mock(NotificationSenderManagementService.class); @@ -1756,6 +2009,7 @@ public void testRegisterDeviceWithNoProviderAndNoDefaultSetsOnlyAvailableSenderA // Return empty config to simulate no default provider configured when(notificationSenderManagementService.getNotificationSenderConfigurations(anyString(), anyBoolean())) .thenReturn(new HashMap<>()); + when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(new ArrayList<>()); // Mock single push sender available PushSenderDTO pushSenderDTO = new PushSenderDTO(); @@ -1819,6 +2073,8 @@ public void testRegisterDeviceWithNoProviderAndNoDefaultFailsWhenNoPushSendersAv PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); mockedPushDeviceHandlerDataHolder.when( PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); NotificationSenderManagementService notificationSenderManagementService = mock(NotificationSenderManagementService.class); @@ -1828,6 +2084,7 @@ public void testRegisterDeviceWithNoProviderAndNoDefaultFailsWhenNoPushSendersAv // Return empty config to simulate no default provider configured when(notificationSenderManagementService.getNotificationSenderConfigurations(anyString(), anyBoolean())) .thenReturn(new HashMap<>()); + when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(new ArrayList<>()); // Return empty list of push senders when(notificationSenderManagementService.getPushSenders(anyBoolean())) @@ -1875,6 +2132,8 @@ public void testRegisterDeviceWithNoProviderAndNoDefaultFailsWhenMultiplePushSen PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); mockedPushDeviceHandlerDataHolder.when( PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); NotificationSenderManagementService notificationSenderManagementService = mock(NotificationSenderManagementService.class); @@ -1884,6 +2143,7 @@ public void testRegisterDeviceWithNoProviderAndNoDefaultFailsWhenMultiplePushSen // Return empty config to simulate no default provider configured when(notificationSenderManagementService.getNotificationSenderConfigurations(anyString(), anyBoolean())) .thenReturn(new HashMap<>()); + when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(new ArrayList<>()); // Return multiple push senders PushSenderDTO pushSenderDTO1 = new PushSenderDTO(); @@ -1898,12 +2158,330 @@ public void testRegisterDeviceWithNoProviderAndNoDefaultFailsWhenMultiplePushSen when(notificationSenderManagementService.getPushSenders(anyBoolean())) .thenReturn(pushSenders); - Assert.assertThrows(PushDeviceHandlerServerException.class, () -> { - deviceHandlerService.registerDevice(registrationRequest, "carbon.super"); - }); + Assert.assertThrows(PushDeviceHandlerServerException.class, + () -> deviceHandlerService.registerDevice(registrationRequest, "carbon.super")); + } + } + + @Test + public void testRegisterDeviceTriggersRegistrationNotificationsWhenEnabled() throws Exception { + + RegistrationRequest registrationRequest = createRegistrationRequest(); + DeviceRegistrationContext deviceRegistrationContext = createDeviceRegistrationContext(); + when(deviceRegistrationContextManager.getContext(anyString(), anyString())) + .thenReturn(deviceRegistrationContext); + + try ( + MockedStatic mockedIdentityTenantUtil = + Mockito.mockStatic(IdentityTenantUtil.class); + MockedStatic mockedPushDeviceHandlerDataHolder = + Mockito.mockStatic(PushDeviceHandlerDataHolder.class); + MockedStatic mockedIdentityContext = + Mockito.mockStatic(IdentityContext.class); + ) { + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234); + UserRealm userRealm = mock(UserRealm.class); + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getRealm(anyString(), anyString())) + .thenReturn(userRealm); + AbstractUserStoreManager abstractUserStoreManager = mock(AbstractUserStoreManager.class); + when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); + when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId"); + + User user = mock(User.class); + when(user.getUserID()).thenReturn("testUserId"); + when(user.getUsername()).thenReturn("testUser"); + when(user.getUserStoreDomain()).thenReturn("PRIMARY"); + Map userAttributes = new HashMap<>(); + userAttributes.put(NotificationChannels.EMAIL_CHANNEL.getClaimUri(), "testuser@wso2.com"); + when(user.getAttributes()).thenReturn(userAttributes); + when(abstractUserStoreManager.getUserWithID(anyString(), any(String[].class), anyString())) + .thenReturn(user); + + PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); + mockedPushDeviceHandlerDataHolder.when( + PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); + when(configManager.getResource(anyString(), anyString(), anyBoolean())) + .thenReturn(createDeviceMgtConfigResource("true", "5", "true", "EMAIL,PUSH_NOTIFICATION")); + + Device existingDevice = new Device(); + existingDevice.setDeviceId("existing-device-id"); + existingDevice.setProvider("FCM"); + existingDevice.setDeviceToken("existing-device-token"); + existingDevice.setDeviceHandle("existing-device-handle"); + when(deviceDAO.getDevicesByUserId(anyString(), anyInt())) + .thenReturn(java.util.Collections.singletonList(existingDevice)); + + mockPushSenderAndProvider(pushDeviceHandlerDataHolder); + doNothing().when(deviceDAO).registerDevice(any(), anyInt()); + + IdentityEventService identityEventService = mock(IdentityEventService.class); + when(pushDeviceHandlerDataHolder.getIdentityEventService()).thenReturn(identityEventService); + doNothing().when(identityEventService).handleEvent(any()); + + IdentityContext identityContext = mock(IdentityContext.class); + mockedIdentityContext.when(IdentityContext::getThreadLocalIdentityContext) + .thenReturn(identityContext); + Request request = mock(Request.class); + List
headers = new ArrayList<>(); + headers.add(new Header("User-Agent", java.util.Collections.singletonList("test-agent"))); + headers.add(new Header("X-Forwarded-For", + java.util.Collections.singletonList("10.0.0.1, 192.168.1.1"))); + when(request.getHeaders()).thenReturn(headers); + when(identityContext.getRequest()).thenReturn(request); + + Device registeredDevice = deviceHandlerService.registerDevice(registrationRequest, "carbon.super"); + + Assert.assertNotNull(registeredDevice); + Assert.assertTrue(deviceRegistrationContext.isRegistered()); + // One email notification plus one push notification to the existing device. + verify(identityEventService, times(2)).handleEvent(any()); + } + } + + @Test + public void testRegisterDeviceSkipsNotificationsWhenEmailClaimNotAvailable() throws Exception { + + RegistrationRequest registrationRequest = createRegistrationRequest(); + DeviceRegistrationContext deviceRegistrationContext = createDeviceRegistrationContext(); + when(deviceRegistrationContextManager.getContext(anyString(), anyString())) + .thenReturn(deviceRegistrationContext); + + try ( + MockedStatic mockedIdentityTenantUtil = + Mockito.mockStatic(IdentityTenantUtil.class); + MockedStatic mockedPushDeviceHandlerDataHolder = + Mockito.mockStatic(PushDeviceHandlerDataHolder.class); + ) { + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234); + UserRealm userRealm = mock(UserRealm.class); + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getRealm(anyString(), anyString())) + .thenReturn(userRealm); + AbstractUserStoreManager abstractUserStoreManager = mock(AbstractUserStoreManager.class); + when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); + when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId"); + // The resolved user has no email claim, so the email notification must be + // skipped and registration must still succeed. + User user = mock(User.class); + when(user.getUserID()).thenReturn("testUserId"); + when(user.getUsername()).thenReturn("testUser"); + when(user.getUserStoreDomain()).thenReturn("PRIMARY"); + when(user.getAttributes()).thenReturn(new HashMap<>()); + when(abstractUserStoreManager.getUserWithID(anyString(), any(String[].class), anyString())) + .thenReturn(user); + + PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); + mockedPushDeviceHandlerDataHolder.when( + PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); + when(configManager.getResource(anyString(), anyString(), anyBoolean())) + .thenReturn(createDeviceMgtConfigResource("true", "5", "true", "EMAIL,PUSH_NOTIFICATION")); + + // No previously registered devices, so the push notification is skipped too. + when(deviceDAO.getDevicesByUserId(anyString(), anyInt())).thenReturn(new ArrayList<>()); + + mockPushSenderAndProvider(pushDeviceHandlerDataHolder); + doNothing().when(deviceDAO).registerDevice(any(), anyInt()); + + IdentityEventService identityEventService = mock(IdentityEventService.class); + when(pushDeviceHandlerDataHolder.getIdentityEventService()).thenReturn(identityEventService); + + Device registeredDevice = deviceHandlerService.registerDevice(registrationRequest, "carbon.super"); + + Assert.assertNotNull(registeredDevice); + Assert.assertTrue(deviceRegistrationContext.isRegistered()); + verify(identityEventService, times(0)).handleEvent(any()); + } + } + + @Test + public void testRegisterDeviceSucceedsWhenNotificationDeliveryFails() throws Exception { + + RegistrationRequest registrationRequest = createRegistrationRequest(); + DeviceRegistrationContext deviceRegistrationContext = createDeviceRegistrationContext(); + when(deviceRegistrationContextManager.getContext(anyString(), anyString())) + .thenReturn(deviceRegistrationContext); + + try ( + MockedStatic mockedIdentityTenantUtil = + Mockito.mockStatic(IdentityTenantUtil.class); + MockedStatic mockedPushDeviceHandlerDataHolder = + Mockito.mockStatic(PushDeviceHandlerDataHolder.class); + MockedStatic mockedIdentityContext = + Mockito.mockStatic(IdentityContext.class); + ) { + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234); + UserRealm userRealm = mock(UserRealm.class); + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getRealm(anyString(), anyString())) + .thenReturn(userRealm); + AbstractUserStoreManager abstractUserStoreManager = mock(AbstractUserStoreManager.class); + when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); + when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId"); + + User user = mock(User.class); + when(user.getUserID()).thenReturn("testUserId"); + when(user.getUsername()).thenReturn("testUser"); + when(user.getUserStoreDomain()).thenReturn("PRIMARY"); + Map userAttributes = new HashMap<>(); + userAttributes.put(NotificationChannels.EMAIL_CHANNEL.getClaimUri(), "testuser@wso2.com"); + when(user.getAttributes()).thenReturn(userAttributes); + when(abstractUserStoreManager.getUserWithID(anyString(), any(String[].class), anyString())) + .thenReturn(user); + + PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); + mockedPushDeviceHandlerDataHolder.when( + PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); + when(configManager.getResource(anyString(), anyString(), anyBoolean())) + .thenReturn(createDeviceMgtConfigResource("true", "5", "true", "EMAIL,PUSH_NOTIFICATION")); + + Device existingDevice = new Device(); + existingDevice.setDeviceId("existing-device-id"); + when(deviceDAO.getDevicesByUserId(anyString(), anyInt())) + .thenReturn(java.util.Collections.singletonList(existingDevice)); + + mockPushSenderAndProvider(pushDeviceHandlerDataHolder); + doNothing().when(deviceDAO).registerDevice(any(), anyInt()); + + IdentityEventService identityEventService = mock(IdentityEventService.class); + when(pushDeviceHandlerDataHolder.getIdentityEventService()).thenReturn(identityEventService); + doThrow(new IdentityEventException("Notification delivery failed.")) + .when(identityEventService).handleEvent(any()); + + // No inbound request captured in the identity context; the IP address + // cannot be resolved and the notification is sent without it. + IdentityContext identityContext = mock(IdentityContext.class); + mockedIdentityContext.when(IdentityContext::getThreadLocalIdentityContext) + .thenReturn(identityContext); + when(identityContext.getRequest()).thenReturn(null); + + Device registeredDevice = deviceHandlerService.registerDevice(registrationRequest, "carbon.super"); + + Assert.assertNotNull(registeredDevice); + Assert.assertTrue(deviceRegistrationContext.isRegistered()); + verify(identityEventService, times(2)).handleEvent(any()); + } + } + + @Test + public void testRegisterDeviceEmitsDiagnosticLogsWhenNotificationDeliveryFails() throws Exception { + + RegistrationRequest registrationRequest = createRegistrationRequest(); + DeviceRegistrationContext deviceRegistrationContext = createDeviceRegistrationContext(); + when(deviceRegistrationContextManager.getContext(anyString(), anyString())) + .thenReturn(deviceRegistrationContext); + + try ( + MockedStatic mockedIdentityTenantUtil = + Mockito.mockStatic(IdentityTenantUtil.class); + MockedStatic mockedPushDeviceHandlerDataHolder = + Mockito.mockStatic(PushDeviceHandlerDataHolder.class); + MockedStatic mockedIdentityContext = + Mockito.mockStatic(IdentityContext.class); + ) { + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(-1234); + UserRealm userRealm = mock(UserRealm.class); + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getRealm(anyString(), anyString())) + .thenReturn(userRealm); + AbstractUserStoreManager abstractUserStoreManager = mock(AbstractUserStoreManager.class); + when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); + when(abstractUserStoreManager.getUserIDFromUserName(anyString())).thenReturn("testUserId"); + + // Enable diagnostic logs so the notification failure paths emit diagnostic log events. + mockedLoggerUtils.when(LoggerUtils::isDiagnosticLogsEnabled).thenReturn(true); + + User user = mock(User.class); + when(user.getUserID()).thenReturn("testUserId"); + when(user.getUsername()).thenReturn("testUser"); + when(user.getUserStoreDomain()).thenReturn("PRIMARY"); + Map userAttributes = new HashMap<>(); + userAttributes.put(NotificationChannels.EMAIL_CHANNEL.getClaimUri(), "testuser@wso2.com"); + when(user.getAttributes()).thenReturn(userAttributes); + when(abstractUserStoreManager.getUserWithID(anyString(), any(String[].class), anyString())) + .thenReturn(user); + + PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder = mock(PushDeviceHandlerDataHolder.class); + mockedPushDeviceHandlerDataHolder.when( + PushDeviceHandlerDataHolder::getInstance).thenReturn(pushDeviceHandlerDataHolder); + ConfigurationManager configManager = mock(ConfigurationManager.class); + when(pushDeviceHandlerDataHolder.getConfigurationManager()).thenReturn(configManager); + when(configManager.getResource(anyString(), anyString(), anyBoolean())) + .thenReturn(createDeviceMgtConfigResource("true", "5", "true", "EMAIL,PUSH_NOTIFICATION")); + + Device existingDevice = new Device(); + existingDevice.setDeviceId("existing-device-id"); + when(deviceDAO.getDevicesByUserId(anyString(), anyInt())) + .thenReturn(java.util.Collections.singletonList(existingDevice)); + + mockPushSenderAndProvider(pushDeviceHandlerDataHolder); + doNothing().when(deviceDAO).registerDevice(any(), anyInt()); + + IdentityEventService identityEventService = mock(IdentityEventService.class); + when(pushDeviceHandlerDataHolder.getIdentityEventService()).thenReturn(identityEventService); + doThrow(new IdentityEventException("Notification delivery failed.")) + .when(identityEventService).handleEvent(any()); + + // An inbound request is captured in the identity context so the IP address + // input parameter is included in the emitted diagnostic logs. + IdentityContext identityContext = mock(IdentityContext.class); + mockedIdentityContext.when(IdentityContext::getThreadLocalIdentityContext) + .thenReturn(identityContext); + org.wso2.carbon.identity.core.context.model.Request request = + mock(org.wso2.carbon.identity.core.context.model.Request.class); + when(request.getIpAddress()).thenReturn("10.0.0.1"); + when(identityContext.getRequest()).thenReturn(request); + + Device registeredDevice = deviceHandlerService.registerDevice(registrationRequest, "carbon.super"); + + Assert.assertNotNull(registeredDevice); + Assert.assertTrue(deviceRegistrationContext.isRegistered()); + verify(identityEventService, times(2)).handleEvent(any()); } } + private void mockPushSenderAndProvider(PushDeviceHandlerDataHolder pushDeviceHandlerDataHolder) + throws NotificationSenderManagementException, PushProviderException { + + NotificationSenderManagementService notificationSenderManagementService = + mock(NotificationSenderManagementService.class); + when(pushDeviceHandlerDataHolder.getNotificationSenderManagementService()) + .thenReturn(notificationSenderManagementService); + PushSenderDTO pushSenderDTO = new PushSenderDTO(); + pushSenderDTO.setName("FCM_PushPublisher"); + pushSenderDTO.setProvider("FCM"); + pushSenderDTO.setProviderId("fcm-provider-id"); + List pushSenders = new ArrayList<>(); + pushSenders.add(pushSenderDTO); + when(notificationSenderManagementService.getPushSenders(anyBoolean())).thenReturn(pushSenders); + + FCMPushProvider fcmPushProvider = mock(FCMPushProvider.class); + when(pushDeviceHandlerDataHolder.getPushProvider(anyString())).thenReturn(fcmPushProvider); + when(fcmPushProvider.getName()).thenReturn("FCM"); + doNothing().when(fcmPushProvider).registerDevice(any(), any()); + } + + private org.wso2.carbon.identity.configuration.mgt.core.model.Resource createDeviceMgtConfigResource( + String enableMultipleDeviceEnrollment, String maximumDeviceLimit, + String enableNotifications, String notificationChannels) { + + org.wso2.carbon.identity.configuration.mgt.core.model.Resource resource = + new org.wso2.carbon.identity.configuration.mgt.core.model.Resource(); + List attributes = new ArrayList<>(); + attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute( + "enableMultipleDeviceEnrollment", enableMultipleDeviceEnrollment)); + attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute( + "maximumDeviceLimit", maximumDeviceLimit)); + attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute( + "enableDeviceRegistrationNotifications", enableNotifications)); + attributes.add(new org.wso2.carbon.identity.configuration.mgt.core.model.Attribute( + "deviceRegistrationNotificationChannels", notificationChannels)); + resource.setAttributes(attributes); + return resource; + } + private RegistrationRequest createRegistrationRequest() { RegistrationRequest registrationRequest = new RegistrationRequest(); diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/impl/PushDefaultConfigResolverImplTest.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/impl/PushDefaultConfigResolverImplTest.java new file mode 100644 index 0000000..73eb6f3 --- /dev/null +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/impl/PushDefaultConfigResolverImplTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.notification.push.device.handler.impl; + +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.configuration.mgt.core.model.Attribute; +import org.wso2.carbon.identity.configuration.mgt.core.model.Resource; +import org.wso2.carbon.identity.configuration.mgt.core.model.ResourceIdentifier; +import org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants; +import org.wso2.carbon.identity.notification.push.device.handler.model.DeviceRegistrationNotificationChannelEnum; + +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Unit tests for PushDefaultConfigResolverImpl. + */ +public class PushDefaultConfigResolverImplTest { + + private PushDefaultConfigResolverImpl pushDefaultConfigResolver; + + @BeforeMethod + public void setUp() { + + pushDefaultConfigResolver = new PushDefaultConfigResolverImpl(); + } + + @Test + public void testGetResourceIdentifier_ReturnsExpectedTypeAndName() { + + ResourceIdentifier identifier = pushDefaultConfigResolver.getResourceIdentifier(); + + Assert.assertNotNull(identifier, "ResourceIdentifier should not be null."); + Assert.assertEquals(identifier.getResourceType(), PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE); + Assert.assertEquals(identifier.getResourceName(), PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_NAME); + } + + @Test + public void testGetDefaultConfigs_ReturnsDefaultPushDeviceMgtConfigs() { + + Resource resource = pushDefaultConfigResolver.getDefaultConfigs( + PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE, + PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_NAME); + + Assert.assertNotNull(resource, "Default config resource should not be null."); + Assert.assertEquals(resource.getResourceName(), PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_NAME); + Assert.assertEquals(resource.getResourceType(), PushDeviceHandlerConstants.PUSH_DEVICE_MGT_RESOURCE_TYPE); + + Map attributes = resource.getAttributes().stream() + .collect(Collectors.toMap(Attribute::getKey, Attribute::getValue)); + Assert.assertEquals(attributes.get(PushDeviceHandlerConstants.ATTR_ENABLE_MULTIPLE_DEVICE_ENROLLMENT), "false"); + Assert.assertEquals(attributes.get(PushDeviceHandlerConstants.ATTR_MAX_DEVICE_LIMIT), "2"); + Assert.assertEquals( + attributes.get(PushDeviceHandlerConstants.ATTR_ENABLE_DEVICE_REGISTRATION_NOTIFICATIONS), + "false"); + Assert.assertEquals( + attributes.get(PushDeviceHandlerConstants.ATTR_DEVICE_REGISTRATION_NOTIFICATION_CHANNELS), + DeviceRegistrationNotificationChannelEnum.EMAIL.name()); + } +} diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerDataHolderTest.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerDataHolderTest.java new file mode 100644 index 0000000..1f7d6fa --- /dev/null +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerDataHolderTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.notification.push.device.handler.internal; + +import org.testng.Assert; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager; + +import static org.mockito.Mockito.mock; + +/** + * Unit tests for PushDeviceHandlerDataHolder. + */ +public class PushDeviceHandlerDataHolderTest { + + private PushDeviceHandlerDataHolder dataHolder; + + @BeforeMethod + public void setUp() { + + dataHolder = PushDeviceHandlerDataHolder.getInstance(); + dataHolder.setConfigurationManager(null); + } + + @AfterMethod + public void tearDown() { + + dataHolder.setConfigurationManager(null); + } + + @Test + public void testGetInstance_ReturnsSingleton() { + + PushDeviceHandlerDataHolder instance1 = PushDeviceHandlerDataHolder.getInstance(); + PushDeviceHandlerDataHolder instance2 = PushDeviceHandlerDataHolder.getInstance(); + Assert.assertNotNull(instance1); + Assert.assertSame(instance1, instance2, "getInstance must always return the same singleton."); + } + + @Test + public void testSetAndGetConfigurationManager_StoresAndRetrievesCorrectly() { + + ConfigurationManager mockManager = mock(ConfigurationManager.class); + + dataHolder.setConfigurationManager(mockManager); + ConfigurationManager result = dataHolder.getConfigurationManager(); + + Assert.assertSame(result, mockManager, "getConfigurationManager must return the same instance that was set."); + } + + @Test + public void testGetConfigurationManager_ReturnsNull_WhenNotSet() { + + ConfigurationManager result = dataHolder.getConfigurationManager(); + + Assert.assertNull(result, "getConfigurationManager must return null when no manager has been set."); + } + + @Test + public void testSetConfigurationManager_AllowsNull() { + + ConfigurationManager mockManager = mock(ConfigurationManager.class); + dataHolder.setConfigurationManager(mockManager); + + dataHolder.setConfigurationManager(null); + + Assert.assertNull(dataHolder.getConfigurationManager(), + "setConfigurationManager(null) must clear the stored manager."); + } + + @Test + public void testSetConfigurationManager_AllowsReplacement() { + + ConfigurationManager firstManager = mock(ConfigurationManager.class); + ConfigurationManager secondManager = mock(ConfigurationManager.class); + + dataHolder.setConfigurationManager(firstManager); + dataHolder.setConfigurationManager(secondManager); + + Assert.assertSame(dataHolder.getConfigurationManager(), secondManager, + "Second manager must replace the first one."); + } +} diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerServiceComponentTest.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerServiceComponentTest.java new file mode 100644 index 0000000..0d98889 --- /dev/null +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/internal/PushDeviceHandlerServiceComponentTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.notification.push.device.handler.internal; + +import org.mockito.MockedStatic; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; + +/** + * Unit tests for PushDeviceHandlerServiceComponent — covering the ConfigurationManager OSGi lifecycle methods. + */ +public class PushDeviceHandlerServiceComponentTest { + + private PushDeviceHandlerServiceComponent serviceComponent; + private PushDeviceHandlerDataHolder mockDataHolderInstance; + private MockedStatic mockedDataHolder; + + @BeforeMethod + public void setUp() { + + serviceComponent = new PushDeviceHandlerServiceComponent(); + mockDataHolderInstance = mock(PushDeviceHandlerDataHolder.class); + mockedDataHolder = mockStatic(PushDeviceHandlerDataHolder.class); + mockedDataHolder.when(PushDeviceHandlerDataHolder::getInstance).thenReturn(mockDataHolderInstance); + } + + @AfterMethod + public void tearDown() { + + mockedDataHolder.close(); + } + + @Test + public void testSetConfigurationManager_DelegatesToDataHolder() { + + ConfigurationManager mockManager = mock(ConfigurationManager.class); + + serviceComponent.setConfigurationManager(mockManager); + + verify(mockDataHolderInstance).setConfigurationManager(mockManager); + } + + @Test + public void testUnsetConfigurationManager_ClearsDataHolderEntry() { + + ConfigurationManager mockManager = mock(ConfigurationManager.class); + + serviceComponent.unsetConfigurationManager(mockManager); + + verify(mockDataHolderInstance).setConfigurationManager(null); + } + + @Test + public void testSetThenUnsetConfigurationManager_LeavesDataHolderNull() { + + ConfigurationManager mockManager = mock(ConfigurationManager.class); + + serviceComponent.setConfigurationManager(mockManager); + serviceComponent.unsetConfigurationManager(mockManager); + + verify(mockDataHolderInstance).setConfigurationManager(mockManager); + verify(mockDataHolderInstance).setConfigurationManager(null); + } +} diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/utils/DeviceHandlerAuditLoggerTest.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/utils/DeviceHandlerAuditLoggerTest.java index 863fd22..5cac41a 100644 --- a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/utils/DeviceHandlerAuditLoggerTest.java +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/utils/DeviceHandlerAuditLoggerTest.java @@ -124,21 +124,63 @@ public void testGetUserWithSystemUser() throws Exception { } /** - * Test the private method 'createAuditLogEntry' with valid data. + * Test the private method 'createAuditLogEntry' for UNREGISTER_DEVICE operation. */ @Test - public void testCreateAuditLogEntryWithValidData() throws Exception { + public void testCreateAuditLogEntryForUnregisterDevice() throws Exception { Method createAuditLogEntryMethod = DeviceHandlerAuditLogger.class - .getDeclaredMethod("createAuditLogEntry", String.class); + .getDeclaredMethod("createAuditLogEntry", String.class, DeviceHandlerAuditLogger.Operation.class, + String.class); createAuditLogEntryMethod.setAccessible(true); - JSONObject result = (JSONObject) createAuditLogEntryMethod.invoke(auditLogger, "testUserId"); + JSONObject result = (JSONObject) createAuditLogEntryMethod.invoke( + auditLogger, "testUserId", DeviceHandlerAuditLogger.Operation.UNREGISTER_DEVICE, "testDeviceId"); Assert.assertNotNull(result); Assert.assertTrue(result.has("UserId")); Assert.assertEquals(result.getString("UserId"), "testUserId"); Assert.assertTrue(result.has("UnregisteredAt")); Assert.assertTrue(result.getLong("UnregisteredAt") > 0); + Assert.assertFalse(result.has("RegisteredAt")); + } + + /** + * Test the private method 'createAuditLogEntry' for REGISTER_DEVICE operation. + */ + @Test + public void testCreateAuditLogEntryForRegisterDevice() throws Exception { + + Method createAuditLogEntryMethod = DeviceHandlerAuditLogger.class + .getDeclaredMethod("createAuditLogEntry", String.class, DeviceHandlerAuditLogger.Operation.class, + String.class); + createAuditLogEntryMethod.setAccessible(true); + JSONObject result = (JSONObject) createAuditLogEntryMethod.invoke( + auditLogger, "testUserId", DeviceHandlerAuditLogger.Operation.REGISTER_DEVICE, "testDeviceId"); + + Assert.assertNotNull(result); + Assert.assertTrue(result.has("UserId")); + Assert.assertEquals(result.getString("UserId"), "testUserId"); + Assert.assertTrue(result.has("RegisteredAt")); + Assert.assertTrue(result.getLong("RegisteredAt") > 0); + Assert.assertFalse(result.has("UnregisteredAt")); + } + + /** + * Test the private method 'createAuditLogEntry' with a null userId. + */ + @Test + public void testCreateAuditLogEntryWithNullUserId() throws Exception { + + Method createAuditLogEntryMethod = DeviceHandlerAuditLogger.class + .getDeclaredMethod("createAuditLogEntry", String.class, DeviceHandlerAuditLogger.Operation.class, + String.class); + createAuditLogEntryMethod.setAccessible(true); + JSONObject result = (JSONObject) createAuditLogEntryMethod.invoke( + auditLogger, (Object) null, DeviceHandlerAuditLogger.Operation.UNREGISTER_DEVICE, "testDeviceId"); + + Assert.assertNotNull(result); + Assert.assertTrue(result.has("UserId")); + Assert.assertTrue(result.isNull("UserId")); } /** @@ -188,4 +230,50 @@ public void testGetInitiatorIdSystemUser() throws Exception { String result = (String) getInitiatorIdMethod.invoke(auditLogger); Assert.assertEquals(result, LoggerUtils.Initiator.System.name()); } + + /** + * Test the private method 'getInitiatorId' falls back to masked content when the resolved initiator is blank + * and the user is not the system user. + */ + @Test + public void testGetInitiatorIdMaskedContent() throws Exception { + + when(carbonContext.getUsername()).thenReturn("testUser"); + when(carbonContext.getTenantDomain()).thenReturn("carbon.super"); + mockedUserCoreUtil.when(() -> UserCoreUtil.addTenantDomainToEntry("testUser", "carbon.super")) + .thenReturn("testUser@carbon.super"); + mockedMultitenantUtils.when(() -> MultitenantUtils.getTenantAwareUsername("testUser@carbon.super")) + .thenReturn("testUser"); + mockedMultitenantUtils.when(() -> MultitenantUtils.getTenantDomain("testUser@carbon.super")) + .thenReturn("carbon.super"); + mockedIdentityUtil.when(() -> IdentityUtil.getInitiatorId("testUser", "carbon.super")) + .thenReturn(null); + + Method getInitiatorIdMethod = DeviceHandlerAuditLogger.class.getDeclaredMethod("getInitiatorId"); + getInitiatorIdMethod.setAccessible(true); + String result = (String) getInitiatorIdMethod.invoke(auditLogger); + Assert.assertEquals(result, "masked-content"); + } + + /** + * Test the private method 'createAuditLogEntry' for UPDATE_DEVICE_MGT_CONFIG operation and a null deviceId. + */ + @Test + public void testCreateAuditLogEntryForUpdateConfigWithNullDeviceId() throws Exception { + + Method createAuditLogEntryMethod = DeviceHandlerAuditLogger.class + .getDeclaredMethod("createAuditLogEntry", String.class, DeviceHandlerAuditLogger.Operation.class, + String.class); + createAuditLogEntryMethod.setAccessible(true); + JSONObject result = (JSONObject) createAuditLogEntryMethod.invoke( + auditLogger, "testUserId", DeviceHandlerAuditLogger.Operation.UPDATE_DEVICE_MGT_CONFIG, null); + + Assert.assertNotNull(result); + Assert.assertTrue(result.has("UpdatedAt")); + Assert.assertTrue(result.getLong("UpdatedAt") > 0); + Assert.assertTrue(result.has("DeviceId")); + Assert.assertTrue(result.isNull("DeviceId")); + Assert.assertFalse(result.has("RegisteredAt")); + Assert.assertFalse(result.has("UnregisteredAt")); + } } diff --git a/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/utils/PushDeviceConfigManagerTest.java b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/utils/PushDeviceConfigManagerTest.java new file mode 100644 index 0000000..d7cb8a3 --- /dev/null +++ b/components/org.wso2.carbon.identity.notification.push.device.handler/src/test/java/org/wso2/carbon/identity/notification/push/device/handler/utils/PushDeviceConfigManagerTest.java @@ -0,0 +1,615 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.notification.push.device.handler.utils; + +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; +import org.testng.Assert; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.base.IdentityException; +import org.wso2.carbon.identity.configuration.mgt.core.ConfigurationManager; +import org.wso2.carbon.identity.configuration.mgt.core.exception.ConfigurationManagementException; +import org.wso2.carbon.identity.configuration.mgt.core.model.Attribute; +import org.wso2.carbon.identity.configuration.mgt.core.model.Resource; +import org.wso2.carbon.identity.configuration.mgt.core.model.ResourceTypeAdd; +import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.notification.push.device.handler.internal.PushDeviceHandlerDataHolder; +import org.wso2.carbon.identity.notification.push.device.handler.model.DeviceRegistrationNotificationChannelEnum; +import org.wso2.carbon.identity.notification.push.device.handler.model.PushDeviceMgtConfigData; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.wso2.carbon.identity.configuration.mgt.core.constant.ConfigurationConstants.ErrorMessages.ERROR_CODE_RESOURCE_DOES_NOT_EXISTS; +import static org.wso2.carbon.identity.configuration.mgt.core.constant.ConfigurationConstants.ErrorMessages.ERROR_CODE_RESOURCE_TYPE_DOES_NOT_EXISTS; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.DEFAULT_MAX_DEVICE_LIMIT_PER_USER; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_DEVICE_LIMIT_PER_USER_EXCEEDS; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_INVALID_DEVICE_LIMIT_CONFIG; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_INVALID_DEVICE_LIMIT_VALUE; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.ErrorMessages.ERROR_CODE_UPDATING_PUSH_DEVICE_CONFIG; +import static org.wso2.carbon.identity.notification.push.device.handler.constant.PushDeviceHandlerConstants.MAX_DEVICE_LIMIT_PER_USER; + +/** + * Unit tests for PushDeviceConfigManager. + */ +public class PushDeviceConfigManagerTest { + + private static final String TENANT_DOMAIN = "carbon.super"; + private static final String RESOURCE_TYPE = "DEVICE_MANAGEMENT"; + private static final String RESOURCE_NAME = "PUSH_DEVICE_MANAGEMENT"; + private static final String ATTR_ENABLE_DEVICE_MANAGEMENT = "enableMultipleDeviceEnrollment"; + private static final String ATTR_MAX_DEVICE_LIMIT = "maximumDeviceLimit"; + private static final String DEFAULT_ENABLE_DEVICE_MANAGEMENT = "false"; + private static final String DEFAULT_MAX_DEVICE_LIMIT = "2"; + + @Mock + private ConfigurationManager configurationManager; + + private MockedStatic mockedDataHolder; + private MockedStatic mockedIdentityUtil; + private PushDeviceHandlerDataHolder dataHolderInstance; + + @BeforeMethod + public void setUp() { + + MockitoAnnotations.openMocks(this); + dataHolderInstance = mock(PushDeviceHandlerDataHolder.class); + mockedDataHolder = mockStatic(PushDeviceHandlerDataHolder.class); + mockedDataHolder.when(PushDeviceHandlerDataHolder::getInstance).thenReturn(dataHolderInstance); + when(dataHolderInstance.getConfigurationManager()).thenReturn(configurationManager); + mockedIdentityUtil = mockStatic(IdentityUtil.class); + mockedIdentityUtil.when(() -> IdentityUtil.getProperty(MAX_DEVICE_LIMIT_PER_USER)) + .thenReturn(null); + } + + @AfterMethod + public void tearDown() { + + mockedDataHolder.close(); + mockedIdentityUtil.close(); + } + + @Test + public void testGetPushDeviceConfig_WhenResourceExists_ReturnsConfigData() throws Exception { + + Resource resource = createResourceWithAttributes("true", "5"); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(resource); + + PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN); + + Assert.assertNotNull(result); + Assert.assertTrue(result.getEnableMultipleDeviceEnrollment(), + "Multiple device enrollment should be enabled as per the resource attributes."); + Assert.assertEquals(result.getMaximumDeviceLimit(), Integer.valueOf(5), + "Maximum device limit should match the resource attribute value."); + } + + @Test + public void testGetPushDeviceConfig_WhenResourceNotFound_ReturnsDefaultConfig() throws Exception { + + ConfigurationManagementException exception = mock(ConfigurationManagementException.class); + when(exception.getErrorCode()).thenReturn(ERROR_CODE_RESOURCE_DOES_NOT_EXISTS.getCode()); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenThrow(exception); + + PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN); + + assertIsDefaultConfig(result); + } + + @Test + public void testGetPushDeviceConfig_WhenResourceTypeNotFound_ReturnsDefaultConfig() throws Exception { + + ConfigurationManagementException exception = mock(ConfigurationManagementException.class); + when(exception.getErrorCode()).thenReturn(ERROR_CODE_RESOURCE_TYPE_DOES_NOT_EXISTS.getCode()); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenThrow(exception); + + PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN); + + assertIsDefaultConfig(result); + } + + @Test(expectedExceptions = IdentityException.class) + public void testGetPushDeviceConfig_WhenUnexpectedConfigException_ThrowsServerException() throws Exception { + + ConfigurationManagementException exception = mock(ConfigurationManagementException.class); + when(exception.getErrorCode()).thenReturn("CONFIGM_UNEXPECTED_00099"); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenThrow(exception); + + PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN); + } + + @Test + public void testGetPushDeviceConfig_WhenUnexpectedConfigException_ErrorCodeMatches() throws Exception { + + ConfigurationManagementException exception = mock(ConfigurationManagementException.class); + when(exception.getErrorCode()).thenReturn("CONFIGM_UNEXPECTED_00099"); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenThrow(exception); + + try { + PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN); + Assert.fail("Expected IdentityException was not thrown."); + } catch (IdentityException e) { + Assert.assertEquals(e.getErrorCode(), ERROR_CODE_GETTING_PUSH_DEVICE_CONFIG.getCode(), + "Error code should be PDH-15014 for config retrieval failure."); + } + } + + @Test + public void testGetPushDeviceConfig_WhenResourceIsNull_ReturnsDefaultConfig() throws Exception { + + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(null); + + PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN); + + assertIsDefaultConfig(result); + } + + @Test + public void testGetPushDeviceConfig_WhenResourceHasNullAttributes_ReturnsDefaultConfig() throws Exception { + + Resource resource = new Resource(); + resource.setResourceName(RESOURCE_NAME); + resource.setAttributes(null); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(resource); + + PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN); + + assertIsDefaultConfig(result); + } + + @Test + public void testGetPushDeviceConfig_WhenResourceHasEmptyAttributes_ReturnsDefaultConfig() throws Exception { + + Resource resource = new Resource(); + resource.setResourceName(RESOURCE_NAME); + resource.setAttributes(new ArrayList<>()); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(resource); + + PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN); + + assertIsDefaultConfig(result); + } + + @Test + public void testGetPushDeviceConfig_DefaultConfig_HasExpectedValues() throws Exception { + + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(null); + + PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN); + + Assert.assertNotNull(result); + Assert.assertEquals(result.getEnableMultipleDeviceEnrollment(), + Boolean.valueOf(DEFAULT_ENABLE_DEVICE_MANAGEMENT), + "Default config must disable multiple device enrollment."); + Assert.assertEquals(result.getMaximumDeviceLimit(), Integer.valueOf(DEFAULT_MAX_DEVICE_LIMIT), + "Default config must set the maximum device limit to 1."); + Assert.assertFalse(result.getEnableDeviceRegistrationNotifications(), + "Default config must disable device registration notifications."); + Assert.assertEquals(result.getDeviceRegistrationNotificationChannels(), + java.util.EnumSet.of(DeviceRegistrationNotificationChannelEnum.EMAIL), + "Default config must default the notification channels to EMAIL only."); + } + + @Test + public void testUpdatePushDeviceConfig_WhenResourceExists_CallsReplaceResource() throws Exception { + + Resource existing = createResourceWithAttributes("false", "1"); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing); + + PushDeviceMgtConfigData config = createConfigData(true, 5); + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + + verify(configurationManager).replaceResource(eq(RESOURCE_TYPE), any(Resource.class)); + } + + @Test + public void testUpdatePushDeviceConfig_WhenResourceNotExists_CallsAddResource() throws Exception { + + ConfigurationManagementException exception = mock(ConfigurationManagementException.class); + when(exception.getErrorCode()).thenReturn(ERROR_CODE_RESOURCE_DOES_NOT_EXISTS.getCode()); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenThrow(exception); + + PushDeviceMgtConfigData config = createConfigData(true, 3); + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + + verify(configurationManager).addResource(eq(RESOURCE_TYPE), any(Resource.class)); + } + + @Test + public void testUpdatePushDeviceConfig_WhenResourceTypeNotExists_CreatesTypeAndAdds() throws Exception { + + ConfigurationManagementException getException = mock(ConfigurationManagementException.class); + when(getException.getErrorCode()).thenReturn(ERROR_CODE_RESOURCE_TYPE_DOES_NOT_EXISTS.getCode()); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenThrow(getException); + + ConfigurationManagementException addException = mock(ConfigurationManagementException.class); + when(addException.getErrorCode()).thenReturn(ERROR_CODE_RESOURCE_TYPE_DOES_NOT_EXISTS.getCode()); + when(configurationManager.addResource(eq(RESOURCE_TYPE), any(Resource.class))) + .thenThrow(addException) + .thenReturn(new Resource()); + + PushDeviceMgtConfigData config = createConfigData(true, 2); + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + + verify(configurationManager).addResourceType(any(ResourceTypeAdd.class)); + } + + @Test(expectedExceptions = IdentityException.class) + public void testUpdatePushDeviceConfig_WhenReplaceThrowsException_ThrowsServerException() throws Exception { + + Resource existing = createResourceWithAttributes("false", "1"); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing); + + ConfigurationManagementException replaceException = mock(ConfigurationManagementException.class); + when(replaceException.getErrorCode()).thenReturn("CONFIGM_UNEXPECTED_00099"); + when(configurationManager.replaceResource(eq(RESOURCE_TYPE), any(Resource.class))) + .thenThrow(replaceException); + + PushDeviceMgtConfigData config = createConfigData(true, 5); + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + } + + @Test + public void testUpdatePushDeviceConfig_WhenReplaceThrowsException_ErrorCodeMatches() throws Exception { + + Resource existing = createResourceWithAttributes("false", "1"); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing); + + ConfigurationManagementException replaceException = mock(ConfigurationManagementException.class); + when(replaceException.getErrorCode()).thenReturn("CONFIGM_UNEXPECTED_00099"); + when(configurationManager.replaceResource(eq(RESOURCE_TYPE), any(Resource.class))) + .thenThrow(replaceException); + + PushDeviceMgtConfigData config = createConfigData(true, 5); + try { + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + Assert.fail("Expected IdentityException was not thrown."); + } catch (IdentityException e) { + Assert.assertEquals(e.getErrorCode(), ERROR_CODE_UPDATING_PUSH_DEVICE_CONFIG.getCode(), + "Error code should be PDH-15016 for config update failure."); + } + } + + @Test + public void testUpdatePushDeviceConfig_BuildsCorrectResourceAttributes() throws Exception { + + Resource existing = createResourceWithAttributes("false", "1"); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing); + + PushDeviceMgtConfigData config = createConfigData(true, 10); + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + + verify(configurationManager).replaceResource(eq(RESOURCE_TYPE), any(Resource.class)); + } + + @Test + public void testUpdatePushDeviceConfig_WhenUpperBoundConfigIsInvalid_FallsBackToDefault() throws Exception { + + mockedIdentityUtil.when(() -> IdentityUtil.getProperty(MAX_DEVICE_LIMIT_PER_USER)) + .thenReturn("not-a-number"); + Resource existing = createResourceWithAttributes("true", "3"); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing); + + PushDeviceMgtConfigData config = createConfigData(true, DEFAULT_MAX_DEVICE_LIMIT_PER_USER); + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + + verify(configurationManager).replaceResource(eq(RESOURCE_TYPE), any(Resource.class)); + } + + @Test(expectedExceptions = IdentityException.class) + public void testUpdatePushDeviceConfig_WhenLimitExceedsServerUpperBound_ThrowsClientException() + throws Exception { + + mockedIdentityUtil.when(() -> IdentityUtil.getProperty(MAX_DEVICE_LIMIT_PER_USER)) + .thenReturn("5"); + + PushDeviceMgtConfigData config = createConfigData(true, 6); + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + } + + @Test + public void testUpdatePushDeviceConfig_WhenLimitExceedsServerUpperBound_ErrorCodeMatches() + throws Exception { + + mockedIdentityUtil.when(() -> IdentityUtil.getProperty(MAX_DEVICE_LIMIT_PER_USER)) + .thenReturn("5"); + + PushDeviceMgtConfigData config = createConfigData(true, 6); + try { + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + Assert.fail("Expected IdentityException was not thrown."); + } catch (IdentityException e) { + Assert.assertEquals(e.getErrorCode(), + ERROR_CODE_DEVICE_LIMIT_PER_USER_EXCEEDS.getCode(), + "Error code should be PDH-15018 when limit exceeds server upper bound."); + } + } + + @Test + public void testUpdatePushDeviceConfig_WhenLimitEqualsServerUpperBound_Succeeds() + throws Exception { + + mockedIdentityUtil.when(() -> IdentityUtil.getProperty(MAX_DEVICE_LIMIT_PER_USER)) + .thenReturn("5"); + Resource existing = createResourceWithAttributes("true", "3"); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing); + + PushDeviceMgtConfigData config = createConfigData(true, 5); + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + + verify(configurationManager).replaceResource(eq(RESOURCE_TYPE), any(Resource.class)); + } + + @Test(expectedExceptions = IdentityException.class) + public void testUpdatePushDeviceConfig_WhenMultiDeviceDisabledAndLimitAboveOne_ThrowsClientException() + throws Exception { + + Resource existing = createResourceWithAttributes("false", "3"); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing); + + PushDeviceMgtConfigData config = createConfigData(false, 3); + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + } + + @Test + public void testUpdatePushDeviceConfig_WhenMultiDeviceDisabledAndLimitAboveOne_ErrorCodeMatches() + throws Exception { + + Resource existing = createResourceWithAttributes("false", "3"); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing); + + PushDeviceMgtConfigData config = createConfigData(false, 3); + try { + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + Assert.fail("Expected IdentityException was not thrown."); + } catch (IdentityException e) { + Assert.assertEquals(e.getErrorCode(), ERROR_CODE_INVALID_DEVICE_LIMIT_CONFIG.getCode(), + "Error code should be PDH-15017 for invalid device limit config."); + } + } + + @Test + public void testUpdatePushDeviceConfig_WhenMultiDeviceDisabledAndLimitIsOne_Succeeds() throws Exception { + + ConfigurationManagementException exception = mock(ConfigurationManagementException.class); + when(exception.getErrorCode()).thenReturn(ERROR_CODE_RESOURCE_DOES_NOT_EXISTS.getCode()); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenThrow(exception); + + PushDeviceMgtConfigData config = createConfigData(false, 1); + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + + verify(configurationManager).addResource(eq(RESOURCE_TYPE), any(Resource.class)); + } + + @Test + public void testUpdatePushDeviceConfig_WhenMultiDeviceEnabledAndLimitAboveOne_Succeeds() throws Exception { + + Resource existing = createResourceWithAttributes("false", "1"); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing); + + PushDeviceMgtConfigData config = createConfigData(true, 5); + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + + verify(configurationManager).replaceResource(eq(RESOURCE_TYPE), any(Resource.class)); + } + + private Resource createResourceWithAttributes(String enableDeviceManagement, String maxDeviceLimit) { + + Resource resource = new Resource(); + resource.setResourceName(RESOURCE_NAME); + List attributes = new ArrayList<>(); + attributes.add(new Attribute(ATTR_ENABLE_DEVICE_MANAGEMENT, enableDeviceManagement)); + attributes.add(new Attribute(ATTR_MAX_DEVICE_LIMIT, maxDeviceLimit)); + resource.setAttributes(attributes); + return resource; + } + + @Test + public void testGetPushDeviceConfig_WhenAllAttributesPresent_MapsNotificationFlags() throws Exception { + + Resource resource = createResourceWithAttributes("true", "5"); + resource.getAttributes().add(new Attribute("enableDeviceRegistrationNotifications", "true")); + resource.getAttributes().add(new Attribute("deviceRegistrationNotificationChannels", + "EMAIL,PUSH_NOTIFICATION")); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(resource); + + PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN); + + Assert.assertTrue(result.getEnableDeviceRegistrationNotifications(), + "Notification enable flag should be mapped from the resource attributes."); + Assert.assertEquals(result.getDeviceRegistrationNotificationChannels(), + java.util.EnumSet.of( + DeviceRegistrationNotificationChannelEnum.EMAIL, + DeviceRegistrationNotificationChannelEnum.PUSH_NOTIFICATION), + "Notification channels should be mapped from the resource attributes."); + } + + @Test + public void testUpdatePushDeviceConfig_WhenMaxDeviceLimitIsNull_SkipsLimitValidations() throws Exception { + + Resource existing = createResourceWithAttributes("true", "3"); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing); + + PushDeviceMgtConfigData config = new PushDeviceMgtConfigData(); + config.setEnableMultipleDeviceEnrollment(false); + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + + verify(configurationManager).replaceResource(eq(RESOURCE_TYPE), any(Resource.class)); + } + + @Test + public void testGetPushDeviceConfig_WhenChannelsAttributeIsBlank_ReturnsEmptyChannelSet() throws Exception { + + Resource resource = createResourceWithAttributes("true", "5"); + resource.getAttributes().add(new Attribute("enableDeviceRegistrationNotifications", "true")); + resource.getAttributes().add(new Attribute("deviceRegistrationNotificationChannels", " ")); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(resource); + + PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN); + + Assert.assertNotNull(result.getDeviceRegistrationNotificationChannels(), + "Channel set should not be null for blank attribute."); + Assert.assertTrue(result.getDeviceRegistrationNotificationChannels().isEmpty(), + "Channel set should be empty when the attribute is blank."); + } + + @Test + public void testGetPushDeviceConfig_WhenChannelsAttributeHasInvalidAndEmptyTokens_SkipsThem() throws Exception { + + Resource resource = createResourceWithAttributes("true", "5"); + resource.getAttributes().add(new Attribute("enableDeviceRegistrationNotifications", "true")); + resource.getAttributes().add(new Attribute("deviceRegistrationNotificationChannels", + "EMAIL, ,UNKNOWN_CHANNEL,push_notification")); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(resource); + + PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN); + + Assert.assertEquals(result.getDeviceRegistrationNotificationChannels(), + java.util.EnumSet.of( + DeviceRegistrationNotificationChannelEnum.EMAIL, + DeviceRegistrationNotificationChannelEnum.PUSH_NOTIFICATION), + "Empty and unknown tokens should be skipped while keeping valid channels."); + } + + @Test + public void testUpdatePushDeviceConfig_WithNotificationChannels_SerialisesIntoAttribute() throws Exception { + + Resource existing = createResourceWithAttributes("true", "3"); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenReturn(existing); + + PushDeviceMgtConfigData config = createConfigData(true, 3); + config.setEnableDeviceRegistrationNotifications(true); + config.setDeviceRegistrationNotificationChannels(java.util.EnumSet.of( + DeviceRegistrationNotificationChannelEnum.EMAIL, + DeviceRegistrationNotificationChannelEnum.PUSH_NOTIFICATION)); + + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + + org.mockito.ArgumentCaptor captor = org.mockito.ArgumentCaptor.forClass(Resource.class); + verify(configurationManager).replaceResource(eq(RESOURCE_TYPE), captor.capture()); + + String serialised = captor.getValue().getAttributes().stream() + .filter(a -> "deviceRegistrationNotificationChannels".equals(a.getKey())) + .map(Attribute::getValue) + .findFirst() + .orElse(null); + Assert.assertNotNull(serialised, "Notification channels attribute should be present."); + java.util.Set tokens = new java.util.HashSet<>(java.util.Arrays.asList(serialised.split(","))); + Assert.assertEquals(tokens, + new java.util.HashSet<>(java.util.Arrays.asList("EMAIL", "PUSH_NOTIFICATION")), + "Serialised channels should contain EMAIL and PUSH_NOTIFICATION tokens."); + } + + @Test + public void testUpdatePushDeviceConfig_WhenMultiDeviceEnabledAndLimitIsNull_ThrowsClientException() + throws Exception { + + PushDeviceMgtConfigData config = new PushDeviceMgtConfigData(); + config.setEnableMultipleDeviceEnrollment(true); + // maximumDeviceLimit intentionally left null. + try { + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + Assert.fail("Expected IdentityException was not thrown."); + } catch (IdentityException e) { + Assert.assertEquals(e.getErrorCode(), ERROR_CODE_INVALID_DEVICE_LIMIT_VALUE.getCode(), + "Error code should be PDH-15021 when multi-device is enabled without a positive limit."); + } + } + + @Test + public void testUpdatePushDeviceConfig_WhenMultiDeviceEnabledAndLimitIsNotPositive_ThrowsClientException() + throws Exception { + + PushDeviceMgtConfigData config = createConfigData(true, 0); + try { + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + Assert.fail("Expected IdentityException was not thrown."); + } catch (IdentityException e) { + Assert.assertEquals(e.getErrorCode(), ERROR_CODE_INVALID_DEVICE_LIMIT_VALUE.getCode(), + "Error code should be PDH-15021 when the limit is not positive."); + } + } + + @Test + public void testUpdatePushDeviceConfig_WhenMultiDeviceDisabledAndLimitIsNull_PersistsDefaultLimit() + throws Exception { + + ConfigurationManagementException exception = mock(ConfigurationManagementException.class); + when(exception.getErrorCode()).thenReturn(ERROR_CODE_RESOURCE_DOES_NOT_EXISTS.getCode()); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, false)).thenThrow(exception); + + PushDeviceMgtConfigData config = new PushDeviceMgtConfigData(); + config.setEnableMultipleDeviceEnrollment(false); + // maximumDeviceLimit intentionally left null. + PushDeviceConfigManager.updatePushDeviceConfig(config, TENANT_DOMAIN); + + org.mockito.ArgumentCaptor captor = org.mockito.ArgumentCaptor.forClass(Resource.class); + verify(configurationManager).addResource(eq(RESOURCE_TYPE), captor.capture()); + + String persistedLimit = captor.getValue().getAttributes().stream() + .filter(a -> ATTR_MAX_DEVICE_LIMIT.equals(a.getKey())) + .map(Attribute::getValue) + .findFirst() + .orElse(null); + // A null limit must never be persisted as the literal string "null". + Assert.assertEquals(persistedLimit, DEFAULT_MAX_DEVICE_LIMIT, + "A null maximum device limit must be normalised to the default before persistence."); + } + + @Test + public void testGetPushDeviceConfig_WhenStoredLimitIsNotNumeric_FallsBackToDefault() throws Exception { + + Resource resource = createResourceWithAttributes("true", "null"); + when(configurationManager.getResource(RESOURCE_TYPE, RESOURCE_NAME, true)).thenReturn(resource); + + // A corrupted/legacy non-numeric limit must not make the config unreadable. + PushDeviceMgtConfigData result = PushDeviceConfigManager.getPushDeviceConfig(TENANT_DOMAIN); + + Assert.assertEquals(result.getMaximumDeviceLimit(), Integer.valueOf(DEFAULT_MAX_DEVICE_LIMIT), + "A non-numeric stored limit must fall back to the default."); + } + + private PushDeviceMgtConfigData createConfigData(boolean enableMultipleDeviceEnrollment, int maximumDeviceLimit) { + + PushDeviceMgtConfigData config = new PushDeviceMgtConfigData(); + config.setEnableMultipleDeviceEnrollment(enableMultipleDeviceEnrollment); + config.setMaximumDeviceLimit(maximumDeviceLimit); + return config; + } + + private void assertIsDefaultConfig(PushDeviceMgtConfigData config) { + + Assert.assertNotNull(config, "Default config must not be null."); + Assert.assertEquals(config.getEnableMultipleDeviceEnrollment(), + Boolean.valueOf(DEFAULT_ENABLE_DEVICE_MANAGEMENT), + "Default config must disable multiple device enrollment."); + Assert.assertEquals(config.getMaximumDeviceLimit(), Integer.valueOf(DEFAULT_MAX_DEVICE_LIMIT), + "Default config must set the maximum device limit to 1."); + } +} diff --git a/components/org.wso2.carbon.identity.notification.push.provider/src/main/java/org/wso2/carbon/identity/notification/push/provider/model/PushNotificationData.java b/components/org.wso2.carbon.identity.notification.push.provider/src/main/java/org/wso2/carbon/identity/notification/push/provider/model/PushNotificationData.java index 619dbc7..d24654f 100644 --- a/components/org.wso2.carbon.identity.notification.push.provider/src/main/java/org/wso2/carbon/identity/notification/push/provider/model/PushNotificationData.java +++ b/components/org.wso2.carbon.identity.notification.push.provider/src/main/java/org/wso2/carbon/identity/notification/push/provider/model/PushNotificationData.java @@ -47,6 +47,9 @@ public class PushNotificationData { private final String ipAddress; private final String deviceOS; private final String browser; + private final String pushDeviceName; + private final String pushDeviceModel; + private final String registrationTime; private PushNotificationData(Builder builder) { @@ -69,6 +72,9 @@ private PushNotificationData(Builder builder) { this.ipAddress = builder.ipAddress; this.deviceOS = builder.deviceOS; this.browser = builder.browser; + this.pushDeviceName = builder.pushDeviceName; + this.pushDeviceModel = builder.pushDeviceModel; + this.registrationTime = builder.registrationTime; } public String getNotificationTitle() { @@ -161,6 +167,21 @@ public String getDeviceOS() { return deviceOS; } + public String getPushDeviceName() { + + return pushDeviceName; + } + + public String getPushDeviceModel() { + + return pushDeviceModel; + } + + public String getRegistrationTime() { + + return registrationTime; + } + public Map getAdditionalData() { Map additionalData = new HashMap<>(); @@ -211,6 +232,15 @@ public Map getAdditionalData() { if (deviceId != null) { additionalData.put("deviceId", deviceId); } + if (pushDeviceName != null) { + additionalData.put("push-device-name", pushDeviceName); + } + if (pushDeviceModel != null) { + additionalData.put("push-device-model", pushDeviceModel); + } + if (registrationTime != null) { + additionalData.put("registration-time", registrationTime); + } return additionalData; } @@ -250,6 +280,9 @@ public static class Builder { private String ipAddress; private String deviceOS; private String browser; + private String pushDeviceName; + private String pushDeviceModel; + private String registrationTime; public Builder setNotificationTitle(String notificationTitle) { @@ -365,6 +398,24 @@ public Builder setBrowser(String browser) { return this; } + public Builder setPushDeviceName(String pushDeviceName) { + + this.pushDeviceName = pushDeviceName; + return this; + } + + public Builder setPushDeviceModel(String pushDeviceModel) { + + this.pushDeviceModel = pushDeviceModel; + return this; + } + + public Builder setRegistrationTime(String registrationTime) { + + this.registrationTime = registrationTime; + return this; + } + public PushNotificationData build() { return new PushNotificationData(this); diff --git a/pom.xml b/pom.xml index ba1e571..5bfc2ec 100644 --- a/pom.xml +++ b/pom.xml @@ -83,10 +83,15 @@ org.wso2.carbon.database.utils ${org.wso2.carbon.database.utils.version} + + org.wso2.carbon.identity.event.handler.notification + org.wso2.carbon.identity.event.handler.notification + ${identity.event.handler.notification.version} + org.wso2.carbon.identity.event.handler.notification org.wso2.carbon.identity.notification.sender.tenant.config - ${identity.notification.sender.tenant.config.version} + ${identity.event.handler.notification.version} org.wso2.orbit.com.nimbusds @@ -124,6 +129,11 @@ org.wso2.carbon.identity.secret.mgt.core ${carbon.identity.framework.version} + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.configuration.mgt.core + ${carbon.identity.framework.version} + org.json.wso2 json @@ -218,6 +228,11 @@ ${com.fasterxml.jackson.databind.version} test + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.event + ${carbon.identity.framework.version} + @@ -352,7 +367,7 @@ - 7.8.635 + 7.11.159 [7.5.0, 8.0.0) 1.1.15 [1.0.0, 2.0.0) @@ -361,8 +376,9 @@ [4.5.0, 5.0.0) 2.1.6 [2.0.0,3.0.0) - 1.12.4 + 1.13.7 [1.3.15, 2.0.0) + [1.0.0, 2.0.0) 1.20.0.wso2v1 1.43.3.wso2v1 [1.41.2,1.44.0)