1919package executor
2020
2121import (
22+ "errors"
23+
2224 "github.com/asgardeo/thunder/internal/flow/common"
2325 "github.com/asgardeo/thunder/internal/flow/core"
26+ "github.com/asgardeo/thunder/internal/ou"
27+ "github.com/asgardeo/thunder/internal/system/error/serviceerror"
2428 "github.com/asgardeo/thunder/internal/system/log"
2529 "github.com/asgardeo/thunder/internal/system/security"
2630)
@@ -29,32 +33,51 @@ import (
2933const (
3034 // ouResolveFromCaller indicates that the caller's OU should be used when creating the user.
3135 ouResolveFromCaller = "caller"
36+ // ouResolveFromPrompt indicates that the user should be prompted to select an OU.
37+ ouResolveFromPrompt = "prompt"
3238)
3339
3440// ouResolverExecutor resolves the organization unit for a user being onboarded.
3541type ouResolverExecutor struct {
3642 core.ExecutorInterface
37- logger * log.Logger
43+ ouService ou.OrganizationUnitServiceInterface
44+ logger * log.Logger
3845}
3946
4047// newOUResolverExecutor creates a new OU resolver executor.
41- func newOUResolverExecutor (flowFactory core.FlowFactoryInterface ) * ouResolverExecutor {
48+ func newOUResolverExecutor (
49+ flowFactory core.FlowFactoryInterface ,
50+ ouService ou.OrganizationUnitServiceInterface ,
51+ ) * ouResolverExecutor {
4252 logger := log .GetLogger ().With (log .String (log .LoggerKeyComponentName , "OUResolverExecutor" ))
53+
54+ defaultInputs := []common.Input {
55+ {
56+ Ref : "ou_selection_input" ,
57+ Identifier : ouIDKey ,
58+ Type : "OU_SELECT" ,
59+ Required : true ,
60+ },
61+ }
62+
4363 base := flowFactory .CreateExecutor (
4464 ExecutorNameOUResolver ,
4565 common .ExecutorTypeUtility ,
46- []common. Input {} ,
66+ defaultInputs ,
4767 []common.Input {},
4868 )
4969 return & ouResolverExecutor {
5070 ExecutorInterface : base ,
71+ ouService : ouService ,
5172 logger : logger ,
5273 }
5374}
5475
5576// Execute resolves the organization unit for the user being onboarded.
5677// It reads the "resolveFrom" node property to determine the OU resolution strategy.
57- // When set to "caller", it overrides the default OU with the caller's OU from the security context.
78+ // Supported strategies:
79+ // - "caller": overrides the default OU with the caller's OU from the security context.
80+ // - "prompt": checks for child OUs and prompts the user to select one if applicable.
5881func (e * ouResolverExecutor ) Execute (ctx * core.NodeContext ) (* common.ExecutorResponse , error ) {
5982 logger := e .logger .With (log .String (log .LoggerKeyFlowID , ctx .FlowID ))
6083
@@ -72,6 +95,8 @@ func (e *ouResolverExecutor) Execute(ctx *core.NodeContext) (*common.ExecutorRes
7295 switch resolveFrom {
7396 case ouResolveFromCaller :
7497 return e .resolveFromCaller (ctx , execResp , logger )
98+ case ouResolveFromPrompt :
99+ return e .resolveFromPrompt (ctx , logger )
75100 default :
76101 logger .Error ("Unsupported resolveFrom value" , log .String ("resolveFrom" , resolveFrom ))
77102 execResp .Status = common .ExecFailure
@@ -97,6 +122,84 @@ func (e *ouResolverExecutor) resolveFromCaller(ctx *core.NodeContext,
97122 return execResp , nil
98123}
99124
125+ // resolveFromPrompt checks whether the user type's OU has child OUs and,
126+ // if so, prompts the admin to select one during the onboarding flow.
127+ func (e * ouResolverExecutor ) resolveFromPrompt (ctx * core.NodeContext ,
128+ logger * log.Logger ) (* common.ExecutorResponse , error ) {
129+ execResp := & common.ExecutorResponse {
130+ RuntimeData : make (map [string ]string ),
131+ AdditionalData : make (map [string ]string ),
132+ ForwardedData : make (map [string ]interface {}),
133+ }
134+
135+ // Read the default OU set by UserTypeResolver.
136+ // The "prompt" strategy requires UserTypeResolver to have run first and set the defaultOUID.
137+ parentOUID := ctx .RuntimeData [defaultOUIDKey ]
138+ if parentOUID == "" {
139+ return nil , errors .New (
140+ "no defaultOUID in runtime data; UserTypeResolver must run before OUResolver with prompt strategy" ,
141+ )
142+ }
143+
144+ // If the user already provided an OU selection, validate and accept it.
145+ if selectedOUID , ok := ctx .UserInputs [ouIDKey ]; ok && selectedOUID != "" {
146+ // Validate that the selected OU belongs to the parent OU's subtree.
147+ isDescendant , svcErr := e .ouService .IsParent (ctx .Context , parentOUID , selectedOUID )
148+ if svcErr != nil {
149+ if svcErr .Type == serviceerror .ClientErrorType {
150+ execResp .Status = common .ExecFailure
151+ execResp .FailureReason = "The selected organization unit is not valid."
152+ return execResp , nil
153+ }
154+
155+ return nil , errors .New ("failed to validate selected organization unit: " + svcErr .Error )
156+ }
157+ if ! isDescendant {
158+ logger .Debug ("Selected OU is not a descendant of the parent OU" ,
159+ log .String (ouIDKey , selectedOUID ),
160+ log .String ("parentOUID" , parentOUID ))
161+ execResp .Status = common .ExecFailure
162+ execResp .FailureReason = "The selected organization unit is not valid for the chosen user type."
163+ return execResp , nil
164+ }
165+
166+ logger .Debug ("OU selected by user" , log .String (ouIDKey , selectedOUID ))
167+ execResp .RuntimeData [ouIDKey ] = selectedOUID
168+ execResp .Status = common .ExecComplete
169+ return execResp , nil
170+ }
171+
172+ // Check if the parent OU has child OUs.
173+ children , svcErr := e .ouService .GetOrganizationUnitChildren (ctx .Context , parentOUID , 1 , 0 )
174+ if svcErr != nil {
175+ return nil , errors .New ("failed to check child organization units: " + svcErr .Error )
176+ }
177+
178+ if children .TotalResults == 0 {
179+ logger .Debug ("No child OUs found, skipping OU selection" )
180+ execResp .Status = common .ExecComplete
181+ return execResp , nil
182+ }
183+
184+ // Child OUs exist — prompt the user to select one.
185+ logger .Debug ("Child OUs found, requesting OU selection" ,
186+ log .String ("parentOUID" , parentOUID ),
187+ log .Int ("totalChildren" , children .TotalResults ))
188+
189+ execResp .Status = common .ExecUserInputRequired
190+
191+ inputs := e .GetDefaultInputs ()
192+ if len (inputs ) > 0 {
193+ input := inputs [0 ]
194+ execResp .Inputs = []common.Input {input }
195+ // Forward the root OU ID so the frontend knows where to start the tree picker.
196+ execResp .AdditionalData [common .DataRootOUID ] = parentOUID
197+ execResp .ForwardedData [common .ForwardedDataKeyInputs ] = execResp .Inputs
198+ }
199+
200+ return execResp , nil
201+ }
202+
100203// getResolveFrom retrieves the resolveFrom strategy from the node properties.
101204func (e * ouResolverExecutor ) getResolveFrom (ctx * core.NodeContext ) string {
102205 if ctx .NodeProperties == nil {
0 commit comments