diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/pom.xml b/components/device-mgt/org.wso2.carbon.identity.device.mgt/pom.xml new file mode 100644 index 000000000000..cd6fc66c1821 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/pom.xml @@ -0,0 +1,180 @@ + + + + + + + org.wso2.carbon.identity.framework + device-mgt + 7.11.153-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.identity.device.mgt + bundle + WSO2 Carbon - Device Management Component + Generic device registration management backend component + http://wso2.org + + + + + org.eclipse.platform + org.eclipse.osgi + + + org.eclipse.platform + org.eclipse.osgi.services + + + + org.wso2.carbon.utils + org.wso2.carbon.database.utils + + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.core + + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.central.log.mgt + + + + org.json.wso2 + json + + + + commons-logging + commons-logging + + + + commons-lang.wso2 + commons-lang + + + + org.testng + testng + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-testng + test + + + com.h2database + h2 + test + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.testutil + test + + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + ${project.artifactId} + ${project.artifactId} + + org.wso2.carbon.identity.device.mgt.internal.* + + + !org.wso2.carbon.identity.device.mgt.internal.*, + org.wso2.carbon.identity.device.mgt.api.*; + version="${carbon.identity.package.export.version}" + + + org.apache.commons.lang; + version="${commons-lang.wso2.osgi.version.range}", + org.apache.commons.logging; + version="${import.package.version.commons.logging}", + org.osgi.framework; + version="${osgi.framework.imp.pkg.version.range}", + org.osgi.service.component; + version="${osgi.service.component.imp.pkg.version.range}", + org.osgi.service.component.annotations; + version="${osgi.service.component.imp.pkg.version.range}", + org.wso2.carbon.database.utils.jdbc; + version="${org.wso2.carbon.database.utils.version.range}", + org.wso2.carbon.database.utils.jdbc.exceptions; + version="${org.wso2.carbon.database.utils.version.range}", + org.wso2.carbon.identity.core.cache; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.core.util; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.central.log.mgt.utils; + version="${carbon.identity.package.import.version.range}", + org.json.*; version="${json.wso2.version.range}", + org.wso2.carbon.context; + version="${carbon.kernel.package.import.version.range}", + org.wso2.carbon.utils; + version="${carbon.kernel.package.import.version.range}" + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire.plugin.version} + + + src/test/resources/testng.xml + + + + + com.github.spotbugs + spotbugs-maven-plugin + + ../../../spotbugs-exclude.xml + Max + High + true + + + + + + + diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/constant/ErrorMessage.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/constant/ErrorMessage.java new file mode 100644 index 000000000000..d74d81b8f366 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/constant/ErrorMessage.java @@ -0,0 +1,84 @@ +/* + * 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.device.mgt.api.constant; + +/** + * Device management error messages. + */ +public enum ErrorMessage { + + ERROR_DEVICE_NOT_FOUND("DM-60001", "Device not found.", + "No registered device found for the given device id: %s."), + ERROR_INVALID_DEVICE_FIELD("DM-60002", "Invalid request.", + "%s is empty or invalid."), + + ERROR_WHILE_REGISTERING_DEVICE("DM-65001", "Error while registering device.", + "Error while registering device in the system."), + ERROR_WHILE_RETRIEVING_DEVICE("DM-65002", "Error while retrieving device.", + "Error while retrieving device from the system."), + ERROR_WHILE_UPDATING_DEVICE("DM-65003", "Error while updating device.", + "Error while updating device in the system."), + ERROR_WHILE_DELETING_DEVICE("DM-65004", "Error while deleting device.", + "Error while deleting device from the system."), + ERROR_USER_ID_REQUIRED("DM-65007", "User identifier required.", + "Cannot register device: a valid user identifier (userId) was not set before registration."), + ERROR_DEVICE_FIELD_REQUIRED("DM-65008", "Required device field missing.", + "Cannot register device: the required field '%s' was not set before registration."); + + private final String code; + private final String message; + private final String description; + + ErrorMessage(String code, String message, String description) { + + this.code = code; + this.message = message; + this.description = description; + } + + /** + * Returns the error code. + * + * @return Error code. + */ + public String getCode() { + + return code; + } + + /** + * Returns the high-level error message. + * + * @return Error message. + */ + public String getMessage() { + + return message; + } + + /** + * Returns the detailed error description. + * + * @return Error description. + */ + public String getDescription() { + + return description; + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/exception/DeviceMgtClientException.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/exception/DeviceMgtClientException.java new file mode 100644 index 000000000000..dff81dcabd27 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/exception/DeviceMgtClientException.java @@ -0,0 +1,51 @@ +/* +* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +* +* 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.device.mgt.api.exception; + +/** + * Client-side validation exception for device management. + */ +public class DeviceMgtClientException extends DeviceMgtException { + + /** + * Creates a new client exception. + * + * @param message Error message. + * @param description Error description. + * @param errorCode Error code. + */ + public DeviceMgtClientException(String message, String description, String errorCode) { + + super(message, description, errorCode); + } + + /** + * Creates a new client exception with the cause. + * + * @param message Error message. + * @param description Error description. + * @param errorCode Error code. + * @param cause Root cause. + */ + public DeviceMgtClientException(String message, String description, String errorCode, Throwable cause) { + + super(message, description, errorCode, cause); + } +} + diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/exception/DeviceMgtException.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/exception/DeviceMgtException.java new file mode 100644 index 000000000000..e809651157cd --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/exception/DeviceMgtException.java @@ -0,0 +1,78 @@ +/* +* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +* +* 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.device.mgt.api.exception; + +/** + * Base checked exception for device management. + */ +public class DeviceMgtException extends Exception { + + private final String errorCode; + private final String description; + + /** + * Creates a new device management exception. + * + * @param message Error message. + * @param description Error description. + * @param errorCode Error code. + */ + public DeviceMgtException(String message, String description, String errorCode) { + + super(message); + this.errorCode = errorCode; + this.description = description; + } + + /** + * Creates a new device management exception with the cause. + * + * @param message Error message. + * @param description Error description. + * @param errorCode Error code. + * @param cause Root cause. + */ + public DeviceMgtException(String message, String description, String errorCode, Throwable cause) { + + super(message, cause); + this.errorCode = errorCode; + this.description = description; + } + + /** + * Returns the error code. + * + * @return Error code. + */ + public String getErrorCode() { + + return errorCode; + } + + /** + * Returns the error description. + * + * @return Error description. + */ + public String getDescription() { + + return description; + } +} + diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/exception/DeviceMgtServerException.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/exception/DeviceMgtServerException.java new file mode 100644 index 000000000000..2666235742f3 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/exception/DeviceMgtServerException.java @@ -0,0 +1,51 @@ +/* +* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +* +* 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.device.mgt.api.exception; + +/** + * Server-side exception for device management failures. + */ +public class DeviceMgtServerException extends DeviceMgtException { + + /** + * Creates a new server exception. + * + * @param message Error message. + * @param description Error description. + * @param errorCode Error code. + */ + public DeviceMgtServerException(String message, String description, String errorCode) { + + super(message, description, errorCode); + } + + /** + * Creates a new server exception with the cause. + * + * @param message Error message. + * @param description Error description. + * @param errorCode Error code. + * @param cause Root cause. + */ + public DeviceMgtServerException(String message, String description, String errorCode, Throwable cause) { + + super(message, description, errorCode, cause); + } +} + diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/model/Device.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/model/Device.java new file mode 100644 index 000000000000..354447c59469 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/model/Device.java @@ -0,0 +1,280 @@ +/* + * 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.device.mgt.api.model; + +import java.sql.Timestamp; + +/** + * Immutable model for a registered device. + */ +public class Device { + + private final String id; + private final String userId; + private final String deviceName; + private final String deviceModel; + private final String publicKey; + private final Status status; + private final Timestamp registeredAt; + private final String metadata; + + private Device(Builder builder) { + + this.id = builder.id; + this.userId = builder.userId; + this.deviceName = builder.deviceName; + this.deviceModel = builder.deviceModel; + this.publicKey = builder.publicKey; + this.status = builder.status; + this.registeredAt = builder.registeredAt; + this.metadata = builder.metadata; + } + + /** + * Returns the device identifier. + * + * @return Device identifier. + */ + public String getId() { + + return id; + } + + /** + * Returns the user identifier. + * + * @return User identifier. + */ + public String getUserId() { + + return userId; + } + + /** + * Returns the display name of the device. + * + * @return Device name. + */ + public String getDeviceName() { + + return deviceName; + } + + /** + * Returns the hardware model. + * + * @return Device model. + */ + public String getDeviceModel() { + + return deviceModel; + } + + /** + * Returns the registered public key. + * + * @return Public key. + */ + public String getPublicKey() { + + return publicKey; + } + + /** + * Returns the current device status. + * + * @return Device status. + */ + public Status getStatus() { + + return status; + } + + /** + * Returns the registration timestamp. + * + * @return Registration timestamp. + */ + public Timestamp getRegisteredAt() { + + return registeredAt; + } + + /** + * Returns the metadata payload. + * + * @return Metadata string. + */ + public String getMetadata() { + + return metadata; + } + + /** + * Builder for {@link Device}. + */ + public static class Builder { + + private String id; + private String userId; + private String deviceName; + private String deviceModel; + private String publicKey; + private Status status = Status.ACTIVE; + private Timestamp registeredAt; + private String metadata; + + /** + * Creates an empty builder. + */ + public Builder() { + } + + /** + * Creates a builder pre-populated with the fields of an existing device. + * + * @param device Source device to copy fields from. + */ + public Builder(Device device) { + + this.id = device.id; + this.userId = device.userId; + this.deviceName = device.deviceName; + this.deviceModel = device.deviceModel; + this.publicKey = device.publicKey; + this.status = device.status; + this.registeredAt = device.registeredAt; + this.metadata = device.metadata; + } + + /** + * Sets the device identifier. + * + * @param id Device identifier. + * @return Builder instance. + */ + public Builder id(String id) { + + this.id = id; + return this; + } + + /** + * Sets the user identifier. + * + * @param userId User identifier. + * @return Builder instance. + */ + public Builder userId(String userId) { + + this.userId = userId; + return this; + } + + /** + * Sets the device name. + * + * @param deviceName Device name. + * @return Builder instance. + */ + public Builder deviceName(String deviceName) { + + this.deviceName = deviceName; + return this; + } + + /** + * Sets the device model. + * + * @param deviceModel Device model. + * @return Builder instance. + */ + public Builder deviceModel(String deviceModel) { + + this.deviceModel = deviceModel; + return this; + } + + /** + * Sets the public key. + * + * @param publicKey Public key. + * @return Builder instance. + */ + public Builder publicKey(String publicKey) { + + this.publicKey = publicKey; + return this; + } + + /** + * Sets the device status. + * + * @param status Device status. + * @return Builder instance. + */ + public Builder status(Status status) { + + this.status = status; + return this; + } + + /** + * Sets the registration timestamp. + * + * @param registeredAt Registration timestamp. + * @return Builder instance. + */ + public Builder registeredAt(Timestamp registeredAt) { + + this.registeredAt = registeredAt; + return this; + } + + /** + * Sets metadata. + * + * @param metadata Metadata value. + * @return Builder instance. + */ + public Builder metadata(String metadata) { + + this.metadata = metadata; + return this; + } + + /** + * Builds a registered device instance. + * + * @return Registered device. + */ + public Device build() { + + return new Device(this); + } + } + + /** + * Device Status Enum. + */ + public enum Status { + ACTIVE, + INACTIVE + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/service/DeviceManagementService.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/service/DeviceManagementService.java new file mode 100644 index 000000000000..61cd3b4656e0 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/api/service/DeviceManagementService.java @@ -0,0 +1,173 @@ +/* + * 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.device.mgt.api.service; + +import org.wso2.carbon.identity.device.mgt.api.exception.DeviceMgtException; +import org.wso2.carbon.identity.device.mgt.api.model.Device; + +import java.util.List; + +/** + * Service interface for device management operations. + */ +public interface DeviceManagementService { + + /** + * Registers a pre-verified {@link Device} in the database. + * + * @param device The verified device to register. + * @param tenantDomain Tenant domain. + */ + void registerDevice(Device device, String tenantDomain) throws DeviceMgtException; + + /** + * Retrieves a device by its UUID. Returns the device regardless of its status (ACTIVE or + * INACTIVE) — this is an admin/tenant-scoped lookup, not a filtered "my devices" view. + * Do not use this to decide whether a device should be trusted for authentication — a + * deactivated (revoked) device is still returned. Use {@link #getActiveDeviceById} for that. + * + * @param deviceId UUID of the device (IDN_DEVICE.ID). + * @param tenantDomain Tenant domain. + * @return The Device, or null if not found. + */ + Device getDeviceById(String deviceId, String tenantDomain) + throws DeviceMgtException; + + /** + * Retrieves a device by its UUID, but only if its status is {@link Device.Status#ACTIVE}. + * Returns {@code null} both when the device does not exist and when it exists but has been + * deactivated (revoked) — callers that use the result to decide whether to trust a device + * (e.g. token/signature validation) must not be able to distinguish the two cases from the + * return value alone, since doing so would leak whether a given device id was ever registered. + * This is the method authentication/authorization paths must use; {@link #getDeviceById} is + * for management/admin views where an inactive device should still be visible. + * + * @param deviceId UUID of the device (IDN_DEVICE.ID). + * @param tenantDomain Tenant domain. + * @return The Device if it exists and is ACTIVE; {@code null} otherwise. + */ + Device getActiveDeviceById(String deviceId, String tenantDomain) + throws DeviceMgtException; + + /** + * Retrieves all ACTIVE devices registered by a user. This is the user-facing "my devices" + * list: devices deactivated via {@link #deactivateDevice(String, String)} are excluded. + * + * @param userId WSO2 user identifier. + * @param tenantDomain Tenant domain. + * @return List of active Device objects. Empty list if none found. + */ + List getDevicesByUserId(String userId, String tenantDomain) + throws DeviceMgtException; + + /** + * Retrieves a page of devices registered in the tenant, ordered by registration time (newest + * first). Returns devices of any status (ACTIVE or INACTIVE) — this is an admin/tenant-wide + * view, not filtered like {@link #getDevicesByUserId}. + * + * @param tenantDomain Tenant domain. + * @param offset Number of records to skip. + * @param limit Maximum number of records to return. + * @return Page of Device objects. Empty list if none found. + */ + List getDevices(String tenantDomain, int offset, int limit) + throws DeviceMgtException; + + /** + * Retrieves a page of devices registered in the tenant, ordered by registration time (newest + * first), optionally filtered to a single user's devices. This is an admin view: it returns + * devices of any status (ACTIVE or INACTIVE), unlike {@link #getDevicesByUserId}, which is the + * user-facing "my devices" list restricted to ACTIVE devices. Do not use + * {@link #getDevicesByUserId} in place of this method for admin views — it silently drops + * INACTIVE devices. + * + * @param tenantDomain Tenant domain. + * @param offset Number of records to skip. + * @param limit Maximum number of records to return. + * @param userId WSO2 user identifier to filter by, or {@code null}/blank for no filtering. + * @return Page of Device objects. Empty list if none found. + */ + List getDevices(String tenantDomain, int offset, int limit, String userId) + throws DeviceMgtException; + + /** + * Counts all devices registered in the tenant, regardless of status (ACTIVE or INACTIVE) — + * this is an admin/tenant-wide count, not filtered like {@link #getDevicesByUserId}. + * + * @param tenantDomain Tenant domain. + * @return Total number of devices in the tenant. + */ + int getDeviceCount(String tenantDomain) + throws DeviceMgtException; + + /** + * Counts devices registered in the tenant, optionally filtered to a single user, regardless of + * status (ACTIVE or INACTIVE). This is an admin/tenant-wide count, not filtered like + * {@link #getDevicesByUserId}, which only counts ACTIVE devices for the user-facing view. + * + * @param tenantDomain Tenant domain. + * @param userId WSO2 user identifier to filter by, or {@code null}/blank for no filtering. + * @return Total number of matching devices in the tenant. + */ + int getDeviceCount(String tenantDomain, String userId) + throws DeviceMgtException; + + /** + * Updates the display name of a device. + * + * @param deviceId UUID of the device. + * @param deviceName New name for the device. + * @param tenantDomain Tenant domain. + * @return The updated Device. + */ + Device updateDeviceName(String deviceId, String deviceName, String tenantDomain) + throws DeviceMgtException; + + /** + * Activates a device, setting its status to {@link Device.Status#ACTIVE}. An activated device + * reappears in {@link #getDevicesByUserId(String, String)} results. + * + * @param deviceId UUID of the device. + * @param tenantDomain Tenant domain. + * @return The updated Device. + * @throws DeviceMgtException If the device does not exist or the update fails. + */ + Device activateDevice(String deviceId, String tenantDomain) throws DeviceMgtException; + + /** + * Deactivates a device, setting its status to {@link Device.Status#INACTIVE}. A deactivated + * device is excluded from {@link #getDevicesByUserId(String, String)} results, but remains + * visible via {@link #getDeviceById} and {@link #getDevices}. + * + * @param deviceId UUID of the device. + * @param tenantDomain Tenant domain. + * @return The updated Device. + * @throws DeviceMgtException If the device does not exist or the update fails. + */ + Device deactivateDevice(String deviceId, String tenantDomain) throws DeviceMgtException; + + /** + * Deletes (hard delete) a device registration record. + * + * @param deviceId UUID of the device. + * @param tenantDomain Tenant domain. + */ + void deleteDevice(String deviceId, String tenantDomain) + throws DeviceMgtException; +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/cache/DeviceCache.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/cache/DeviceCache.java new file mode 100644 index 000000000000..78fb39bffd2d --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/cache/DeviceCache.java @@ -0,0 +1,47 @@ +/* + * 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.device.mgt.internal.cache; + +import org.wso2.carbon.identity.core.cache.BaseCache; +import org.wso2.carbon.utils.CarbonUtils; + +/** + * Cache for Device Management. + */ +public class DeviceCache extends BaseCache { + + private static final String CACHE_NAME = "DeviceCache"; + private static final DeviceCache INSTANCE = new DeviceCache(); + + private DeviceCache() { + + super(CACHE_NAME); + } + + /** + * Returns the cache singleton instance. + * + * @return Cache singleton. + */ + public static DeviceCache getInstance() { + + CarbonUtils.checkSecurity(); + return INSTANCE; + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/cache/DeviceCacheEntry.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/cache/DeviceCacheEntry.java new file mode 100644 index 000000000000..8ea29f8d711f --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/cache/DeviceCacheEntry.java @@ -0,0 +1,63 @@ +/* + * 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.device.mgt.internal.cache; + +import org.wso2.carbon.identity.core.cache.CacheEntry; +import org.wso2.carbon.identity.device.mgt.api.model.Device; + +/** + * Cache entry for Device Management. + * This class is used to store a Device object in cache. + */ +public class DeviceCacheEntry extends CacheEntry { + + private static final long serialVersionUID = 1L; + + private Device device; + + /** + * Creates a cache entry wrapping the given device. + * + * @param device Device to be cached. + */ + public DeviceCacheEntry(Device device) { + + this.device = device; + } + + /** + * Returns the cached device. + * + * @return Cached device. + */ + public Device getDevice() { + + return device; + } + + /** + * Sets the cached device. + * + * @param device Device to be cached. + */ + public void setDevice(Device device) { + + this.device = device; + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/cache/DeviceCacheKey.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/cache/DeviceCacheKey.java new file mode 100644 index 000000000000..64f92193d698 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/cache/DeviceCacheKey.java @@ -0,0 +1,67 @@ +/* + * 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.device.mgt.internal.cache; + +import org.wso2.carbon.identity.core.cache.CacheKey; + +/** + * Cache key for Device Management. + * This class is used to store Device ID in cache. + */ +public class DeviceCacheKey extends CacheKey { + + private static final long serialVersionUID = 1L; + + private final String deviceId; + + /** + * Creates a cache key for the given device identifier. + * + * @param deviceId Device identifier. + */ + public DeviceCacheKey(String deviceId) { + + this.deviceId = deviceId; + } + + /** + * Returns the device identifier. + * + * @return Device identifier. + */ + public String getDeviceId() { + + return deviceId; + } + + @Override + public boolean equals(Object o) { + + if (!(o instanceof DeviceCacheKey)) { + return false; + } + return deviceId.equals(((DeviceCacheKey) o).getDeviceId()); + } + + @Override + public int hashCode() { + + return deviceId.hashCode(); + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/component/DeviceMgtServiceComponent.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/component/DeviceMgtServiceComponent.java new file mode 100644 index 000000000000..333cec30cf2c --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/component/DeviceMgtServiceComponent.java @@ -0,0 +1,76 @@ +/* + * 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.device.mgt.internal.component; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.wso2.carbon.identity.device.mgt.api.service.DeviceManagementService; +import org.wso2.carbon.identity.device.mgt.internal.service.impl.DeviceManagementServiceImpl; + +/** + * OSGi component that registers the device management service. + */ +@Component( + name = "device.mgt.service.component", + immediate = true +) +public class DeviceMgtServiceComponent { + + private static final Log LOG = LogFactory.getLog(DeviceMgtServiceComponent.class); + + /** + * Activates the component and registers the service. + * + * @param context OSGi component context. + */ + @Activate + protected void activate(ComponentContext context) { + + try { + BundleContext bundleCtx = context.getBundleContext(); + bundleCtx.registerService( + DeviceManagementService.class.getName(), + DeviceManagementServiceImpl.getInstance(), + null); + if (LOG.isDebugEnabled()) { + LOG.debug("Device management bundle is activated."); + } + } catch (Throwable e) { + LOG.error("Error while initializing device management service component.", e); + } + } + + /** + * Deactivates the component. + * + * @param context OSGi component context. + */ + @Deactivate + protected void deactivate(ComponentContext context) { + + if (LOG.isDebugEnabled()) { + LOG.debug("Device management bundle is deactivated."); + } + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/constant/DeviceMgtSQLConstants.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/constant/DeviceMgtSQLConstants.java new file mode 100644 index 000000000000..dac7fcb916b9 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/constant/DeviceMgtSQLConstants.java @@ -0,0 +1,202 @@ +/* + * 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.device.mgt.internal.constant; + +/** + * SQL constants used by the device management DAO layer. + */ +public final class DeviceMgtSQLConstants { + + private DeviceMgtSQLConstants() { + } + + /** + * Column and named-parameter names. + */ + public static final class Column { + + public static final String ID = "ID"; + public static final String DEVICE_ID = "DEVICE_ID"; + public static final String USER_ID = "USER_ID"; + public static final String DEVICE_NAME = "DEVICE_NAME"; + public static final String DEVICE_MODEL = "DEVICE_MODEL"; + public static final String PUBLIC_KEY = "PUBLIC_KEY"; + public static final String STATUS = "STATUS"; + public static final String REGISTERED_AT = "REGISTERED_AT"; + public static final String METADATA = "METADATA"; + public static final String TENANT_ID = "TENANT_ID"; + + public static final String LIMIT = "LIMIT"; + public static final String OFFSET = "OFFSET"; + public static final String LOWER_BOUND = "LOWER_BOUND"; + public static final String UPPER_BOUND = "UPPER_BOUND"; + + private Column() { + } + } + + /** + * SQL query definitions. + */ + public static final class Query { + + public static final String REGISTER_DEVICE = + "INSERT INTO IDN_DEVICE " + + "(ID, DEVICE_NAME, DEVICE_MODEL, PUBLIC_KEY, STATUS, REGISTERED_AT, " + + "TENANT_ID, METADATA) " + + "VALUES (:ID;, :DEVICE_NAME;, :DEVICE_MODEL;, :PUBLIC_KEY;, :STATUS;, " + + ":REGISTERED_AT;, :TENANT_ID;, :METADATA;)"; + + public static final String ADD_USER_DEVICE = + "INSERT INTO IDN_USER_DEVICE " + + "(DEVICE_ID, USER_ID, TENANT_ID) " + + "VALUES (:DEVICE_ID;, :USER_ID;, :TENANT_ID;)"; + + public static final String GET_DEVICE_BY_ID = + "SELECT D.ID, UD.USER_ID, D.DEVICE_NAME, D.DEVICE_MODEL, D.PUBLIC_KEY, D.STATUS, " + + "D.REGISTERED_AT, D.METADATA, D.TENANT_ID " + + "FROM IDN_DEVICE D " + + "INNER JOIN IDN_USER_DEVICE UD ON D.ID = UD.DEVICE_ID AND D.TENANT_ID = UD.TENANT_ID " + + "WHERE D.ID = :ID; AND D.TENANT_ID = :TENANT_ID;"; + + public static final String GET_DEVICES_BY_USER_ID = + "SELECT D.ID, UD.USER_ID, D.DEVICE_NAME, D.DEVICE_MODEL, D.PUBLIC_KEY, D.STATUS, " + + "D.REGISTERED_AT, D.METADATA, D.TENANT_ID " + + "FROM IDN_DEVICE D " + + "INNER JOIN IDN_USER_DEVICE UD ON D.ID = UD.DEVICE_ID AND D.TENANT_ID = UD.TENANT_ID " + + "WHERE UD.USER_ID = :USER_ID; AND D.STATUS = 'ACTIVE' AND D.TENANT_ID = :TENANT_ID; " + + "ORDER BY D.REGISTERED_AT DESC"; + + public static final String UPDATE_DEVICE_NAME = + "UPDATE IDN_DEVICE SET DEVICE_NAME = :DEVICE_NAME; " + + "WHERE ID = :ID; AND TENANT_ID = :TENANT_ID;"; + + public static final String CHANGE_DEVICE_STATUS = + "UPDATE IDN_DEVICE SET STATUS = :STATUS; " + + "WHERE ID = :ID; AND TENANT_ID = :TENANT_ID;"; + + // Paginated tenant-wide device listing. Pagination syntax differs per database, so a variant + // is selected at runtime based on the detected database type. + + // Default: H2, MySQL, MariaDB, PostgreSQL. + public static final String GET_ALL_DEVICES_PAGINATED = + "SELECT D.ID, UD.USER_ID, D.DEVICE_NAME, D.DEVICE_MODEL, D.PUBLIC_KEY, D.STATUS, " + + "D.REGISTERED_AT, D.METADATA, D.TENANT_ID " + + "FROM IDN_DEVICE D " + + "INNER JOIN IDN_USER_DEVICE UD ON D.ID = UD.DEVICE_ID AND D.TENANT_ID = UD.TENANT_ID " + + "WHERE D.TENANT_ID = :TENANT_ID; ORDER BY D.REGISTERED_AT DESC, D.ID DESC " + + "LIMIT :LIMIT; OFFSET :OFFSET;"; + + // MS SQL Server. + public static final String GET_ALL_DEVICES_PAGINATED_MSSQL = + "SELECT D.ID, UD.USER_ID, D.DEVICE_NAME, D.DEVICE_MODEL, D.PUBLIC_KEY, D.STATUS, " + + "D.REGISTERED_AT, D.METADATA, D.TENANT_ID " + + "FROM IDN_DEVICE D " + + "INNER JOIN IDN_USER_DEVICE UD ON D.ID = UD.DEVICE_ID AND D.TENANT_ID = UD.TENANT_ID " + + "WHERE D.TENANT_ID = :TENANT_ID; ORDER BY D.REGISTERED_AT DESC, D.ID DESC " + + "OFFSET :OFFSET; ROWS FETCH NEXT :LIMIT; ROWS ONLY"; + + // Oracle. + public static final String GET_ALL_DEVICES_PAGINATED_ORACLE = + "SELECT ID, USER_ID, DEVICE_NAME, DEVICE_MODEL, PUBLIC_KEY, STATUS, REGISTERED_AT, " + + "METADATA, TENANT_ID FROM (SELECT ID, USER_ID, DEVICE_NAME, DEVICE_MODEL, PUBLIC_KEY, " + + "STATUS, REGISTERED_AT, METADATA, TENANT_ID, rownum AS rnum FROM " + + "(SELECT D.ID, UD.USER_ID, D.DEVICE_NAME, D.DEVICE_MODEL, D.PUBLIC_KEY, D.STATUS, " + + "D.REGISTERED_AT, D.METADATA, D.TENANT_ID FROM IDN_DEVICE D " + + "INNER JOIN IDN_USER_DEVICE UD ON D.ID = UD.DEVICE_ID AND D.TENANT_ID = UD.TENANT_ID " + + "WHERE D.TENANT_ID = :TENANT_ID; ORDER BY D.REGISTERED_AT DESC, D.ID DESC) " + + "WHERE rownum <= :UPPER_BOUND;) WHERE rnum > :OFFSET; ORDER BY rnum"; + + // DB2. + public static final String GET_ALL_DEVICES_PAGINATED_DB2 = + "SELECT ID, USER_ID, DEVICE_NAME, DEVICE_MODEL, PUBLIC_KEY, STATUS, REGISTERED_AT, " + + "METADATA, TENANT_ID FROM (SELECT " + + "ROW_NUMBER() OVER(ORDER BY D.REGISTERED_AT DESC, D.ID DESC) AS rn, " + + "D.ID, UD.USER_ID, D.DEVICE_NAME, D.DEVICE_MODEL, D.PUBLIC_KEY, D.STATUS, " + + "D.REGISTERED_AT, D.METADATA, D.TENANT_ID FROM IDN_DEVICE D " + + "INNER JOIN IDN_USER_DEVICE UD ON D.ID = UD.DEVICE_ID AND D.TENANT_ID = UD.TENANT_ID " + + "WHERE D.TENANT_ID = :TENANT_ID;) WHERE rn BETWEEN :LOWER_BOUND; AND :UPPER_BOUND; " + + "ORDER BY rn"; + + public static final String GET_DEVICES_COUNT = + "SELECT COUNT(*) FROM IDN_DEVICE D " + + "INNER JOIN IDN_USER_DEVICE UD ON D.ID = UD.DEVICE_ID AND D.TENANT_ID = UD.TENANT_ID " + + "WHERE D.TENANT_ID = :TENANT_ID;"; + + // Paginated tenant-wide device listing filtered to a single user. Same DB-specific pagination + // variants as GET_ALL_DEVICES_PAGINATED, with an added user id condition. + + // Default: H2, MySQL, MariaDB, PostgreSQL. + public static final String GET_ALL_DEVICES_PAGINATED_BY_USER = + "SELECT D.ID, UD.USER_ID, D.DEVICE_NAME, D.DEVICE_MODEL, D.PUBLIC_KEY, D.STATUS, " + + "D.REGISTERED_AT, D.METADATA, D.TENANT_ID " + + "FROM IDN_DEVICE D " + + "INNER JOIN IDN_USER_DEVICE UD ON D.ID = UD.DEVICE_ID AND D.TENANT_ID = UD.TENANT_ID " + + "WHERE D.TENANT_ID = :TENANT_ID; AND UD.USER_ID = :USER_ID; " + + "ORDER BY D.REGISTERED_AT DESC, D.ID DESC LIMIT :LIMIT; OFFSET :OFFSET;"; + + // MS SQL Server. + public static final String GET_ALL_DEVICES_PAGINATED_BY_USER_MSSQL = + "SELECT D.ID, UD.USER_ID, D.DEVICE_NAME, D.DEVICE_MODEL, D.PUBLIC_KEY, D.STATUS, " + + "D.REGISTERED_AT, D.METADATA, D.TENANT_ID " + + "FROM IDN_DEVICE D " + + "INNER JOIN IDN_USER_DEVICE UD ON D.ID = UD.DEVICE_ID AND D.TENANT_ID = UD.TENANT_ID " + + "WHERE D.TENANT_ID = :TENANT_ID; AND UD.USER_ID = :USER_ID; " + + "ORDER BY D.REGISTERED_AT DESC, D.ID DESC OFFSET :OFFSET; ROWS FETCH NEXT :LIMIT; ROWS ONLY"; + + // Oracle. + public static final String GET_ALL_DEVICES_PAGINATED_BY_USER_ORACLE = + "SELECT ID, USER_ID, DEVICE_NAME, DEVICE_MODEL, PUBLIC_KEY, STATUS, REGISTERED_AT, " + + "METADATA, TENANT_ID FROM (SELECT ID, USER_ID, DEVICE_NAME, DEVICE_MODEL, PUBLIC_KEY, " + + "STATUS, REGISTERED_AT, METADATA, TENANT_ID, rownum AS rnum FROM " + + "(SELECT D.ID, UD.USER_ID, D.DEVICE_NAME, D.DEVICE_MODEL, D.PUBLIC_KEY, D.STATUS, " + + "D.REGISTERED_AT, D.METADATA, D.TENANT_ID FROM IDN_DEVICE D " + + "INNER JOIN IDN_USER_DEVICE UD ON D.ID = UD.DEVICE_ID AND D.TENANT_ID = UD.TENANT_ID " + + "WHERE D.TENANT_ID = :TENANT_ID; AND UD.USER_ID = :USER_ID; " + + "ORDER BY D.REGISTERED_AT DESC, D.ID DESC) " + + "WHERE rownum <= :UPPER_BOUND;) WHERE rnum > :OFFSET; ORDER BY rnum"; + + // DB2. + public static final String GET_ALL_DEVICES_PAGINATED_BY_USER_DB2 = + "SELECT ID, USER_ID, DEVICE_NAME, DEVICE_MODEL, PUBLIC_KEY, STATUS, REGISTERED_AT, " + + "METADATA, TENANT_ID FROM (SELECT " + + "ROW_NUMBER() OVER(ORDER BY D.REGISTERED_AT DESC, D.ID DESC) AS rn, " + + "D.ID, UD.USER_ID, D.DEVICE_NAME, D.DEVICE_MODEL, D.PUBLIC_KEY, D.STATUS, " + + "D.REGISTERED_AT, D.METADATA, D.TENANT_ID FROM IDN_DEVICE D " + + "INNER JOIN IDN_USER_DEVICE UD ON D.ID = UD.DEVICE_ID AND D.TENANT_ID = UD.TENANT_ID " + + "WHERE D.TENANT_ID = :TENANT_ID; AND UD.USER_ID = :USER_ID;) " + + "WHERE rn BETWEEN :LOWER_BOUND; AND :UPPER_BOUND; ORDER BY rn"; + + public static final String GET_DEVICES_COUNT_BY_USER = + "SELECT COUNT(*) FROM IDN_DEVICE D " + + "INNER JOIN IDN_USER_DEVICE UD ON D.ID = UD.DEVICE_ID AND D.TENANT_ID = UD.TENANT_ID " + + "WHERE D.TENANT_ID = :TENANT_ID; AND UD.USER_ID = :USER_ID;"; + + public static final String DELETE_USER_DEVICE = + "DELETE FROM IDN_USER_DEVICE " + + "WHERE DEVICE_ID = :DEVICE_ID; AND TENANT_ID = :TENANT_ID;"; + + public static final String DELETE_DEVICE = + "DELETE FROM IDN_DEVICE " + + "WHERE ID = :ID; AND TENANT_ID = :TENANT_ID;"; + + private Query() { + } + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/dao/DeviceManagementDAO.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/dao/DeviceManagementDAO.java new file mode 100644 index 000000000000..e98c3d4d0c4d --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/dao/DeviceManagementDAO.java @@ -0,0 +1,146 @@ +/* + * 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.device.mgt.internal.dao; + +import org.wso2.carbon.identity.device.mgt.api.exception.DeviceMgtException; +import org.wso2.carbon.identity.device.mgt.api.model.Device; + +import java.util.List; + +/** + * DAO contract for device registration. + */ +public interface DeviceManagementDAO { + + /** + * Registers a new device. + * + * @param device Device to register. + * @param tenantId Tenant identifier. + * @return Registered device. + * @throws DeviceMgtException If registration fails. + */ + Device registerDevice(Device device, int tenantId) + throws DeviceMgtException; + + /** + * Finds a device by id. + * + * @param deviceId Device identifier. + * @param tenantId Tenant identifier. + * @return Device or {@code null}. + * @throws DeviceMgtException If retrieval fails. + */ + Device getDeviceById(String deviceId, int tenantId) + throws DeviceMgtException; + + /** + * Finds all active devices by user id. + * + * @param userId User identifier. + * @param tenantId Tenant identifier. + * @return Active devices. + * @throws DeviceMgtException If retrieval fails. + */ + List getDevicesByUserId(String userId, int tenantId) + throws DeviceMgtException; + + /** + * Finds a page of devices registered in the tenant, ordered by registration time (newest first). + * + * @param tenantId Tenant identifier. + * @param offset Number of records to skip. + * @param limit Maximum number of records to return. + * @return Page of devices in the tenant. + * @throws DeviceMgtException If retrieval fails. + */ + List getDevices(int tenantId, int offset, int limit) + throws DeviceMgtException; + + /** + * Finds a page of devices registered in the tenant, ordered by registration time (newest + * first), optionally filtered to a single user. This is an admin/tenant-scoped listing that + * returns devices of any status (ACTIVE or INACTIVE). + * + * @param tenantId Tenant identifier. + * @param offset Number of records to skip. + * @param limit Maximum number of records to return. + * @param userId User identifier to filter by, or {@code null}/blank for no filtering. + * @return Page of devices in the tenant. + * @throws DeviceMgtException If retrieval fails. + */ + List getDevices(int tenantId, int offset, int limit, String userId) + throws DeviceMgtException; + + /** + * Counts all devices registered in the tenant. + * + * @param tenantId Tenant identifier. + * @return Total number of devices in the tenant. + * @throws DeviceMgtException If the count fails. + */ + int getDeviceCount(int tenantId) + throws DeviceMgtException; + + /** + * Counts devices registered in the tenant, optionally filtered to a single user. This is an + * admin/tenant-scoped count that includes devices of any status (ACTIVE or INACTIVE). + * + * @param tenantId Tenant identifier. + * @param userId User identifier to filter by, or {@code null}/blank for no filtering. + * @return Total number of matching devices in the tenant. + * @throws DeviceMgtException If the count fails. + */ + int getDeviceCount(int tenantId, String userId) + throws DeviceMgtException; + + /** + * Updates the name of a device. + * + * @param deviceId Device identifier. + * @param deviceName Device name. + * @param tenantId Tenant identifier. + * @return Updated device. + * @throws DeviceMgtException If update fails. + */ + Device updateDeviceName(String deviceId, String deviceName, int tenantId) + throws DeviceMgtException; + + /** + * Updates the status of a device and returns the updated record. + * + * @param deviceId Device identifier. + * @param status New status for the device. + * @param tenantId Tenant identifier. + * @return Updated device. + * @throws DeviceMgtException If the status update fails. + */ + Device changeDeviceStatus(String deviceId, Device.Status status, int tenantId) + throws DeviceMgtException; + + /** + * Deletes a device. + * + * @param deviceId Device identifier. + * @param tenantId Tenant identifier. + * @throws DeviceMgtException If deletion fails. + */ + void deleteDevice(String deviceId, int tenantId) + throws DeviceMgtException; +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/dao/impl/CacheBackedDeviceManagementDAO.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/dao/impl/CacheBackedDeviceManagementDAO.java new file mode 100644 index 000000000000..418a5d0eb7f5 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/dao/impl/CacheBackedDeviceManagementDAO.java @@ -0,0 +1,241 @@ +/* + * 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.device.mgt.internal.dao.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.device.mgt.api.exception.DeviceMgtException; +import org.wso2.carbon.identity.device.mgt.api.model.Device; +import org.wso2.carbon.identity.device.mgt.internal.cache.DeviceCache; +import org.wso2.carbon.identity.device.mgt.internal.cache.DeviceCacheEntry; +import org.wso2.carbon.identity.device.mgt.internal.cache.DeviceCacheKey; +import org.wso2.carbon.identity.device.mgt.internal.dao.DeviceManagementDAO; + +import java.util.List; + +/** + * Cache backed Device Management DAO. + * This class implements the caching on top of the data layer operations. + * This caches the Device object, keyed by device ID and tenant. List/aggregate reads + * (getDevicesByUserId, getDevices, getDeviceCount) are intentionally not cached since + * invalidating a list on every write is a different, harder problem. + */ +public class CacheBackedDeviceManagementDAO implements DeviceManagementDAO { + + private static final Log LOG = LogFactory.getLog(CacheBackedDeviceManagementDAO.class); + + private final DeviceManagementDAO deviceManagementDAO; + private final DeviceCache deviceCache; + + /** + * Wraps the given DAO with a read-through, write-invalidate cache. + * + * @param deviceManagementDAO The underlying DAO to delegate to. + */ + public CacheBackedDeviceManagementDAO(DeviceManagementDAO deviceManagementDAO) { + + this.deviceManagementDAO = deviceManagementDAO; + deviceCache = DeviceCache.getInstance(); + } + + /** + * Registers a new device. + * This method directly invokes the data layer operation — a brand-new device has nothing + * to invalidate in the cache. + * + * @param device Device to register. + * @param tenantId Tenant identifier. + * @return Registered device. + * @throws DeviceMgtException If registration fails. + */ + @Override + public Device registerDevice(Device device, int tenantId) throws DeviceMgtException { + + return deviceManagementDAO.registerDevice(device, tenantId); + } + + /** + * Finds a device by id. + * This method first checks the cache for the Device object. If the Device object is not + * found in the cache, it invokes the data layer operation to get the Device and, if found, + * adds it to the cache. + * + * @param deviceId Device identifier. + * @param tenantId Tenant identifier. + * @return Device or {@code null}. + * @throws DeviceMgtException If retrieval fails. + */ + @Override + public Device getDeviceById(String deviceId, int tenantId) throws DeviceMgtException { + + DeviceCacheKey cacheKey = new DeviceCacheKey(deviceId); + DeviceCacheEntry cacheEntry = deviceCache.getValueFromCache(cacheKey, tenantId); + if (cacheEntry != null && cacheEntry.getDevice() != null) { + if (LOG.isDebugEnabled()) { + LOG.debug("Device cache hit for device id: " + deviceId + ". Returning from cache."); + } + return cacheEntry.getDevice(); + } + + Device device = deviceManagementDAO.getDeviceById(deviceId, tenantId); + if (device != null) { + if (LOG.isDebugEnabled()) { + LOG.debug("Device cache miss for device id: " + deviceId + ". Adding to cache."); + } + deviceCache.addToCacheOnRead(cacheKey, new DeviceCacheEntry(device), tenantId); + } + return device; + } + + /** + * Finds all active devices by user id. + * This method is not cached; list results are not invalidated entry-by-entry. + * + * @param userId User identifier. + * @param tenantId Tenant identifier. + * @return Active devices. + * @throws DeviceMgtException If retrieval fails. + */ + @Override + public List getDevicesByUserId(String userId, int tenantId) throws DeviceMgtException { + + return deviceManagementDAO.getDevicesByUserId(userId, tenantId); + } + + /** + * Finds a page of devices registered in the tenant, ordered by registration time (newest first). + * This method is not cached; list results are not invalidated entry-by-entry. + * + * @param tenantId Tenant identifier. + * @param offset Number of records to skip. + * @param limit Maximum number of records to return. + * @return Page of devices in the tenant. + * @throws DeviceMgtException If retrieval fails. + */ + @Override + public List getDevices(int tenantId, int offset, int limit) throws DeviceMgtException { + + return deviceManagementDAO.getDevices(tenantId, offset, limit); + } + + /** + * Finds a page of devices registered in the tenant, optionally filtered to a single user. + * This method is not cached; list results are not invalidated entry-by-entry. + * + * @param tenantId Tenant identifier. + * @param offset Number of records to skip. + * @param limit Maximum number of records to return. + * @param userId User identifier to filter by, or {@code null}/blank for no filtering. + * @return Page of devices in the tenant. + * @throws DeviceMgtException If retrieval fails. + */ + @Override + public List getDevices(int tenantId, int offset, int limit, String userId) throws DeviceMgtException { + + return deviceManagementDAO.getDevices(tenantId, offset, limit, userId); + } + + /** + * Counts all devices registered in the tenant. + * This method is not cached. + * + * @param tenantId Tenant identifier. + * @return Total number of devices in the tenant. + * @throws DeviceMgtException If the count fails. + */ + @Override + public int getDeviceCount(int tenantId) throws DeviceMgtException { + + return deviceManagementDAO.getDeviceCount(tenantId); + } + + /** + * Counts devices registered in the tenant, optionally filtered to a single user. + * This method is not cached. + * + * @param tenantId Tenant identifier. + * @param userId User identifier to filter by, or {@code null}/blank for no filtering. + * @return Total number of matching devices in the tenant. + * @throws DeviceMgtException If the count fails. + */ + @Override + public int getDeviceCount(int tenantId, String userId) throws DeviceMgtException { + + return deviceManagementDAO.getDeviceCount(tenantId, userId); + } + + /** + * Updates the name of a device. + * This method clears the cache entry upon device name update. + * + * @param deviceId Device identifier. + * @param deviceName Device name. + * @param tenantId Tenant identifier. + * @return Updated device. + * @throws DeviceMgtException If update fails. + */ + @Override + public Device updateDeviceName(String deviceId, String deviceName, int tenantId) throws DeviceMgtException { + + deviceCache.clearCacheEntry(new DeviceCacheKey(deviceId), tenantId); + if (LOG.isDebugEnabled()) { + LOG.debug("Device cache entry is cleared for device id: " + deviceId + " for device name update."); + } + return deviceManagementDAO.updateDeviceName(deviceId, deviceName, tenantId); + } + + /** + * Updates the status of a device and returns the updated record. + * This method clears the cache entry upon status change. + * + * @param deviceId Device identifier. + * @param status New status for the device. + * @param tenantId Tenant identifier. + * @return Updated device. + * @throws DeviceMgtException If the status update fails. + */ + @Override + public Device changeDeviceStatus(String deviceId, Device.Status status, int tenantId) + throws DeviceMgtException { + + deviceCache.clearCacheEntry(new DeviceCacheKey(deviceId), tenantId); + if (LOG.isDebugEnabled()) { + LOG.debug("Device cache entry is cleared for device id: " + deviceId + " for status change."); + } + return deviceManagementDAO.changeDeviceStatus(deviceId, status, tenantId); + } + + /** + * Deletes a device. + * This method clears the cache entry upon device deletion. + * + * @param deviceId Device identifier. + * @param tenantId Tenant identifier. + * @throws DeviceMgtException If deletion fails. + */ + @Override + public void deleteDevice(String deviceId, int tenantId) throws DeviceMgtException { + + deviceCache.clearCacheEntry(new DeviceCacheKey(deviceId), tenantId); + if (LOG.isDebugEnabled()) { + LOG.debug("Device cache entry is cleared for device id: " + deviceId + " for device deletion."); + } + deviceManagementDAO.deleteDevice(deviceId, tenantId); + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/dao/impl/DeviceManagementDAOImpl.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/dao/impl/DeviceManagementDAOImpl.java new file mode 100644 index 000000000000..14cb2c5962af --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/dao/impl/DeviceManagementDAOImpl.java @@ -0,0 +1,412 @@ +/* + * 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.device.mgt.internal.dao.impl; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.database.utils.jdbc.NamedJdbcTemplate; +import org.wso2.carbon.database.utils.jdbc.NamedPreparedStatement; +import org.wso2.carbon.database.utils.jdbc.exceptions.DataAccessException; +import org.wso2.carbon.database.utils.jdbc.exceptions.TransactionException; +import org.wso2.carbon.identity.core.util.IdentityDatabaseUtil; +import org.wso2.carbon.identity.core.util.JdbcUtils; +import org.wso2.carbon.identity.device.mgt.api.constant.ErrorMessage; +import org.wso2.carbon.identity.device.mgt.api.exception.DeviceMgtException; +import org.wso2.carbon.identity.device.mgt.api.model.Device; +import org.wso2.carbon.identity.device.mgt.internal.constant.DeviceMgtSQLConstants; +import org.wso2.carbon.identity.device.mgt.internal.dao.DeviceManagementDAO; +import org.wso2.carbon.identity.device.mgt.internal.util.DeviceManagementExceptionHandler; + +import java.sql.SQLException; +import java.util.Collections; +import java.util.List; + +/** + * JDBC implementation for device registration. + */ +public class DeviceManagementDAOImpl implements DeviceManagementDAO { + + private static final Log LOG = LogFactory.getLog(DeviceManagementDAOImpl.class); + + @Override + public Device registerDevice(Device device, int tenantId) + throws DeviceMgtException { + + NamedJdbcTemplate jdbcTemplate = new NamedJdbcTemplate(IdentityDatabaseUtil.getDataSource()); + + try { + jdbcTemplate.withTransaction(template -> { + template.executeInsert( + DeviceMgtSQLConstants.Query.REGISTER_DEVICE, + preparedStatement -> { + preparedStatement.setString(DeviceMgtSQLConstants.Column.ID, device.getId()); + preparedStatement.setString( + DeviceMgtSQLConstants.Column.DEVICE_NAME, device.getDeviceName()); + preparedStatement.setString( + DeviceMgtSQLConstants.Column.DEVICE_MODEL, device.getDeviceModel()); + preparedStatement.setString(DeviceMgtSQLConstants.Column.PUBLIC_KEY, device.getPublicKey()); + preparedStatement.setString( + DeviceMgtSQLConstants.Column.STATUS, device.getStatus().name()); + preparedStatement.setObject( + DeviceMgtSQLConstants.Column.REGISTERED_AT, device.getRegisteredAt()); + preparedStatement.setInt(DeviceMgtSQLConstants.Column.TENANT_ID, tenantId); + preparedStatement.setString(DeviceMgtSQLConstants.Column.METADATA, device.getMetadata()); + }, + device, + false); + template.executeInsert( + DeviceMgtSQLConstants.Query.ADD_USER_DEVICE, + preparedStatement -> { + preparedStatement.setString(DeviceMgtSQLConstants.Column.DEVICE_ID, device.getId()); + preparedStatement.setString(DeviceMgtSQLConstants.Column.USER_ID, device.getUserId()); + preparedStatement.setInt(DeviceMgtSQLConstants.Column.TENANT_ID, tenantId); + }, + device, + false); + return null; + }); + + } catch (TransactionException e) { + throw DeviceManagementExceptionHandler.handleServerException( + ErrorMessage.ERROR_WHILE_REGISTERING_DEVICE, e); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Device registered successfully with ID: " + device.getId()); + } + return device; + } + + @Override + public Device getDeviceById(String deviceId, int tenantId) + throws DeviceMgtException { + + NamedJdbcTemplate jdbcTemplate = new NamedJdbcTemplate(IdentityDatabaseUtil.getDataSource()); + + try { + return jdbcTemplate.withTransaction( + template -> template.fetchSingleRecord( + DeviceMgtSQLConstants.Query.GET_DEVICE_BY_ID, + (resultSet, rowNumber) -> new Device.Builder() + .id(resultSet.getString(DeviceMgtSQLConstants.Column.ID)) + .userId(resultSet.getString(DeviceMgtSQLConstants.Column.USER_ID)) + .deviceName(resultSet.getString(DeviceMgtSQLConstants.Column.DEVICE_NAME)) + .deviceModel(resultSet.getString(DeviceMgtSQLConstants.Column.DEVICE_MODEL)) + .publicKey(resultSet.getString(DeviceMgtSQLConstants.Column.PUBLIC_KEY)) + .status(Device.Status.valueOf( + resultSet.getString(DeviceMgtSQLConstants.Column.STATUS))) + .registeredAt(resultSet.getTimestamp(DeviceMgtSQLConstants.Column.REGISTERED_AT)) + .metadata(resultSet.getString(DeviceMgtSQLConstants.Column.METADATA)) + .build(), + preparedStatement -> { + preparedStatement.setString(DeviceMgtSQLConstants.Column.ID, deviceId); + preparedStatement.setInt(DeviceMgtSQLConstants.Column.TENANT_ID, tenantId); + })); + + } catch (TransactionException e) { + throw DeviceManagementExceptionHandler.handleServerException( + ErrorMessage.ERROR_WHILE_RETRIEVING_DEVICE, e); + } + } + + @Override + public List getDevicesByUserId(String userId, int tenantId) + throws DeviceMgtException { + + NamedJdbcTemplate jdbcTemplate = new NamedJdbcTemplate(IdentityDatabaseUtil.getDataSource()); + + try { + return jdbcTemplate., RuntimeException>withTransaction( + template -> template.executeQuery( + DeviceMgtSQLConstants.Query.GET_DEVICES_BY_USER_ID, + (resultSet, rowNumber) -> new Device.Builder() + .id(resultSet.getString(DeviceMgtSQLConstants.Column.ID)) + .userId(resultSet.getString(DeviceMgtSQLConstants.Column.USER_ID)) + .deviceName(resultSet.getString(DeviceMgtSQLConstants.Column.DEVICE_NAME)) + .deviceModel(resultSet.getString(DeviceMgtSQLConstants.Column.DEVICE_MODEL)) + .publicKey(resultSet.getString(DeviceMgtSQLConstants.Column.PUBLIC_KEY)) + .status(Device.Status.valueOf( + resultSet.getString(DeviceMgtSQLConstants.Column.STATUS))) + .registeredAt(resultSet.getTimestamp(DeviceMgtSQLConstants.Column.REGISTERED_AT)) + .metadata(resultSet.getString(DeviceMgtSQLConstants.Column.METADATA)) + .build(), + preparedStatement -> { + preparedStatement.setString(DeviceMgtSQLConstants.Column.USER_ID, userId); + preparedStatement.setInt(DeviceMgtSQLConstants.Column.TENANT_ID, tenantId); + })); + + } catch (TransactionException e) { + throw DeviceManagementExceptionHandler.handleServerException( + ErrorMessage.ERROR_WHILE_RETRIEVING_DEVICE, e); + } + } + + @Override + public List getDevices(int tenantId, int offset, int limit) throws DeviceMgtException { + + return getDevices(tenantId, offset, limit, null); + } + + @Override + public List getDevices(int tenantId, int offset, int limit, String userId) throws DeviceMgtException { + + // FETCH NEXT 0 ROWS (MS SQL) is invalid and an empty page is meaningless, so short-circuit. + if (limit <= 0) { + return Collections.emptyList(); + } + int safeOffset = Math.max(offset, 0); + boolean filterByUser = StringUtils.isNotBlank(userId); + + NamedJdbcTemplate jdbcTemplate = new NamedJdbcTemplate(IdentityDatabaseUtil.getDataSource()); + try { + PaginationStyle style = resolvePaginationStyle(); + String query = resolvePaginatedQuery(style, filterByUser); + List devices = jdbcTemplate., RuntimeException>withTransaction( + template -> template.executeQuery( + query, + (resultSet, rowNumber) -> new Device.Builder() + .id(resultSet.getString(DeviceMgtSQLConstants.Column.ID)) + .userId(resultSet.getString(DeviceMgtSQLConstants.Column.USER_ID)) + .deviceName(resultSet.getString(DeviceMgtSQLConstants.Column.DEVICE_NAME)) + .deviceModel(resultSet.getString(DeviceMgtSQLConstants.Column.DEVICE_MODEL)) + .publicKey(resultSet.getString(DeviceMgtSQLConstants.Column.PUBLIC_KEY)) + .status(Device.Status.valueOf( + resultSet.getString(DeviceMgtSQLConstants.Column.STATUS))) + .registeredAt(resultSet.getTimestamp(DeviceMgtSQLConstants.Column.REGISTERED_AT)) + .metadata(resultSet.getString(DeviceMgtSQLConstants.Column.METADATA)) + .build(), + preparedStatement -> { + preparedStatement.setInt(DeviceMgtSQLConstants.Column.TENANT_ID, tenantId); + if (filterByUser) { + preparedStatement.setString(DeviceMgtSQLConstants.Column.USER_ID, userId); + } + bindPaginationParams(preparedStatement, style, safeOffset, limit); + })); + List result = devices != null ? devices : Collections.emptyList(); + if (LOG.isDebugEnabled()) { + LOG.debug("Retrieved " + result.size() + " devices for tenant ID: " + tenantId); + } + return result; + } catch (TransactionException | DataAccessException e) { + throw DeviceManagementExceptionHandler.handleServerException( + ErrorMessage.ERROR_WHILE_RETRIEVING_DEVICE, e); + } + } + + @Override + public int getDeviceCount(int tenantId) throws DeviceMgtException { + + return getDeviceCount(tenantId, null); + } + + @Override + public int getDeviceCount(int tenantId, String userId) throws DeviceMgtException { + + boolean filterByUser = StringUtils.isNotBlank(userId); + String query = filterByUser + ? DeviceMgtSQLConstants.Query.GET_DEVICES_COUNT_BY_USER + : DeviceMgtSQLConstants.Query.GET_DEVICES_COUNT; + + NamedJdbcTemplate jdbcTemplate = new NamedJdbcTemplate(IdentityDatabaseUtil.getDataSource()); + try { + Integer count = jdbcTemplate.withTransaction( + template -> template.fetchSingleRecord( + query, + (resultSet, rowNumber) -> resultSet.getInt(1), + preparedStatement -> { + preparedStatement.setInt(DeviceMgtSQLConstants.Column.TENANT_ID, tenantId); + if (filterByUser) { + preparedStatement.setString(DeviceMgtSQLConstants.Column.USER_ID, userId); + } + })); + int result = count != null ? count : 0; + if (LOG.isDebugEnabled()) { + LOG.debug("Retrieved device count: " + result + " for tenant ID: " + tenantId); + } + return result; + } catch (TransactionException e) { + throw DeviceManagementExceptionHandler.handleServerException( + ErrorMessage.ERROR_WHILE_RETRIEVING_DEVICE, e); + } + } + + /** + * Supported database-specific pagination dialects. + */ + private enum PaginationStyle { + DEFAULT, MSSQL, ORACLE, DB2 + } + + private PaginationStyle resolvePaginationStyle() throws DataAccessException { + + // H2, MySQL, MariaDB and PostgreSQL all accept the LIMIT ... OFFSET ... syntax (DEFAULT). + if (JdbcUtils.isOracleDB()) { + return PaginationStyle.ORACLE; + } + if (JdbcUtils.isDB2DB()) { + return PaginationStyle.DB2; + } + if (JdbcUtils.isMSSqlDB()) { + return PaginationStyle.MSSQL; + } + return PaginationStyle.DEFAULT; + } + + private String resolvePaginatedQuery(PaginationStyle style, boolean filterByUser) { + + if (filterByUser) { + switch (style) { + case ORACLE: + return DeviceMgtSQLConstants.Query.GET_ALL_DEVICES_PAGINATED_BY_USER_ORACLE; + case DB2: + return DeviceMgtSQLConstants.Query.GET_ALL_DEVICES_PAGINATED_BY_USER_DB2; + case MSSQL: + return DeviceMgtSQLConstants.Query.GET_ALL_DEVICES_PAGINATED_BY_USER_MSSQL; + default: + return DeviceMgtSQLConstants.Query.GET_ALL_DEVICES_PAGINATED_BY_USER; + } + } + switch (style) { + case ORACLE: + return DeviceMgtSQLConstants.Query.GET_ALL_DEVICES_PAGINATED_ORACLE; + case DB2: + return DeviceMgtSQLConstants.Query.GET_ALL_DEVICES_PAGINATED_DB2; + case MSSQL: + return DeviceMgtSQLConstants.Query.GET_ALL_DEVICES_PAGINATED_MSSQL; + default: + return DeviceMgtSQLConstants.Query.GET_ALL_DEVICES_PAGINATED; + } + } + + private void bindPaginationParams(NamedPreparedStatement preparedStatement, PaginationStyle style, + int offset, int limit) throws SQLException { + + switch (style) { + case ORACLE: + preparedStatement.setInt(DeviceMgtSQLConstants.Column.UPPER_BOUND, offset + limit); + preparedStatement.setInt(DeviceMgtSQLConstants.Column.OFFSET, offset); + break; + case DB2: + preparedStatement.setInt(DeviceMgtSQLConstants.Column.LOWER_BOUND, offset + 1); + preparedStatement.setInt(DeviceMgtSQLConstants.Column.UPPER_BOUND, offset + limit); + break; + case MSSQL: + preparedStatement.setInt(DeviceMgtSQLConstants.Column.OFFSET, offset); + preparedStatement.setInt(DeviceMgtSQLConstants.Column.LIMIT, limit); + break; + default: + preparedStatement.setInt(DeviceMgtSQLConstants.Column.LIMIT, limit); + preparedStatement.setInt(DeviceMgtSQLConstants.Column.OFFSET, offset); + } + } + + @Override + public Device updateDeviceName(String deviceId, String deviceName, int tenantId) + throws DeviceMgtException { + + NamedJdbcTemplate jdbcTemplate = new NamedJdbcTemplate(IdentityDatabaseUtil.getDataSource()); + + try { + jdbcTemplate.withTransaction(template -> { + template.executeUpdate( + DeviceMgtSQLConstants.Query.UPDATE_DEVICE_NAME, + preparedStatement -> { + preparedStatement.setString(DeviceMgtSQLConstants.Column.DEVICE_NAME, deviceName); + preparedStatement.setString(DeviceMgtSQLConstants.Column.ID, deviceId); + preparedStatement.setInt(DeviceMgtSQLConstants.Column.TENANT_ID, tenantId); + }); + return null; + }); + + } catch (TransactionException e) { + throw DeviceManagementExceptionHandler.handleServerException( + ErrorMessage.ERROR_WHILE_UPDATING_DEVICE, e); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Device name updated for device ID: " + deviceId); + } + + return getDeviceById(deviceId, tenantId); + } + + @Override + public Device changeDeviceStatus(String deviceId, Device.Status status, int tenantId) + throws DeviceMgtException { + + NamedJdbcTemplate jdbcTemplate = new NamedJdbcTemplate(IdentityDatabaseUtil.getDataSource()); + + try { + jdbcTemplate.withTransaction(template -> { + template.executeUpdate( + DeviceMgtSQLConstants.Query.CHANGE_DEVICE_STATUS, + preparedStatement -> { + preparedStatement.setString(DeviceMgtSQLConstants.Column.STATUS, status.name()); + preparedStatement.setString(DeviceMgtSQLConstants.Column.ID, deviceId); + preparedStatement.setInt(DeviceMgtSQLConstants.Column.TENANT_ID, tenantId); + }); + return null; + }); + + } catch (TransactionException e) { + throw DeviceManagementExceptionHandler.handleServerException( + ErrorMessage.ERROR_WHILE_UPDATING_DEVICE, e); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Device status changed to " + status.name() + " for device ID: " + deviceId); + } + + return getDeviceById(deviceId, tenantId); + } + + @Override + public void deleteDevice(String deviceId, int tenantId) + throws DeviceMgtException { + + NamedJdbcTemplate jdbcTemplate = new NamedJdbcTemplate(IdentityDatabaseUtil.getDataSource()); + + try { + jdbcTemplate.withTransaction(template -> { + template.executeUpdate( + DeviceMgtSQLConstants.Query.DELETE_USER_DEVICE, + preparedStatement -> { + preparedStatement.setString(DeviceMgtSQLConstants.Column.DEVICE_ID, deviceId); + preparedStatement.setInt(DeviceMgtSQLConstants.Column.TENANT_ID, tenantId); + }); + template.executeUpdate( + DeviceMgtSQLConstants.Query.DELETE_DEVICE, + preparedStatement -> { + preparedStatement.setString(DeviceMgtSQLConstants.Column.ID, deviceId); + preparedStatement.setInt(DeviceMgtSQLConstants.Column.TENANT_ID, tenantId); + }); + return null; + }); + + } catch (TransactionException e) { + throw DeviceManagementExceptionHandler.handleServerException( + ErrorMessage.ERROR_WHILE_DELETING_DEVICE, e); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Device deleted with ID: " + deviceId); + } + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/service/impl/DeviceManagementServiceImpl.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/service/impl/DeviceManagementServiceImpl.java new file mode 100644 index 000000000000..38da3a2d0b09 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/service/impl/DeviceManagementServiceImpl.java @@ -0,0 +1,207 @@ +/* +* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +* +* 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.device.mgt.internal.service.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.device.mgt.api.constant.ErrorMessage; +import org.wso2.carbon.identity.device.mgt.api.exception.DeviceMgtException; +import org.wso2.carbon.identity.device.mgt.api.model.Device; +import org.wso2.carbon.identity.device.mgt.api.service.DeviceManagementService; +import org.wso2.carbon.identity.device.mgt.internal.dao.DeviceManagementDAO; +import org.wso2.carbon.identity.device.mgt.internal.dao.impl.CacheBackedDeviceManagementDAO; +import org.wso2.carbon.identity.device.mgt.internal.dao.impl.DeviceManagementDAOImpl; +import org.wso2.carbon.identity.device.mgt.internal.util.DeviceManagementAuditLogger; +import org.wso2.carbon.identity.device.mgt.internal.util.DeviceManagementExceptionHandler; +import org.wso2.carbon.identity.device.mgt.internal.util.DeviceValidator; + +import java.util.List; + +/** + * Default implementation of {@link DeviceManagementService}. + */ +public class DeviceManagementServiceImpl implements DeviceManagementService { + + private static final Log LOG = LogFactory.getLog(DeviceManagementServiceImpl.class); + private static final DeviceManagementServiceImpl INSTANCE = new DeviceManagementServiceImpl(); + private static final DeviceManagementAuditLogger AUDIT_LOGGER = new DeviceManagementAuditLogger(); + private static final DeviceValidator DEVICE_VALIDATOR = new DeviceValidator(); + private final DeviceManagementDAO deviceManagementDAO; + + private DeviceManagementServiceImpl() { + deviceManagementDAO = new CacheBackedDeviceManagementDAO(new DeviceManagementDAOImpl()); + } + + /** + * Returns the service singleton instance. + * + * @return Service singleton. + */ + public static DeviceManagementServiceImpl getInstance() { + return INSTANCE; + } + + @Override + public void registerDevice(Device device, String tenantDomain) throws DeviceMgtException { + + DEVICE_VALIDATOR.validateDeviceForRegistration(device); + deviceManagementDAO.registerDevice(device, IdentityTenantUtil.getTenantId(tenantDomain)); + + AUDIT_LOGGER.printAuditLog(DeviceManagementAuditLogger.Operation.REGISTER, device); + + if (LOG.isDebugEnabled()) { + LOG.debug("Device registered for user: " + device.getUserId() + + " in tenant: " + tenantDomain + " with device ID: " + device.getId()); + } + } + + @Override + public Device getDeviceById(String deviceId, String tenantDomain) + throws DeviceMgtException { + + DEVICE_VALIDATOR.validateRequiredField(deviceId, "deviceId"); + return deviceManagementDAO.getDeviceById( + deviceId, IdentityTenantUtil.getTenantId(tenantDomain)); + } + + @Override + public Device getActiveDeviceById(String deviceId, String tenantDomain) throws DeviceMgtException { + + Device device = getDeviceById(deviceId, tenantDomain); + if (device == null || device.getStatus() != Device.Status.ACTIVE) { + return null; + } + return device; + } + + @Override + public List getDevicesByUserId(String userId, String tenantDomain) + throws DeviceMgtException { + + DEVICE_VALIDATOR.validateRequiredField(userId, "userId"); + return deviceManagementDAO.getDevicesByUserId( + userId, IdentityTenantUtil.getTenantId(tenantDomain)); + } + + @Override + public List getDevices(String tenantDomain, int offset, int limit) throws DeviceMgtException { + + return deviceManagementDAO.getDevices( + IdentityTenantUtil.getTenantId(tenantDomain), offset, DEVICE_VALIDATOR.validateLimit(limit)); + } + + @Override + public List getDevices(String tenantDomain, int offset, int limit, String userId) + throws DeviceMgtException { + + return deviceManagementDAO.getDevices( + IdentityTenantUtil.getTenantId(tenantDomain), offset, DEVICE_VALIDATOR.validateLimit(limit), userId); + } + + @Override + public int getDeviceCount(String tenantDomain) throws DeviceMgtException { + + return deviceManagementDAO.getDeviceCount(IdentityTenantUtil.getTenantId(tenantDomain)); + } + + @Override + public int getDeviceCount(String tenantDomain, String userId) throws DeviceMgtException { + + return deviceManagementDAO.getDeviceCount(IdentityTenantUtil.getTenantId(tenantDomain), userId); + } + + @Override + public Device updateDeviceName(String deviceId, String deviceName, String tenantDomain) + throws DeviceMgtException { + + DEVICE_VALIDATOR.validateRequiredField(deviceId, "deviceId"); + DEVICE_VALIDATOR.validateRequiredField(deviceName, "deviceName"); + + Device updatedDevice = deviceManagementDAO.updateDeviceName( + deviceId, deviceName, IdentityTenantUtil.getTenantId(tenantDomain)); + + if (updatedDevice == null) { + throw DeviceManagementExceptionHandler.handleClientException(ErrorMessage.ERROR_DEVICE_NOT_FOUND, deviceId); + } + + AUDIT_LOGGER.printAuditLog(DeviceManagementAuditLogger.Operation.UPDATE, updatedDevice); + return updatedDevice; + } + + @Override + public Device activateDevice(String deviceId, String tenantDomain) throws DeviceMgtException { + + DEVICE_VALIDATOR.validateRequiredField(deviceId, "deviceId"); + + Device updated = deviceManagementDAO.changeDeviceStatus( + deviceId, Device.Status.ACTIVE, IdentityTenantUtil.getTenantId(tenantDomain)); + + if (updated == null) { + throw DeviceManagementExceptionHandler.handleClientException(ErrorMessage.ERROR_DEVICE_NOT_FOUND, deviceId); + } + + AUDIT_LOGGER.printAuditLog(DeviceManagementAuditLogger.Operation.ACTIVATE, updated); + + if (LOG.isDebugEnabled()) { + LOG.debug("Device activated with ID: " + deviceId + " in tenant: " + tenantDomain); + } + return updated; + } + + @Override + public Device deactivateDevice(String deviceId, String tenantDomain) throws DeviceMgtException { + + DEVICE_VALIDATOR.validateRequiredField(deviceId, "deviceId"); + + Device updated = deviceManagementDAO.changeDeviceStatus( + deviceId, Device.Status.INACTIVE, IdentityTenantUtil.getTenantId(tenantDomain)); + + if (updated == null) { + throw DeviceManagementExceptionHandler.handleClientException(ErrorMessage.ERROR_DEVICE_NOT_FOUND, deviceId); + } + + AUDIT_LOGGER.printAuditLog(DeviceManagementAuditLogger.Operation.DEACTIVATE, updated); + + if (LOG.isDebugEnabled()) { + LOG.debug("Device deactivated with ID: " + deviceId + " in tenant: " + tenantDomain); + } + return updated; + } + + @Override + public void deleteDevice(String deviceId, String tenantDomain) + throws DeviceMgtException { + + DEVICE_VALIDATOR.validateRequiredField(deviceId, "deviceId"); + + Device existing = deviceManagementDAO.getDeviceById( + deviceId, IdentityTenantUtil.getTenantId(tenantDomain)); + DEVICE_VALIDATOR.validateDeviceExists(existing, deviceId); + + deviceManagementDAO.deleteDevice( + deviceId, IdentityTenantUtil.getTenantId(tenantDomain)); + + AUDIT_LOGGER.printAuditLog(DeviceManagementAuditLogger.Operation.DELETE, deviceId); + + if (LOG.isDebugEnabled()) { + LOG.debug("Device deleted with ID: " + deviceId + " in tenant: " + tenantDomain); + } + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/util/DeviceManagementAuditLogger.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/util/DeviceManagementAuditLogger.java new file mode 100644 index 000000000000..dcf92bfe023d --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/util/DeviceManagementAuditLogger.java @@ -0,0 +1,203 @@ +/* + * 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.device.mgt.internal.util; + +import org.apache.commons.lang.StringUtils; +import org.json.JSONObject; +import org.wso2.carbon.context.CarbonContext; +import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; +import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.device.mgt.api.model.Device; +import org.wso2.carbon.utils.AuditLog; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +import static org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils.jsonObjectToMap; +import static org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils.triggerAuditLogEvent; + +/** + * Audit logger for device management state changes. + * Emits an audit record whenever a device credential is registered, renamed or deleted so the + * "who changed which device" trail is preserved. Read operations are intentionally not audited. + */ +public class DeviceManagementAuditLogger { + + private static final String DEVICE_TARGET_TYPE = "Device"; + private static final String FINGERPRINT_UNAVAILABLE = "unavailable"; + + /** + * Print an audit log for an operation that carries the full device state. + * + * @param operation Operation associated with the state change. + * @param device Device to be logged, or {@code null} if unavailable. + */ + public void printAuditLog(Operation operation, Device device) { + + if (device == null) { + printAuditLog(operation, (String) null); + return; + } + JSONObject data = createAuditLogEntry(device); + buildAuditLog(device.getId(), operation, data); + } + + /** + * Print an audit log for an operation identified only by the device ID (e.g. delete). + * + * @param operation Operation associated with the state change. + * @param deviceId ID of the device to be logged. + */ + public void printAuditLog(Operation operation, String deviceId) { + + JSONObject data = new JSONObject(); + data.put(LogConstants.ID_FIELD, deviceId != null ? deviceId : JSONObject.NULL); + buildAuditLog(deviceId, operation, data); + } + + /** + * Build and trigger the audit log event. + * + * @param targetId Target device ID. + * @param operation Operation to be logged. + * @param data Data to be logged. + */ + private void buildAuditLog(String targetId, Operation operation, JSONObject data) { + + String initiatorId = getInitiatorId(); + AuditLog.AuditLogBuilder auditLogBuilder = new AuditLog.AuditLogBuilder( + initiatorId, + LoggerUtils.getInitiatorType(initiatorId), + targetId, + DEVICE_TARGET_TYPE, + operation.getLogAction()) + .data(jsonObjectToMap(data)); + triggerAuditLogEvent(auditLogBuilder); + } + + /** + * Create audit log data for the given device. + * The device owner is recorded as the opaque user ID (a UUID, not PII, so not masked) and the + * public key is reduced to a fingerprint; the raw public key and free-form metadata are never logged. + * + * @param device Device to be logged. + * @return Audit log data as a JSONObject. + */ + private JSONObject createAuditLogEntry(Device device) { + + JSONObject data = new JSONObject(); + data.put(LogConstants.ID_FIELD, device.getId() != null ? device.getId() : JSONObject.NULL); + data.put(LogConstants.DEVICE_NAME_FIELD, + device.getDeviceName() != null ? device.getDeviceName() : JSONObject.NULL); + data.put(LogConstants.DEVICE_MODEL_FIELD, + device.getDeviceModel() != null ? device.getDeviceModel() : JSONObject.NULL); + data.put(LogConstants.STATUS_FIELD, + device.getStatus() != null ? device.getStatus().name() : JSONObject.NULL); + data.put(LogConstants.REGISTERED_AT_FIELD, + device.getRegisteredAt() != null ? String.valueOf(device.getRegisteredAt()) : JSONObject.NULL); + data.put(LogConstants.USER_ID_FIELD, device.getUserId() != null + ? device.getUserId() : JSONObject.NULL); + data.put(LogConstants.PUBLIC_KEY_FINGERPRINT_FIELD, device.getPublicKey() != null + ? fingerprint(device.getPublicKey()) : JSONObject.NULL); + return data; + } + + /** + * Resolve the initiator of the audit event from the carbon context, falling back to System. + * + * @return Initiator ID. + */ + private String getInitiatorId() { + + CarbonContext carbonContext = CarbonContext.getThreadLocalCarbonContext(); + String username = carbonContext.getUsername(); + String tenantDomain = carbonContext.getTenantDomain(); + + if (StringUtils.isBlank(username)) { + return LoggerUtils.Initiator.System.name(); + } + String initiator = null; + if (StringUtils.isNotBlank(tenantDomain)) { + initiator = IdentityUtil.getInitiatorId(username, tenantDomain); + } + return StringUtils.isNotBlank(initiator) ? initiator : LoggerUtils.getMaskedContent(username); + } + + /** + * Produce a short, non-reversible fingerprint of the device public key for correlation. + * + * @param publicKey Base64 encoded public key. + * @return Truncated SHA-256 fingerprint, or a placeholder if hashing is unavailable. + */ + private String fingerprint(String publicKey) { + + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(publicKey.getBytes(StandardCharsets.UTF_8)); + String encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(hash); + return encoded.length() > 16 ? encoded.substring(0, 16) : encoded; + } catch (NoSuchAlgorithmException e) { + return FINGERPRINT_UNAVAILABLE; + } + } + + /** + * Device management operations to be audited. + */ + public enum Operation { + REGISTER("register-device"), + UPDATE("update-device"), + DELETE("delete-device"), + ACTIVATE("activate-device"), + DEACTIVATE("deactivate-device"); + + private final String logAction; + + Operation(String logAction) { + + this.logAction = logAction; + } + + /** + * Get the log action associated with the operation. + * + * @return Log action string. + */ + public String getLogAction() { + + return this.logAction; + } + } + + /** + * Device management audit log field constants. + */ + private static class LogConstants { + + private static final String ID_FIELD = "Id"; + private static final String DEVICE_NAME_FIELD = "DeviceName"; + private static final String DEVICE_MODEL_FIELD = "DeviceModel"; + private static final String STATUS_FIELD = "Status"; + private static final String REGISTERED_AT_FIELD = "RegisteredAt"; + private static final String USER_ID_FIELD = "UserId"; + private static final String PUBLIC_KEY_FINGERPRINT_FIELD = "PublicKeyFingerprint"; + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/util/DeviceManagementExceptionHandler.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/util/DeviceManagementExceptionHandler.java new file mode 100644 index 000000000000..e39cd12e2694 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/util/DeviceManagementExceptionHandler.java @@ -0,0 +1,93 @@ +/* + * 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.device.mgt.internal.util; + +import org.wso2.carbon.identity.device.mgt.api.constant.ErrorMessage; +import org.wso2.carbon.identity.device.mgt.api.exception.DeviceMgtClientException; +import org.wso2.carbon.identity.device.mgt.api.exception.DeviceMgtServerException; + +/** + * Utility class for constructing device management exceptions with the standard + * (message, description, code[, cause]) shape backed by ErrorMessage entries. + */ +public class DeviceManagementExceptionHandler { + + private DeviceManagementExceptionHandler() { + + } + + /** + * Builds a client exception from the given error. + * + * @param error Error message entry. + * @param data Optional values to format into the description. + * @return Device management client exception. + */ + public static DeviceMgtClientException handleClientException(ErrorMessage error, String... data) { + + return new DeviceMgtClientException(error.getMessage(), formatDescription(error, data), error.getCode()); + } + + /** + * Builds a client exception from the given error and root cause. + * + * @param error Error message entry. + * @param e Root cause. + * @param data Optional values to format into the description. + * @return Device management client exception. + */ + public static DeviceMgtClientException handleClientException(ErrorMessage error, Throwable e, String... data) { + + return new DeviceMgtClientException(error.getMessage(), formatDescription(error, data), error.getCode(), e); + } + + /** + * Builds a server exception from the given error and root cause. + * + * @param error Error message entry. + * @param e Root cause. + * @param data Optional values to format into the description. + * @return Device management server exception. + */ + public static DeviceMgtServerException handleServerException(ErrorMessage error, Throwable e, String... data) { + + return new DeviceMgtServerException(error.getMessage(), formatDescription(error, data), error.getCode(), e); + } + + /** + * Builds a server exception from the given error. + * + * @param error Error message entry. + * @param data Optional values to format into the description. + * @return Device management server exception. + */ + public static DeviceMgtServerException handleServerException(ErrorMessage error, String... data) { + + return new DeviceMgtServerException(error.getMessage(), formatDescription(error, data), error.getCode()); + } + + private static String formatDescription(ErrorMessage error, String... data) { + + String description = error.getDescription(); + if (data != null && data.length > 0) { + description = String.format(description, (Object[]) data); + } + return description; + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/util/DeviceValidator.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/util/DeviceValidator.java new file mode 100644 index 000000000000..b9092f9bff66 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/main/java/org/wso2/carbon/identity/device/mgt/internal/util/DeviceValidator.java @@ -0,0 +1,145 @@ +/* + * 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.device.mgt.internal.util; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.device.mgt.api.constant.ErrorMessage; +import org.wso2.carbon.identity.device.mgt.api.exception.DeviceMgtClientException; +import org.wso2.carbon.identity.device.mgt.api.exception.DeviceMgtException; +import org.wso2.carbon.identity.device.mgt.api.model.Device; + +/** + * Device validator class. + */ +public class DeviceValidator { + + private static final Log LOG = LogFactory.getLog(DeviceValidator.class); + + /** + * Validates that a required field is not blank. + * + * @param value Value of the field. + * @param fieldName Name of the field. + * @throws DeviceMgtClientException If the field is not set. + */ + public void validateRequiredField(String value, String fieldName) + throws DeviceMgtClientException { + + if (StringUtils.isBlank(value)) { + throw DeviceManagementExceptionHandler.handleClientException( + ErrorMessage.ERROR_INVALID_DEVICE_FIELD, fieldName); + } + } + + /** + * Validates that the device carries every field the registration layer requires. + * The device is constructed internally during the registration flow, so a missing field indicates + * that the device was not fully built before registration rather than invalid user input. Validating + * here keeps such a failure a clear, coded error instead of a constraint violation or a + * NullPointerException raised inside the data layer. + * The device model and the metadata are optional and are therefore not validated. + * + * @param device Device to be registered. + * @throws DeviceMgtException If the device or any of its required fields is not set. + */ + public void validateDeviceForRegistration(Device device) throws DeviceMgtException { + + if (device == null) { + throw DeviceManagementExceptionHandler.handleServerException( + ErrorMessage.ERROR_DEVICE_FIELD_REQUIRED, "device"); + } + if (StringUtils.isBlank(device.getUserId())) { + throw DeviceManagementExceptionHandler.handleServerException( + ErrorMessage.ERROR_USER_ID_REQUIRED); + } + + validateRequiredDeviceField(device.getId(), "id"); + validateRequiredDeviceField(device.getDeviceName(), "deviceName"); + validateRequiredDeviceField(device.getPublicKey(), "publicKey"); + + if (device.getStatus() == null) { + throw DeviceManagementExceptionHandler.handleServerException( + ErrorMessage.ERROR_DEVICE_FIELD_REQUIRED, "status"); + } + if (device.getRegisteredAt() == null) { + throw DeviceManagementExceptionHandler.handleServerException( + ErrorMessage.ERROR_DEVICE_FIELD_REQUIRED, "registeredAt"); + } + } + + /** + * Validates a required device field that is expected to be set before registration. + * + * @param value Value of the field. + * @param fieldName Name of the field. + * @throws DeviceMgtException If the field is not set. + */ + public void validateRequiredDeviceField(String value, String fieldName) throws DeviceMgtException { + + if (StringUtils.isBlank(value)) { + throw DeviceManagementExceptionHandler.handleServerException( + ErrorMessage.ERROR_DEVICE_FIELD_REQUIRED, fieldName); + } + } + + /** + * Validates the requested page size against the maximum items per page configured for the server. + * A page size larger than the configured maximum is capped, so that a caller cannot load an + * unbounded number of devices into memory. + * + * @param limit Requested page size. + * @return The page size to use, capped at the configured maximum. + * @throws DeviceMgtClientException If the requested page size is negative. + */ + public int validateLimit(int limit) throws DeviceMgtClientException { + + if (limit < 0) { + throw DeviceManagementExceptionHandler.handleClientException( + ErrorMessage.ERROR_INVALID_DEVICE_FIELD, "limit"); + } + + int maximumItemsPerPage = IdentityUtil.getMaximumItemPerPage(); + if (limit > maximumItemsPerPage) { + if (LOG.isDebugEnabled()) { + LOG.debug("Given limit: " + limit + " exceeds the maximum items per page. Using the maximum: " + + maximumItemsPerPage); + } + return maximumItemsPerPage; + } + return limit; + } + + /** + * Validates that a device looked up by its identifier exists. + * + * @param device Device previously looked up by {@code deviceId}, or {@code null} if not found. + * @param deviceId UUID of the device, used only for the error message. + * @throws DeviceMgtException If the device does not exist. + */ + public void validateDeviceExists(Device device, String deviceId) throws DeviceMgtException { + + if (device == null) { + throw DeviceManagementExceptionHandler.handleClientException( + ErrorMessage.ERROR_DEVICE_NOT_FOUND, deviceId); + } + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/java/org/wso2/carbon/identity/device/mgt/dao/CacheBackedDeviceManagementDAOTest.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/java/org/wso2/carbon/identity/device/mgt/dao/CacheBackedDeviceManagementDAOTest.java new file mode 100644 index 000000000000..11895d888376 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/java/org/wso2/carbon/identity/device/mgt/dao/CacheBackedDeviceManagementDAOTest.java @@ -0,0 +1,256 @@ +/* +* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +* +* 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.device.mgt.dao; + +import org.mockito.MockedStatic; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.common.testng.WithCarbonHome; +import org.wso2.carbon.identity.common.testng.WithRealmService; +import org.wso2.carbon.identity.core.internal.component.IdentityCoreServiceDataHolder; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.device.mgt.api.exception.DeviceMgtException; +import org.wso2.carbon.identity.device.mgt.api.model.Device; +import org.wso2.carbon.identity.device.mgt.internal.cache.DeviceCache; +import org.wso2.carbon.identity.device.mgt.internal.cache.DeviceCacheEntry; +import org.wso2.carbon.identity.device.mgt.internal.cache.DeviceCacheKey; +import org.wso2.carbon.identity.device.mgt.internal.dao.DeviceManagementDAO; +import org.wso2.carbon.identity.device.mgt.internal.dao.impl.CacheBackedDeviceManagementDAO; + +import java.util.Collections; +import java.util.List; + +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +/** + * Unit tests for {@link CacheBackedDeviceManagementDAO}. + */ +@WithCarbonHome +@WithRealmService(injectToSingletons = {IdentityCoreServiceDataHolder.class}) +public class CacheBackedDeviceManagementDAOTest { + + private static final String DEVICE_ID = "deviceId"; + private static final int TENANT_ID = 1; + private static final int OTHER_TENANT_ID = 2; + private static final String TENANT_DOMAIN = "tenant1.example.com"; + private static final String OTHER_TENANT_DOMAIN = "tenant2.example.com"; + + private DeviceManagementDAO deviceManagementDAO; + private CacheBackedDeviceManagementDAO cacheBackedDeviceManagementDAO; + private DeviceCache deviceCache; + private MockedStatic identityTenantUtilMocked; + + @BeforeClass + public void setUpClass() { + + // BaseCache resolves a tenant domain per tenantId internally (for its own OSGi cache + // ownership checks); stub distinct domains so the two tenants used below are genuinely + // isolated instead of both falling back to the same default domain. + identityTenantUtilMocked = mockStatic(IdentityTenantUtil.class, CALLS_REAL_METHODS); + when(IdentityTenantUtil.getTenantDomain(TENANT_ID)).thenReturn(TENANT_DOMAIN); + when(IdentityTenantUtil.getTenantDomain(OTHER_TENANT_ID)).thenReturn(OTHER_TENANT_DOMAIN); + + deviceCache = DeviceCache.getInstance(); + } + + @AfterClass + public void tearDownClass() { + + identityTenantUtilMocked.close(); + } + + @BeforeMethod + public void setUp() { + + deviceManagementDAO = mock(DeviceManagementDAO.class); + cacheBackedDeviceManagementDAO = new CacheBackedDeviceManagementDAO(deviceManagementDAO); + + // Reset cache state so tests don't leak into each other. + deviceCache.clear(TENANT_ID); + deviceCache.clear(OTHER_TENANT_ID); + } + + @Test + public void testRegisterDeviceDoesNotTouchCache() throws DeviceMgtException { + + Device device = mock(Device.class); + when(deviceManagementDAO.registerDevice(device, TENANT_ID)).thenReturn(device); + + cacheBackedDeviceManagementDAO.registerDevice(device, TENANT_ID); + + verify(deviceManagementDAO).registerDevice(device, TENANT_ID); + assertNull(deviceCache.getValueFromCache(new DeviceCacheKey(DEVICE_ID), TENANT_ID)); + } + + @Test + public void testGetDeviceByIdCacheMissThenCacheHit() throws DeviceMgtException { + + Device device = mock(Device.class); + when(deviceManagementDAO.getDeviceById(DEVICE_ID, TENANT_ID)).thenReturn(device); + + Device firstResult = cacheBackedDeviceManagementDAO.getDeviceById(DEVICE_ID, TENANT_ID); + + assertEquals(firstResult, device); + verify(deviceManagementDAO, times(1)).getDeviceById(DEVICE_ID, TENANT_ID); + assertEquals(deviceCache.getValueFromCache(new DeviceCacheKey(DEVICE_ID), TENANT_ID).getDevice(), device); + + Device secondResult = cacheBackedDeviceManagementDAO.getDeviceById(DEVICE_ID, TENANT_ID); + + assertEquals(secondResult, device); + // The inner DAO must not be invoked again — the second read is served from the cache. + verify(deviceManagementDAO, times(1)).getDeviceById(DEVICE_ID, TENANT_ID); + } + + @Test + public void testGetDeviceByIdCacheHitWhenPrePopulated() throws DeviceMgtException { + + Device device = mock(Device.class); + deviceCache.addToCache(new DeviceCacheKey(DEVICE_ID), new DeviceCacheEntry(device), TENANT_ID); + + Device result = cacheBackedDeviceManagementDAO.getDeviceById(DEVICE_ID, TENANT_ID); + + assertEquals(result, device); + verify(deviceManagementDAO, never()).getDeviceById(DEVICE_ID, TENANT_ID); + } + + @Test + public void testGetDeviceByIdNullResultIsNotCached() throws DeviceMgtException { + + when(deviceManagementDAO.getDeviceById(DEVICE_ID, TENANT_ID)).thenReturn(null); + + Device result = cacheBackedDeviceManagementDAO.getDeviceById(DEVICE_ID, TENANT_ID); + + assertNull(result); + assertNull(deviceCache.getValueFromCache(new DeviceCacheKey(DEVICE_ID), TENANT_ID)); + } + + @Test + public void testUpdateDeviceNameInvalidatesCache() throws DeviceMgtException { + + Device cached = mock(Device.class); + deviceCache.addToCache(new DeviceCacheKey(DEVICE_ID), new DeviceCacheEntry(cached), TENANT_ID); + + cacheBackedDeviceManagementDAO.updateDeviceName(DEVICE_ID, "New Name", TENANT_ID); + + verify(deviceManagementDAO).updateDeviceName(DEVICE_ID, "New Name", TENANT_ID); + assertNull(deviceCache.getValueFromCache(new DeviceCacheKey(DEVICE_ID), TENANT_ID)); + + // The next read must miss the cache and hit the inner DAO again. + Device fresh = mock(Device.class); + when(deviceManagementDAO.getDeviceById(DEVICE_ID, TENANT_ID)).thenReturn(fresh); + Device result = cacheBackedDeviceManagementDAO.getDeviceById(DEVICE_ID, TENANT_ID); + + assertEquals(result, fresh); + verify(deviceManagementDAO).getDeviceById(DEVICE_ID, TENANT_ID); + } + + @Test + public void testChangeDeviceStatusInvalidatesCache() throws DeviceMgtException { + + Device cached = mock(Device.class); + deviceCache.addToCache(new DeviceCacheKey(DEVICE_ID), new DeviceCacheEntry(cached), TENANT_ID); + + cacheBackedDeviceManagementDAO.changeDeviceStatus(DEVICE_ID, Device.Status.INACTIVE, TENANT_ID); + + verify(deviceManagementDAO).changeDeviceStatus(DEVICE_ID, Device.Status.INACTIVE, TENANT_ID); + assertNull(deviceCache.getValueFromCache(new DeviceCacheKey(DEVICE_ID), TENANT_ID)); + + Device fresh = mock(Device.class); + when(deviceManagementDAO.getDeviceById(DEVICE_ID, TENANT_ID)).thenReturn(fresh); + Device result = cacheBackedDeviceManagementDAO.getDeviceById(DEVICE_ID, TENANT_ID); + + assertEquals(result, fresh); + verify(deviceManagementDAO).getDeviceById(DEVICE_ID, TENANT_ID); + } + + @Test + public void testDeleteDeviceInvalidatesCache() throws DeviceMgtException { + + Device cached = mock(Device.class); + deviceCache.addToCache(new DeviceCacheKey(DEVICE_ID), new DeviceCacheEntry(cached), TENANT_ID); + + cacheBackedDeviceManagementDAO.deleteDevice(DEVICE_ID, TENANT_ID); + + verify(deviceManagementDAO).deleteDevice(DEVICE_ID, TENANT_ID); + assertNull(deviceCache.getValueFromCache(new DeviceCacheKey(DEVICE_ID), TENANT_ID)); + + Device fresh = mock(Device.class); + when(deviceManagementDAO.getDeviceById(DEVICE_ID, TENANT_ID)).thenReturn(fresh); + Device result = cacheBackedDeviceManagementDAO.getDeviceById(DEVICE_ID, TENANT_ID); + + assertEquals(result, fresh); + verify(deviceManagementDAO).getDeviceById(DEVICE_ID, TENANT_ID); + } + + @Test + public void testGetDevicesFilteredByUserDelegatesToDao() throws DeviceMgtException { + + Device device = mock(Device.class); + when(deviceManagementDAO.getDevices(TENANT_ID, 0, 100, "alice@example.com")) + .thenReturn(Collections.singletonList(device)); + + List result = cacheBackedDeviceManagementDAO.getDevices(TENANT_ID, 0, 100, "alice@example.com"); + + assertEquals(result, Collections.singletonList(device)); + verify(deviceManagementDAO).getDevices(TENANT_ID, 0, 100, "alice@example.com"); + } + + @Test + public void testGetDeviceCountFilteredByUserDelegatesToDao() throws DeviceMgtException { + + when(deviceManagementDAO.getDeviceCount(TENANT_ID, "alice@example.com")).thenReturn(2); + + int result = cacheBackedDeviceManagementDAO.getDeviceCount(TENANT_ID, "alice@example.com"); + + assertEquals(result, 2); + verify(deviceManagementDAO).getDeviceCount(TENANT_ID, "alice@example.com"); + } + + @Test + public void testTenantIsolation() throws DeviceMgtException { + + Device tenantDevice = mock(Device.class); + Device otherTenantDevice = mock(Device.class); + when(deviceManagementDAO.getDeviceById(DEVICE_ID, TENANT_ID)).thenReturn(tenantDevice); + when(deviceManagementDAO.getDeviceById(DEVICE_ID, OTHER_TENANT_ID)).thenReturn(otherTenantDevice); + + Device resultTenant = cacheBackedDeviceManagementDAO.getDeviceById(DEVICE_ID, TENANT_ID); + Device resultOtherTenant = cacheBackedDeviceManagementDAO.getDeviceById(DEVICE_ID, OTHER_TENANT_ID); + + assertEquals(resultTenant, tenantDevice); + assertEquals(resultOtherTenant, otherTenantDevice); + + // Each tenant's cache entry is independent — re-reading each still hits the cache, not the DAO. + cacheBackedDeviceManagementDAO.getDeviceById(DEVICE_ID, TENANT_ID); + cacheBackedDeviceManagementDAO.getDeviceById(DEVICE_ID, OTHER_TENANT_ID); + + verify(deviceManagementDAO, times(1)).getDeviceById(DEVICE_ID, TENANT_ID); + verify(deviceManagementDAO, times(1)).getDeviceById(DEVICE_ID, OTHER_TENANT_ID); + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/java/org/wso2/carbon/identity/device/mgt/dao/DeviceManagementDAOImplTest.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/java/org/wso2/carbon/identity/device/mgt/dao/DeviceManagementDAOImplTest.java new file mode 100644 index 000000000000..6faf38031a66 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/java/org/wso2/carbon/identity/device/mgt/dao/DeviceManagementDAOImplTest.java @@ -0,0 +1,432 @@ +/* +* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +* +* 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.device.mgt.dao; + +import org.testng.Assert; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.common.testng.WithCarbonHome; +import org.wso2.carbon.identity.common.testng.WithH2Database; +import org.wso2.carbon.identity.device.mgt.api.exception.DeviceMgtException; +import org.wso2.carbon.identity.device.mgt.api.model.Device; +import org.wso2.carbon.identity.device.mgt.internal.dao.impl.DeviceManagementDAOImpl; + +import java.sql.Timestamp; +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +/** + * Unit tests for {@link DeviceManagementDAOImpl}. + */ +@WithH2Database(files = {"dbscripts/h2.sql"}) +@WithCarbonHome +public class DeviceManagementDAOImplTest { + + private static final int TENANT_ID = -1234; + private static final int OTHER_TENANT_ID = -5678; + private static final String TEST_USER_ID = "alice@example.com"; + private static final String SECOND_USER_ID = "carol@example.com"; + + DeviceManagementDAOImpl deviceManagementDAO = new DeviceManagementDAOImpl(); + private String createdDeviceId; + + /** + * Tests registering a device. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 1) + public void testRegisterDevice() throws DeviceMgtException { + + Device device = buildDevice(UUID.randomUUID().toString(), "Alice Phone", Device.Status.ACTIVE); + Device result = deviceManagementDAO.registerDevice(device, TENANT_ID); + + Assert.assertNotNull(result); + Assert.assertEquals(result.getId(), device.getId()); + Assert.assertEquals(result.getDeviceName(), "Alice Phone"); + Assert.assertEquals(result.getStatus(), Device.Status.ACTIVE); + + createdDeviceId = result.getId(); + } + + /** + * Tests retrieving a device by id. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 2, dependsOnMethods = {"testRegisterDevice"}) + public void testGetDeviceById() throws DeviceMgtException { + + Device result = deviceManagementDAO.getDeviceById(createdDeviceId, TENANT_ID); + + Assert.assertNotNull(result); + Assert.assertEquals(result.getId(), createdDeviceId); + Assert.assertEquals(result.getUserId(), TEST_USER_ID); + } + + /** + * Tests retrieving a non-existing device by id. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 3) + public void testGetDeviceByIdNotFound() throws DeviceMgtException { + + Device result = deviceManagementDAO.getDeviceById(UUID.randomUUID().toString(), TENANT_ID); + Assert.assertNull(result); + } + + /** + * Tests that getDevicesByUserId returns only ACTIVE devices. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 4, dependsOnMethods = {"testRegisterDevice"}) + public void testGetDevicesByUserIdOnlyActive() throws DeviceMgtException { + + Device inactiveDevice = buildDevice(UUID.randomUUID().toString(), "Old Device", Device.Status.INACTIVE); + deviceManagementDAO.registerDevice(inactiveDevice, TENANT_ID); + + List devices = deviceManagementDAO.getDevicesByUserId(TEST_USER_ID, TENANT_ID); + + Assert.assertEquals(devices.size(), 1); + Assert.assertEquals(devices.get(0).getStatus(), Device.Status.ACTIVE); + Assert.assertEquals(devices.get(0).getId(), createdDeviceId); + } + + /** + * Tests updating a device name. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 5, dependsOnMethods = {"testRegisterDevice"}) + public void testUpdateDeviceName() throws DeviceMgtException { + + Device updated = deviceManagementDAO.updateDeviceName(createdDeviceId, "Alice Updated", TENANT_ID); + + Assert.assertNotNull(updated); + Assert.assertEquals(updated.getDeviceName(), "Alice Updated"); + } + + /** + * Tests deleting a device. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 6, dependsOnMethods = {"testRegisterDevice"}) + public void testDeleteDevice() throws DeviceMgtException { + + deviceManagementDAO.deleteDevice(createdDeviceId, TENANT_ID); + Device afterDelete = deviceManagementDAO.getDeviceById(createdDeviceId, TENANT_ID); + + Assert.assertNull(afterDelete); + } + + /** + * Tests that all fields survive a register + getDeviceById round-trip. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 7) + public void testRegisterDeviceFullFieldRoundTrip() throws DeviceMgtException { + + String id = UUID.randomUUID().toString(); + Device device = new Device.Builder() + .id(id) + .userId(SECOND_USER_ID) + .deviceName("Carol Phone") + .deviceModel("Pixel 8 Pro") + .publicKey("pk-full-" + id) + .status(Device.Status.ACTIVE) + .registeredAt(Timestamp.from(Instant.now())) + .metadata("{\"env\":\"test\"}") + .build(); + + deviceManagementDAO.registerDevice(device, TENANT_ID); + Device result = deviceManagementDAO.getDeviceById(id, TENANT_ID); + + Assert.assertNotNull(result); + Assert.assertEquals(result.getId(), id); + Assert.assertEquals(result.getUserId(), SECOND_USER_ID); + Assert.assertEquals(result.getDeviceName(), "Carol Phone"); + Assert.assertEquals(result.getDeviceModel(), "Pixel 8 Pro"); + Assert.assertEquals(result.getPublicKey(), "pk-full-" + id); + Assert.assertEquals(result.getStatus(), Device.Status.ACTIVE); + Assert.assertNotNull(result.getRegisteredAt()); + Assert.assertEquals(result.getMetadata(), "{\"env\":\"test\"}"); + } + + /** + * Tests that a device registered under one tenant is not visible under another. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 8) + public void testTenantIsolation() throws DeviceMgtException { + + String id = UUID.randomUUID().toString(); + deviceManagementDAO.registerDevice(buildDevice(id, "Tenant Device", Device.Status.ACTIVE), TENANT_ID); + + Device fromOtherTenant = deviceManagementDAO.getDeviceById(id, OTHER_TENANT_ID); + Assert.assertNull(fromOtherTenant); + + List allOtherTenant = deviceManagementDAO.getDevices(OTHER_TENANT_ID, 0, 100); + long found = allOtherTenant.stream().filter(d -> d.getId().equals(id)).count(); + Assert.assertEquals(found, 0); + } + + /** + * Tests that getDevices returns every device registered under the tenant. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 9) + public void testGetDevicesReturnsAllForTenant() throws DeviceMgtException { + + String userId = "dave@example.com"; + String id1 = UUID.randomUUID().toString(); + String id2 = UUID.randomUUID().toString(); + deviceManagementDAO.registerDevice( + buildDeviceForUser(id1, "Dave Phone 1", userId, Device.Status.ACTIVE), OTHER_TENANT_ID); + deviceManagementDAO.registerDevice( + buildDeviceForUser(id2, "Dave Phone 2", userId, Device.Status.ACTIVE), OTHER_TENANT_ID); + + List all = deviceManagementDAO.getDevices(OTHER_TENANT_ID, 0, 100); + + Assert.assertEquals(all.size(), 2); + long count = all.stream().filter(d -> d.getUserId().equals(userId)).count(); + Assert.assertEquals(count, 2); + } + + /** + * Tests that getDevicesByUserId returns all active devices for a user. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 10) + public void testGetDevicesByUserIdMultiple() throws DeviceMgtException { + + String userId = "eve@example.com"; + String id1 = UUID.randomUUID().toString(); + String id2 = UUID.randomUUID().toString(); + deviceManagementDAO.registerDevice( + buildDeviceForUser(id1, "Eve Phone 1", userId, Device.Status.ACTIVE), TENANT_ID); + deviceManagementDAO.registerDevice( + buildDeviceForUser(id2, "Eve Phone 2", userId, Device.Status.ACTIVE), TENANT_ID); + + List devices = deviceManagementDAO.getDevicesByUserId(userId, TENANT_ID); + + Assert.assertEquals(devices.size(), 2); + } + + /** + * Tests that getDevicesByUserId returns an empty list for an unknown user. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 11) + public void testGetDevicesByUserIdEmpty() throws DeviceMgtException { + + List devices = deviceManagementDAO.getDevicesByUserId("unknown@example.com", TENANT_ID); + + Assert.assertNotNull(devices); + Assert.assertTrue(devices.isEmpty()); + } + + /** + * Tests that updateDeviceName with a wrong tenant id does not affect the row. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 12) + public void testUpdateDeviceNameWrongTenantNoOp() throws DeviceMgtException { + + String id = UUID.randomUUID().toString(); + deviceManagementDAO.registerDevice(buildDevice(id, "Frank Phone", Device.Status.ACTIVE), TENANT_ID); + + deviceManagementDAO.updateDeviceName(id, "Frank New Name", OTHER_TENANT_ID); + + Device result = deviceManagementDAO.getDeviceById(id, TENANT_ID); + Assert.assertNotNull(result); + Assert.assertEquals(result.getDeviceName(), "Frank Phone"); + } + + /** + * Tests that deleteDevice with a wrong tenant id does not remove the row. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 13) + public void testDeleteDeviceWrongTenantNoOp() throws DeviceMgtException { + + String id = UUID.randomUUID().toString(); + deviceManagementDAO.registerDevice(buildDevice(id, "Grace Phone", Device.Status.ACTIVE), TENANT_ID); + + deviceManagementDAO.deleteDevice(id, OTHER_TENANT_ID); + + Device result = deviceManagementDAO.getDeviceById(id, TENANT_ID); + Assert.assertNotNull(result); + Assert.assertEquals(result.getDeviceName(), "Grace Phone"); + } + + /** + * Tests that changeDeviceStatus(INACTIVE) removes the device from getDevicesByUserId results + * while it remains visible via getDeviceById and getDevices. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 14) + public void testChangeDeviceStatusDeactivateExcludesFromUserDeviceList() throws DeviceMgtException { + + String userId = "heidi@example.com"; + String id = UUID.randomUUID().toString(); + deviceManagementDAO.registerDevice( + buildDeviceForUser(id, "Heidi Phone", userId, Device.Status.ACTIVE), TENANT_ID); + + Device updated = deviceManagementDAO.changeDeviceStatus(id, Device.Status.INACTIVE, TENANT_ID); + + Assert.assertNotNull(updated); + Assert.assertEquals(updated.getStatus(), Device.Status.INACTIVE); + + List userDevices = deviceManagementDAO.getDevicesByUserId(userId, TENANT_ID); + Assert.assertTrue(userDevices.isEmpty()); + + Device byId = deviceManagementDAO.getDeviceById(id, TENANT_ID); + Assert.assertNotNull(byId); + Assert.assertEquals(byId.getStatus(), Device.Status.INACTIVE); + + List pagedDevices = deviceManagementDAO.getDevices(TENANT_ID, 0, 100); + long foundInPage = pagedDevices.stream().filter(d -> d.getId().equals(id)).count(); + Assert.assertEquals(foundInPage, 1); + } + + /** + * Tests that changeDeviceStatus(ACTIVE) reinstates a device into getDevicesByUserId results. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 15, dependsOnMethods = {"testChangeDeviceStatusDeactivateExcludesFromUserDeviceList"}) + public void testChangeDeviceStatusActivateReincludesInUserDeviceList() throws DeviceMgtException { + + String userId = "heidi@example.com"; + List beforeActivation = deviceManagementDAO.getDevicesByUserId(userId, TENANT_ID); + Assert.assertTrue(beforeActivation.isEmpty()); + + List allDevices = deviceManagementDAO.getDevices(TENANT_ID, 0, 100); + String id = allDevices.stream() + .filter(d -> d.getUserId().equals(userId)) + .findFirst() + .orElseThrow(() -> new AssertionError("Expected Heidi's device to still exist")) + .getId(); + + Device updated = deviceManagementDAO.changeDeviceStatus(id, Device.Status.ACTIVE, TENANT_ID); + + Assert.assertEquals(updated.getStatus(), Device.Status.ACTIVE); + + List userDevices = deviceManagementDAO.getDevicesByUserId(userId, TENANT_ID); + Assert.assertEquals(userDevices.size(), 1); + Assert.assertEquals(userDevices.get(0).getId(), id); + } + + /** + * Tests that getDevices(tenantId, offset, limit, userId) returns only the given user's + * devices, including one that is INACTIVE. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 16) + public void testGetDevicesFilteredByUserIncludesInactive() throws DeviceMgtException { + + String userId = "ivan@example.com"; + String activeId = UUID.randomUUID().toString(); + String inactiveId = UUID.randomUUID().toString(); + deviceManagementDAO.registerDevice( + buildDeviceForUser(activeId, "Ivan Phone", userId, Device.Status.ACTIVE), TENANT_ID); + deviceManagementDAO.registerDevice( + buildDeviceForUser(inactiveId, "Ivan Tablet", userId, Device.Status.INACTIVE), TENANT_ID); + deviceManagementDAO.registerDevice( + buildDevice(UUID.randomUUID().toString(), "Someone Else Phone", Device.Status.ACTIVE), TENANT_ID); + + List devices = deviceManagementDAO.getDevices(TENANT_ID, 0, 100, userId); + + Assert.assertEquals(devices.size(), 2); + Assert.assertTrue(devices.stream().allMatch(d -> d.getUserId().equals(userId))); + Assert.assertTrue(devices.stream().anyMatch( + d -> d.getId().equals(inactiveId) && d.getStatus() == Device.Status.INACTIVE)); + } + + /** + * Tests that getDeviceCount(tenantId, userId) matches the number of devices returned by the + * filtered getDevices overload. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 17, dependsOnMethods = {"testGetDevicesFilteredByUserIncludesInactive"}) + public void testGetDeviceCountFilteredByUser() throws DeviceMgtException { + + int count = deviceManagementDAO.getDeviceCount(TENANT_ID, "ivan@example.com"); + + Assert.assertEquals(count, 2); + } + + /** + * Tests that a null userId behaves exactly like the unfiltered getDevices/getDeviceCount. + * + * @throws DeviceMgtException If the DAO operation fails. + */ + @Test(priority = 18) + public void testGetDevicesAndCountWithNullUserIdMatchesUnfiltered() throws DeviceMgtException { + + List unfiltered = deviceManagementDAO.getDevices(TENANT_ID, 0, 100); + List withNullUser = deviceManagementDAO.getDevices(TENANT_ID, 0, 100, null); + int unfilteredCount = deviceManagementDAO.getDeviceCount(TENANT_ID); + int countWithNullUser = deviceManagementDAO.getDeviceCount(TENANT_ID, null); + + Assert.assertEquals(withNullUser.size(), unfiltered.size()); + Assert.assertEquals(countWithNullUser, unfilteredCount); + } + + private Device buildDevice(String id, String deviceName, Device.Status status) { + + return new Device.Builder() + .id(id) + .userId(TEST_USER_ID) + .deviceName(deviceName) + .deviceModel("iPhone 15 Pro") + .publicKey("base64-public-key-" + id) + .status(status) + .registeredAt(Timestamp.from(Instant.now())) + .metadata("{\"label\":\"primary\"}") + .build(); + } + + private Device buildDeviceForUser(String id, String deviceName, String userId, Device.Status status) { + + return new Device.Builder() + .id(id) + .userId(userId) + .deviceName(deviceName) + .deviceModel("Pixel 8") + .publicKey("pk-" + id) + .status(status) + .registeredAt(Timestamp.from(Instant.now())) + .metadata(null) + .build(); + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/java/org/wso2/carbon/identity/device/mgt/service/DeviceManagementServiceImplTest.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/java/org/wso2/carbon/identity/device/mgt/service/DeviceManagementServiceImplTest.java new file mode 100644 index 000000000000..97a4851c4ce9 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/java/org/wso2/carbon/identity/device/mgt/service/DeviceManagementServiceImplTest.java @@ -0,0 +1,497 @@ +/* +* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +* +* 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.device.mgt.service; + +import org.mockito.MockedStatic; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.common.testng.WithCarbonHome; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.device.mgt.api.constant.ErrorMessage; +import org.wso2.carbon.identity.device.mgt.api.exception.DeviceMgtException; +import org.wso2.carbon.identity.device.mgt.api.model.Device; +import org.wso2.carbon.identity.device.mgt.internal.dao.DeviceManagementDAO; +import org.wso2.carbon.identity.device.mgt.internal.service.impl.DeviceManagementServiceImpl; + +import java.lang.reflect.Field; +import java.sql.Timestamp; +import java.time.Instant; + +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; + +/** + * Unit tests for {@link DeviceManagementServiceImpl}. + */ +@WithCarbonHome +public class DeviceManagementServiceImplTest { + + private static final String TEST_USERNAME = "alice"; + private static final String TENANT_DOMAIN = "test.com"; + private static final int TENANT_ID = 1; + + private DeviceManagementServiceImpl service; + private DeviceManagementDAO dao; + private MockedStatic identityTenantUtilMocked; + private Field daoField; + private DeviceManagementDAO originalDao; + + @BeforeClass + public void setUpClass() throws Exception { + + service = DeviceManagementServiceImpl.getInstance(); + identityTenantUtilMocked = mockStatic(IdentityTenantUtil.class); + when(IdentityTenantUtil.getTenantId(TENANT_DOMAIN)).thenReturn(TENANT_ID); + + daoField = DeviceManagementServiceImpl.class.getDeclaredField("deviceManagementDAO"); + daoField.setAccessible(true); + originalDao = (DeviceManagementDAO) daoField.get(service); + } + + @AfterClass + public void tearDownClass() throws Exception { + + daoField.set(service, originalDao); + identityTenantUtilMocked.close(); + } + + @BeforeMethod + public void setUp() throws Exception { + + dao = mock(DeviceManagementDAO.class); + daoField.set(service, dao); + } + + @Test + public void testRegisterDeviceDelegatesToDao() throws Exception { + + Device device = buildDevice("d1", "alice@example.com"); + when(dao.registerDevice(any(), eq(TENANT_ID))).thenReturn(device); + + service.registerDevice(device, TENANT_DOMAIN); + + verify(dao).registerDevice(any(), eq(TENANT_ID)); + } + + @Test + public void testRegisterDeviceWithoutUserIdThrows() { + + Device device = buildDevice("d1", null); + + try { + service.registerDevice(device, TENANT_DOMAIN); + Assert.fail("Expected DeviceMgtServerException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_USER_ID_REQUIRED.getCode()); + } + } + + @Test + public void testRegisterNullDeviceThrows() { + + assertRegisterFailsWithFieldRequired(null); + } + + @DataProvider(name = "incompleteDevices") + public Object[][] incompleteDevices() { + + return new Object[][]{ + {"id", completeDeviceBuilder().id(null).build()}, + {"deviceName", completeDeviceBuilder().deviceName(null).build()}, + {"publicKey", completeDeviceBuilder().publicKey(null).build()}, + {"status", completeDeviceBuilder().status(null).build()}, + {"registeredAt", completeDeviceBuilder().registeredAt(null).build()}, + }; + } + + @Test(dataProvider = "incompleteDevices") + public void testRegisterDeviceWithMissingRequiredFieldThrows(String fieldName, Device device) { + + // A required field left unset must surface as a coded error, not as a DB constraint + // violation or an NPE raised inside the DAO. + assertRegisterFailsWithFieldRequired(device); + } + + @Test + public void testRegisterDeviceWithoutOptionalFieldsSucceeds() throws Exception { + + // Device model and metadata are nullable in the schema, so they must not be validated. + Device device = completeDeviceBuilder().deviceModel(null).metadata(null).build(); + + service.registerDevice(device, TENANT_DOMAIN); + + verify(dao).registerDevice(any(), eq(TENANT_ID)); + } + + private void assertRegisterFailsWithFieldRequired(Device device) { + + try { + service.registerDevice(device, TENANT_DOMAIN); + Assert.fail("Expected DeviceMgtServerException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_DEVICE_FIELD_REQUIRED.getCode()); + } + } + + private static Device.Builder completeDeviceBuilder() { + + return new Device.Builder() + .id("d1") + .userId("alice@example.com") + .deviceName("Alice's iPhone") + .deviceModel("iPhone 15") + .publicKey("dummy-public-key") + .status(Device.Status.ACTIVE) + .registeredAt(Timestamp.from(Instant.now())); + } + + @Test + public void testGetDeviceByIdDelegatesToDao() throws Exception { + + Device device = mock(Device.class); + when(dao.getDeviceById("d1", TENANT_ID)).thenReturn(device); + + Device result = service.getDeviceById("d1", TENANT_DOMAIN); + + Assert.assertEquals(result, device); + } + + @Test + public void testGetDeviceByIdWithBlankIdThrows() { + + try { + service.getDeviceById("", TENANT_DOMAIN); + Assert.fail("Expected DeviceMgtClientException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_INVALID_DEVICE_FIELD.getCode()); + } + } + + @Test + public void testGetActiveDeviceByIdReturnsDeviceWhenActive() throws Exception { + + Device active = buildDevice("d1", "alice@example.com", Device.Status.ACTIVE); + when(dao.getDeviceById("d1", TENANT_ID)).thenReturn(active); + + Device result = service.getActiveDeviceById("d1", TENANT_DOMAIN); + + Assert.assertEquals(result, active); + } + + @Test + public void testGetActiveDeviceByIdReturnsNullWhenInactive() throws Exception { + + // The security-critical case: a deactivated (revoked) device must not be returned, + // even though the record still exists and getDeviceById would return it. + Device inactive = buildDevice("d1", "alice@example.com", Device.Status.INACTIVE); + when(dao.getDeviceById("d1", TENANT_ID)).thenReturn(inactive); + + Device result = service.getActiveDeviceById("d1", TENANT_DOMAIN); + + Assert.assertNull(result); + } + + @Test + public void testGetActiveDeviceByIdReturnsNullWhenDeviceDoesNotExist() throws Exception { + + when(dao.getDeviceById("unknown", TENANT_ID)).thenReturn(null); + + Device result = service.getActiveDeviceById("unknown", TENANT_DOMAIN); + + Assert.assertNull(result); + } + + @Test + public void testGetActiveDeviceByIdWithBlankIdThrows() { + + try { + service.getActiveDeviceById("", TENANT_DOMAIN); + Assert.fail("Expected DeviceMgtClientException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_INVALID_DEVICE_FIELD.getCode()); + } + } + + @Test + public void testUpdateDeviceNameWhenDeviceMissingThrows() throws Exception { + + when(dao.getDeviceById("d1", TENANT_ID)).thenReturn(null); + + try { + service.updateDeviceName("d1", "New Name", TENANT_DOMAIN); + Assert.fail("Expected DeviceMgtClientException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_DEVICE_NOT_FOUND.getCode()); + } + } + + @Test + public void testUpdateDeviceNameWithBlankArgsThrows() { + + try { + service.updateDeviceName("", "New Name", TENANT_DOMAIN); + Assert.fail("Expected DeviceMgtClientException for blank deviceId"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_INVALID_DEVICE_FIELD.getCode()); + } + + try { + service.updateDeviceName("d1", " ", TENANT_DOMAIN); + Assert.fail("Expected DeviceMgtClientException for blank deviceName"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_INVALID_DEVICE_FIELD.getCode()); + } + } + + @Test + public void testUpdateDeviceNameWhenDaoReturnsNullAfterRefetchThrowsNotNPE() throws Exception { + + Device existing = mock(Device.class); + when(dao.getDeviceById("d1", TENANT_ID)).thenReturn(existing); + when(dao.updateDeviceName("d1", "New Name", TENANT_ID)).thenReturn(null); + + try { + service.updateDeviceName("d1", "New Name", TENANT_DOMAIN); + Assert.fail("Expected DeviceMgtClientException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_DEVICE_NOT_FOUND.getCode()); + } + } + + @Test + public void testDeleteDeviceWhenDeviceMissingThrows() throws Exception { + + when(dao.getDeviceById("d1", TENANT_ID)).thenReturn(null); + + try { + service.deleteDevice("d1", TENANT_DOMAIN); + Assert.fail("Expected DeviceMgtClientException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_DEVICE_NOT_FOUND.getCode()); + } + } + + @Test + public void testDeleteDeviceDelegatesToDao() throws Exception { + + Device device = mock(Device.class); + when(dao.getDeviceById("d1", TENANT_ID)).thenReturn(device); + + service.deleteDevice("d1", TENANT_DOMAIN); + + verify(dao).deleteDevice("d1", TENANT_ID); + } + + @Test + public void testGetDevicesByUserIdWithBlankUserThrows() { + + try { + service.getDevicesByUserId("", TENANT_DOMAIN); + Assert.fail("Expected DeviceMgtClientException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_INVALID_DEVICE_FIELD.getCode()); + } + } + + @Test + public void testGetDevicesPassesValidLimitThrough() throws Exception { + + service.getDevices(TENANT_DOMAIN, 5, 20); + + verify(dao).getDevices(TENANT_ID, 5, 20); + } + + @Test + public void testGetDevicesCapsLimitAtMaximumItemsPerPage() throws Exception { + + int maximumItemsPerPage = IdentityUtil.getMaximumItemPerPage(); + + service.getDevices(TENANT_DOMAIN, 0, maximumItemsPerPage + 5000); + + // The oversized page size must be capped, so the DAO never sees the caller's value. + verify(dao).getDevices(TENANT_ID, 0, maximumItemsPerPage); + } + + @Test + public void testGetDevicesWithNegativeLimitThrows() { + + try { + service.getDevices(TENANT_DOMAIN, 0, -1); + Assert.fail("Expected DeviceMgtClientException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_INVALID_DEVICE_FIELD.getCode()); + } + } + + @Test + public void testGetDevicesFilteredByUserPassesUserIdThrough() throws Exception { + + service.getDevices(TENANT_DOMAIN, 5, 20, "alice@example.com"); + + verify(dao).getDevices(TENANT_ID, 5, 20, "alice@example.com"); + } + + @Test + public void testGetDevicesFilteredByUserCapsLimitAtMaximumItemsPerPage() throws Exception { + + int maximumItemsPerPage = IdentityUtil.getMaximumItemPerPage(); + + service.getDevices(TENANT_DOMAIN, 0, maximumItemsPerPage + 5000, "alice@example.com"); + + verify(dao).getDevices(TENANT_ID, 0, maximumItemsPerPage, "alice@example.com"); + } + + @Test + public void testGetDevicesFilteredByUserWithNullUserIdDelegatesWithNull() throws Exception { + + service.getDevices(TENANT_DOMAIN, 0, 20, null); + + verify(dao).getDevices(TENANT_ID, 0, 20, null); + } + + @Test + public void testGetDeviceCountFilteredByUserPassesUserIdThrough() throws Exception { + + when(dao.getDeviceCount(TENANT_ID, "alice@example.com")).thenReturn(3); + + int result = service.getDeviceCount(TENANT_DOMAIN, "alice@example.com"); + + Assert.assertEquals(result, 3); + verify(dao).getDeviceCount(TENANT_ID, "alice@example.com"); + } + + @Test + public void testGetDeviceCountFilteredByUserWithNullUserIdDelegatesWithNull() throws Exception { + + service.getDeviceCount(TENANT_DOMAIN, null); + + verify(dao).getDeviceCount(TENANT_ID, null); + } + + @Test + public void testDeactivateDeviceDelegatesToDao() throws Exception { + + Device existing = mock(Device.class); + Device deactivated = buildDevice("d1", "alice@example.com", Device.Status.INACTIVE); + when(dao.getDeviceById("d1", TENANT_ID)).thenReturn(existing); + when(dao.changeDeviceStatus("d1", Device.Status.INACTIVE, TENANT_ID)).thenReturn(deactivated); + + Device result = service.deactivateDevice("d1", TENANT_DOMAIN); + + Assert.assertEquals(result.getStatus(), Device.Status.INACTIVE); + verify(dao).changeDeviceStatus("d1", Device.Status.INACTIVE, TENANT_ID); + } + + @Test + public void testActivateDeviceDelegatesToDao() throws Exception { + + Device existing = mock(Device.class); + Device activated = buildDevice("d1", "alice@example.com", Device.Status.ACTIVE); + when(dao.getDeviceById("d1", TENANT_ID)).thenReturn(existing); + when(dao.changeDeviceStatus("d1", Device.Status.ACTIVE, TENANT_ID)).thenReturn(activated); + + Device result = service.activateDevice("d1", TENANT_DOMAIN); + + Assert.assertEquals(result.getStatus(), Device.Status.ACTIVE); + verify(dao).changeDeviceStatus("d1", Device.Status.ACTIVE, TENANT_ID); + } + + @Test + public void testDeactivateDeviceWhenDeviceMissingThrows() throws Exception { + + when(dao.getDeviceById("unknown", TENANT_ID)).thenReturn(null); + + try { + service.deactivateDevice("unknown", TENANT_DOMAIN); + Assert.fail("Expected DeviceMgtClientException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_DEVICE_NOT_FOUND.getCode()); + } + } + + @Test + public void testActivateDeviceWhenDeviceMissingThrows() throws Exception { + + when(dao.getDeviceById("unknown", TENANT_ID)).thenReturn(null); + + try { + service.activateDevice("unknown", TENANT_DOMAIN); + Assert.fail("Expected DeviceMgtClientException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_DEVICE_NOT_FOUND.getCode()); + } + } + + @Test + public void testDeactivateDeviceWhenDaoReturnsNullAfterRefetchThrowsNotNPE() throws Exception { + + Device existing = mock(Device.class); + when(dao.getDeviceById("d1", TENANT_ID)).thenReturn(existing); + when(dao.changeDeviceStatus("d1", Device.Status.INACTIVE, TENANT_ID)).thenReturn(null); + + try { + service.deactivateDevice("d1", TENANT_DOMAIN); + Assert.fail("Expected DeviceMgtClientException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_DEVICE_NOT_FOUND.getCode()); + } + } + + @Test + public void testActivateDeviceWhenDaoReturnsNullAfterRefetchThrowsNotNPE() throws Exception { + + Device existing = mock(Device.class); + when(dao.getDeviceById("d1", TENANT_ID)).thenReturn(existing); + when(dao.changeDeviceStatus("d1", Device.Status.ACTIVE, TENANT_ID)).thenReturn(null); + + try { + service.activateDevice("d1", TENANT_DOMAIN); + Assert.fail("Expected DeviceMgtClientException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_DEVICE_NOT_FOUND.getCode()); + } + } + + private static Device buildDevice(String id, String userId) { + + return buildDevice(id, userId, Device.Status.ACTIVE); + } + + private static Device buildDevice(String id, String userId, Device.Status status) { + + return new Device.Builder() + .id(id) + .userId(userId) + .deviceName("Alice's iPhone") + .deviceModel("iPhone 15") + .publicKey("dummy-public-key") + .status(status) + .registeredAt(Timestamp.from(Instant.now())) + .build(); + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/java/org/wso2/carbon/identity/device/mgt/util/DeviceManagementExceptionHandlerTest.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/java/org/wso2/carbon/identity/device/mgt/util/DeviceManagementExceptionHandlerTest.java new file mode 100644 index 000000000000..42c7acc3256e --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/java/org/wso2/carbon/identity/device/mgt/util/DeviceManagementExceptionHandlerTest.java @@ -0,0 +1,100 @@ +/* +* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +* +* 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.device.mgt.util; + +import org.testng.Assert; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.device.mgt.api.constant.ErrorMessage; +import org.wso2.carbon.identity.device.mgt.api.exception.DeviceMgtClientException; +import org.wso2.carbon.identity.device.mgt.api.exception.DeviceMgtServerException; +import org.wso2.carbon.identity.device.mgt.internal.util.DeviceManagementExceptionHandler; + +/** + * Unit tests for {@link DeviceManagementExceptionHandler}. + */ +public class DeviceManagementExceptionHandlerTest { + + @Test + public void testHandleClientException() { + + ErrorMessage error = ErrorMessage.ERROR_DEVICE_NOT_FOUND; + DeviceMgtClientException ex = DeviceManagementExceptionHandler.handleClientException(error, "device123"); + + Assert.assertEquals(ex.getErrorCode(), error.getCode()); + Assert.assertTrue(ex.getDescription().contains("device123")); + Assert.assertNull(ex.getCause()); + } + + @Test + public void testHandleClientExceptionWithCause() { + + ErrorMessage error = ErrorMessage.ERROR_INVALID_DEVICE_FIELD; + Throwable cause = new RuntimeException("field error"); + DeviceMgtClientException ex = + DeviceManagementExceptionHandler.handleClientException(error, cause, "deviceId"); + + Assert.assertEquals(ex.getErrorCode(), error.getCode()); + Assert.assertTrue(ex.getDescription().contains("deviceId")); + Assert.assertEquals(ex.getCause(), cause); + } + + @Test + public void testHandleServerExceptionWithCause() { + + ErrorMessage error = ErrorMessage.ERROR_WHILE_UPDATING_DEVICE; + Throwable cause = new RuntimeException("update error"); + DeviceMgtServerException ex = DeviceManagementExceptionHandler.handleServerException(error, cause); + + Assert.assertEquals(ex.getErrorCode(), error.getCode()); + Assert.assertNotNull(ex.getDescription()); + Assert.assertEquals(ex.getCause(), cause); + } + + @Test + public void testHandleServerExceptionNoCause() { + + ErrorMessage error = ErrorMessage.ERROR_WHILE_REGISTERING_DEVICE; + DeviceMgtServerException ex = DeviceManagementExceptionHandler.handleServerException(error); + + Assert.assertEquals(ex.getErrorCode(), error.getCode()); + Assert.assertNotNull(ex.getDescription()); + Assert.assertNull(ex.getCause()); + } + + @Test + public void testDescriptionSubstitutionFormatting() { + + ErrorMessage error = ErrorMessage.ERROR_DEVICE_NOT_FOUND; + String deviceId = "test-device-id-999"; + DeviceMgtClientException ex = DeviceManagementExceptionHandler.handleClientException(error, deviceId); + + Assert.assertEquals(ex.getErrorCode(), error.getCode()); + Assert.assertTrue(ex.getDescription().contains(deviceId), + "Description should contain substituted value: " + deviceId); + } + + @Test + public void testMessageIsPreserved() { + + ErrorMessage error = ErrorMessage.ERROR_DEVICE_NOT_FOUND; + DeviceMgtClientException ex = DeviceManagementExceptionHandler.handleClientException(error, "d1"); + + Assert.assertEquals(ex.getMessage(), error.getMessage()); + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/java/org/wso2/carbon/identity/device/mgt/util/DeviceValidatorTest.java b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/java/org/wso2/carbon/identity/device/mgt/util/DeviceValidatorTest.java new file mode 100644 index 000000000000..54f0cd6685b3 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/java/org/wso2/carbon/identity/device/mgt/util/DeviceValidatorTest.java @@ -0,0 +1,201 @@ +/* +* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. +* +* 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.device.mgt.util; + +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.common.testng.WithCarbonHome; +import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.device.mgt.api.constant.ErrorMessage; +import org.wso2.carbon.identity.device.mgt.api.exception.DeviceMgtException; +import org.wso2.carbon.identity.device.mgt.api.model.Device; +import org.wso2.carbon.identity.device.mgt.internal.util.DeviceValidator; + +import java.sql.Timestamp; +import java.time.Instant; + +/** + * Unit tests for {@link DeviceValidator}. + */ +@WithCarbonHome +public class DeviceValidatorTest { + + private final DeviceValidator deviceValidator = new DeviceValidator(); + + @Test + public void testValidateRequiredFieldWithBlankValueThrows() { + + try { + deviceValidator.validateRequiredField(" ", "deviceId"); + Assert.fail("Expected DeviceMgtClientException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_INVALID_DEVICE_FIELD.getCode()); + } + } + + @Test + public void testValidateRequiredFieldWithNullValueThrows() { + + try { + deviceValidator.validateRequiredField(null, "deviceId"); + Assert.fail("Expected DeviceMgtClientException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_INVALID_DEVICE_FIELD.getCode()); + } + } + + @Test + public void testValidateRequiredFieldWithValidValueSucceeds() throws Exception { + + deviceValidator.validateRequiredField("d1", "deviceId"); + } + + @Test + public void testValidateDeviceForRegistrationWithNullDeviceThrows() { + + try { + deviceValidator.validateDeviceForRegistration(null); + Assert.fail("Expected DeviceMgtServerException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_DEVICE_FIELD_REQUIRED.getCode()); + } + } + + @Test + public void testValidateDeviceForRegistrationWithoutUserIdThrows() { + + Device device = completeDeviceBuilder().userId(null).build(); + + try { + deviceValidator.validateDeviceForRegistration(device); + Assert.fail("Expected DeviceMgtServerException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_USER_ID_REQUIRED.getCode()); + } + } + + @DataProvider(name = "incompleteDevices") + public Object[][] incompleteDevices() { + + return new Object[][]{ + {completeDeviceBuilder().id(null).build()}, + {completeDeviceBuilder().deviceName(null).build()}, + {completeDeviceBuilder().publicKey(null).build()}, + {completeDeviceBuilder().status(null).build()}, + {completeDeviceBuilder().registeredAt(null).build()}, + }; + } + + @Test(dataProvider = "incompleteDevices") + public void testValidateDeviceForRegistrationWithMissingRequiredFieldThrows(Device device) { + + try { + deviceValidator.validateDeviceForRegistration(device); + Assert.fail("Expected DeviceMgtServerException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_DEVICE_FIELD_REQUIRED.getCode()); + } + } + + @Test + public void testValidateDeviceForRegistrationWithoutOptionalFieldsSucceeds() throws Exception { + + Device device = completeDeviceBuilder().deviceModel(null).metadata(null).build(); + + deviceValidator.validateDeviceForRegistration(device); + } + + @Test + public void testValidateRequiredDeviceFieldWithBlankValueThrows() { + + try { + deviceValidator.validateRequiredDeviceField("", "id"); + Assert.fail("Expected DeviceMgtException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_DEVICE_FIELD_REQUIRED.getCode()); + } + } + + @Test + public void testValidateRequiredDeviceFieldWithValidValueSucceeds() throws Exception { + + deviceValidator.validateRequiredDeviceField("d1", "id"); + } + + @Test + public void testValidateLimitWithNegativeValueThrows() { + + try { + deviceValidator.validateLimit(-1); + Assert.fail("Expected DeviceMgtClientException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_INVALID_DEVICE_FIELD.getCode()); + } + } + + @Test + public void testValidateLimitWithinBoundsIsUnchanged() throws Exception { + + int result = deviceValidator.validateLimit(20); + + Assert.assertEquals(result, 20); + } + + @Test + public void testValidateLimitOverMaximumIsCapped() throws Exception { + + int maximumItemsPerPage = IdentityUtil.getMaximumItemPerPage(); + + int result = deviceValidator.validateLimit(maximumItemsPerPage + 5000); + + Assert.assertEquals(result, maximumItemsPerPage); + } + + @Test + public void testValidateDeviceExistsWithNullDeviceThrows() { + + try { + deviceValidator.validateDeviceExists(null, "d1"); + Assert.fail("Expected DeviceMgtClientException"); + } catch (DeviceMgtException ex) { + Assert.assertEquals(ex.getErrorCode(), ErrorMessage.ERROR_DEVICE_NOT_FOUND.getCode()); + } + } + + @Test + public void testValidateDeviceExistsWithExistingDeviceSucceeds() throws Exception { + + Device device = completeDeviceBuilder().build(); + + deviceValidator.validateDeviceExists(device, "d1"); + } + + private static Device.Builder completeDeviceBuilder() { + + return new Device.Builder() + .id("d1") + .userId("alice@example.com") + .deviceName("Alice's iPhone") + .deviceModel("iPhone 15") + .publicKey("dummy-public-key") + .status(Device.Status.ACTIVE) + .registeredAt(Timestamp.from(Instant.now())); + } +} diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/resources/dbscripts/h2.sql b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/resources/dbscripts/h2.sql new file mode 100644 index 000000000000..6269afa183c6 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/resources/dbscripts/h2.sql @@ -0,0 +1,22 @@ +CREATE TABLE IF NOT EXISTS IDN_DEVICE ( + ID CHAR(36) NOT NULL, + DEVICE_NAME VARCHAR(255) NOT NULL, + DEVICE_MODEL VARCHAR(255), + PUBLIC_KEY VARCHAR(2048) NOT NULL, + STATUS VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + REGISTERED_AT TIMESTAMP NOT NULL, + METADATA VARCHAR(2048) NULL, + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (ID) +); + +CREATE TABLE IF NOT EXISTS IDN_USER_DEVICE ( + DEVICE_ID CHAR(36) NOT NULL, + USER_ID VARCHAR(255) NOT NULL, + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (DEVICE_ID), + FOREIGN KEY (DEVICE_ID) REFERENCES IDN_DEVICE (ID) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS IDX_IUD_USER_TENANT ON IDN_USER_DEVICE (USER_ID, TENANT_ID); +CREATE INDEX IF NOT EXISTS IDX_IDV_TENANT_REGISTERED ON IDN_DEVICE (TENANT_ID, REGISTERED_AT); diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/resources/repository/conf/carbon.xml b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/resources/repository/conf/carbon.xml new file mode 100644 index 000000000000..a5a1a6470cbc --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/resources/repository/conf/carbon.xml @@ -0,0 +1,686 @@ + + + + + + + + WSO2 Identity Server + + + IS + + + 5.3.0 + + + localhost + + + localhost + + + local:/${carbon.context}/services/ + + + + + + + IdentityServer + + + + + + + org.wso2.carbon + + + / + + + + + + + + + 15 + + + + + + + + + 0 + + + + + 9999 + + 11111 + + + + + + 10389 + + 8000 + + + + + + 10500 + + + + + + + + + org.wso2.carbon.tomcat.jndi.CarbonJavaURLContextFactory + + + + + + + + + java + + + + + + + + + + false + + + false + + + 600 + + + + false + + + + + + + + 30 + + + + + + + + + 15 + + + + + + ${carbon.home}/repository/deployment/server/ + + + 15 + + + ${carbon.home}/repository/conf/axis2/axis2.xml + + + 30000 + + + ${carbon.home}/repository/deployment/client/ + + ${carbon.home}/repository/conf/axis2/axis2_client.xml + + true + + + + + + + + + + admin + Default Administrator Role + + + user + Default User Role + + + + + + + + + + + + ${carbon.home}/repository/resources/security/wso2carbon.jks + + JKS + + wso2carbon + + wso2carbon + + wso2carbon + + + + + + ${carbon.home}/repository/resources/security/client-truststore.jks + + JKS + + wso2carbon + + + + + + + + + + + + + + + + + + + UserManager + + + false + + org.wso2.carbon.identity.provider.AttributeCallbackHandler + + + org.wso2.carbon.identity.sts.store.DBTokenStore + + + true + allow + + + + + + + claim_mgt_menu + identity_mgt_emailtemplate_menu + identity_security_questions_menu + + + + ${carbon.home}/tmp/work + + + + + + true + + + 10 + + + 30 + + + + + + 100 + + + + keystore + certificate + * + + org.wso2.carbon.ui.transports.fileupload.AnyFileUploadExecutor + + + + + jarZip + + org.wso2.carbon.ui.transports.fileupload.JarZipUploadExecutor + + + + dbs + + org.wso2.carbon.ui.transports.fileupload.DBSFileUploadExecutor + + + + tools + + org.wso2.carbon.ui.transports.fileupload.ToolsFileUploadExecutor + + + + toolsAny + + org.wso2.carbon.ui.transports.fileupload.ToolsAnyFileUploadExecutor + + + + + + + + + + info + org.wso2.carbon.core.transports.util.InfoProcessor + + + wsdl + org.wso2.carbon.core.transports.util.Wsdl11Processor + + + wsdl2 + org.wso2.carbon.core.transports.util.Wsdl20Processor + + + xsd + org.wso2.carbon.core.transports.util.XsdProcessor + + + + + + false + false + true + svn + http://svnrepo.example.com/repos/ + username + password + true + + + + + + + + + + + + + + + ${require.carbon.servlet} + + + + + true + + + + + + + default repository + http://product-dist.wso2.com/p2/carbon/releases/wilkes/ + + + + + + + + true + + + + + + true + + diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/resources/repository/conf/identity/identity.xml b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/resources/repository/conf/identity/identity.xml new file mode 100644 index 000000000000..07de6831dbf4 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/resources/repository/conf/identity/identity.xml @@ -0,0 +1,743 @@ + + + + + + + + + jdbc/WSO2IdentityDB + + + + + true + true + 0 + + true + 20160 + 1140 + + + true + 720 + + + + + + + 15 + 20160 + + + + + + ${carbon.home}/conf/keystores + SunX509 + SunX509 + + + + SelfAndManaged + CertValidate + + + + + + + + + + + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/openidserver + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/openid + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/authenticationendpoint/openid_login.do + + + false + + 7200 + + false + + + + + + + + + + + + + + + + + + + + + + -1 + -1 + -1 + -1 + + + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/oauth/request-token + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/oauth/authorize-url + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/oauth/access-token + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/oauth2/authorize + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/oauth2/token + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/oauth2/revoke + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/oauth2/introspect + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/oauth2/userinfo + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/oidc/checksession + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/oidc/logout + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/authenticationendpoint/oauth2_authz.do + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/authenticationendpoint/oauth2_error.do + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/authenticationendpoint/oauth2_consent.do + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/authenticationendpoint/oauth2_logout_consent.do + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/authenticationendpoint/oauth2_logout.do + + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/.well-known/webfinger + + + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/identity/connect/register + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/oauth2/jwks + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/oauth2/oidcdiscovery + + + 300 + + 3600 + + 3600 + + 84600 + + 300 + + false + + true + + org.wso2.carbon.identity.oauth.tokenprocessor.PlainTextPersistenceProcessor + + + + false + + + + + + token + org.wso2.carbon.identity.oauth2.authz.handlers.AccessTokenResponseTypeHandler + + + code + org.wso2.carbon.identity.oauth2.authz.handlers.CodeResponseTypeHandler + + + id_token + org.wso2.carbon.identity.oauth2.authz.handlers.IDTokenResponseTypeHandler + + + id_token token + org.wso2.carbon.identity.oauth2.authz.handlers.IDTokenTokenResponseTypeHandler + + + + + + authorization_code + org.wso2.carbon.identity.oauth2.token.handlers.grant.AuthorizationCodeGrantHandler + + + password + org.wso2.carbon.identity.oauth2.token.handlers.grant.PasswordGrantHandler + + + refresh_token + org.wso2.carbon.identity.oauth2.token.handlers.grant.RefreshGrantHandler + + + client_credentials + org.wso2.carbon.identity.oauth2.token.handlers.grant.ClientCredentialsGrantHandler + + + urn:ietf:params:oauth:grant-type:saml2-bearer + org.wso2.carbon.identity.oauth2.token.handlers.grant.saml.SAML2BearerGrantHandler + + + iwa:ntlm + org.wso2.carbon.identity.oauth2.token.handlers.grant.iwa.ntlm.NTLMAuthenticationGrantHandler + + + idTokenNotAllowedGrantType + org.wso2.carbon.identity.oauth2.token.handlers.grant.idTokenNotAllowedGrantHandler + false + + + + + + + + + false + + + + false + + + + false + org.wso2.carbon.identity.oauth2.authcontext.JWTTokenGenerator + org.wso2.carbon.identity.oauth2.authcontext.DefaultClaimsRetriever + http://wso2.org/claims + SHA256withRSA + 10 + + + + + + org.wso2.carbon.identity.openidconnect.DefaultIDTokenBuilder + SHA256withRSA + + + + + + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/oauth2/token + org.wso2.carbon.identity.openidconnect.DefaultOIDCClaimsCallbackHandler + 3600 + org.wso2.carbon.identity.oauth.endpoint.user.impl.UserInfoUserStoreClaimRetriever + org.wso2.carbon.identity.oauth.endpoint.user.impl.UserInforRequestDefaultValidator + org.wso2.carbon.identity.oauth.endpoint.user.impl.UserInfoISAccessTokenValidator + org.wso2.carbon.identity.oauth.endpoint.user.impl.UserInfoJSONResponseBuilder + false + + + + + + + + gtalk + talk.google.com + 5222 + gmail.com + multifactor1@gmail.com + wso2carbon + + + + + + 157680000 + 157680000 + ${carbon.host} + + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/samlsso + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/authenticationendpoint/samlsso_logout.do + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/authenticationendpoint/samlsso_notification.do + 5 + 60000 + + false + http://wso2.org/claims + org.wso2.carbon.identity.sso.saml.builders.assertion.ExtendedDefaultAssertionBuilder + + org.wso2.carbon.identity.sso.saml.builders.encryption.DefaultSSOEncrypter + org.wso2.carbon.identity.sso.saml.builders.signature.DefaultSSOSigner + org.wso2.carbon.identity.sso.saml.validators.SAML2HTTPRedirectDeflateSignatureValidator + + + + 5 + false + http://www.w3.org/2000/09/xmldsig#rsa-sha1 + http://www.w3.org/2000/09/xmldsig#sha1 + true + + + + + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/services/wso2carbon-sts + + + + + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/passivests + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/authenticationendpoint/retry.do + org.wso2.carbon.identity.sts.passive.utils.NoPersistenceTokenStore + true + + + + + false + ${Ports.ThriftEntitlementReceivePort} + 10000 + + ${carbon.home}/repository/resources/security/wso2carbon.jks + wso2carbon + + + ${carbon.host} + + + + + + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/wso2/scim/Users + ${carbon.protocol}://${carbon.host}:${carbon.management.port}/wso2/scim/Groups + + + 5 + + + 10 + local://services + + + + + + + + + + + + + org.wso2.carbon.identity.governance.store.JDBCIdentityDataStore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /permission/admin/manage/identity/identitymgt + + + + + + /permission/admin/manage/identity/usermgt/view + + + /permission/admin/manage/identity/usermgt/view + + + + /permission/admin/manage/identity/configmgt/list + + + + /permission/admin/manage/identity/configmgt/add + + + /permission/admin/manage/identity/configmgt/update + + + + /permission/admin/manage/identity/configmgt/delete + + + + /permission/admin/manage/identity/configmgt/add + + + /permission/admin/manage/identity/configmgt/update + + + + /permission/admin/manage/identity/configmgt/delete + + + + /permission/admin/manage/identity/configmgt/add + + + /permission/admin/manage/identity/configmgt/update + + + + /permission/admin/manage/identity/configmgt/delete + + + + + + + /permission/admin/manage/identity/consentmgt/add + + + + /permission/admin/manage/identity/consentmgt/delete + + + + /permission/admin/manage/identity/consentmgt/add + + + + /permission/admin/manage/identity/consentmgt/delete + + + + /permission/admin/manage/identity/consentmgt/add + + + + /permission/admin/manage/identity/consentmgt/delete + + + + /permission/admin/manage/identity/identitymgt + + + + /permission/admin/manage/identity/applicationmgt/create + + + /permission/admin/manage/identity/applicationmgt/delete + + + /permission/admin/manage/identity/applicationmgt/update + + + /permission/admin/manage/identity/applicationmgt/view + + + /permission/admin/manage/identity/applicationmgt/delete + + + /permission/admin/manage/identity/applicationmgt/create + + + /permission/admin/manage/identity/applicationmgt/view + + + /permission/admin/manage/identity/pep + + + /permission/admin/manage/identity/usermgt/create + + + /permission/admin/manage/identity/usermgt/list + + + /permission/admin/manage/identity/rolemgt/create + + + /permission/admin/manage/identity/rolemgt/view + + + /permission/admin/manage/identity/usermgt/view + + + /permission/admin/manage/identity/usermgt/update + + + /permission/admin/manage/identity/usermgt/update + + + /permission/admin/manage/identity/usermgt/delete + + + /permission/admin/manage/identity/rolemgt/view + + + /permission/admin/manage/identity/rolemgt/update + + + /permission/admin/manage/identity/rolemgt/update + + + /permission/admin/manage/identity/rolemgt/delete + + + /permission/admin/login + + + /permission/admin/manage/identity/usermgt/delete + + + /permission/admin/login + + + /permission/admin/login + + + /permission/admin/manage/identity/usermgt/create + + + + + + + + + /permission/admin/manage/identity/usermgt + + + /permission/admin/manage/identity/applicationmgt + + + + + + + /permission/admin/manage/identity/usermgt/update + + + + + + /permission/admin/manage/humantask/viewtasks + + + /permission/admin/login + + + /permission/admin/manage/identity/usermgt + + + /permission/admin/manage/identity/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /api/identity/user/v0.9 + /api/identity/recovery/v0.9 + /oauth2 + /api/identity/entitlement + + + /identity/(.*) + + + + + + applications,connections + + + + 300 + diff --git a/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/resources/testng.xml b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/resources/testng.xml new file mode 100644 index 000000000000..1a080bd451e8 --- /dev/null +++ b/components/device-mgt/org.wso2.carbon.identity.device.mgt/src/test/resources/testng.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/components/device-mgt/pom.xml b/components/device-mgt/pom.xml new file mode 100644 index 000000000000..98d6e0a24a36 --- /dev/null +++ b/components/device-mgt/pom.xml @@ -0,0 +1,56 @@ + + + + + + + org.wso2.carbon.identity.framework + identity-framework + 7.11.153-SNAPSHOT + ../../pom.xml + + + 4.0.0 + device-mgt + pom + WSO2 Carbon - Device Management Aggregator Module + + Aggregates device management modules for generic device registration. + + http://wso2.org + + + org.wso2.carbon.identity.device.mgt + + + + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.device.mgt + ${project.version} + + + + + + diff --git a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/db2.sql b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/db2.sql index e97fdf3c105e..dfafef18d88f 100644 --- a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/db2.sql +++ b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/db2.sql @@ -3183,3 +3183,29 @@ CREATE INDEX IDX_CONSUMER_KEY_STATE ON IDN_OAUTH2_REFRESH_TOKEN (CONSUMER_KEY_ID / CREATE INDEX IDX_CONSUMER_USER_SCOPE_IDP ON IDN_OAUTH2_REFRESH_TOKEN (CONSUMER_KEY_ID, AUTHZ_USER, TENANT_ID, USER_DOMAIN, TOKEN_SCOPE_HASH, TOKEN_STATE, IDP_ID) / + +-- IDN_DEVICE -- +CREATE TABLE IDN_DEVICE ( + ID CHAR(36) NOT NULL, + DEVICE_NAME VARCHAR(255) NOT NULL, + DEVICE_MODEL VARCHAR(255), + PUBLIC_KEY VARCHAR(2048) NOT NULL, + STATUS VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + REGISTERED_AT TIMESTAMP NOT NULL, + METADATA VARCHAR(2048), + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (ID) +) +/ +CREATE TABLE IDN_USER_DEVICE ( + DEVICE_ID CHAR(36) NOT NULL, + USER_ID VARCHAR(255) NOT NULL, + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (DEVICE_ID), + FOREIGN KEY (DEVICE_ID) REFERENCES IDN_DEVICE (ID) ON DELETE CASCADE +) +/ +CREATE INDEX IDX_IDV_TENANT_REGISTERED ON IDN_DEVICE (TENANT_ID, REGISTERED_AT) +/ +CREATE INDEX IDX_IUD_USER_TENANT ON IDN_USER_DEVICE (USER_ID, TENANT_ID) +/ diff --git a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/h2.sql b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/h2.sql index e4424c873059..17cf4ba9256f 100644 --- a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/h2.sql +++ b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/h2.sql @@ -2110,3 +2110,27 @@ CREATE INDEX IDX_CONSUMER_USER_SCOPE_IDP ON IDN_OAUTH2_REFRESH_TOKEN (CONSUMER_K -- WORKFLOWS -- CREATE INDEX IDX_WF_APPROVAL_STATE ON WF_WORKFLOW_APPROVAL_STATE(EVENT_ID,WORKFLOW_ID); CREATE INDEX IDX_WF_APPROVAL_RELATION ON WF_WORKFLOW_APPROVAL_RELATION(TASK_ID, APPROVER_TYPE, APPROVER_NAME); + +-- IDN_DEVICE -- +CREATE TABLE IF NOT EXISTS IDN_DEVICE ( + ID CHAR(36) NOT NULL, + DEVICE_NAME VARCHAR(255) NOT NULL, + DEVICE_MODEL VARCHAR(255), + PUBLIC_KEY VARCHAR(2048) NOT NULL, + STATUS VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + REGISTERED_AT TIMESTAMP NOT NULL, + METADATA VARCHAR(2048), + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (ID) +); + +CREATE TABLE IF NOT EXISTS IDN_USER_DEVICE ( + DEVICE_ID CHAR(36) NOT NULL, + USER_ID VARCHAR(255) NOT NULL, + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (DEVICE_ID), + FOREIGN KEY (DEVICE_ID) REFERENCES IDN_DEVICE (ID) ON DELETE CASCADE +); + +CREATE INDEX IDX_IDV_TENANT_REGISTERED ON IDN_DEVICE (TENANT_ID, REGISTERED_AT); +CREATE INDEX IDX_IUD_USER_TENANT ON IDN_USER_DEVICE (USER_ID, TENANT_ID); diff --git a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/mssql.sql b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/mssql.sql index dfd5fe9720a9..5f57f2b9cf0d 100644 --- a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/mssql.sql +++ b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/mssql.sql @@ -2318,3 +2318,27 @@ GO CREATE TRIGGER API_RESOURCE_DELETE_TRIGGER ON API_RESOURCE INSTEAD OF DELETE AS BEGIN DELETE FROM AUTHORIZED_API WHERE API_ID IN (SELECT ID FROM DELETED) DELETE FROM API_RESOURCE WHERE ID IN (SELECT ID FROM deleted) END; GO + +-- IDN_DEVICE -- +CREATE TABLE IDN_DEVICE ( + ID CHAR(36) NOT NULL, + DEVICE_NAME VARCHAR(255) NOT NULL, + DEVICE_MODEL VARCHAR(255), + PUBLIC_KEY VARCHAR(2048) NOT NULL, + STATUS VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + REGISTERED_AT DATETIME NOT NULL, + METADATA VARCHAR(2048), + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (ID) +); + +CREATE TABLE IDN_USER_DEVICE ( + DEVICE_ID CHAR(36) NOT NULL, + USER_ID VARCHAR(255) NOT NULL, + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (DEVICE_ID), + FOREIGN KEY (DEVICE_ID) REFERENCES IDN_DEVICE (ID) ON DELETE CASCADE +); + +CREATE INDEX IDX_IDV_TENANT_REGISTERED ON IDN_DEVICE (TENANT_ID, REGISTERED_AT); +CREATE INDEX IDX_IUD_USER_TENANT ON IDN_USER_DEVICE (USER_ID, TENANT_ID); diff --git a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/mysql-cluster.sql b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/mysql-cluster.sql index 8a4db2322261..bdf4914534e7 100644 --- a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/mysql-cluster.sql +++ b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/mysql-cluster.sql @@ -2325,3 +2325,27 @@ CREATE INDEX IDX_REFRESH_TOKEN_HASH ON IDN_OAUTH2_REFRESH_TOKEN (REFRESH_TOKEN_H CREATE INDEX IDX_AUTHZ_USER_TENANT_DOMAIN_STATE ON IDN_OAUTH2_REFRESH_TOKEN (AUTHZ_USER, TENANT_ID, USER_DOMAIN, TOKEN_STATE); CREATE INDEX IDX_CONSUMER_KEY_STATE ON IDN_OAUTH2_REFRESH_TOKEN (CONSUMER_KEY_ID, TOKEN_STATE); CREATE INDEX IDX_CONSUMER_USER_SCOPE_IDP ON IDN_OAUTH2_REFRESH_TOKEN (CONSUMER_KEY_ID, AUTHZ_USER, TENANT_ID, USER_DOMAIN, TOKEN_SCOPE_HASH, TOKEN_STATE, IDP_ID); + +-- IDN_DEVICE -- +CREATE TABLE IF NOT EXISTS IDN_DEVICE ( + ID CHAR(36) NOT NULL, + DEVICE_NAME VARCHAR(255) NOT NULL, + DEVICE_MODEL VARCHAR(255), + PUBLIC_KEY VARCHAR(2048) NOT NULL, + STATUS VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + REGISTERED_AT TIMESTAMP NOT NULL, + METADATA VARCHAR(2048), + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (ID) +)ENGINE NDB; + +CREATE TABLE IF NOT EXISTS IDN_USER_DEVICE ( + DEVICE_ID CHAR(36) NOT NULL, + USER_ID VARCHAR(255) NOT NULL, + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (DEVICE_ID), + FOREIGN KEY (DEVICE_ID) REFERENCES IDN_DEVICE (ID) ON DELETE CASCADE +)ENGINE NDB; + +CREATE INDEX IDX_IDV_TENANT_REGISTERED ON IDN_DEVICE (TENANT_ID, REGISTERED_AT); +CREATE INDEX IDX_IUD_USER_TENANT ON IDN_USER_DEVICE (USER_ID, TENANT_ID); diff --git a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/mysql.sql b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/mysql.sql index a769a5544062..f4ff8dbe2e0f 100644 --- a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/mysql.sql +++ b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/mysql.sql @@ -2137,3 +2137,27 @@ CREATE INDEX IDX_REFRESH_TOKEN_HASH ON IDN_OAUTH2_REFRESH_TOKEN (REFRESH_TOKEN_H CREATE INDEX IDX_AUTHZ_USER_TENANT_DOMAIN_STATE ON IDN_OAUTH2_REFRESH_TOKEN (AUTHZ_USER, TENANT_ID, USER_DOMAIN, TOKEN_STATE); CREATE INDEX IDX_CONSUMER_KEY_STATE ON IDN_OAUTH2_REFRESH_TOKEN (CONSUMER_KEY_ID, TOKEN_STATE); CREATE INDEX IDX_CONSUMER_USER_SCOPE_IDP ON IDN_OAUTH2_REFRESH_TOKEN (CONSUMER_KEY_ID, AUTHZ_USER, TENANT_ID, USER_DOMAIN, TOKEN_SCOPE_HASH, TOKEN_STATE, IDP_ID); + +-- IDN_DEVICE -- +CREATE TABLE IF NOT EXISTS IDN_DEVICE ( + ID CHAR(36) NOT NULL, + DEVICE_NAME VARCHAR(255) NOT NULL, + DEVICE_MODEL VARCHAR(255), + PUBLIC_KEY VARCHAR(2048) NOT NULL, + STATUS VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + REGISTERED_AT TIMESTAMP NOT NULL, + METADATA VARCHAR(2048), + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (ID) +)DEFAULT CHARACTER SET latin1 ENGINE INNODB; + +CREATE TABLE IF NOT EXISTS IDN_USER_DEVICE ( + DEVICE_ID CHAR(36) NOT NULL, + USER_ID VARCHAR(255) NOT NULL, + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (DEVICE_ID), + FOREIGN KEY (DEVICE_ID) REFERENCES IDN_DEVICE (ID) ON DELETE CASCADE +)DEFAULT CHARACTER SET latin1 ENGINE INNODB; + +CREATE INDEX IDX_IDV_TENANT_REGISTERED ON IDN_DEVICE (TENANT_ID, REGISTERED_AT); +CREATE INDEX IDX_IUD_USER_TENANT ON IDN_USER_DEVICE (USER_ID, TENANT_ID); diff --git a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/oracle.sql b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/oracle.sql index f6f1d0e64c5d..c80494dd28a9 100644 --- a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/oracle.sql +++ b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/oracle.sql @@ -3336,3 +3336,29 @@ CREATE INDEX IDX_CONSUMER_KEY_STATE ON IDN_OAUTH2_REFRESH_TOKEN (CONSUMER_KEY_ID / CREATE INDEX IDX_CONSUMER_USER_SCOPE_IDP ON IDN_OAUTH2_REFRESH_TOKEN (CONSUMER_KEY_ID, AUTHZ_USER, TENANT_ID, USER_DOMAIN, TOKEN_SCOPE_HASH, TOKEN_STATE, IDP_ID) / + +-- IDN_DEVICE -- +CREATE TABLE IDN_DEVICE ( + ID CHAR(36) NOT NULL, + DEVICE_NAME VARCHAR2(255) NOT NULL, + DEVICE_MODEL VARCHAR2(255), + PUBLIC_KEY VARCHAR2(2048) NOT NULL, + STATUS VARCHAR2(20) DEFAULT 'ACTIVE' NOT NULL, + REGISTERED_AT TIMESTAMP NOT NULL, + METADATA VARCHAR2(2048), + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (ID) +) +/ +CREATE TABLE IDN_USER_DEVICE ( + DEVICE_ID CHAR(36) NOT NULL, + USER_ID VARCHAR2(255) NOT NULL, + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (DEVICE_ID), + FOREIGN KEY (DEVICE_ID) REFERENCES IDN_DEVICE (ID) ON DELETE CASCADE +) +/ +CREATE INDEX IDX_IDV_TENANT_REGISTERED ON IDN_DEVICE (TENANT_ID, REGISTERED_AT) +/ +CREATE INDEX IDX_IUD_USER_TENANT ON IDN_USER_DEVICE (USER_ID, TENANT_ID) +/ diff --git a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/oracle_rac.sql b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/oracle_rac.sql index 97dec658d71e..fc31a47e225a 100644 --- a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/oracle_rac.sql +++ b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/oracle_rac.sql @@ -3236,3 +3236,29 @@ CREATE INDEX IDX_CONSUMER_KEY_STATE ON IDN_OAUTH2_REFRESH_TOKEN (CONSUMER_KEY_ID / CREATE INDEX IDX_CONSUMER_USER_SCOPE_IDP ON IDN_OAUTH2_REFRESH_TOKEN (CONSUMER_KEY_ID, AUTHZ_USER, TENANT_ID, USER_DOMAIN, TOKEN_SCOPE_HASH, TOKEN_STATE, IDP_ID) / + +-- IDN_DEVICE -- +CREATE TABLE IDN_DEVICE ( + ID CHAR(36) NOT NULL, + DEVICE_NAME VARCHAR2(255) NOT NULL, + DEVICE_MODEL VARCHAR2(255), + PUBLIC_KEY VARCHAR2(2048) NOT NULL, + STATUS VARCHAR2(20) DEFAULT 'ACTIVE' NOT NULL, + REGISTERED_AT TIMESTAMP NOT NULL, + METADATA VARCHAR2(2048), + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (ID) +) +/ +CREATE TABLE IDN_USER_DEVICE ( + DEVICE_ID CHAR(36) NOT NULL, + USER_ID VARCHAR2(255) NOT NULL, + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (DEVICE_ID), + FOREIGN KEY (DEVICE_ID) REFERENCES IDN_DEVICE (ID) ON DELETE CASCADE +) +/ +CREATE INDEX IDX_IDV_TENANT_REGISTERED ON IDN_DEVICE (TENANT_ID, REGISTERED_AT) +/ +CREATE INDEX IDX_IUD_USER_TENANT ON IDN_USER_DEVICE (USER_ID, TENANT_ID) +/ diff --git a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/postgresql.sql b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/postgresql.sql index 2ef282070a35..7b6211af18ee 100644 --- a/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/postgresql.sql +++ b/features/identity-core/org.wso2.carbon.identity.core.server.feature/resources/dbscripts/postgresql.sql @@ -2229,3 +2229,27 @@ CREATE INDEX IDX_REFRESH_TOKEN_HASH ON IDN_OAUTH2_REFRESH_TOKEN (REFRESH_TOKEN_H CREATE INDEX IDX_AUTHZ_USER_TENANT_DOMAIN_STATE ON IDN_OAUTH2_REFRESH_TOKEN (AUTHZ_USER, TENANT_ID, USER_DOMAIN, TOKEN_STATE); CREATE INDEX IDX_CONSUMER_KEY_STATE ON IDN_OAUTH2_REFRESH_TOKEN (CONSUMER_KEY_ID, TOKEN_STATE); CREATE INDEX IDX_CONSUMER_USER_SCOPE_IDP ON IDN_OAUTH2_REFRESH_TOKEN (CONSUMER_KEY_ID, AUTHZ_USER, TENANT_ID, USER_DOMAIN, TOKEN_SCOPE_HASH, TOKEN_STATE, IDP_ID); + +-- IDN_DEVICE -- +CREATE TABLE IF NOT EXISTS IDN_DEVICE ( + ID CHAR(36) NOT NULL, + DEVICE_NAME VARCHAR(255) NOT NULL, + DEVICE_MODEL VARCHAR(255), + PUBLIC_KEY VARCHAR(2048) NOT NULL, + STATUS VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + REGISTERED_AT TIMESTAMP NOT NULL, + METADATA VARCHAR(2048), + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (ID) +); + +CREATE TABLE IF NOT EXISTS IDN_USER_DEVICE ( + DEVICE_ID CHAR(36) NOT NULL, + USER_ID VARCHAR(255) NOT NULL, + TENANT_ID INTEGER NOT NULL, + PRIMARY KEY (DEVICE_ID), + FOREIGN KEY (DEVICE_ID) REFERENCES IDN_DEVICE (ID) ON DELETE CASCADE +); + +CREATE INDEX IDX_IDV_TENANT_REGISTERED ON IDN_DEVICE (TENANT_ID, REGISTERED_AT); +CREATE INDEX IDX_IUD_USER_TENANT ON IDN_USER_DEVICE (USER_ID, TENANT_ID); diff --git a/pom.xml b/pom.xml index 63f704c39dab..96cdb361d4e8 100644 --- a/pom.xml +++ b/pom.xml @@ -72,6 +72,7 @@ components/client-attestation-mgt components/trusted-app-mgt components/rule-mgt + components/device-mgt components/webhook-mgt components/action-mgt components/ai-services-mgt