Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import org.zstack.header.message.Message;
import org.zstack.header.vo.ResourceVO;

import org.zstack.utils.TaskContext;

import javax.persistence.EntityManager;
import java.util.*;

Expand Down Expand Up @@ -46,6 +48,38 @@ public class ExternalTenantResourceTracker implements
*/
private static final String HEADER_EXTERNAL_TENANT = "external-tenant-context";

/**
* TaskContext key used to propagate ExternalTenantContext across thread-pool
* boundaries (e.g. thdf.chainSubmit). ZStack's HasThreadContextAspect
* automatically snapshots TaskContext when a ChainTask/Completion is created,
* and SetThreadContextAspect restores it before execution in the worker thread.
* By storing the tenant context here in addition to the ThreadLocal, we ensure
* cascade resources (Volume, NIC, CdRom) created inside FlowChains that run
* in a different thread still see the correct tenant context.
*/
static final String TASK_CONTEXT_EXTERNAL_TENANT = "__externalTenantContext__";

/**
* Resolve the current ExternalTenantContext using ThreadLocal first, then
* falling back to TaskContext. This covers the case where ThreadLocal is
* lost after a thread-pool handoff (e.g. thdf.chainSubmit) but TaskContext
* was propagated by HasThreadContextAspect/SetThreadContextAspect.
*
* Note: this method only reads — it does NOT restore the ThreadLocal, because
* the caller's thread-local state should be managed by SetThreadContextAspect
* at the thread entry point.
*/
private static ExternalTenantContext resolveCurrentContext() {
ExternalTenantContext ctx = ExternalTenantContext.getCurrent();
if (ctx == null || ctx.getTenantId() == null) {
Object taskCtx = TaskContext.getTaskContextItem(TASK_CONTEXT_EXTERNAL_TENANT);
if (taskCtx instanceof ExternalTenantContext) {
ctx = (ExternalTenantContext) taskCtx;
}
}
return ctx;
}

@Override
public boolean start() {
for (ExternalTenantProvider p : pluginRgty.getExtensionList(ExternalTenantProvider.class)) {
Expand All @@ -65,7 +99,7 @@ public boolean start() {
bus.installBeforeSendMessageInterceptor(new AbstractBeforeSendMessageInterceptor() {
@Override
public void beforeSendMessage(Message msg) {
ExternalTenantContext ctx = ExternalTenantContext.getCurrent();
ExternalTenantContext ctx = resolveCurrentContext();
if (ctx != null && ctx.getTenantId() != null) {
msg.putHeaderEntry(HEADER_EXTERNAL_TENANT,
new String[]{ctx.getSource(), ctx.getTenantId(), ctx.getUserId()});
Expand All @@ -90,19 +124,28 @@ public void beforeDeliveryMessage(Message msg) {
SessionInventory session = ((APIMessage) msg).getSession();
if (session != null && session.hasExternalTenant()) {
ExternalTenantContext ctx = session.getExternalTenantContext();
ExternalTenantContext.setCurrent(ctx);
if (ctx != null) {
ExternalTenantContext.setCurrent(ctx);
TaskContext.putTaskContextItem(TASK_CONTEXT_EXTERNAL_TENANT, ctx);
} else {
ExternalTenantContext.clearCurrent();
TaskContext.removeTaskContextItem(TASK_CONTEXT_EXTERNAL_TENANT);
}
} else {
ExternalTenantContext.clearCurrent();
TaskContext.removeTaskContextItem(TASK_CONTEXT_EXTERNAL_TENANT);
}
} else {
// Non-APIMessage: restore from dedicated message header (message-scoped)
String[] tenantData = msg.getHeaderEntry(HEADER_EXTERNAL_TENANT);
if (tenantData != null && tenantData.length >= 2) {
String userId = tenantData.length >= 3 ? tenantData[2] : null;
ExternalTenantContext.setCurrent(
new ExternalTenantContext(tenantData[0], tenantData[1], userId));
ExternalTenantContext ctx = new ExternalTenantContext(tenantData[0], tenantData[1], userId);
ExternalTenantContext.setCurrent(ctx);
TaskContext.putTaskContextItem(TASK_CONTEXT_EXTERNAL_TENANT, ctx);
} else {
ExternalTenantContext.clearCurrent();
TaskContext.removeTaskContextItem(TASK_CONTEXT_EXTERNAL_TENANT);
}
}
}
Expand All @@ -119,7 +162,8 @@ public boolean stop() {

@Override
public void notifyResourceOwnershipCreated(AccountResourceRefVO ref, EntityManager entityManager) {
ExternalTenantContext ctx = ExternalTenantContext.getCurrent();
ExternalTenantContext ctx = resolveCurrentContext();

if (ctx == null || ctx.getTenantId() == null) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,25 @@ public String restrictByExpr(ZQLExtensionContext context, ASTNode.RestrictExpr e
String primaryKey = EntityMetadata.getPrimaryKeyField(src.inventoryAnnotation.mappingVOClass()).getName();
String inventoryAlias = src.simpleInventoryName();

// Generate subquery, filter associated resources by source + tenantId
// Generate subquery, filter associated resources by source + tenantId (+ userId if present)
// Add userId filter only when present and valid; invalid userId is
// silently ignored (falls back to tenant-level isolation) rather than
// throwing SkipThisRestrictExprException which would remove the entire
// tenant filter — a security escalation.
String userId = tenantCtx.getUserId();
String userFilter = "";
if (userId != null && !userId.isEmpty() && SAFE_TENANT_VALUE.matcher(userId).matches()) {
userFilter = String.format(" AND etref.userId = '%s'", escapeSql(userId));
}

return String.format(
"(%s.%s IN (SELECT etref.resourceUuid FROM ExternalTenantResourceRefVO etref" +
" WHERE etref.source = '%s' AND etref.tenantId = '%s'))",
" WHERE etref.source = '%s' AND etref.tenantId = '%s'%s))",
inventoryAlias,
primaryKey,
escapeSql(tenantCtx.getSource()),
escapeSql(tenantCtx.getTenantId())
escapeSql(tenantCtx.getTenantId()),
userFilter
);
}

Expand Down