diff --git a/pom.xml b/pom.xml index d21e05f3efd0f24e65712ba5480a062529eb93a8..1eb9a854a0890ea5d4b43a2712a9688160209c4d 100644 --- a/pom.xml +++ b/pom.xml @@ -170,6 +170,9 @@ 1.3.3 3.4.6 + 0.9.1 + 2.3.1 + 3.8.1 3.2.0 diff --git a/web/pom.xml b/web/pom.xml index ae705d60931a01339aca1a68d4d428a59f1deea9..3c1291b01e6e334f7c3210fce371effa0acbabfe 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -302,6 +302,17 @@ mail + + io.jsonwebtoken + jjwt + ${jjwt.version} + + + javax.xml.bind + jaxb-api + ${jaxb.version} + + com.navercorp.pinpoint diff --git a/web/src/main/angular/angular.json b/web/src/main/angular/angular.json index 02e116d02d0d0969fe3b988c148599b518f90f43..2dc534cd0574cddca92c506bf88c51abc14b5cae 100644 --- a/web/src/main/angular/angular.json +++ b/web/src/main/angular/angular.json @@ -21,6 +21,7 @@ "assets": [ "src/favicon.ico", "src/assets", + "src/not_authorized.html", { "glob": "utils.js", "input": "node_modules/intl-tel-input/build/js", diff --git a/web/src/main/angular/src/not_authorized.html b/web/src/main/angular/src/not_authorized.html new file mode 100644 index 0000000000000000000000000000000000000000..040414eb4dacf255659bc2c6ac8a1041bb8620d2 --- /dev/null +++ b/web/src/main/angular/src/not_authorized.html @@ -0,0 +1,93 @@ + + + + PINPOINT + + + + + + + + + +
+
+ +

+
+

+
+ + + \ No newline at end of file diff --git a/web/src/main/java/com/navercorp/pinpoint/web/PinpointBasicLoginConfig.java b/web/src/main/java/com/navercorp/pinpoint/web/PinpointBasicLoginConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8b9207a7319b68b0adcb90cc706767596b868a6e --- /dev/null +++ b/web/src/main/java/com/navercorp/pinpoint/web/PinpointBasicLoginConfig.java @@ -0,0 +1,97 @@ +/* + * Copyright 2021 NAVER Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.navercorp.pinpoint.web; + +import com.navercorp.pinpoint.web.security.login.BasicLoginConstants; +import com.navercorp.pinpoint.web.security.login.BasicLoginService; +import com.navercorp.pinpoint.web.security.login.JwtRequestFilter; +import com.navercorp.pinpoint.web.security.login.PreAuthenticationCheckFilter; +import com.navercorp.pinpoint.web.security.login.SaveJwtTokenAuthenticationSuccessHandler; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; + +/** + * @author Taejin Koo + */ +@Configuration +@EnableWebSecurity +@Profile("basicLogin") +public class PinpointBasicLoginConfig extends WebSecurityConfigurerAdapter { + + @Autowired + private BasicLoginService basicLoginService; + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.eraseCredentials(false); + auth.userDetailsService(basicLoginService.getUserDetailsService()); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + // for common + http + .csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .formLogin() + .successHandler(new SaveJwtTokenAuthenticationSuccessHandler(basicLoginService)) + .and() + .httpBasic() + .and() + .logout() + .deleteCookies(BasicLoginConstants.PINPOINT_JWT_COOKIE_NAME); + + // for admin + http.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN") + .and() + .exceptionHandling() + .accessDeniedPage(BasicLoginConstants.URI_NOT_AUTHORIZED); + + // for user + http.authorizeRequests().anyRequest().authenticated(); + + http.addFilterBefore(new JwtRequestFilter(basicLoginService), UsernamePasswordAuthenticationFilter.class); + + http.addFilterBefore(new PreAuthenticationCheckFilter(), DefaultLoginPageGeneratingFilter.class); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Override + @Bean + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + +} \ No newline at end of file diff --git a/web/src/main/java/com/navercorp/pinpoint/web/WebApp.java b/web/src/main/java/com/navercorp/pinpoint/web/WebApp.java index 7abfd74c9fa35e3c3633c7597afa7251ed765b53..6118f60ad936be05f915336ef28aed84246e31c4 100644 --- a/web/src/main/java/com/navercorp/pinpoint/web/WebApp.java +++ b/web/src/main/java/com/navercorp/pinpoint/web/WebApp.java @@ -1,3 +1,19 @@ +/* + * Copyright 2021 NAVER Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.navercorp.pinpoint.web; import com.navercorp.pinpoint.common.server.util.ServerBootLogger; @@ -22,7 +38,7 @@ public class WebApp { public static void main(String[] args) { try { - WebStarter starter = new WebStarter(WebApp.class, WebMvcConfig.class); + WebStarter starter = new WebStarter(WebApp.class, WebMvcConfig.class, PinpointBasicLoginConfig.class); starter.start(args); } catch (Exception exception) { logger.error("[WebApp] could not launch app.", exception); diff --git a/web/src/main/java/com/navercorp/pinpoint/web/config/BasicLoginConfig.java b/web/src/main/java/com/navercorp/pinpoint/web/config/BasicLoginConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8c45a03a918cc100bac55c8bbf4f180c9ef260d1 --- /dev/null +++ b/web/src/main/java/com/navercorp/pinpoint/web/config/BasicLoginConfig.java @@ -0,0 +1,133 @@ +/* + * Copyright 2021 NAVER Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.navercorp.pinpoint.web.config; + +import com.navercorp.pinpoint.common.util.CollectionUtils; +import com.navercorp.pinpoint.common.util.StringUtils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * @author Taejin Koo + */ +@Profile("basicLogin") +@Configuration +public class BasicLoginConfig { + + private static final String DEFAULT_JWT_SECRET_KEY = "PINPOINT_JWT_SECRET"; + + private final int DEFAULT_EXPIRATION_TIME_SECONDS = new Long(TimeUnit.HOURS.toSeconds(12)).intValue(); + + private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + + @Value("${web.security.auth.user:}") + private List userIdAndPasswordPairList; + + @Value("${web.security.auth.admin:}") + private List adminIdAndPasswordPairList; + + @Value("${web.security.auth.jwt.secretkey:#{null}}") + private String jwtSecretKey = DEFAULT_JWT_SECRET_KEY; + + private List userList; + + private List adminList; + + public List getUserList() { + return userList; + } + + public List getAdminList() { + return adminList; + } + + public String getJwtSecretKey() { + return jwtSecretKey; + } + + public int getExpirationTimeSeconds() { + return DEFAULT_EXPIRATION_TIME_SECONDS; + } + + @PostConstruct + public void setup() { + List userList = createUser(userIdAndPasswordPairList); + this.userList = userList; + + List adminList = createAdmin(adminIdAndPasswordPairList); + this.adminList = adminList; + + if (StringUtils.isEmpty(jwtSecretKey)) { + throw new IllegalArgumentException("'jwtSecretKey' may not be empty"); + } + } + + private List createUser(List idAndPasswordList) { + List users = createUserDetails(idAndPasswordList, "ROLE_USER"); + return users; + } + + private List createAdmin(List idAndPasswordList) { + List users = createUserDetails(idAndPasswordList, "ROLE_ADMIN"); + return users; + } + + private List createUserDetails(List idAndPasswordList, String role) { + if (CollectionUtils.isEmpty(idAndPasswordList)) { + return Collections.emptyList(); + } + + List users = new ArrayList<>(idAndPasswordList.size()); + + for (String idAndPassword : idAndPasswordList) { + List tokenize = StringUtils.tokenizeToStringList(idAndPassword, ":"); + if (CollectionUtils.nullSafeSize(tokenize) == 2) { + String id = tokenize.get(0); + String password = tokenize.get(1); + + User role_user = new User(id, passwordEncoder.encode(password), Arrays.asList(new SimpleGrantedAuthority(role))); + users.add(role_user); + } + } + return users; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("BasicLoginConfig{"); + sb.append("userList=").append(userList); + sb.append(", adminList=").append(adminList); + sb.append('}'); + return sb.toString(); + } + +} \ No newline at end of file diff --git a/web/src/main/java/com/navercorp/pinpoint/web/security/login/BasicLoginConstants.java b/web/src/main/java/com/navercorp/pinpoint/web/security/login/BasicLoginConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..b69855166dfed0c29130b438bd39fb885b9b2dea --- /dev/null +++ b/web/src/main/java/com/navercorp/pinpoint/web/security/login/BasicLoginConstants.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021 NAVER Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.navercorp.pinpoint.web.security.login; + +/** + * @author Taejin Koo + */ +public final class BasicLoginConstants { + + public static final String URI_MAIN = "/"; + + public static final String URI_LOGIN = "/login"; + + public static final String URI_NOT_AUTHORIZED = "/not_authorized.html"; + + public static final String PINPOINT_JWT_COOKIE_NAME = "pinpointJwt"; + +} \ No newline at end of file diff --git a/web/src/main/java/com/navercorp/pinpoint/web/security/login/BasicLoginService.java b/web/src/main/java/com/navercorp/pinpoint/web/security/login/BasicLoginService.java new file mode 100644 index 0000000000000000000000000000000000000000..2e283e5e4348d6e2c2916c1e4f8a9ee4679adf13 --- /dev/null +++ b/web/src/main/java/com/navercorp/pinpoint/web/security/login/BasicLoginService.java @@ -0,0 +1,106 @@ +/* + * Copyright 2021 NAVER Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.navercorp.pinpoint.web.security.login; + +import com.navercorp.pinpoint.web.config.BasicLoginConfig; + +import io.jsonwebtoken.ExpiredJwtException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Service; + +import javax.servlet.http.Cookie; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +/** + * @author Taejin Koo + */ +@Service +@Profile("basicLogin") +public class BasicLoginService { + + private final PinpointMemoryUserDetailsService pinpointMemoryUserDetailsService; + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + private final JwtService jwtService; + + @Autowired + public BasicLoginService(BasicLoginConfig basicLoginConfig) { + PinpointMemoryUserDetailsService pinpointMemoryUserDetailsService = new PinpointMemoryUserDetailsService(basicLoginConfig); + this.pinpointMemoryUserDetailsService = pinpointMemoryUserDetailsService; + + this.jwtService = new JwtService(basicLoginConfig); + } + + public UserDetails getUserDetails(Cookie[] cookies) { + if (cookies == null) { + return null; + } + + for (Cookie cookie : cookies) { + String name = cookie.getName(); + + if (BasicLoginConstants.PINPOINT_JWT_COOKIE_NAME.equals(name)) { + String pinpointJwtToken = cookie.getValue(); + + try { + Date expirationDate = jwtService.getExpirationDate(pinpointJwtToken); + if (expirationDate.getTime() > System.currentTimeMillis()) { + String userId = jwtService.getUserId(pinpointJwtToken); + + UserDetails userDetails = pinpointMemoryUserDetailsService.loadUserByUsername(String.valueOf(userId)); + if (userDetails != null) { + return userDetails; + } + } else { + logger.warn("This token already expired."); + } + } catch (ExpiredJwtException e) { + logger.warn("This token already expired. message:{}", e.getMessage(), e); + } + } + } + + return null; + } + + public Cookie createNewCookie(String userId) { + UserDetails userDetails = pinpointMemoryUserDetailsService.loadUserByUsername(userId); + if (userDetails == null) { + throw new IllegalArgumentException("Could not load User information."); + } + + String token = jwtService.createToken(userDetails); + Cookie cookie = new Cookie(BasicLoginConstants.PINPOINT_JWT_COOKIE_NAME, token); + cookie.setPath("/"); + + long maxAge = TimeUnit.MILLISECONDS.toSeconds(jwtService.getExpirationTimeMillis()); + cookie.setMaxAge(new Long(maxAge).intValue()); + return cookie; + } + + public UserDetailsService getUserDetailsService() { + return pinpointMemoryUserDetailsService; + } + +} \ No newline at end of file diff --git a/web/src/main/java/com/navercorp/pinpoint/web/security/login/JwtRequestFilter.java b/web/src/main/java/com/navercorp/pinpoint/web/security/login/JwtRequestFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..a5273f8e43568793cc779c01e86ae552017c4d56 --- /dev/null +++ b/web/src/main/java/com/navercorp/pinpoint/web/security/login/JwtRequestFilter.java @@ -0,0 +1,73 @@ +/* + * Copyright 2021 NAVER Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.navercorp.pinpoint.web.security.login; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; + +/** + * @author Taejin Koo + */ +public class JwtRequestFilter extends OncePerRequestFilter { + + private final BasicLoginService basicLoginService; + + private final WebAuthenticationDetailsSource webAuthenticationDetailsSource = new WebAuthenticationDetailsSource(); + + public JwtRequestFilter(BasicLoginService basicLoginService) { + this.basicLoginService = Objects.requireNonNull(basicLoginService, "basicLoginService"); + } + + @Override + protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { + Cookie[] cookies = httpServletRequest.getCookies(); + if (cookies == null) { + filterChain.doFilter(httpServletRequest, httpServletResponse); + return; + } + + UserDetails userDetails = basicLoginService.getUserDetails(cookies); + if (userDetails == null) { + filterChain.doFilter(httpServletRequest, httpServletResponse); + return; + } + + SecurityContext securityContext = SecurityContextHolder.getContext(); + if (securityContext != null && securityContext.getAuthentication() == null) { + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + usernamePasswordAuthenticationToken + .setDetails(webAuthenticationDetailsSource.buildDetails(httpServletRequest)); + securityContext.setAuthentication(usernamePasswordAuthenticationToken); + } + + filterChain.doFilter(httpServletRequest, httpServletResponse); + } + +} \ No newline at end of file diff --git a/web/src/main/java/com/navercorp/pinpoint/web/security/login/JwtService.java b/web/src/main/java/com/navercorp/pinpoint/web/security/login/JwtService.java new file mode 100644 index 0000000000000000000000000000000000000000..dda93d883c85e6a74cc892c60d06b6f08d190e7e --- /dev/null +++ b/web/src/main/java/com/navercorp/pinpoint/web/security/login/JwtService.java @@ -0,0 +1,112 @@ +/* + * Copyright 2021 NAVER Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.navercorp.pinpoint.web.security.login; + +import com.navercorp.pinpoint.common.util.Assert; +import com.navercorp.pinpoint.common.util.StringUtils; +import com.navercorp.pinpoint.web.config.BasicLoginConfig; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @author Taejin Koo + */ +public class JwtService { + + private final String KEY_CLAIMS_USER_ID = "userId"; + private final String KEY_CLAIMS_USER_ROLE = "userRole"; + + private final JwtParser jwtParser; + + private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; + + private final String secretKey; + + private final long expirationTimeMillis; + + public JwtService(BasicLoginConfig basicLoginConfig) { + String secretKey = basicLoginConfig.getJwtSecretKey(); + if (StringUtils.isEmpty(secretKey)) { + throw new IllegalArgumentException("secretKey may not be empty"); + } + this.secretKey = secretKey; + + Assert.isTrue(basicLoginConfig.getExpirationTimeSeconds() > 0, "expirationTimeSeconds must be '>= 0'"); + this.expirationTimeMillis = TimeUnit.SECONDS.toMillis(basicLoginConfig.getExpirationTimeSeconds()); + + JwtParser jwtParser = Jwts.parser(); + jwtParser.setSigningKey(secretKey); + this.jwtParser = jwtParser; + } + + public String createToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + + Collection authorities = userDetails.getAuthorities(); + + List collect = authorities.stream().map(e -> ((GrantedAuthority) e).getAuthority()).collect(Collectors.toList()); + + claims.put(KEY_CLAIMS_USER_ID, userDetails.getUsername()); + claims.put(KEY_CLAIMS_USER_ROLE, collect); + + return createToken(claims); + } + + private String createToken(Map claims) { + JwtBuilder jwtBuilder = Jwts.builder(); + + jwtBuilder.setClaims(claims); + jwtBuilder.setIssuedAt(new Date(System.currentTimeMillis())); + jwtBuilder.setExpiration(new Date(System.currentTimeMillis() + expirationTimeMillis)); + jwtBuilder.signWith(signatureAlgorithm, secretKey); + + return jwtBuilder.compact(); + } + + public String getUserId(String token) { + Jws claimsJws = jwtParser.parseClaimsJws(token); + Claims body = claimsJws.getBody(); + + return body.get(KEY_CLAIMS_USER_ID, String.class); + } + + public Date getExpirationDate(String token) { + Jws claimsJws = jwtParser.parseClaimsJws(token); + Claims body = claimsJws.getBody(); + return body.getExpiration(); + } + + public long getExpirationTimeMillis() { + return expirationTimeMillis; + } + +} \ No newline at end of file diff --git a/web/src/main/java/com/navercorp/pinpoint/web/security/login/PinpointMemoryUserDetailsService.java b/web/src/main/java/com/navercorp/pinpoint/web/security/login/PinpointMemoryUserDetailsService.java new file mode 100644 index 0000000000000000000000000000000000000000..55addade3194e5a3c7e12940ac74c8dd2ec2bdaf --- /dev/null +++ b/web/src/main/java/com/navercorp/pinpoint/web/security/login/PinpointMemoryUserDetailsService.java @@ -0,0 +1,81 @@ +/* + * Copyright 2021 NAVER Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.navercorp.pinpoint.web.security.login; + +import com.navercorp.pinpoint.web.config.BasicLoginConfig; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Taejin Koo + */ +@Service +@Profile("basicLogin") +public class PinpointMemoryUserDetailsService implements UserDetailsService { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + private final Map userDetailsMap; + + @Autowired + public PinpointMemoryUserDetailsService(BasicLoginConfig basicLoginConfig) { + Map userDetailsMap = new HashMap<>(); + + final List userList = basicLoginConfig.getUserList(); + + for (UserDetails user : userList) { + userDetailsMap.put(user.getUsername(), user); + } + + if (logger.isDebugEnabled()) { + Collection userRoleUserNameList = userList.stream().map(user -> user.getUsername()).collect(Collectors.toList()); + logger.debug("Has been registered {} that has USER role.", userRoleUserNameList); + } + + List adminList = basicLoginConfig.getAdminList(); + for (UserDetails admin : adminList) { + userDetailsMap.put(admin.getUsername(), admin); + } + + if (logger.isDebugEnabled()) { + Collection adminRoleUserNameList = adminList.stream().map(user -> user.getUsername()).collect(Collectors.toList()); + logger.debug("Has been registered {} that has ADMIN role.", adminRoleUserNameList); + } + + this.userDetailsMap = Collections.unmodifiableMap(userDetailsMap); + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return userDetailsMap.get(username); + } + +} \ No newline at end of file diff --git a/web/src/main/java/com/navercorp/pinpoint/web/security/login/PreAuthenticationCheckFilter.java b/web/src/main/java/com/navercorp/pinpoint/web/security/login/PreAuthenticationCheckFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..aba76fb9bc38e9aaa1cbac0b08cf87ba4aa25cf1 --- /dev/null +++ b/web/src/main/java/com/navercorp/pinpoint/web/security/login/PreAuthenticationCheckFilter.java @@ -0,0 +1,63 @@ +/* + * Copyright 2021 NAVER Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.navercorp.pinpoint.web.security.login; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author Taejin Koo + */ +public class PreAuthenticationCheckFilter extends GenericFilterBean { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { + chain.doFilter(request, response); + return; + } + + SecurityContext securityContext = SecurityContextHolder.getContext(); + if (securityContext == null) { + chain.doFilter(request, response); + return; + } + + Authentication authentication = securityContext.getAuthentication(); + if (authentication == null) { + chain.doFilter(request, response); + return; + } + + if (authentication.isAuthenticated() && ((HttpServletRequest) request).getRequestURI().equals(BasicLoginConstants.URI_LOGIN)) { + ((HttpServletResponse) response).sendRedirect(BasicLoginConstants.URI_MAIN); + } else { + chain.doFilter(request, response); + } + } + +} \ No newline at end of file diff --git a/web/src/main/java/com/navercorp/pinpoint/web/security/login/SaveJwtTokenAuthenticationSuccessHandler.java b/web/src/main/java/com/navercorp/pinpoint/web/security/login/SaveJwtTokenAuthenticationSuccessHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..e75a8900976b61aff78a8a14038e5e7425b8aeac --- /dev/null +++ b/web/src/main/java/com/navercorp/pinpoint/web/security/login/SaveJwtTokenAuthenticationSuccessHandler.java @@ -0,0 +1,63 @@ +/* + * Copyright 2021 NAVER Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.navercorp.pinpoint.web.security.login; + +import com.navercorp.pinpoint.common.util.Assert; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author Taejin Koo + */ +public class SaveJwtTokenAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + private final BasicLoginService basicLoginService; + + public SaveJwtTokenAuthenticationSuccessHandler(BasicLoginService basicLoginService) { + this.basicLoginService = Assert.requireNonNull(basicLoginService, "basicLoginService"); + } + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + logger.debug("onAuthenticationSuccess() started"); + + String userId = authentication.getName(); + + Cookie newCookie = basicLoginService.createNewCookie(userId); + response.addCookie(newCookie); + + response.setStatus(HttpStatus.OK.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + + response.sendRedirect(BasicLoginConstants.URI_MAIN); + return; + } + +} \ No newline at end of file diff --git a/web/src/main/resources/applicationContext-web.xml b/web/src/main/resources/applicationContext-web.xml index 46d165f115051817e6cab0e05104809f03ddb4dd..f1620537cd263b3e8f0ebfbba82b6362404c0928 100644 --- a/web/src/main/resources/applicationContext-web.xml +++ b/web/src/main/resources/applicationContext-web.xml @@ -201,4 +201,10 @@ + + + + + + diff --git a/web/src/main/resources/pinpoint-web-root.properties b/web/src/main/resources/pinpoint-web-root.properties index 8db99d7d2edaea39bfad0ef3a30d3586961d891d..badbec34369cc66999a8259c08a27f2f17d2d3d7 100644 --- a/web/src/main/resources/pinpoint-web-root.properties +++ b/web/src/main/resources/pinpoint-web-root.properties @@ -84,4 +84,10 @@ websocket.allowedOrigins= # If you have own downloadUrl information, please include the pinpoint version and downloadUrl information. # default value is the github's pinpoint page. web.installation.pinpointVersion= -web.installation.downloadUrl= \ No newline at end of file +web.installation.downloadUrl= + +# Declares user:password in pinpoint-web.properties. (You can declare multiple people using,.) +# Role (User : Can use whole function except for admin rest api, Admin : Can use whole function) +#web.security.auth.user=alice:foo, bob:bar +#web.security.auth.admin=eve:baz +#web.security.auth.jwt.secretkey=PINPOINT_JWT_SECRET \ No newline at end of file