Skip to content

Commit e341019

Browse files
committed
<feature>[identity]: add generic external tenant resource isolation framework
Implement a generic framework for external services (ZCF, AIOS, etc.) to isolate resources by tenant via HTTP headers (X-Tenant-Source/Id/User). - ExternalTenantContext: ThreadLocal DTO bridging REST layer to AOP layer - ExternalTenantProvider: SPI interface for tenant validation and tracking - ExternalTenantResourceRefVO: JPA entity with CASCADE delete on ResourceVO - ExternalTenantResourceTracker: AOP-driven resource binding on creation, cleanup on hard/soft delete via extension points - ExternalTenantZQLExtension: two-phase ZQL AST injection for automatic tenant-scoped query filtering with SQL injection protection - RestServer integration: header parsing, provider validation, ThreadLocal lifecycle management with try/finally cleanup - SessionInventory: @APINoSee externalTenantContext field + hasExternalTenant() - DB migration: V5.5.1__schema.sql for ExternalTenantResourceRefVO table Resolves: ZCF-1147 Change-Id: I7778676171646874706164777869707288976172
1 parent 6095632 commit e341019

15 files changed

Lines changed: 764 additions & 1 deletion

conf/db/upgrade/V5.5.1__schema.sql

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
CREATE TABLE IF NOT EXISTS `zstack`.`ExternalTenantResourceRefVO` (
2+
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
3+
`source` VARCHAR(64) NOT NULL COMMENT 'source service identifier (zcf, svcX, ...)',
4+
`tenantId` VARCHAR(128) NOT NULL COMMENT 'external tenant identifier',
5+
`userId` VARCHAR(128) DEFAULT NULL COMMENT 'external user identifier (optional)',
6+
`resourceUuid` VARCHAR(32) NOT NULL COMMENT 'resource UUID',
7+
`resourceType` VARCHAR(256) NOT NULL COMMENT 'resource type (VO SimpleName)',
8+
`accountUuid` VARCHAR(32) NOT NULL COMMENT 'associated ZStack Account',
9+
`createDate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
10+
`lastOpDate` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
11+
INDEX idx_source_tenant (`source`, `tenantId`),
12+
INDEX idx_source_tenant_user (`source`, `tenantId`, `userId`),
13+
INDEX idx_resource (`resourceUuid`),
14+
UNIQUE KEY uk_resource_source_tenant (`resourceUuid`, `source`, `tenantId`),
15+
CONSTRAINT fk_ext_tenant_resource FOREIGN KEY (`resourceUuid`)
16+
REFERENCES `ResourceVO`(`uuid`) ON DELETE CASCADE,
17+
CONSTRAINT fk_ext_tenant_account FOREIGN KEY (`accountUuid`)
18+
REFERENCES `AccountVO`(`uuid`) ON DELETE CASCADE
19+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

conf/springConfigXml/AccountManager.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,22 @@
111111
<bean id = "RoleUtils" class="org.zstack.identity.rbac.RoleUtils"/>
112112

113113
<bean id="AccountQuotaUpdateChecker" class="org.zstack.identity.AccountQuotaUpdateChecker"/>
114+
115+
<bean id="ExternalTenantResourceTracker" class="org.zstack.identity.ExternalTenantResourceTracker">
116+
<zstack:plugin>
117+
<zstack:extension interface="org.zstack.header.Component"/>
118+
<zstack:extension interface="org.zstack.header.identity.ResourceOwnershipCreatedExtensionPoint"/>
119+
<zstack:extension interface="org.zstack.core.db.HardDeleteEntityExtensionPoint"/>
120+
<zstack:extension interface="org.zstack.core.db.SoftDeleteEntityByEOExtensionPoint"/>
121+
<zstack:extension interface="org.zstack.header.aspect.OwnedByAccountAspectHelper$ResourceOwnershipCreationNotifier"/>
122+
</zstack:plugin>
123+
</bean>
124+
125+
<bean id="ExternalTenantZQLExtension" class="org.zstack.identity.ExternalTenantZQLExtension">
126+
<zstack:plugin>
127+
<zstack:extension interface="org.zstack.header.zql.MarshalZQLASTTreeExtensionPoint"/>
128+
<zstack:extension interface="org.zstack.header.zql.RestrictByExprExtensionPoint"/>
129+
</zstack:plugin>
130+
</bean>
114131
</beans>
115132

header/src/main/java/org/zstack/header/aspect/OwnedByAccountAspectHelper.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
import javax.persistence.EntityManager;
99

1010
public class OwnedByAccountAspectHelper {
11+
12+
// static field for resource ownership creation extension point callback
13+
private static ResourceOwnershipCreationNotifier notifier;
14+
15+
public static void setResourceOwnershipCreationNotifier(ResourceOwnershipCreationNotifier notifier) {
16+
OwnedByAccountAspectHelper.notifier = notifier;
17+
}
18+
1119
public static void createAccountResourceRefVO(OwnedByAccount oa, EntityManager entityManager, Object entity) {
1220
AccountResourceRefVO ref = new AccountResourceRefVO();
1321
ref.setAccountUuid(oa.getAccountUuid());
@@ -19,5 +27,21 @@ public static void createAccountResourceRefVO(OwnedByAccount oa, EntityManager e
1927
ref.setShared(false);
2028

2129
entityManager.persist(ref);
30+
31+
// notify resource ownership creation event
32+
if (notifier != null) {
33+
try {
34+
notifier.notifyResourceOwnershipCreated(ref);
35+
} catch (Exception e) {
36+
// extension point failure should not affect the main flow, silently ignored
37+
}
38+
}
39+
}
40+
41+
/**
42+
* Resource ownership creation notifier interface to avoid circular dependency
43+
*/
44+
public static interface ResourceOwnershipCreationNotifier {
45+
void notifyResourceOwnershipCreated(AccountResourceRefVO ref);
2246
}
2347
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package org.zstack.header.identity;
2+
3+
import java.io.Serializable;
4+
5+
/**
6+
* External tenant context DTO.
7+
* Passed by external services (like ZCF, AIOS, etc.) through HTTP Headers,
8+
* attached to SessionInventory throughout the entire request chain.
9+
*/
10+
public class ExternalTenantContext implements Serializable {
11+
private static final long serialVersionUID = 1L;
12+
13+
// ThreadLocal used to pass current request's external tenant context at AOP level
14+
// Set by RestServer after Header parsing, cleaned up after request completion
15+
private static final ThreadLocal<ExternalTenantContext> current = new ThreadLocal<>();
16+
17+
public static void setCurrent(ExternalTenantContext ctx) {
18+
current.set(ctx);
19+
}
20+
21+
public static ExternalTenantContext getCurrent() {
22+
return current.get();
23+
}
24+
25+
public static void clearCurrent() {
26+
current.remove();
27+
}
28+
29+
private String source; // Source service identifier, such as "zcf", "svcX"
30+
private String tenantId; // External tenant identifier
31+
private String userId; // External user identifier (optional)
32+
33+
public ExternalTenantContext() {
34+
}
35+
36+
public ExternalTenantContext(String source, String tenantId, String userId) {
37+
this.source = source;
38+
this.tenantId = tenantId;
39+
this.userId = userId;
40+
}
41+
42+
public String getSource() {
43+
return source;
44+
}
45+
46+
public void setSource(String source) {
47+
this.source = source;
48+
}
49+
50+
public String getTenantId() {
51+
return tenantId;
52+
}
53+
54+
public void setTenantId(String tenantId) {
55+
this.tenantId = tenantId;
56+
}
57+
58+
public String getUserId() {
59+
return userId;
60+
}
61+
62+
public void setUserId(String userId) {
63+
this.userId = userId;
64+
}
65+
66+
@Override
67+
public String toString() {
68+
return String.format("ExternalTenantContext{source='%s', tenantId='%s', userId='%s'}", source, tenantId, userId);
69+
}
70+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package org.zstack.header.identity;
2+
3+
/**
4+
* External tenant Provider SPI.
5+
* Each external service (ZCF, AIOS, etc.) implements this interface to integrate with the universal tenant resource isolation framework.
6+
*
7+
* The framework automatically collects all implementations through {@link org.zstack.core.componentloader.PluginRegistry}.
8+
* Each Provider returns a unique source identifier (such as "zcf") through {@link #getSource()},
9+
* corresponding to the HTTP Header X-Tenant-Source value.
10+
*/
11+
public interface ExternalTenantProvider {
12+
/**
13+
* Source identifier, such as "zcf", "svcX".
14+
* Corresponds to X-Tenant-Source header value.
15+
* Must be globally unique.
16+
*/
17+
String getSource();
18+
19+
/**
20+
* Validate tenant context validity.
21+
* Called after RestServer parses Header and before injecting into Session.
22+
* Throwing an exception indicates validation failure, and the request will be rejected.
23+
*
24+
* @param ctx External tenant context (already parsed from Header)
25+
*/
26+
void validateTenant(ExternalTenantContext ctx);
27+
28+
/**
29+
* Whether to track this type of resource.
30+
* After resource creation, the framework calls this method to decide whether to write to ExternalTenantResourceRefVO.
31+
* Returning false indicates that this resource type does not need to be associated with tenant.
32+
* Default is true (track all resources).
33+
*
34+
* @param resourceType Resource type (VO SimpleName, such as "VmInstanceVO")
35+
*/
36+
default boolean shouldTrackResource(String resourceType) {
37+
return true;
38+
}
39+
40+
/**
41+
* Resource binding callback (optional).
42+
* Called after ExternalTenantResourceRefVO is written,
43+
* Provider can use this for custom logic such as sending notifications or writing audit logs.
44+
*
45+
* @param ctx External tenant context
46+
* @param resourceUuid Resource UUID
47+
* @param resourceType Resource type (VO SimpleName)
48+
*/
49+
default void onResourceBound(ExternalTenantContext ctx,
50+
String resourceUuid, String resourceType) {
51+
}
52+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package org.zstack.header.identity;
2+
3+
import org.zstack.header.configuration.PythonClassInventory;
4+
import org.zstack.header.search.Inventory;
5+
6+
import java.sql.Timestamp;
7+
import java.util.ArrayList;
8+
import java.util.Collection;
9+
import java.util.List;
10+
11+
/**
12+
* Inventory for ExternalTenantResourceRefVO
13+
*/
14+
@PythonClassInventory
15+
@Inventory(mappingVOClass = ExternalTenantResourceRefVO.class)
16+
public class ExternalTenantResourceRefInventory {
17+
private long id;
18+
private String source;
19+
private String tenantId;
20+
private String userId;
21+
private String resourceUuid;
22+
private String accountUuid;
23+
private String resourceType;
24+
private Timestamp createDate;
25+
private Timestamp lastOpDate;
26+
27+
public ExternalTenantResourceRefInventory() {
28+
}
29+
30+
public static List<ExternalTenantResourceRefInventory> valueOf(Collection<ExternalTenantResourceRefVO> vos) {
31+
List<ExternalTenantResourceRefInventory> invs = new ArrayList<>();
32+
for (ExternalTenantResourceRefVO vo : vos) {
33+
invs.add(valueOf(vo));
34+
}
35+
return invs;
36+
}
37+
38+
public static ExternalTenantResourceRefInventory valueOf(ExternalTenantResourceRefVO vo) {
39+
return new ExternalTenantResourceRefInventory(vo);
40+
}
41+
42+
public ExternalTenantResourceRefInventory(ExternalTenantResourceRefVO vo) {
43+
this.id = vo.getId();
44+
this.source = vo.getSource();
45+
this.tenantId = vo.getTenantId();
46+
this.userId = vo.getUserId();
47+
this.resourceUuid = vo.getResourceUuid();
48+
this.accountUuid = vo.getAccountUuid();
49+
this.resourceType = vo.getResourceType();
50+
this.createDate = vo.getCreateDate();
51+
this.lastOpDate = vo.getLastOpDate();
52+
}
53+
54+
public long getId() {
55+
return id;
56+
}
57+
58+
public void setId(long id) {
59+
this.id = id;
60+
}
61+
62+
public String getSource() {
63+
return source;
64+
}
65+
66+
public void setSource(String source) {
67+
this.source = source;
68+
}
69+
70+
public String getTenantId() {
71+
return tenantId;
72+
}
73+
74+
public void setTenantId(String tenantId) {
75+
this.tenantId = tenantId;
76+
}
77+
78+
public String getUserId() {
79+
return userId;
80+
}
81+
82+
public void setUserId(String userId) {
83+
this.userId = userId;
84+
}
85+
86+
public String getResourceUuid() {
87+
return resourceUuid;
88+
}
89+
90+
public void setResourceUuid(String resourceUuid) {
91+
this.resourceUuid = resourceUuid;
92+
}
93+
94+
public String getAccountUuid() {
95+
return accountUuid;
96+
}
97+
98+
public void setAccountUuid(String accountUuid) {
99+
this.accountUuid = accountUuid;
100+
}
101+
102+
public String getResourceType() {
103+
return resourceType;
104+
}
105+
106+
public void setResourceType(String resourceType) {
107+
this.resourceType = resourceType;
108+
}
109+
110+
public Timestamp getCreateDate() {
111+
return createDate;
112+
}
113+
114+
public void setCreateDate(Timestamp createDate) {
115+
this.createDate = createDate;
116+
}
117+
118+
public Timestamp getLastOpDate() {
119+
return lastOpDate;
120+
}
121+
122+
public void setLastOpDate(Timestamp lastOpDate) {
123+
this.lastOpDate = lastOpDate;
124+
}
125+
}

0 commit comments

Comments
 (0)