diff --git a/api/flow-execution.yaml b/api/flow-execution.yaml index 65b85becc1..15d94acf74 100644 --- a/api/flow-execution.yaml +++ b/api/flow-execution.yaml @@ -446,9 +446,9 @@ components: type: type: string description: | - Action type forwarded from the PROMPT node definition. Predefined values are - `SUBMIT` (approve/confirm) and `REJECT` (deny/cancel). Custom strings are also - accepted for executor-specific routing. + Action type forwarded from the PROMPT node definition. The value is an opaque string: + the flow author and the executor that consumes it agree on the vocabulary, so the + recognized values are documented by that executor rather than fixed here. example: "SUBMIT" Input: diff --git a/api/flow-management.yaml b/api/flow-management.yaml index e128dcdd7d..adc78d62f2 100644 --- a/api/flow-management.yaml +++ b/api/flow-management.yaml @@ -1411,9 +1411,11 @@ components: type: type: string description: | - Optional action type forwarded to the next executor so it can determine which - processing path to take. Predefined values are `SUBMIT` (approve/confirm) and - `REJECT` (deny/cancel). If omitted, no action-type hint is forwarded. + Optional action type forwarded to the next node's executor so it can determine which + processing path to take. The value is an opaque string: the flow author and the + executor that consumes it agree on the vocabulary, so the recognized values are + documented by that executor rather than fixed here. If omitted, no action-type hint + is forwarded. example: SUBMIT nextNode: type: string diff --git a/backend/internal/flow/common/constants.go b/backend/internal/flow/common/constants.go index 373361d9ed..df5db90cfb 100644 --- a/backend/internal/flow/common/constants.go +++ b/backend/internal/flow/common/constants.go @@ -248,10 +248,6 @@ const ( // requested without a valid id_token_hint. A sign-out flow's session sign-out node reads it to // decide whether the End-User must confirm the logout before the session is terminated. RuntimeKeyLogoutPromptRequired = "logoutPromptRequired" - // RuntimeKeyLogoutPromptShown is the session sign-out node's own guard, set when it routes to the - // confirmation prompt so that on re-run (after the user confirms) it terminates instead of - // prompting again. - RuntimeKeyLogoutPromptShown = "logoutPromptShown" ) // SSOCheckpointKey scopes a per-checkpoint SSO control key (RuntimeKeySSOSessionPresent, @@ -291,6 +287,11 @@ const ( ActionTypeSubmit ActionType = "SUBMIT" // ActionTypeReject represents a reject/deny action ActionTypeReject ActionType = "REJECT" + // ActionTypeSignOutConfirm marks the confirmation prompt's action edge in a sign-out flow. When the + // End-User confirms, the prompt node forwards this type to the session sign-out node (as the action + // type in ForwardedData), which reads it to tell a confirmed re-run apart from the initial request, + // so no runtime flag has to be persisted. + ActionTypeSignOutConfirm ActionType = "SIGN_OUT_CONFIRM" ) // ForwardedData key constants define keys used in the ForwardedData map. diff --git a/backend/internal/flow/executor/session_signout_executor.go b/backend/internal/flow/executor/session_signout_executor.go index 2a7e46548e..d331f56ad0 100644 --- a/backend/internal/flow/executor/session_signout_executor.go +++ b/backend/internal/flow/executor/session_signout_executor.go @@ -65,7 +65,7 @@ func newSessionSignOutExecutor(flowFactory core.FlowFactoryInterface, sso sessio // a valid id_token_hint (RuntimeKeyLogoutPromptRequired), the executor first routes to the node's // onIncomplete confirmation prompt and only terminates the session once the End-User confirms. This // keeps the confirmation logic in the executor rather than a node condition the flow editor cannot -// represent. +// represent. See decide for how the prompt's action types map to outcomes. func (e *sessionSignOutExecutor) Execute(ctx *providers.NodeContext) (*providers.ExecutorResponse, error) { logger := e.logger.With(log.String(log.LoggerKeyExecutionID, ctx.ExecutionID)) @@ -76,10 +76,10 @@ func (e *sessionSignOutExecutor) Execute(ctx *providers.NodeContext) (*providers } // Ask the End-User to confirm before terminating when the node requests it and no valid - // id_token_hint established the request's legitimacy. The prompt is shown once: the marker is - // persisted in RuntimeData so the re-run (after confirmation) proceeds to terminate the session. - if e.confirmationRequired(ctx) { - execResp.RuntimeData[common.RuntimeKeyLogoutPromptShown] = dataValueTrue + // id_token_hint established the request's legitimacy. Routing to the onIncomplete prompt and back + // is enough: the prompt forwards the chosen action's type here on the re-run, so the decision is + // read off that type without persisting any marker. + if e.decide(ctx) == signOutPrompt { execResp.Status = providers.ExecUserInputRequired logger.Debug(ctx.Context, "Routing to sign-out confirmation prompt") return execResp, nil @@ -99,17 +99,42 @@ func (e *sessionSignOutExecutor) Execute(ctx *providers.NodeContext) (*providers return execResp, nil } -// confirmationRequired reports whether the executor should route to its onIncomplete confirmation -// prompt before terminating the session. It is true only when the node opts in (promptOnSignOut), -// the RP-initiated logout requires a prompt (no valid id_token_hint), and the prompt has not already -// been shown in this flow run. -func (e *sessionSignOutExecutor) confirmationRequired(ctx *providers.NodeContext) bool { +// signOutOutcome is what the executor does with the current request. +type signOutOutcome int + +const ( + // signOutTerminate ends the SSO session. + signOutTerminate signOutOutcome = iota + // signOutPrompt routes to the node's onIncomplete confirmation prompt. + signOutPrompt +) + +// decide reports what the executor should do with the current request. +// +// Confirmation is skipped entirely unless the node opts in (promptOnSignOut) and the RP-initiated +// logout requires a prompt because it carried no valid id_token_hint. Past that, the outcome comes +// from the action type the confirmation prompt forwards on the re-run, so the End-User's choice is +// read off the flow definition rather than a persisted marker. +// +// Each action the confirmation prompt can raise maps to one outcome here: a new button on that +// prompt is supported by giving its action type a case, and a new outcome by adding a +// signOutOutcome and handling it in Execute. +func (e *sessionSignOutExecutor) decide(ctx *providers.NodeContext) signOutOutcome { promptEnabled, _ := ctx.NodeProperties[propertyKeyPromptOnSignOut].(bool) if !promptEnabled { - return false + return signOutTerminate } if ctx.RuntimeData[common.RuntimeKeyLogoutPromptRequired] != dataValueTrue { - return false + return signOutTerminate + } + + actionType, _ := ctx.ForwardedData[common.ForwardedDataKeyActionType].(string) + switch common.ActionType(actionType) { + case common.ActionTypeSignOutConfirm: + return signOutTerminate + default: + // The initial request forwards no action type at all, and a type this executor does not + // recognize is not consent to end the session. Both ask the End-User to confirm. + return signOutPrompt } - return ctx.RuntimeData[common.RuntimeKeyLogoutPromptShown] != dataValueTrue } diff --git a/backend/internal/flow/executor/session_signout_executor_test.go b/backend/internal/flow/executor/session_signout_executor_test.go index bb90deaf70..edf0233c8b 100644 --- a/backend/internal/flow/executor/session_signout_executor_test.go +++ b/backend/internal/flow/executor/session_signout_executor_test.go @@ -111,8 +111,8 @@ func (suite *SessionSignOutExecutorTestSuite) TestTerminateError() { } // TestPromptsWhenConfirmationRequired covers a prompt-enabled node whose logout arrived without a -// valid id_token_hint: the executor routes to the confirmation prompt (incomplete), marks the prompt -// as shown, and does not terminate the session. +// valid id_token_hint and no confirmation yet: the executor routes to the confirmation prompt +// (incomplete) and does not terminate the session. func (suite *SessionSignOutExecutorTestSuite) TestPromptsWhenConfirmationRequired() { sso := sessionmock.NewServiceMock(suite.T()) exec := suite.newExecutor(sso) @@ -125,13 +125,13 @@ func (suite *SessionSignOutExecutorTestSuite) TestPromptsWhenConfirmationRequire suite.Require().NoError(err) suite.Equal(providers.ExecUserInputRequired, resp.Status) - suite.Equal(dataValueTrue, resp.RuntimeData[common.RuntimeKeyLogoutPromptShown]) suite.Empty(resp.EngineData[common.RuntimeKeySSOSessionCleared]) sso.AssertNotCalled(suite.T(), "Terminate", mock.Anything, mock.Anything, mock.Anything) } -// TestTerminatesAfterConfirmation covers the re-run once the prompt has been shown: the guard marker -// is present, so the executor terminates the session instead of prompting again. +// TestTerminatesAfterConfirmation covers the re-run after the End-User confirms: the confirmation +// prompt forwards its sign-out confirm action type, so the executor terminates the session instead of +// prompting again. func (suite *SessionSignOutExecutorTestSuite) TestTerminatesAfterConfirmation() { sso := sessionmock.NewServiceMock(suite.T()) sso.EXPECT().Terminate(mock.Anything, "handle-abc", "flow-1"). @@ -140,9 +140,9 @@ func (suite *SessionSignOutExecutorTestSuite) TestTerminatesAfterConfirmation() ctx := signOutNodeContext() ctx.NodeProperties = map[string]interface{}{propertyKeyPromptOnSignOut: true} - ctx.RuntimeData = map[string]string{ - common.RuntimeKeyLogoutPromptRequired: dataValueTrue, - common.RuntimeKeyLogoutPromptShown: dataValueTrue, + ctx.RuntimeData = map[string]string{common.RuntimeKeyLogoutPromptRequired: dataValueTrue} + ctx.ForwardedData = map[string]interface{}{ + common.ForwardedDataKeyActionType: string(common.ActionTypeSignOutConfirm), } resp, err := exec.Execute(ctx) @@ -152,6 +152,28 @@ func (suite *SessionSignOutExecutorTestSuite) TestTerminatesAfterConfirmation() suite.Equal(dataValueTrue, resp.EngineData[common.RuntimeKeySSOSessionCleared]) } +// TestPromptsWhenActionTypeUnrecognized covers a confirmation prompt that forwarded an action type +// this executor has no case for: an unrecognized type is not consent to end the session, so the +// executor prompts rather than terminating. +func (suite *SessionSignOutExecutorTestSuite) TestPromptsWhenActionTypeUnrecognized() { + sso := sessionmock.NewServiceMock(suite.T()) + exec := suite.newExecutor(sso) + + ctx := signOutNodeContext() + ctx.NodeProperties = map[string]interface{}{propertyKeyPromptOnSignOut: true} + ctx.RuntimeData = map[string]string{common.RuntimeKeyLogoutPromptRequired: dataValueTrue} + ctx.ForwardedData = map[string]interface{}{ + common.ForwardedDataKeyActionType: "SOME_OTHER_ACTION", + } + + resp, err := exec.Execute(ctx) + + suite.Require().NoError(err) + suite.Equal(providers.ExecUserInputRequired, resp.Status) + suite.Empty(resp.EngineData[common.RuntimeKeySSOSessionCleared]) + sso.AssertNotCalled(suite.T(), "Terminate", mock.Anything, mock.Anything, mock.Anything) +} + // TestTerminatesWhenHintProvided covers a prompt-enabled node whose logout carried a valid // id_token_hint (no prompt flag): the executor terminates directly without confirming. func (suite *SessionSignOutExecutorTestSuite) TestTerminatesWhenHintProvided() { diff --git a/docs/content/guides/flows/advanced-configurations.mdx b/docs/content/guides/flows/advanced-configurations.mdx index 0f2a5b5443..6fc8cb454a 100644 --- a/docs/content/guides/flows/advanced-configurations.mdx +++ b/docs/content/guides/flows/advanced-configurations.mdx @@ -40,7 +40,7 @@ A prompt entry can carry an action only, or both. The node collects all input va |---|---|---| | `ref` | Yes | Unique identifier for the action. | | `nextNode` | Yes | ID of the node to advance to when this action is selected. | -| `type` | No | `SUBMIT` or `REJECT`. | +| `type` | No | Action type forwarded to the next node's executor, so it can tell which action was taken. Recognized values are defined by that executor. | **Example** diff --git a/frontend/apps/console/src/features/flows/data/templates.json b/frontend/apps/console/src/features/flows/data/templates.json index e6bd3f6ea4..c5511e0b7a 100644 --- a/frontend/apps/console/src/features/flows/data/templates.json +++ b/frontend/apps/console/src/features/flows/data/templates.json @@ -13177,6 +13177,7 @@ "category": "BLOCK", "components": [ { + "actionType": "SIGN_OUT_CONFIRM", "category": "ACTION", "eventType": "SUBMIT", "id": "action_confirm", @@ -13196,6 +13197,7 @@ { "action": { "ref": "action_confirm", + "type": "SIGN_OUT_CONFIRM", "nextNode": "session_signout" } }