WorkspaceContextResolver.java

package com.taxonomy.workspace.service;

import com.taxonomy.workspace.model.UserWorkspace;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

/**
 * Resolves the current {@link WorkspaceContext} from the security context
 * and the persistent workspace metadata.
 *
 * <p>Combines the username (from Spring Security) with the
 * workspace metadata (from {@link WorkspaceManager}) to produce a
 * consistent context value that downstream services can use for
 * data isolation (workspace-scoped relations, hypotheses, proposals, etc.).
 *
 * <p>Falls back to {@link WorkspaceContext#SHARED} when no provisioned
 * workspace exists for the current user.
 */
@Service
public class WorkspaceContextResolver {

    private static final Logger log = LoggerFactory.getLogger(WorkspaceContextResolver.class);

    private final WorkspaceManager workspaceManager;
    private final SystemRepositoryService systemRepositoryService;

    public WorkspaceContextResolver(WorkspaceManager workspaceManager,
                                     SystemRepositoryService systemRepositoryService) {
        this.workspaceManager = workspaceManager;
        this.systemRepositoryService = systemRepositoryService;
    }

    /**
     * Resolve the workspace context for the currently authenticated user.
     *
     * @return the active workspace context (never {@code null})
     */
    public WorkspaceContext resolveCurrentContext() {
        String username = resolveUsername();
        return resolveForUser(username);
    }

    /**
     * Resolve the workspace context for a specific user.
     *
     * <p>Only users with an explicitly provisioned persistent workspace
     * receive a workspace-scoped context. Users with only a default
     * in-memory workspace state receive the {@link WorkspaceContext#SHARED}
     * fallback (backward-compatible, no data isolation).
     *
     * @param username the username to resolve context for
     * @return the workspace context (never {@code null})
     */
    public WorkspaceContext resolveForUser(String username) {
        if (username == null || username.isBlank()
                || WorkspaceManager.DEFAULT_USER.equals(username)) {
            return WorkspaceContext.SHARED;
        }

        // Try active workspace first (multi-workspace aware)
        UserWorkspace ws = workspaceManager.findActiveWorkspace(username);
        if (ws == null) {
            // Fall back to legacy single-workspace lookup for backward compatibility
            ws = workspaceManager.findUserWorkspace(username);
        }

        if (ws != null && ws.getWorkspaceId() != null) {
            String branch = ws.getCurrentBranch() != null
                    ? ws.getCurrentBranch()
                    : systemRepositoryService.getSharedBranch();
            log.debug("Resolved workspace context for user '{}': workspace={}, branch={}",
                    username, ws.getWorkspaceId(), branch);
            return new WorkspaceContext(username, ws.getWorkspaceId(), branch);
        }

        log.debug("No provisioned workspace for user '{}'; falling back to SHARED", username);
        return WorkspaceContext.SHARED;
    }

    private String resolveUsername() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null && auth.isAuthenticated()
                && !"anonymousUser".equals(auth.getPrincipal())) {
            return auth.getName();
        }
        return WorkspaceManager.DEFAULT_USER;
    }
}