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
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package de.tum.in.www1.hephaestus.account;

import de.tum.in.www1.hephaestus.config.KeycloakProperties;
import de.tum.in.www1.hephaestus.gitprovider.user.AuthenticatedGitProviderUserService;
import de.tum.in.www1.hephaestus.gitprovider.user.User;
import de.tum.in.www1.hephaestus.gitprovider.user.UserRepository;
import de.tum.in.www1.hephaestus.integrations.posthog.PosthogClientException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Optional;
import org.keycloak.admin.client.Keycloak;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -44,17 +47,20 @@ public class AccountController {
private final Keycloak keycloak;
private final UserRepository userRepository;
private final KeycloakProperties keycloakProperties;
private final AuthenticatedGitProviderUserService authenticatedGitProviderUserService;

public AccountController(
AccountService accountService,
Keycloak keycloak,
UserRepository userRepository,
KeycloakProperties keycloakProperties
KeycloakProperties keycloakProperties,
AuthenticatedGitProviderUserService authenticatedGitProviderUserService
) {
this.accountService = accountService;
this.keycloak = keycloak;
this.userRepository = userRepository;
this.keycloakProperties = keycloakProperties;
this.authenticatedGitProviderUserService = authenticatedGitProviderUserService;
}

@DeleteMapping
Expand Down Expand Up @@ -96,8 +102,8 @@ public ResponseEntity<Void> deleteUser(@AuthenticationPrincipal JwtAuthenticatio
summary = "Get user settings",
description = "Get the current user's notification, research participation, and AI review preferences"
)
public ResponseEntity<UserSettingsDTO> getUserSettings() {
var user = userRepository.getCurrentUser();
public ResponseEntity<UserSettingsDTO> getUserSettings(@AuthenticationPrincipal JwtAuthenticationToken auth) {
var user = resolveOrProvisionCurrentUser(auth);
if (user.isEmpty()) {
return ResponseEntity.notFound().build();
}
Expand All @@ -115,7 +121,7 @@ public ResponseEntity<UserSettingsDTO> updateUserSettings(
@AuthenticationPrincipal JwtAuthenticationToken auth,
@Valid @RequestBody UserSettingsDTO userSettings
) {
var user = userRepository.getCurrentUser();
var user = resolveOrProvisionCurrentUser(auth);
if (user.isEmpty()) {
return ResponseEntity.notFound().build();
}
Expand Down Expand Up @@ -205,4 +211,12 @@ private JwtAuthenticationToken resolveAuthentication(JwtAuthenticationToken inje
}
return null;
}

private Optional<User> resolveOrProvisionCurrentUser(JwtAuthenticationToken auth) {
if (resolveAuthentication(auth) == null) {
return Optional.empty();
}

return authenticatedGitProviderUserService.resolveOrProvisionCurrentUser(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package de.tum.in.www1.hephaestus.gitprovider.user;

import de.tum.in.www1.hephaestus.SecurityUtils;
import de.tum.in.www1.hephaestus.core.LoggingUtils;
import de.tum.in.www1.hephaestus.gitprovider.common.GitProvider;
import de.tum.in.www1.hephaestus.gitprovider.common.GitProviderRepository;
import de.tum.in.www1.hephaestus.gitprovider.common.GitProviderType;
import de.tum.in.www1.hephaestus.gitprovider.common.gitlab.GitLabProperties;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

@Service
public class AuthenticatedGitProviderUserService {

private static final Logger log = LoggerFactory.getLogger(AuthenticatedGitProviderUserService.class);
private static final String GITHUB_SERVER_URL = "https://github.com";

private final UserRepository userRepository;
private final GitProviderRepository gitProviderRepository;
private final GitLabProperties gitLabProperties;

public AuthenticatedGitProviderUserService(
UserRepository userRepository,
GitProviderRepository gitProviderRepository,
GitLabProperties gitLabProperties
) {
this.userRepository = userRepository;
this.gitProviderRepository = gitProviderRepository;
this.gitLabProperties = gitLabProperties;
}

@Transactional
public Optional<User> resolveOrProvisionCurrentUser(@Nullable String gitLabServerUrl) {
var currentUser = userRepository.getCurrentUser();
if (currentUser.isPresent()) {
return currentUser;
}

Optional<String> currentLogin = SecurityUtils.getCurrentUserLogin();
if (currentLogin.isEmpty()) {
return Optional.empty();
}
String login = currentLogin.orElseThrow();

Jwt jwt = getCurrentJwt();
if (jwt == null) {
return Optional.empty();
}

Long gitlabId = jwt.getClaim("gitlab_id");
if (gitlabId != null) {
String resolvedUrl = resolveGitLabServerUrl(gitLabServerUrl);
Long userId = upsertGitLabUser(
gitlabId,
login,
login,
"",
resolvedUrl + "/" + login,
resolvedUrl,
User.Type.USER
);
return userRepository.findById(userId);
}

Long githubId = jwt.getClaim("github_id");
if (githubId != null) {
Long userId = upsertGitHubUser(githubId, login, login, "", GITHUB_SERVER_URL + "/" + login, User.Type.USER);
return userRepository.findById(userId);
}

return Optional.empty();
}

@Transactional
public void ensureCurrentGitLabUserExists(@Nullable String gitLabServerUrl) {
String login = SecurityUtils.getCurrentUserLoginOrThrow();
if (userRepository.findByLogin(login).isPresent()) {
return;
}
Comment on lines +86 to +88

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

ensureCurrentGitLabUserExists returns early if any User with the current login exists, even if that user belongs to a non-GitLab provider. This can bypass the intended 409 conflict when a GitHub-authenticated user (no gitlab_id) already has a GitHub user row and then tries to create a GitLab PAT workspace. Consider only short-circuiting when a GitLab-scoped user exists (matching the resolved GitLab provider), or always validating presence of gitlab_id regardless of existing users.

Suggested change
if (userRepository.findByLogin(login).isPresent()) {
return;
}

Copilot uses AI. Check for mistakes.

Jwt jwt = getCurrentJwt();
if (jwt == null) {
throw new IllegalStateException("No JWT found for authenticated user");
}

Long gitlabId = jwt.getClaim("gitlab_id");
if (gitlabId != null) {
String resolvedUrl = resolveGitLabServerUrl(gitLabServerUrl);
upsertGitLabUser(gitlabId, login, login, "", resolvedUrl + "/" + login, resolvedUrl, User.Type.USER);
return;
}

Long githubId = jwt.getClaim("github_id");
if (githubId != null) {
throw new ResponseStatusException(
HttpStatus.CONFLICT,
"You need to link your GitLab account before creating a GitLab workspace. Go to Settings → Linked Accounts to connect your GitLab identity."
);
}

throw new ResponseStatusException(
HttpStatus.CONFLICT,
"No GitLab identity found. Please link your GitLab account in Settings → Linked Accounts."
);
}

@Nullable
private Jwt getCurrentJwt() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !(auth.getPrincipal() instanceof Jwt jwt)) {
return null;
}
return jwt;
}

private String resolveGitLabServerUrl(@Nullable String configServerUrl) {
if (configServerUrl != null && !configServerUrl.isBlank()) {
String url = configServerUrl.trim();
return url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
}
return gitLabProperties.defaultServerUrl();
}

private Long upsertGitHubUser(
Long nativeId,
String login,
String name,
String avatarUrl,
String webUrl,
User.Type userType
) {
GitProvider provider = gitProviderRepository
.findByTypeAndServerUrl(GitProviderType.GITHUB, GITHUB_SERVER_URL)
.orElseGet(() -> gitProviderRepository.save(new GitProvider(GitProviderType.GITHUB, GITHUB_SERVER_URL)));

return upsertUser(nativeId, login, name, avatarUrl, webUrl, userType, provider);
}

private Long upsertGitLabUser(
Long nativeId,
String login,
String name,
String avatarUrl,
String webUrl,
String serverUrl,
User.Type userType
) {
String safeAvatar = avatarUrl != null ? (avatarUrl.startsWith("/") ? serverUrl + avatarUrl : avatarUrl) : "";
GitProvider provider = gitProviderRepository
.findByTypeAndServerUrl(GitProviderType.GITLAB, serverUrl)
.orElseGet(() -> {
log.info("Creating GitProvider for self-hosted GitLab: serverUrl={}", serverUrl);
return gitProviderRepository.save(new GitProvider(GitProviderType.GITLAB, serverUrl));
});

return upsertUser(nativeId, login, name, safeAvatar, webUrl, userType, provider);
}

private Long upsertUser(
Long nativeId,
String login,
String name,
String avatarUrl,
String webUrl,
User.Type userType,
GitProvider provider
) {
String safeName = name != null ? name : login;
String safeAvatar = avatarUrl != null ? avatarUrl : "";
String safeWebUrl = webUrl != null ? webUrl : "";
Long providerId = provider.getId();

userRepository.acquireLoginLock(login, providerId);
userRepository.freeLoginConflicts(login, nativeId, providerId);
userRepository.upsertUser(
nativeId,
providerId,
login,
safeName,
safeAvatar,
safeWebUrl,
userType.name(),
null,
null,
null
);
log.info(
"Upserted authenticated git provider user: userLogin={}, nativeId={}, providerType={}, type={}",
LoggingUtils.sanitizeForLog(login),
nativeId,
provider.getType(),
userType
);
return userRepository
.findByLoginAndProviderId(login, providerId)
.map(User::getId)
.orElseThrow(() -> new IllegalStateException("User not found after upsert: login=" + login));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import de.tum.in.www1.hephaestus.gitprovider.common.github.app.GitHubAppTokenService;
import de.tum.in.www1.hephaestus.gitprovider.common.gitlab.GitLabProperties;
import de.tum.in.www1.hephaestus.gitprovider.common.spi.ProvisioningListener;
import de.tum.in.www1.hephaestus.gitprovider.user.AuthenticatedGitProviderUserService;
import de.tum.in.www1.hephaestus.gitprovider.user.User;
import de.tum.in.www1.hephaestus.gitprovider.user.UserRepository;
import de.tum.in.www1.hephaestus.workspace.WorkspaceMembership.WorkspaceRole;
Expand All @@ -21,7 +22,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.reactive.function.client.WebClient;
Expand Down Expand Up @@ -50,6 +50,7 @@ public class WorkspaceProvisioningService {
private final WorkspaceMembershipService workspaceMembershipService;
private final WorkspaceScopeFilter workspaceScopeFilter;
private final GitLabProperties gitLabProperties;
private final AuthenticatedGitProviderUserService authenticatedGitProviderUserService;
private final WebClient webClient;

public WorkspaceProvisioningService(
Expand All @@ -65,7 +66,8 @@ public WorkspaceProvisioningService(
WorkspaceMembershipRepository workspaceMembershipRepository,
WorkspaceMembershipService workspaceMembershipService,
WorkspaceScopeFilter workspaceScopeFilter,

Copilot AI Apr 7, 2026

Copy link

Choose a reason for hiding this comment

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

The constructor signature was changed to include AuthenticatedGitProviderUserService; there are direct instantiations of WorkspaceProvisioningService in tests (e.g., WorkspaceProvisioningServiceTest) that will no longer compile until updated to pass this dependency (likely as a mock).

Suggested change
WorkspaceScopeFilter workspaceScopeFilter,
WorkspaceScopeFilter workspaceScopeFilter,
GitLabProperties gitLabProperties
) {
this(
workspaceProperties,
workspaceRepository,
repositoryToMonitorRepository,
workspaceService,
workspaceInstallationService,
workspaceRepositoryMonitorService,
gitHubAppTokenService,
userRepository,
gitProviderRepository,
workspaceMembershipRepository,
workspaceMembershipService,
workspaceScopeFilter,
gitLabProperties,
null
);
}
public WorkspaceProvisioningService(
WorkspaceProperties workspaceProperties,
WorkspaceRepository workspaceRepository,
RepositoryToMonitorRepository repositoryToMonitorRepository,
WorkspaceService workspaceService,
WorkspaceInstallationService workspaceInstallationService,
WorkspaceRepositoryMonitorService workspaceRepositoryMonitorService,
GitHubAppTokenService gitHubAppTokenService,
UserRepository userRepository,
GitProviderRepository gitProviderRepository,
WorkspaceMembershipRepository workspaceMembershipRepository,
WorkspaceMembershipService workspaceMembershipService,
WorkspaceScopeFilter workspaceScopeFilter,

Copilot uses AI. Check for mistakes.
GitLabProperties gitLabProperties
GitLabProperties gitLabProperties,
AuthenticatedGitProviderUserService authenticatedGitProviderUserService
) {
this.workspaceProperties = workspaceProperties;
this.workspaceRepository = workspaceRepository;
Expand All @@ -80,6 +82,7 @@ public WorkspaceProvisioningService(
this.workspaceMembershipService = workspaceMembershipService;
this.workspaceScopeFilter = workspaceScopeFilter;
this.gitLabProperties = gitLabProperties;
this.authenticatedGitProviderUserService = authenticatedGitProviderUserService;
this.webClient = WebClient.builder()
.baseUrl(GITHUB_API_BASE_URL)
.defaultHeader(HttpHeaders.ACCEPT, "application/vnd.github+json")
Expand Down Expand Up @@ -282,41 +285,7 @@ public Long resolveOrCreateGitLabUser(String patToken, String serverUrl, String
*/
@Transactional
public void ensureAuthenticatedUserExists(String gitLabServerUrl) {
String login = SecurityUtils.getCurrentUserLoginOrThrow();

// Fast path: if user already exists in ANY provider, we're done
if (userRepository.findByLogin(login).isPresent()) {
return;
}

// Read identity from JWT
Authentication auth =
org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !(auth.getPrincipal() instanceof org.springframework.security.oauth2.jwt.Jwt jwt)) {
throw new IllegalStateException("No JWT found for authenticated user");
}

Long gitlabId = jwt.getClaim("gitlab_id");
if (gitlabId != null) {
String resolvedUrl = resolveGitLabServerUrl(gitLabServerUrl);
upsertGitLabUser(gitlabId, login, login, "", resolvedUrl + "/" + login, resolvedUrl, User.Type.USER);
return;
}

// No GitLab identity — check if they at least have a GitHub identity
Long githubId = jwt.getClaim("github_id");
if (githubId != null) {
throw new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.CONFLICT,
"You need to link your GitLab account before creating a GitLab workspace. " +
"Go to Settings → Linked Accounts to connect your GitLab identity."
);
}

throw new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.CONFLICT,
"No GitLab identity found. Please link your GitLab account in Settings → Linked Accounts."
);
authenticatedGitProviderUserService.ensureCurrentGitLabUserExists(gitLabServerUrl);
}
Comment on lines 286 to 289

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.

⚠️ Potential issue | 🟡 Minor

Update Javadoc to reflect new exception types.

The remaining Javadoc at line 284 states @throws IllegalStateException but the delegated method ensureCurrentGitLabUserExists throws ResponseStatusException (HTTP 409) for missing GitLab identity scenarios. Update the documentation to reflect the actual exception behavior.

📝 Fix throws documentation
      * `@param` gitLabServerUrl the GitLab server URL (resolved to default if blank)
-     * `@throws` IllegalStateException if the user has no GitLab identity linked
+     * `@throws` ResponseStatusException with HTTP 409 CONFLICT if the user has no GitLab identity linked
+     * `@throws` IllegalStateException if no JWT is found for the authenticated user
      */
     `@Transactional`
     public void ensureAuthenticatedUserExists(String gitLabServerUrl) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@server/application-server/src/main/java/de/tum/in/www1/hephaestus/workspace/WorkspaceProvisioningService.java`
around lines 286 - 289, Update the Javadoc for
WorkspaceProvisioningService.ensureAuthenticatedUserExists to reflect the actual
exception type and scenario: replace or augment the existing `@throws`
IllegalStateException with `@throws` ResponseStatusException (HTTP 409) to
indicate the method delegates to
authenticatedGitProviderUserService.ensureCurrentGitLabUserExists and will throw
a ResponseStatusException when the GitLab identity is missing or conflicts;
mention the HTTP 409 status and the missing GitLab identity condition so the
documentation matches the delegated method's behavior.


/**
Expand Down
Loading
Loading