Skip to content

Commit a1038b2

Browse files
fix(server): provision first-login git provider users
1 parent 6dc5fee commit a1038b2

5 files changed

Lines changed: 289 additions & 41 deletions

File tree

server/application-server/src/main/java/de/tum/in/www1/hephaestus/account/AccountController.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package de.tum.in.www1.hephaestus.account;
22

33
import de.tum.in.www1.hephaestus.config.KeycloakProperties;
4+
import de.tum.in.www1.hephaestus.gitprovider.user.AuthenticatedGitProviderUserService;
5+
import de.tum.in.www1.hephaestus.gitprovider.user.User;
46
import de.tum.in.www1.hephaestus.gitprovider.user.UserRepository;
57
import de.tum.in.www1.hephaestus.integrations.posthog.PosthogClientException;
68
import io.swagger.v3.oas.annotations.Operation;
79
import io.swagger.v3.oas.annotations.tags.Tag;
810
import jakarta.validation.Valid;
911
import java.util.List;
12+
import java.util.Optional;
1013
import org.keycloak.admin.client.Keycloak;
1114
import org.slf4j.Logger;
1215
import org.slf4j.LoggerFactory;
@@ -44,17 +47,20 @@ public class AccountController {
4447
private final Keycloak keycloak;
4548
private final UserRepository userRepository;
4649
private final KeycloakProperties keycloakProperties;
50+
private final AuthenticatedGitProviderUserService authenticatedGitProviderUserService;
4751

4852
public AccountController(
4953
AccountService accountService,
5054
Keycloak keycloak,
5155
UserRepository userRepository,
52-
KeycloakProperties keycloakProperties
56+
KeycloakProperties keycloakProperties,
57+
AuthenticatedGitProviderUserService authenticatedGitProviderUserService
5358
) {
5459
this.accountService = accountService;
5560
this.keycloak = keycloak;
5661
this.userRepository = userRepository;
5762
this.keycloakProperties = keycloakProperties;
63+
this.authenticatedGitProviderUserService = authenticatedGitProviderUserService;
5864
}
5965

6066
@DeleteMapping
@@ -96,8 +102,8 @@ public ResponseEntity<Void> deleteUser(@AuthenticationPrincipal JwtAuthenticatio
96102
summary = "Get user settings",
97103
description = "Get the current user's notification, research participation, and AI review preferences"
98104
)
99-
public ResponseEntity<UserSettingsDTO> getUserSettings() {
100-
var user = userRepository.getCurrentUser();
105+
public ResponseEntity<UserSettingsDTO> getUserSettings(@AuthenticationPrincipal JwtAuthenticationToken auth) {
106+
var user = resolveOrProvisionCurrentUser(auth);
101107
if (user.isEmpty()) {
102108
return ResponseEntity.notFound().build();
103109
}
@@ -115,7 +121,7 @@ public ResponseEntity<UserSettingsDTO> updateUserSettings(
115121
@AuthenticationPrincipal JwtAuthenticationToken auth,
116122
@Valid @RequestBody UserSettingsDTO userSettings
117123
) {
118-
var user = userRepository.getCurrentUser();
124+
var user = resolveOrProvisionCurrentUser(auth);
119125
if (user.isEmpty()) {
120126
return ResponseEntity.notFound().build();
121127
}
@@ -205,4 +211,12 @@ private JwtAuthenticationToken resolveAuthentication(JwtAuthenticationToken inje
205211
}
206212
return null;
207213
}
214+
215+
private Optional<User> resolveOrProvisionCurrentUser(JwtAuthenticationToken auth) {
216+
if (resolveAuthentication(auth) == null) {
217+
return Optional.empty();
218+
}
219+
220+
return authenticatedGitProviderUserService.resolveOrProvisionCurrentUser(null);
221+
}
208222
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
package de.tum.in.www1.hephaestus.gitprovider.user;
2+
3+
import de.tum.in.www1.hephaestus.SecurityUtils;
4+
import de.tum.in.www1.hephaestus.core.LoggingUtils;
5+
import de.tum.in.www1.hephaestus.gitprovider.common.GitProvider;
6+
import de.tum.in.www1.hephaestus.gitprovider.common.GitProviderRepository;
7+
import de.tum.in.www1.hephaestus.gitprovider.common.GitProviderType;
8+
import de.tum.in.www1.hephaestus.gitprovider.common.gitlab.GitLabProperties;
9+
import java.util.Optional;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
import org.springframework.http.HttpStatus;
13+
import org.springframework.lang.Nullable;
14+
import org.springframework.security.core.Authentication;
15+
import org.springframework.security.core.context.SecurityContextHolder;
16+
import org.springframework.security.oauth2.jwt.Jwt;
17+
import org.springframework.stereotype.Service;
18+
import org.springframework.transaction.annotation.Transactional;
19+
import org.springframework.web.server.ResponseStatusException;
20+
21+
@Service
22+
public class AuthenticatedGitProviderUserService {
23+
24+
private static final Logger log = LoggerFactory.getLogger(AuthenticatedGitProviderUserService.class);
25+
private static final String GITHUB_SERVER_URL = "https://github.com";
26+
27+
private final UserRepository userRepository;
28+
private final GitProviderRepository gitProviderRepository;
29+
private final GitLabProperties gitLabProperties;
30+
31+
public AuthenticatedGitProviderUserService(
32+
UserRepository userRepository,
33+
GitProviderRepository gitProviderRepository,
34+
GitLabProperties gitLabProperties
35+
) {
36+
this.userRepository = userRepository;
37+
this.gitProviderRepository = gitProviderRepository;
38+
this.gitLabProperties = gitLabProperties;
39+
}
40+
41+
@Transactional
42+
public Optional<User> resolveOrProvisionCurrentUser(@Nullable String gitLabServerUrl) {
43+
var currentUser = userRepository.getCurrentUser();
44+
if (currentUser.isPresent()) {
45+
return currentUser;
46+
}
47+
48+
Optional<String> currentLogin = SecurityUtils.getCurrentUserLogin();
49+
if (currentLogin.isEmpty()) {
50+
return Optional.empty();
51+
}
52+
String login = currentLogin.orElseThrow();
53+
54+
Jwt jwt = getCurrentJwt();
55+
if (jwt == null) {
56+
return Optional.empty();
57+
}
58+
59+
Long gitlabId = jwt.getClaim("gitlab_id");
60+
if (gitlabId != null) {
61+
String resolvedUrl = resolveGitLabServerUrl(gitLabServerUrl);
62+
Long userId = upsertGitLabUser(
63+
gitlabId,
64+
login,
65+
login,
66+
"",
67+
resolvedUrl + "/" + login,
68+
resolvedUrl,
69+
User.Type.USER
70+
);
71+
return userRepository.findById(userId);
72+
}
73+
74+
Long githubId = jwt.getClaim("github_id");
75+
if (githubId != null) {
76+
Long userId = upsertGitHubUser(githubId, login, login, "", GITHUB_SERVER_URL + "/" + login, User.Type.USER);
77+
return userRepository.findById(userId);
78+
}
79+
80+
return Optional.empty();
81+
}
82+
83+
@Transactional
84+
public void ensureCurrentGitLabUserExists(@Nullable String gitLabServerUrl) {
85+
String login = SecurityUtils.getCurrentUserLoginOrThrow();
86+
if (userRepository.findByLogin(login).isPresent()) {
87+
return;
88+
}
89+
90+
Jwt jwt = getCurrentJwt();
91+
if (jwt == null) {
92+
throw new IllegalStateException("No JWT found for authenticated user");
93+
}
94+
95+
Long gitlabId = jwt.getClaim("gitlab_id");
96+
if (gitlabId != null) {
97+
String resolvedUrl = resolveGitLabServerUrl(gitLabServerUrl);
98+
upsertGitLabUser(gitlabId, login, login, "", resolvedUrl + "/" + login, resolvedUrl, User.Type.USER);
99+
return;
100+
}
101+
102+
Long githubId = jwt.getClaim("github_id");
103+
if (githubId != null) {
104+
throw new ResponseStatusException(
105+
HttpStatus.CONFLICT,
106+
"You need to link your GitLab account before creating a GitLab workspace. Go to Settings → Linked Accounts to connect your GitLab identity."
107+
);
108+
}
109+
110+
throw new ResponseStatusException(
111+
HttpStatus.CONFLICT,
112+
"No GitLab identity found. Please link your GitLab account in Settings → Linked Accounts."
113+
);
114+
}
115+
116+
@Nullable
117+
private Jwt getCurrentJwt() {
118+
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
119+
if (auth == null || !(auth.getPrincipal() instanceof Jwt jwt)) {
120+
return null;
121+
}
122+
return jwt;
123+
}
124+
125+
private String resolveGitLabServerUrl(@Nullable String configServerUrl) {
126+
if (configServerUrl != null && !configServerUrl.isBlank()) {
127+
String url = configServerUrl.trim();
128+
return url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
129+
}
130+
return gitLabProperties.defaultServerUrl();
131+
}
132+
133+
private Long upsertGitHubUser(
134+
Long nativeId,
135+
String login,
136+
String name,
137+
String avatarUrl,
138+
String webUrl,
139+
User.Type userType
140+
) {
141+
GitProvider provider = gitProviderRepository
142+
.findByTypeAndServerUrl(GitProviderType.GITHUB, GITHUB_SERVER_URL)
143+
.orElseGet(() -> gitProviderRepository.save(new GitProvider(GitProviderType.GITHUB, GITHUB_SERVER_URL)));
144+
145+
return upsertUser(nativeId, login, name, avatarUrl, webUrl, userType, provider);
146+
}
147+
148+
private Long upsertGitLabUser(
149+
Long nativeId,
150+
String login,
151+
String name,
152+
String avatarUrl,
153+
String webUrl,
154+
String serverUrl,
155+
User.Type userType
156+
) {
157+
String safeAvatar = avatarUrl != null ? (avatarUrl.startsWith("/") ? serverUrl + avatarUrl : avatarUrl) : "";
158+
GitProvider provider = gitProviderRepository
159+
.findByTypeAndServerUrl(GitProviderType.GITLAB, serverUrl)
160+
.orElseGet(() -> {
161+
log.info("Creating GitProvider for self-hosted GitLab: serverUrl={}", serverUrl);
162+
return gitProviderRepository.save(new GitProvider(GitProviderType.GITLAB, serverUrl));
163+
});
164+
165+
return upsertUser(nativeId, login, name, safeAvatar, webUrl, userType, provider);
166+
}
167+
168+
private Long upsertUser(
169+
Long nativeId,
170+
String login,
171+
String name,
172+
String avatarUrl,
173+
String webUrl,
174+
User.Type userType,
175+
GitProvider provider
176+
) {
177+
String safeName = name != null ? name : login;
178+
String safeAvatar = avatarUrl != null ? avatarUrl : "";
179+
String safeWebUrl = webUrl != null ? webUrl : "";
180+
Long providerId = provider.getId();
181+
182+
userRepository.acquireLoginLock(login, providerId);
183+
userRepository.freeLoginConflicts(login, nativeId, providerId);
184+
userRepository.upsertUser(
185+
nativeId,
186+
providerId,
187+
login,
188+
safeName,
189+
safeAvatar,
190+
safeWebUrl,
191+
userType.name(),
192+
null,
193+
null,
194+
null
195+
);
196+
log.info(
197+
"Upserted authenticated git provider user: userLogin={}, nativeId={}, providerType={}, type={}",
198+
LoggingUtils.sanitizeForLog(login),
199+
nativeId,
200+
provider.getType(),
201+
userType
202+
);
203+
return userRepository
204+
.findByLoginAndProviderId(login, providerId)
205+
.map(User::getId)
206+
.orElseThrow(() -> new IllegalStateException("User not found after upsert: login=" + login));
207+
}
208+
}

server/application-server/src/main/java/de/tum/in/www1/hephaestus/workspace/WorkspaceProvisioningService.java

Lines changed: 6 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import de.tum.in.www1.hephaestus.gitprovider.common.github.app.GitHubAppTokenService;
1212
import de.tum.in.www1.hephaestus.gitprovider.common.gitlab.GitLabProperties;
1313
import de.tum.in.www1.hephaestus.gitprovider.common.spi.ProvisioningListener;
14+
import de.tum.in.www1.hephaestus.gitprovider.user.AuthenticatedGitProviderUserService;
1415
import de.tum.in.www1.hephaestus.gitprovider.user.User;
1516
import de.tum.in.www1.hephaestus.gitprovider.user.UserRepository;
1617
import de.tum.in.www1.hephaestus.workspace.WorkspaceMembership.WorkspaceRole;
@@ -21,7 +22,6 @@
2122
import org.slf4j.Logger;
2223
import org.slf4j.LoggerFactory;
2324
import org.springframework.http.HttpHeaders;
24-
import org.springframework.security.core.Authentication;
2525
import org.springframework.stereotype.Service;
2626
import org.springframework.transaction.annotation.Transactional;
2727
import org.springframework.web.reactive.function.client.WebClient;
@@ -50,6 +50,7 @@ public class WorkspaceProvisioningService {
5050
private final WorkspaceMembershipService workspaceMembershipService;
5151
private final WorkspaceScopeFilter workspaceScopeFilter;
5252
private final GitLabProperties gitLabProperties;
53+
private final AuthenticatedGitProviderUserService authenticatedGitProviderUserService;
5354
private final WebClient webClient;
5455

5556
public WorkspaceProvisioningService(
@@ -65,7 +66,8 @@ public WorkspaceProvisioningService(
6566
WorkspaceMembershipRepository workspaceMembershipRepository,
6667
WorkspaceMembershipService workspaceMembershipService,
6768
WorkspaceScopeFilter workspaceScopeFilter,
68-
GitLabProperties gitLabProperties
69+
GitLabProperties gitLabProperties,
70+
AuthenticatedGitProviderUserService authenticatedGitProviderUserService
6971
) {
7072
this.workspaceProperties = workspaceProperties;
7173
this.workspaceRepository = workspaceRepository;
@@ -80,6 +82,7 @@ public WorkspaceProvisioningService(
8082
this.workspaceMembershipService = workspaceMembershipService;
8183
this.workspaceScopeFilter = workspaceScopeFilter;
8284
this.gitLabProperties = gitLabProperties;
85+
this.authenticatedGitProviderUserService = authenticatedGitProviderUserService;
8386
this.webClient = WebClient.builder()
8487
.baseUrl(GITHUB_API_BASE_URL)
8588
.defaultHeader(HttpHeaders.ACCEPT, "application/vnd.github+json")
@@ -282,41 +285,7 @@ public Long resolveOrCreateGitLabUser(String patToken, String serverUrl, String
282285
*/
283286
@Transactional
284287
public void ensureAuthenticatedUserExists(String gitLabServerUrl) {
285-
String login = SecurityUtils.getCurrentUserLoginOrThrow();
286-
287-
// Fast path: if user already exists in ANY provider, we're done
288-
if (userRepository.findByLogin(login).isPresent()) {
289-
return;
290-
}
291-
292-
// Read identity from JWT
293-
Authentication auth =
294-
org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
295-
if (auth == null || !(auth.getPrincipal() instanceof org.springframework.security.oauth2.jwt.Jwt jwt)) {
296-
throw new IllegalStateException("No JWT found for authenticated user");
297-
}
298-
299-
Long gitlabId = jwt.getClaim("gitlab_id");
300-
if (gitlabId != null) {
301-
String resolvedUrl = resolveGitLabServerUrl(gitLabServerUrl);
302-
upsertGitLabUser(gitlabId, login, login, "", resolvedUrl + "/" + login, resolvedUrl, User.Type.USER);
303-
return;
304-
}
305-
306-
// No GitLab identity — check if they at least have a GitHub identity
307-
Long githubId = jwt.getClaim("github_id");
308-
if (githubId != null) {
309-
throw new org.springframework.web.server.ResponseStatusException(
310-
org.springframework.http.HttpStatus.CONFLICT,
311-
"You need to link your GitLab account before creating a GitLab workspace. " +
312-
"Go to Settings → Linked Accounts to connect your GitLab identity."
313-
);
314-
}
315-
316-
throw new org.springframework.web.server.ResponseStatusException(
317-
org.springframework.http.HttpStatus.CONFLICT,
318-
"No GitLab identity found. Please link your GitLab account in Settings → Linked Accounts."
319-
);
288+
authenticatedGitProviderUserService.ensureCurrentGitLabUserExists(gitLabServerUrl);
320289
}
321290

322291
/**

0 commit comments

Comments
 (0)