Skip to content

fix: populate feature flags before auth notification [IDE-1901]#1194

Open
nick-y-snyk wants to merge 3 commits intorefactor/IDE-1786_folder-config-refactoringfrom
fix/IDE-1901-sast-not-enabled-after-login
Open

fix: populate feature flags before auth notification [IDE-1901]#1194
nick-y-snyk wants to merge 3 commits intorefactor/IDE-1786_folder-config-refactoringfrom
fix/IDE-1901-sast-not-enabled-after-login

Conversation

@nick-y-snyk
Copy link
Copy Markdown
Contributor

@nick-y-snyk nick-y-snyk commented Apr 7, 2026

Summary

  • Fix race condition where scans after login fail with "Snyk Code is not enabled" because the IDE triggers a scan before SAST settings are cached
  • postCredentialUpdateHook runs populateAllFolderConfigs between credential storage and auth notification, ensuring feature flags and SAST settings are available before the IDE reacts
  • sendFolderConfigs made synchronous in login command

Review feedback addressed

  • Use unenriched folder config (no git enrichment needed for feature flag fetch)
  • Loop only on trusted folders
  • Remove unnecessary Clone()PopulateFolderConfig only reads org from config
  • Set ConfigResolver on folder config so SetFeatureFlag persists to GAF (was silently no-op without it)
  • Case-insensitive error string matching in shouldCauseLogout
  • Extract isTransientNetworkError helper to reduce cyclomatic complexity

Test plan

  • TestLoginCommand_Execute_FeatureFlagsPopulatedBeforeAuthNotification — regression test verifying feature flags populate BEFORE $/snyk.hasAuthenticated notification (fails when hook is removed)
  • Test_PostCredentialUpdateHook_CalledBeforeNotification — hook runs between token storage and notification
  • Test_shouldCauseLogout — all 13 cases pass including expired refresh token and transient network errors
  • TestIsAuthenticated_ConcurrentCallsSendOnlyOneNotification — concurrent dedup still works
  • All existing TestLoginCommand_* tests pass
  • Lint clean, zero issues

@snyk-io
Copy link
Copy Markdown

snyk-io bot commented Apr 7, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

// canceled (e.g. when the IDE cancels the snyk.login request after auth completes).
cmd.ldxSyncService.RefreshConfigFromLdxSync(context.Background(), conf, cmd.engine, logger, config.GetWorkspace(conf).Folders(), cmd.notifier)
go sendFolderConfigs(conf, cmd.engine, logger, cmd.notifier, cmd.featureFlagService, cmd.configResolver)
sendFolderConfigs(conf, cmd.engine, logger, cmd.notifier, cmd.featureFlagService, cmd.configResolver)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ideally, if a user has large workspaces with 100+ projects, we want a separation between the login flow and the setup folders. With clear indications for the user that they are logged in, but snyk is not ready to scan until all configs are ready.

Copy link
Copy Markdown
Collaborator

@ShawkyZ ShawkyZ Apr 9, 2026

Choose a reason for hiding this comment

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

This can still run in a go routine, since it will get to this point after the postAuth hook

// Expired or revoked refresh tokens cause the OAuth2 token endpoint to reject
// the request. http.Client wraps this in url.Error, so detect it before the
// generic transport-error guard.
errMsg := err.Error()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we user a re.Response.StatusCode == 401 or 400 to catch expired/invalid API credentials?

https://github.com/snyk/sweater-comb/blob/main/docs/standards/rest.md#401---unauthorized

Needs to be verified for both token an oauth.

if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
return false
}
// Expired or revoked refresh tokens cause the OAuth2 token endpoint to reject
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should the change here be submitted as a separate PR for for changelog clarity?

}
log := logger.With().Str("method", "populateAllFolderConfigs").Logger()
for _, folder := range ws.Folders() {
fc, err := folderconfig.GetOrCreateFolderConfig(conf, folder.Path(), &log)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Use Unenriched variant

if fc == nil {
continue
}
featureFlagService.PopulateFolderConfig(fc.Clone())
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why the clone ?

return
}
log := logger.With().Str("method", "populateAllFolderConfigs").Logger()
for _, folder := range ws.Folders() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

loop on trusted folders

