Skip to content
Merged
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
6 changes: 3 additions & 3 deletions api/flow-execution.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 5 additions & 3 deletions api/flow-management.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions backend/internal/flow/common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"
Comment thread
ThaminduDilshan marked this conversation as resolved.
)

// ForwardedData key constants define keys used in the ForwardedData map.
Expand Down
51 changes: 38 additions & 13 deletions backend/internal/flow/executor/session_signout_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -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
Expand All @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can there be a case where there are more than 2 signout outcomes? If not we could simply make decide() function to return a boolean instead of defining s dedicated type.
WDYT?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 3 outcomes

  • Prompt for confirmation
  • Terminate the session (Sign out button in the prompt)
  • Cancel the termination request (future planned cancel button in the prompt)

Hence keeping the current implementation.


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
}
38 changes: 30 additions & 8 deletions backend/internal/flow/executor/session_signout_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
func (suite *SessionSignOutExecutorTestSuite) TestPromptsWhenConfirmationRequired() {
sso := sessionmock.NewServiceMock(suite.T())
exec := suite.newExecutor(sso)
Expand All @@ -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").
Expand All @@ -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)
Expand All @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion docs/content/guides/flows/advanced-configurations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13177,6 +13177,7 @@
"category": "BLOCK",
"components": [
{
"actionType": "SIGN_OUT_CONFIRM",
"category": "ACTION",
"eventType": "SUBMIT",
"id": "action_confirm",
Expand All @@ -13196,6 +13197,7 @@
{
"action": {
"ref": "action_confirm",
"type": "SIGN_OUT_CONFIRM",
"nextNode": "session_signout"
}
}
Expand Down
Loading