Skip to content

Commit ec87ad3

Browse files
svoramvanbaren
andcommitted
feat: make login optional
Co-authored-by: amvanbaren <[email protected]>
1 parent 3bbb6a8 commit ec87ad3

File tree

11 files changed

+130
-49
lines changed

11 files changed

+130
-49
lines changed

server/src/main/java/org/eclipse/openvsx/UserAPI.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
import org.springframework.web.bind.annotation.*;
3434
import org.springframework.web.multipart.MultipartFile;
3535
import org.springframework.web.server.ResponseStatusException;
36-
import org.springframework.web.servlet.ModelAndView;
3736

37+
import java.net.URI;
3838
import java.util.LinkedHashMap;
3939
import java.util.List;
4040
import java.util.concurrent.TimeUnit;
@@ -66,14 +66,30 @@ public UserAPI(
6666
this.storageUtil = storageUtil;
6767
}
6868

69+
@GetMapping(
70+
path = "/can-login",
71+
produces = MediaType.APPLICATION_JSON_VALUE
72+
)
73+
public ResponseEntity<Boolean> canLogin() {
74+
return ResponseEntity.ok()
75+
.cacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES).cachePublic())
76+
.body(users.canLogin());
77+
}
78+
6979
/**
7080
* Redirect to GitHub Oauth2 login as default login provider.
7181
*/
7282
@GetMapping(
7383
path = "/login"
7484
)
75-
public ModelAndView login(ModelMap model) {
76-
return new ModelAndView("redirect:/oauth2/authorization/github", model);
85+
public ResponseEntity<Void> login(ModelMap model) {
86+
if(users.canLogin()) {
87+
return ResponseEntity.status(HttpStatus.FOUND)
88+
.location(URI.create(UrlUtil.createApiUrl(UrlUtil.getBaseUrl(), "oauth2", "authorization", "github")))
89+
.build();
90+
} else {
91+
return ResponseEntity.notFound().build();
92+
}
7793
}
7894

7995
/**
@@ -84,7 +100,7 @@ public ModelAndView login(ModelMap model) {
84100
produces = MediaType.APPLICATION_JSON_VALUE
85101
)
86102
public ErrorJson getAuthError(HttpServletRequest request) {
87-
var authException = request.getSession().getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
103+
var authException = users.canLogin() ? request.getSession().getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION) : null;
88104
if (!(authException instanceof AuthenticationException))
89105
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
90106

server/src/main/java/org/eclipse/openvsx/UserService.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@
3030
import org.eclipse.openvsx.security.IdPrincipal;
3131
import org.eclipse.openvsx.storage.StorageUtilService;
3232
import org.eclipse.openvsx.util.*;
33+
import org.springframework.beans.factory.annotation.Autowired;
3334
import org.springframework.cache.annotation.CacheEvict;
3435
import org.springframework.security.core.context.SecurityContextHolder;
36+
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
3537
import org.springframework.security.oauth2.core.user.OAuth2User;
3638
import org.springframework.stereotype.Component;
3739
import org.springframework.web.multipart.MultipartFile;
@@ -53,22 +55,29 @@ public class UserService {
5355
private final StorageUtilService storageUtil;
5456
private final CacheService cache;
5557
private final ExtensionValidator validator;
58+
private final ClientRegistrationRepository clientRegistrationRepository;
5659

5760
public UserService(
5861
EntityManager entityManager,
5962
RepositoryService repositories,
6063
StorageUtilService storageUtil,
6164
CacheService cache,
62-
ExtensionValidator validator
65+
ExtensionValidator validator,
66+
@Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository
6367
) {
6468
this.entityManager = entityManager;
6569
this.repositories = repositories;
6670
this.storageUtil = storageUtil;
6771
this.cache = cache;
6872
this.validator = validator;
73+
this.clientRegistrationRepository = clientRegistrationRepository;
6974
}
7075

7176
public UserData findLoggedInUser() {
77+
if(!canLogin()) {
78+
return null;
79+
}
80+
7281
var authentication = SecurityContextHolder.getContext().getAuthentication();
7382
if (authentication != null) {
7483
if (authentication.getPrincipal() instanceof IdPrincipal) {
@@ -321,4 +330,8 @@ public ResultJson deleteAccessToken(UserData user, long id) {
321330
token.setActive(false);
322331
return ResultJson.success("Deleted access token for user " + user.getLoginName() + ".");
323332
}
333+
334+
public boolean canLogin() {
335+
return clientRegistrationRepository != null && clientRegistrationRepository.findByRegistrationId("github") != null;
336+
}
324337
}

server/src/main/java/org/eclipse/openvsx/security/OAuth2UserServices.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ public IdPrincipal loadUser(OAuth2UserRequest userRequest) {
110110
}
111111
}
112112

113+
public boolean canLogin() {
114+
return users.canLogin();
115+
}
116+
113117
private IdPrincipal loadGitHubUser(OAuth2UserRequest userRequest) {
114118
var authUser = delegate.loadUser(userRequest);
115119
String loginName = authUser.getAttribute("login");

server/src/main/java/org/eclipse/openvsx/security/SecurityConfig.java

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
package org.eclipse.openvsx.security;
1111

1212
import org.apache.commons.lang3.StringUtils;
13+
import org.springframework.beans.factory.annotation.Autowired;
1314
import org.springframework.beans.factory.annotation.Value;
1415
import org.springframework.context.annotation.Bean;
1516
import org.springframework.context.annotation.Configuration;
1617
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
1718
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
19+
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
1820
import org.springframework.security.web.SecurityFilterChain;
1921
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
2022
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@@ -30,12 +32,18 @@ public class SecurityConfig {
3032
@Value("${ovsx.webui.frontendRoutes:/extension/**,/namespace/**,/user-settings/**,/admin-dashboard/**}")
3133
String[] frontendRoutes;
3234

35+
private final ClientRegistrationRepository clientRegistrationRepository;
36+
37+
@Autowired
38+
public SecurityConfig(@Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository) {
39+
this.clientRegistrationRepository = clientRegistrationRepository;
40+
}
41+
3342
@Bean
3443
public SecurityFilterChain filterChain(HttpSecurity http, OAuth2UserServices userServices) throws Exception {
35-
var redirectUrl = StringUtils.isEmpty(webuiUrl) ? "/" : webuiUrl;
36-
return http.authorizeHttpRequests(
44+
var filterChain = http.authorizeHttpRequests(
3745
registry -> registry
38-
.requestMatchers(antMatchers("/*", "/login/**", "/oauth2/**", "/user", "/user/auth-error", "/logout", "/actuator/health/**", "/actuator/metrics", "/actuator/metrics/**", "/actuator/prometheus", "/v3/api-docs/**", "/swagger-resources/**", "/swagger-ui/**", "/webjars/**"))
46+
.requestMatchers(antMatchers("/*", "/login/**", "/oauth2/**", "/can-login", "/user", "/user/auth-error", "/logout", "/actuator/health/**", "/actuator/metrics", "/actuator/metrics/**", "/actuator/prometheus", "/v3/api-docs/**", "/swagger-resources/**", "/swagger-ui/**", "/webjars/**"))
3947
.permitAll()
4048
.requestMatchers(antMatchers("/api/*/*/review", "/api/*/*/review/delete", "/api/user/publish", "/api/user/namespace/create"))
4149
.authenticated()
@@ -52,15 +60,20 @@ public SecurityFilterChain filterChain(HttpSecurity http, OAuth2UserServices use
5260
.csrf(configurer -> {
5361
configurer.ignoringRequestMatchers(antMatchers("/api/-/publish", "/api/-/namespace/create", "/api/-/query", "/vscode/**"));
5462
})
55-
.exceptionHandling(configurer -> configurer.authenticationEntryPoint(new Http403ForbiddenEntryPoint()))
56-
.oauth2Login(configurer -> {
57-
configurer.defaultSuccessUrl(redirectUrl);
58-
configurer.successHandler(new CustomAuthenticationSuccessHandler(redirectUrl));
59-
configurer.failureUrl(redirectUrl + "?auth-error");
60-
configurer.userInfoEndpoint(customizer -> customizer.oidcUserService(userServices.getOidc()).userService(userServices.getOauth2()));
61-
})
62-
.logout(configurer -> configurer.logoutSuccessUrl(redirectUrl))
63-
.build();
63+
.exceptionHandling(configurer -> configurer.authenticationEntryPoint(new Http403ForbiddenEntryPoint()));
64+
65+
if(userServices.canLogin()) {
66+
var redirectUrl = StringUtils.isEmpty(webuiUrl) ? "/" : webuiUrl;
67+
filterChain.oauth2Login(configurer -> {
68+
configurer.defaultSuccessUrl(redirectUrl);
69+
configurer.successHandler(new CustomAuthenticationSuccessHandler(redirectUrl));
70+
configurer.failureUrl(redirectUrl + "?auth-error");
71+
configurer.userInfoEndpoint(customizer -> customizer.oidcUserService(userServices.getOidc()).userService(userServices.getOauth2()));
72+
})
73+
.logout(configurer -> configurer.logoutSuccessUrl(redirectUrl));
74+
}
75+
76+
return filterChain.build();
6477
}
6578

6679
private RequestMatcher[] antMatchers(String... patterns)

server/src/main/java/org/eclipse/openvsx/security/TokenService.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.transaction.support.TransactionTemplate;
2929
import org.springframework.web.client.RestClientException;
3030
import org.springframework.web.client.RestTemplate;
31+
import org.springframework.beans.factory.annotation.Autowired;
3132

3233
import java.time.Instant;
3334
import java.util.Arrays;
@@ -44,16 +45,20 @@ public class TokenService {
4445
public TokenService(
4546
TransactionTemplate transactions,
4647
EntityManager entityManager,
47-
ClientRegistrationRepository clientRegistrationRepository
48+
@Autowired(required = false) ClientRegistrationRepository clientRegistrationRepository
4849
) {
4950
this.transactions = transactions;
5051
this.entityManager = entityManager;
5152
this.clientRegistrationRepository = clientRegistrationRepository;
5253
}
5354

55+
private boolean isEnabled() {
56+
return clientRegistrationRepository != null;
57+
}
58+
5459
public AuthToken updateTokens(long userId, String registrationId, OAuth2AccessToken accessToken,
5560
OAuth2RefreshToken refreshToken) {
56-
var userData = entityManager.find(UserData.class, userId);
61+
var userData = isEnabled() ? entityManager.find(UserData.class, userId) : null;
5762
if (userData == null) {
5863
return null;
5964
}
@@ -119,6 +124,10 @@ private AuthToken updateEclipseToken(UserData userData, AuthToken token) {
119124
}
120125

121126
public AuthToken getActiveToken(UserData userData, String registrationId) {
127+
if(!isEnabled()) {
128+
return null;
129+
}
130+
122131
switch (registrationId) {
123132
case "github": {
124133
return userData.getGithubToken();
@@ -148,7 +157,7 @@ private boolean isExpired(Instant instant) {
148157
return instant != null && Instant.now().isAfter(instant);
149158
}
150159

151-
protected Pair<OAuth2AccessToken, OAuth2RefreshToken> refreshEclipseToken(AuthToken token) {
160+
private Pair<OAuth2AccessToken, OAuth2RefreshToken> refreshEclipseToken(AuthToken token) {
152161
if(token.refreshToken() == null || isExpired(token.refreshExpiresAt())) {
153162
return null;
154163
}

server/src/main/java/org/eclipse/openvsx/web/WebConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public void addCorsMappings(CorsRegistry registry) {
5353
.allowedOrigins(webuiUrl)
5454
.allowCredentials(true);
5555
}
56+
registry.addMapping("/can-login")
57+
.allowedOrigins(webuiUrl);
5658
registry.addMapping("/documents/**")
5759
.allowedOrigins("*");
5860
registry.addMapping("/api/**")

server/src/test/java/org/eclipse/openvsx/adapter/VSCodeAPITest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -918,9 +918,10 @@ UserService userService(
918918
RepositoryService repositories,
919919
StorageUtilService storageUtil,
920920
CacheService cache,
921-
ExtensionValidator validator
921+
ExtensionValidator validator,
922+
ClientRegistrationRepository clientRegistrationRepository
922923
) {
923-
return new UserService(entityManager, repositories, storageUtil, cache, validator);
924+
return new UserService(entityManager, repositories, storageUtil, cache, validator, clientRegistrationRepository);
924925
}
925926

926927
@Bean

webui/src/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface MainContext {
2020
handleError: (err: Error | Partial<ErrorResponse>) => void;
2121
user?: UserData;
2222
updateUser: () => void;
23+
canLogin: boolean;
2324
}
2425

2526
// We don't include `undefined` as context value to avoid checking the value in all components

webui/src/default/menu-content.tsx

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -116,35 +116,34 @@ export const MobileUserAvatar: FunctionComponent = () => {
116116
};
117117

118118
export const MobileMenuContent: FunctionComponent = () => {
119-
120119
const location = useLocation();
121-
const { service, user } = useContext(MainContext);
120+
const { service, user, canLogin } = useContext(MainContext);
122121

123122
return <>
124-
{
125-
user
126-
? <MobileUserAvatar/>
127-
: <MobileMenuItem>
123+
{canLogin && (
124+
user ? (
125+
<MobileUserAvatar />
126+
) : (
127+
<MobileMenuItem>
128128
<Link href={service.getLoginUrl()}>
129129
<MobileMenuItemText>
130130
<AccountBoxIcon sx={itemIcon} />
131131
Log In
132132
</MobileMenuItemText>
133133
</Link>
134134
</MobileMenuItem>
135-
}
136-
{
137-
!location.pathname.startsWith(UserSettingsRoutes.ROOT)
138-
? <MobileMenuItem>
135+
)
136+
)}
137+
{canLogin && !location.pathname.startsWith(UserSettingsRoutes.ROOT) && (
138+
<MobileMenuItem>
139139
<RouteLink to='/user-settings/extensions'>
140140
<MobileMenuItemText>
141141
<PublishIcon sx={itemIcon} />
142142
Publish Extension
143143
</MobileMenuItemText>
144144
</RouteLink>
145145
</MobileMenuItem>
146-
: null
147-
}
146+
)}
148147
<MobileMenuItem>
149148
<Link target='_blank' href='https://github.com/eclipse/openvsx'>
150149
<MobileMenuItemText>
@@ -200,7 +199,7 @@ export const MenuLink = styled(Link)(headerItem);
200199
export const MenuRouteLink = styled(RouteLink)(headerItem);
201200

202201
export const DefaultMenuContent: FunctionComponent = () => {
203-
const { service, user } = useContext(MainContext);
202+
const { service, user, canLogin } = useContext(MainContext);
204203
return <>
205204
<MenuLink href='https://github.com/eclipse/openvsx/wiki'>
206205
Documentation
@@ -211,19 +210,23 @@ export const DefaultMenuContent: FunctionComponent = () => {
211210
<MenuRouteLink to='/about'>
212211
About
213212
</MenuRouteLink>
214-
<Button variant='contained' color='secondary' href='/user-settings/extensions' sx={{ mx: 2.5 }}>
215-
Publish
216-
</Button>
217-
{
218-
user ?
219-
<UserAvatar />
220-
:
221-
<IconButton
222-
href={service.getLoginUrl()}
223-
title='Log In'
224-
aria-label='Log In' >
225-
<AccountBoxIcon />
226-
</IconButton>
227-
}
213+
{canLogin && (
214+
<>
215+
<Button variant='contained' color='secondary' href='/user-settings/extensions' sx={{ mx: 2.5 }}>
216+
Publish
217+
</Button>
218+
{
219+
user ?
220+
<UserAvatar />
221+
:
222+
<IconButton
223+
href={service.getLoginUrl()}
224+
title='Log In'
225+
aria-label='Log In' >
226+
<AccountBoxIcon />
227+
</IconButton>
228+
}
229+
</>
230+
)}
228231
</>;
229232
};

webui/src/extension-registry-service.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,11 @@ export class ExtensionRegistryService {
420420
const endpoint = createAbsoluteURL([this.serverUrl, 'api', 'version']);
421421
return sendRequest({ abortController, endpoint });
422422
}
423+
424+
async canLogin(abortController: AbortController): Promise<Readonly<boolean>> {
425+
const endpoint = createAbsoluteURL([this.serverUrl, 'can-login']);
426+
return sendRequest({ abortController, endpoint });
427+
}
423428
}
424429

425430
export interface AdminService {

0 commit comments

Comments
 (0)