// the request. http.Client wraps this in url.Error, so detect it before the
// generic transport-error guard.
errMsg := err.Error()
if strings.Contains(errMsg, "oauth2/token") && strings.Contains(errMsg, "authentication failed") {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nitpick: make the comparison case insensitive

@snyk-pr-review-bot

This comment has been minimized.

Race condition: after login, IDE receives $/snyk.hasAuthenticated and
triggers scan before SAST settings are cached → "Snyk Code not enabled".

Fix: postCredentialUpdateHook runs populateAllFolderConfigs between
credential storage and auth notification, ensuring feature flags and
SAST settings are available before the IDE reacts.

Review feedback addressed:
- Use unenriched folder config (no git enrichment for feature flag fetch)
- Loop only on trusted folders
- Remove unnecessary Clone()
- Set ConfigResolver on folder config so SetFeatureFlag persists to GAF
- Make sendFolderConfigs synchronous in login command
- Case-insensitive error string matching in shouldCauseLogout
- Extract isTransientNetworkError to reduce cyclomatic complexity

Includes regression test verifying feature flags populate before auth
notification is sent (fails when hook is removed).
@snyk-pr-review-bot

This comment has been minimized.

@nick-y-snyk nick-y-snyk changed the title fix: call sendFolderConfigs synchronously after login [IDE-1901] fix: populate feature flags before auth notification [IDE-1901] Apr 14, 2026
- Synchronize SetPostCredentialUpdateHook with a.m mutex to prevent data race
- Skip hook when token is empty (logout path) to avoid wasteful API calls
- Wrap hook invocation in recover() to prevent LSP server crash on panic
@snyk-pr-review-bot

This comment has been minimized.

func() {
defer func() {
if r := recover(); r != nil {
a.engine.GetLogger().Error().Interface("panic", r).Msg("postCredentialUpdateHook panicked")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why are you expecting it to panic?

Copy link
Copy Markdown
Contributor Author

@nick-y-snyk nick-y-snyk Apr 14, 2026

Choose a reason for hiding this comment

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

I didn't. just a double safety (ai suggestion to prevent ls crash). Do you want this removed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I also added an actual test which fails without other changes. But haven't tested manually yet

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we have a global recovery that should catch it if it happens, I think this is not necessary here.

@snyk-pr-review-bot
Copy link
Copy Markdown

PR Reviewer Guide 🔍

🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Blocking Global Lock 🔴 [critical]

The updateCredentials function invokes the postCredentialUpdateHook (line 248) while holding the global a.m mutex lock. The registered hook in login.go calls populateAllFolderConfigs, which performs network requests to fetch feature flags and SAST settings via featureFlagService.PopulateFolderConfig. Holding a global mutex across sequential network calls will block all other authentication-related operations—including high-frequency calls like IsAuthenticated() from the status bar or scanner—making the Language Server unresponsive during the entire feature flag fetch sequence.

if a.postCredentialUpdateHook != nil && newToken != "" {
	func() {
		defer func() {
			if r := recover(); r != nil {
				a.engine.GetLogger().Error().Interface("panic", r).Msg("postCredentialUpdateHook panicked")
			}
		}()
		a.postCredentialUpdateHook()
	}()
}
Regression in Dynamic Population 🟠 [major]

Removing PopulateFolderConfig from buildLspFolderConfigs (lines 115-117 in the old hunk) breaks the 'on-demand' population of feature flags. The new mechanism relies on explicit calls in HandleFolders and the login hook. However, HandleFolders calls populateAllFolderConfigs before handling untrusted folders. If a user trusts a folder via the trust dialog, that folder will never have its feature flags populated because buildLspFolderConfigs no longer performs this check, leading to 'Snyk Code is not enabled' errors for newly trusted folders.

func buildLspFolderConfigs(conf configuration.Configuration, engine workflow.Engine, logger *zerolog.Logger, featureFlagService featureflag.Service, configResolver types.ConfigResolverInterface) []types.LspFolderConfig {
	ws := config.GetWorkspace(conf)
	if ws == nil {
		return nil
	}
	log := logger.With().Str("method", "buildLspFolderConfigs").Logger()
	engineConfig := conf
	var lspFolderConfigs []types.LspFolderConfig

	for _, folder := range ws.Folders() {
		fc, err := folderconfig.GetOrCreateFolderConfig(engineConfig, folder.Path(), &log)
		if err != nil {
			log.Err(err).Msg("unable to load folderConfig")
			continue
		}

		if fc == nil {
			log.Warn().Str("path", string(folder.Path())).Msg("folder config is nil, skipping")
			continue
		}
		folderConfig := fc.Clone()

		// AutoDeterminedOrg is written to FolderMetadataKey by LDX-Sync (SetAutoDeterminedOrg);
		// no separate cache lookup is needed here.

		folderConfig.ConfigResolver = configResolver
		lspConfig := folderConfig.ToLspFolderConfig()
		if lspConfig != nil {
			lspFolderConfigs = append(lspFolderConfigs, *lspConfig)
📚 Repository Context Analyzed

This review considered 25 relevant code sections from 12 files (average relevance: 0.97)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants