Skip to content

Commit d92728d

Browse files
fix(datasource/dingtalk): Validate 只校验所选范围,不再扫全租户 (#4421)
1 parent e849eaa commit d92728d

2 files changed

Lines changed: 808 additions & 14 deletions

File tree

‎internal/datasource/connector/dingtalk/connector.go‎

Lines changed: 180 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,19 @@ func (c *Connector) Type() string {
112112
const maxValidateProbes = 5
113113

114114
// Validate checks the application credentials and operator access, including
115-
// node listing and a sample document read when one is visible in a workspace.
115+
// node listing and a sample document read when one is visible.
116116
//
117-
// The operator is not expected to reach every workspace in the tenant: app
118-
// credentials are valid as long as one reachable workspace yields one readable
119-
// document. A failed probe does not condemn that workspace; the walk keeps
117+
// A data source that selects resources is validated against those resources
118+
// only. They are what the operator asked to sync, so an unrelated workspace
119+
// that happens to be listed first, and that the operator cannot read, must not
120+
// decide whether the selection is usable: a selected folder or workspace is
121+
// walked for one readable document, and a selected document is probed
122+
// directly. Without a selection there is nothing narrower to sample, so the
123+
// whole tenant is walked as before.
124+
//
125+
// In both modes the operator is not expected to reach every object: app
126+
// credentials are valid as long as one reachable document can be read. A
127+
// failed probe does not condemn the object it was found in; the walk keeps
120128
// looking, inside the probe cap, for another document the operator can read.
121129
// When the cap itself stops a conclusive walk, the error says how many
122130
// documents were read.
@@ -127,6 +135,15 @@ func (c *Connector) Validate(ctx context.Context, dataSourceConfig *types.DataSo
127135
}
128136
api := c.api(cfg)
129137
settings := parseDocumentSettings(dataSourceConfig)
138+
if selected := uniqueIDs(dataSourceConfig.ResourceIDs); len(selected) > 0 {
139+
return validateSelectedResources(ctx, api, selected, settings)
140+
}
141+
return validateTenantCredentials(ctx, api, settings)
142+
}
143+
144+
// validateTenantCredentials samples every workspace in the tenant, which is
145+
// what a data source without a selection is going to sync.
146+
func validateTenantCredentials(ctx context.Context, api dingTalkAPI, settings documentSettings) error {
130147
workspaces, err := api.listWorkspaces(ctx)
131148
if err != nil {
132149
return fmt.Errorf("validate DingTalk data source: %w", err)
@@ -174,19 +191,168 @@ func (c *Connector) Validate(ctx context.Context, dataSourceConfig *types.DataSo
174191
break
175192
}
176193
}
177-
// Folders were left unexplored, because the listing budget ran out or a
178-
// folder could not be listed, so an unreadable document elsewhere does not
179-
// prove the data source is unusable. A probe cap hit in that situation
180-
// stays quiet too: the walk cannot claim it sampled every visible document.
194+
return validateOutcome(inconclusive, sawDocument, workspaceFail, probeCapHit, lastErr)
195+
}
196+
197+
// validateSelectedResources checks only the scopes a data source selects.
198+
// References are resolved through the same code path a sync uses, so Validate
199+
// answers the question the operator asked — would this selection sync
200+
// anything? — instead of sampling workspaces the selection never mentions.
201+
//
202+
// A reference this build cannot resolve is reported rather than quietly
203+
// replaced by the tenant walk: the data source would fail on that resource,
204+
// and a selection the picker can no longer resolve is exactly what the
205+
// operator needs to see.
206+
func validateSelectedResources(
207+
ctx context.Context,
208+
api dingTalkAPI,
209+
selected []string,
210+
settings documentSettings,
211+
) error {
212+
if err := ctx.Err(); err != nil {
213+
return fmt.Errorf("validate DingTalk data source: %w", err)
214+
}
215+
workspaces, err := api.listWorkspaces(ctx)
216+
if err != nil {
217+
return fmt.Errorf("validate DingTalk data source: %w", err)
218+
}
219+
scopes, failures, err := resolveSyncScopes(ctx, api, workspaces, selected, settings)
220+
if err != nil {
221+
return fmt.Errorf("validate DingTalk data source: %w", err)
222+
}
223+
names := make(map[string]string, len(workspaces))
224+
for _, item := range workspaces {
225+
names[item.ID] = item.Name
226+
}
227+
228+
var (
229+
lastErr error
230+
resolveErr error
231+
sawDocument bool
232+
inconclusive bool
233+
scopeFail int
234+
probes int
235+
listings int
236+
probeCapHit bool
237+
)
238+
for _, resourceID := range selected {
239+
failure := failures[resourceID]
240+
if failure == nil {
241+
continue
242+
}
243+
scopeFail++
244+
if resolveErr == nil {
245+
resolveErr = fmt.Errorf("resource %s: %w", resourceID, failure)
246+
}
247+
}
248+
for i, scope := range scopes {
249+
// A failed probe names the workspace it was found in, the way the
250+
// tenant-wide sample does, so it stays actionable.
251+
name := names[scope.Reference.WorkspaceID]
252+
if name == "" {
253+
name = scope.Reference.WorkspaceID
254+
}
255+
readable, unexplored, hitCap, err := sampleScope(
256+
ctx, api, scope, name, settings,
257+
&listings, &probes, &sawDocument, &lastErr,
258+
)
259+
if err != nil {
260+
// Name the selected node when the selection is one: a folder that
261+
// cannot be listed is what the operator has to look at, and the
262+
// workspace alone would not say which folder.
263+
label := fmt.Sprintf("workspace %q", name)
264+
if scope.Reference.NodeID != "" {
265+
label = fmt.Sprintf("workspace %q node %q", name, scope.Reference.NodeID)
266+
}
267+
lastErr = fmt.Errorf("%s: %w", label, err)
268+
// A cancelled or timed-out request proves nothing about the
269+
// credentials, so it must never fall through to acceptance.
270+
if ctx.Err() != nil || isContextError(err) {
271+
return fmt.Errorf("validate DingTalk data source: %w", lastErr)
272+
}
273+
scopeFail++
274+
continue
275+
}
276+
if readable {
277+
return nil
278+
}
279+
if unexplored {
280+
inconclusive = true
281+
}
282+
if probes >= maxValidateProbes {
283+
probeCapHit = hitCap || i+1 < len(scopes)
284+
break
285+
}
286+
}
287+
if lastErr == nil {
288+
// A failed probe names a workspace and a document the operator can act
289+
// on; a reference that does not resolve can only name itself.
290+
lastErr = resolveErr
291+
}
292+
return validateOutcome(inconclusive, sawDocument, scopeFail, probeCapHit, lastErr)
293+
}
294+
295+
// sampleScope probes one resolved selection scope. A selected document is read
296+
// through its own read API, while a selected folder or workspace is walked the
297+
// way the tenant sample walks a workspace root, so a readable document below
298+
// the selected folder is still found.
299+
func sampleScope(
300+
ctx context.Context,
301+
api dingTalkAPI,
302+
scope syncScope,
303+
workspaceName string,
304+
settings documentSettings,
305+
listings *int,
306+
probes *int,
307+
sawDocument *bool,
308+
lastErr *error,
309+
) (readable, unexplored, capHit bool, err error) {
310+
if scope.Document == nil {
311+
return sampleWorkspace(
312+
ctx, api, workspaceName, scope.StartNodeID, settings,
313+
listings, probes, sawDocument, lastErr,
314+
)
315+
}
316+
document := *scope.Document
317+
*probes++
318+
*sawDocument = true
319+
if probeErr := verifyDocument(ctx, api, document); probeErr != nil {
320+
if ctx.Err() != nil || isContextError(probeErr) {
321+
return false, false, false, fmt.Errorf("document %q: %w", document.title(), probeErr)
322+
}
323+
*lastErr = fmt.Errorf(
324+
"workspace %q document %q: %w", workspaceName, document.title(), probeErr,
325+
)
326+
return false, false, false, nil
327+
}
328+
return true, false, false, nil
329+
}
330+
331+
// validateOutcome folds the counters of a validate walk into its result. The
332+
// tenant-wide sample and the selection-scoped one both end here, because they
333+
// answer the same question: is there a document in what this data source syncs
334+
// that the operator can read?
335+
//
336+
// Folders were left unexplored, because the listing budget ran out or a folder
337+
// could not be listed, so an unreadable document elsewhere does not prove the
338+
// data source is unusable. A probe cap hit in that situation stays quiet too:
339+
// the walk cannot claim it sampled every visible document.
340+
//
341+
// Nothing readable anywhere. Only a scope that exposed no document at all is
342+
// accepted: there was nothing to read, so credentials could not be disproved.
343+
// A document that exists but cannot be read means the data source would sync
344+
// nothing, which is worth reporting.
345+
func validateOutcome(
346+
inconclusive bool,
347+
sawDocument bool,
348+
failures int,
349+
probeCapHit bool,
350+
lastErr error,
351+
) error {
181352
if inconclusive {
182353
return nil
183354
}
184-
185-
// Nothing readable anywhere. Only a tenant that exposed no document at all
186-
// is accepted: there was nothing to read, so credentials could not be
187-
// disproved. A document that exists but cannot be read means the data
188-
// source would sync nothing, which is worth reporting.
189-
if !sawDocument && workspaceFail == 0 {
355+
if !sawDocument && failures == 0 {
190356
return nil
191357
}
192358
if lastErr == nil {

0 commit comments

Comments
 (0)