Repository navigation
Record the creator on a workflow's sharing record #2300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
5909377
069a8a3
60b457e
e13ec29
d88ce61
f3dcd02
bb19a0f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,6 +49,7 @@ import org.opensearch.alerting.util.indexStashed | |
| import org.opensearch.alerting.util.isADMonitor | ||
| import org.opensearch.alerting.util.isQueryLevelMonitor | ||
| import org.opensearch.alerting.util.use | ||
| import org.opensearch.alerting.util.withStoredContext | ||
| import org.opensearch.alerting.workflow.CompositeWorkflowRunner | ||
| import org.opensearch.cluster.service.ClusterService | ||
| import org.opensearch.common.inject.Inject | ||
|
|
@@ -227,6 +228,8 @@ class TransportIndexWorkflowAction @Inject constructor( | |
| } | ||
|
|
||
| val tenantId = client.threadPool().threadContext.getHeader(AlertingPlugin.TENANT_ID_HEADER) | ||
| // Capture before launch: Dispatchers.IO drops the thread context, and the workflow write needs the caller's auth | ||
| val storedContext = client.threadPool().threadContext.newStoredContext(false) | ||
| scope.launch(TenantContext(tenantId)) { | ||
| try { | ||
| val triggerCount = transformedRequest.workflow.triggers.size | ||
|
|
@@ -246,10 +249,6 @@ class TransportIndexWorkflowAction @Inject constructor( | |
| client, | ||
| object : ActionListener<AcknowledgedResponse> { | ||
| override fun onResponse(response: AcknowledgedResponse) { | ||
| // Capture pre-stash context so the write path can restore the auth-user | ||
| // transient — the security plugin's ResourceIndexListener needs it to | ||
| // record the workflow's resource-share entry with createdBy=<caller>. | ||
| val storedContext = client.threadPool().threadContext.newStoredContext(false) | ||
| client.threadPool().threadContext.stashContext().use { | ||
| IndexWorkflowHandler( | ||
| client, actionListener, transformedRequest, user, tenantId, storedContext | ||
|
|
@@ -285,7 +284,7 @@ class TransportIndexWorkflowAction @Inject constructor( | |
| private val request: IndexWorkflowRequest, | ||
| private val user: User?, | ||
| private val tenantId: String?, | ||
| private val storedThreadContext: org.opensearch.common.util.concurrent.ThreadContext.StoredContext? = null, | ||
| private val storedThreadContext: org.opensearch.common.util.concurrent.ThreadContext.StoredContext, | ||
| ) { | ||
| fun resolveUserAndStart() { | ||
| scope.launch(TenantContext(tenantId)) { | ||
|
|
@@ -432,12 +431,11 @@ class TransportIndexWorkflowAction @Inject constructor( | |
| .setIfPrimaryTerm(request.primaryTerm) | ||
| .timeout(indexTimeout) | ||
|
|
||
| // Restore the caller's persistent auth header so the shard-level ResourceIndexListener sees | ||
| // the caller when recording the workflow's share entry. [Client.indexStashed] stashes | ||
| // per-call so the internal-index write isn't gated by the caller's privileges. | ||
| storedThreadContext?.restore() | ||
| try { | ||
| val indexResponse: IndexResponse = client.indexStashed(indexRequest) | ||
| // The caller's context reaches only this write, where the security plugin records the creator | ||
| val indexResponse: IndexResponse = client.threadPool().threadContext.withStoredContext(storedThreadContext) { | ||
| client.indexStashed(indexRequest) | ||
| } | ||
| val failureReasons = checkShardsFailure(indexResponse) | ||
| if (failureReasons != null) { | ||
| log.error("Failed to create workflow: $failureReasons") | ||
|
|
@@ -600,11 +598,10 @@ class TransportIndexWorkflowAction @Inject constructor( | |
| .setIfPrimaryTerm(request.primaryTerm) | ||
| .timeout(indexTimeout) | ||
|
|
||
| // Restore the caller's persistent auth header for the shard-level ResourceIndexListener; | ||
| // [Client.indexStashed] stashes per-call so the internal-index write isn't gated by the caller. | ||
| storedThreadContext?.restore() | ||
| try { | ||
| val indexResponse: IndexResponse = client.indexStashed(indexRequest) | ||
| val indexResponse: IndexResponse = client.threadPool().threadContext.withStoredContext(storedThreadContext) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: on update, this restores the context captured on the request thread, which is the updater's, not the creator's. That's fine if the security plugin only records
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed. The security plugin writes the sharing record only when the document is created. On a later write to the resource, "Update" in the existing test meant an update of the workflow document by the owner. In bb19a0f I added |
||
| client.indexStashed(indexRequest) | ||
| } | ||
| val failureReasons = checkShardsFailure(indexResponse) | ||
| if (failureReasons != null) { | ||
| actionListener.onFailure( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -64,6 +64,20 @@ suspend fun SdkClient.deleteDataObjectStashed( | |
| .await() | ||
| } | ||
|
|
||
| /** | ||
| * Runs [block] under [stored], then restores the context the coroutine had, on whichever thread it resumes. This keeps | ||
| * the caller's context from reaching the calls that follow [block]. | ||
| */ | ||
| suspend fun <T> ThreadContext.withStoredContext(stored: ThreadContext.StoredContext, block: suspend () -> T): T { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can leave the caller's identity on a worker thread
The next coroutine that captures context on A can then pick up this caller, which is the "Alice's workflow recorded as Bob's" symptom from the description. The KDoc's promise ("keeps the caller's context from reaching the calls that follow") holds for the coroutine, but not for thread A. One way to avoid it is to do the restore, stash and dispatch synchronously on A before suspending, and reset A right away: suspendCoroutine { cont ->
val own = ctx.newStoredContext(false)
stored.restore()
try {
ctx.stashContext().use { client.index(request, listenerFor(cont)) }
} finally {
own.restore()
}
}The listener resumes the coroutine, and no thread keeps the caller's context after this returns. I haven't reproduced the leak; this is from the ThreadContext semantics, so please correct me if the security plugin clears the persistent header somewhere I missed.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right. In bb19a0f I replaced |
||
| val own = newStoredContext(false) | ||
| stored.restore() | ||
| try { | ||
| return block() | ||
| } finally { | ||
| own.restore() | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Executes [Client.index] with the caller's [ThreadContext] stashed just before dispatch; the stash | ||
| * is closed via the completion callback so it works across the async / coroutine-resume boundary. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this only compiles against a security SPI that has
matchesResource(security#6627), andbuild.gradledoesn't change, so it relies on the current security snapshot. Since the description says the two must ship together, would a minimum security version check or a release-notes entry be worth adding so that dependency is enforced rather than only described?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alerting compiles against the security SPI of its own OpenSearch version (
opensearch-security-spi:${opensearch_build}), and the two ship in the same release. A build against an SPI withoutmatchesResourcedoes not compile, so that is the check. Release notes are generated from the PR metadata now, so the description carries the dependency.