feat: introduce TenantOwned interface for type-safe tenant filtering

Replace reflective getTenantId() in BaseController with a typed
TenantOwned interface. All tenant-scoped BO classes now implement
TenantOwned, enabling compile-time safety instead of runtime
reflection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Copilot
2026-05-10 01:48:24 +08:00
co-authored by Claude Opus 4.7
parent b0094f69b7
commit a08f15904d
13 changed files with 58 additions and 26 deletions
@@ -18,6 +18,7 @@
package io.github.pnoker.common.auth.entity.bo;
import io.github.pnoker.common.entity.base.BaseBO;
import io.github.pnoker.common.entity.common.TenantOwned;
import io.github.pnoker.common.entity.ext.RoleExt;
import io.github.pnoker.common.enums.EnableFlagEnum;
import lombok.AllArgsConstructor;
@@ -38,7 +39,7 @@ import lombok.ToString;
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
public class RoleBO extends BaseBO {
public class RoleBO extends BaseBO implements TenantOwned {
/**
* ID
@@ -18,6 +18,7 @@
package io.github.pnoker.common.dal.entity.bo;
import io.github.pnoker.common.entity.base.BaseBO;
import io.github.pnoker.common.entity.common.TenantOwned;
import io.github.pnoker.common.enums.EnableFlagEnum;
import io.github.pnoker.common.enums.GroupTypeFlagEnum;
import lombok.AllArgsConstructor;
@@ -38,7 +39,7 @@ import lombok.ToString;
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
public class GroupBO extends BaseBO {
public class GroupBO extends BaseBO implements TenantOwned {
/**
* Parent group ID.
@@ -18,6 +18,7 @@
package io.github.pnoker.common.dal.entity.bo;
import io.github.pnoker.common.entity.base.BaseBO;
import io.github.pnoker.common.entity.common.TenantOwned;
import io.github.pnoker.common.enums.EnableFlagEnum;
import io.github.pnoker.common.enums.EntityTypeFlagEnum;
import lombok.AllArgsConstructor;
@@ -38,7 +39,7 @@ import lombok.ToString;
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
public class LabelBO extends BaseBO {
public class LabelBO extends BaseBO implements TenantOwned {
/**
* Label name.
@@ -18,6 +18,7 @@
package io.github.pnoker.common.manager.entity.bo;
import io.github.pnoker.common.entity.base.BaseBO;
import io.github.pnoker.common.entity.common.TenantOwned;
import io.github.pnoker.common.entity.ext.DeviceExt;
import io.github.pnoker.common.enums.EnableFlagEnum;
import lombok.AllArgsConstructor;
@@ -40,7 +41,7 @@ import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
public class DeviceBO extends BaseBO {
public class DeviceBO extends BaseBO implements TenantOwned {
/**
* Device Name
@@ -18,6 +18,7 @@
package io.github.pnoker.common.manager.entity.bo;
import io.github.pnoker.common.entity.base.BaseBO;
import io.github.pnoker.common.entity.common.TenantOwned;
import io.github.pnoker.common.entity.ext.DriverAttributeExt;
import io.github.pnoker.common.enums.AttributeTypeFlagEnum;
import io.github.pnoker.common.enums.EnableFlagEnum;
@@ -39,7 +40,7 @@ import lombok.ToString;
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
public class DriverAttributeBO extends BaseBO {
public class DriverAttributeBO extends BaseBO implements TenantOwned {
/**
* Name
@@ -18,6 +18,7 @@
package io.github.pnoker.common.manager.entity.bo;
import io.github.pnoker.common.entity.base.BaseBO;
import io.github.pnoker.common.entity.common.TenantOwned;
import io.github.pnoker.common.entity.ext.JsonExt;
import io.github.pnoker.common.enums.EnableFlagEnum;
import lombok.AllArgsConstructor;
@@ -38,7 +39,7 @@ import lombok.ToString;
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
public class DriverAttributeConfigBO extends BaseBO {
public class DriverAttributeConfigBO extends BaseBO implements TenantOwned {
/**
* ID
@@ -18,6 +18,7 @@
package io.github.pnoker.common.manager.entity.bo;
import io.github.pnoker.common.entity.base.BaseBO;
import io.github.pnoker.common.entity.common.TenantOwned;
import io.github.pnoker.common.entity.ext.DriverExt;
import io.github.pnoker.common.enums.DriverTypeFlagEnum;
import io.github.pnoker.common.enums.EnableFlagEnum;
@@ -39,7 +40,7 @@ import lombok.ToString;
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
public class DriverBO extends BaseBO {
public class DriverBO extends BaseBO implements TenantOwned {
/**
* Name
@@ -18,6 +18,7 @@
package io.github.pnoker.common.manager.entity.bo;
import io.github.pnoker.common.entity.base.BaseBO;
import io.github.pnoker.common.entity.common.TenantOwned;
import io.github.pnoker.common.entity.ext.PointAttributeExt;
import io.github.pnoker.common.enums.AttributeTypeFlagEnum;
import io.github.pnoker.common.enums.EnableFlagEnum;
@@ -39,7 +40,7 @@ import lombok.ToString;
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
public class PointAttributeBO extends BaseBO {
public class PointAttributeBO extends BaseBO implements TenantOwned {
/**
* Name
@@ -18,6 +18,7 @@
package io.github.pnoker.common.manager.entity.bo;
import io.github.pnoker.common.entity.base.BaseBO;
import io.github.pnoker.common.entity.common.TenantOwned;
import io.github.pnoker.common.entity.ext.JsonExt;
import io.github.pnoker.common.enums.EnableFlagEnum;
import lombok.AllArgsConstructor;
@@ -38,7 +39,7 @@ import lombok.ToString;
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
public class PointAttributeConfigBO extends BaseBO {
public class PointAttributeConfigBO extends BaseBO implements TenantOwned {
/**
* ID
@@ -18,6 +18,7 @@
package io.github.pnoker.common.manager.entity.bo;
import io.github.pnoker.common.entity.base.BaseBO;
import io.github.pnoker.common.entity.common.TenantOwned;
import io.github.pnoker.common.entity.ext.PointExt;
import io.github.pnoker.common.enums.EnableFlagEnum;
import io.github.pnoker.common.enums.PointTypeFlagEnum;
@@ -42,7 +43,7 @@ import java.math.BigDecimal;
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
public class PointBO extends BaseBO {
public class PointBO extends BaseBO implements TenantOwned {
/**
* Name
@@ -18,6 +18,7 @@
package io.github.pnoker.common.manager.entity.bo;
import io.github.pnoker.common.entity.base.BaseBO;
import io.github.pnoker.common.entity.common.TenantOwned;
import io.github.pnoker.common.entity.ext.ProfileExt;
import io.github.pnoker.common.enums.EnableFlagEnum;
import io.github.pnoker.common.enums.ProfileShareFlagEnum;
@@ -40,7 +41,7 @@ import lombok.ToString;
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
public class ProfileBO extends BaseBO {
public class ProfileBO extends BaseBO implements TenantOwned {
/**
* Name
@@ -0,0 +1,31 @@
/*
* Copyright 2016-present the IoT DC3 original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.github.pnoker.common.entity.common;
/**
* Marker contract for tenant-scoped objects.
*
* @author pnoker
* @version 2026.5.10
* @since 2026.5.10
*/
public interface TenantOwned {
Long getTenantId();
}
@@ -19,12 +19,12 @@ package io.github.pnoker.common.base;
import io.github.pnoker.common.entity.R;
import io.github.pnoker.common.entity.common.RequestHeader;
import io.github.pnoker.common.entity.common.TenantOwned;
import io.github.pnoker.common.exception.NotFoundException;
import io.github.pnoker.common.utils.UserHeaderUtil;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
@@ -65,8 +65,8 @@ public interface BaseController {
* Fail closed when an ID-based lookup returns an entity outside the caller's tenant.
* Returning 404 avoids exposing whether a cross-tenant resource exists.
*/
default <T> T requireTenant(Long tenantId, T entity) {
if (Objects.isNull(entity) || !Objects.equals(tenantId, resolveTenantId(entity))) {
default <T extends TenantOwned> T requireTenant(Long tenantId, T entity) {
if (Objects.isNull(entity) || !Objects.equals(tenantId, entity.getTenantId())) {
throw new NotFoundException("Resource does not exist");
}
return entity;
@@ -75,26 +75,16 @@ public interface BaseController {
/**
* Keep only entities that belong to the caller's tenant for bulk lookup endpoints.
*/
default <T> List<T> filterTenant(Long tenantId, Collection<T> entities) {
default <T extends TenantOwned> List<T> filterTenant(Long tenantId, Collection<T> entities) {
if (Objects.isNull(entities) || entities.isEmpty()) {
return List.of();
}
return entities.stream()
.filter(Objects::nonNull)
.filter(entity -> Objects.equals(tenantId, resolveTenantId(entity)))
.filter(entity -> Objects.equals(tenantId, entity.getTenantId()))
.toList();
}
private Long resolveTenantId(Object entity) {
try {
Method method = entity.getClass().getMethod("getTenantId");
Object tenantId = method.invoke(entity);
return tenantId instanceof Long value ? value : null;
} catch (ReflectiveOperationException e) {
throw new NotFoundException("Resource does not exist");
}
}
/**
* Get user ID from user header
*