diff --git a/.gitignore b/.gitignore index 14e70ad..57cd48d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ /frontend/src/.DS_Store /frontend/src/assets/.DS_Store /backend/MixewayFlowAPI.iml -/frontend/src/environments \ No newline at end of file +frontend/src/environments \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d210864..8a6cfd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## [0.9.1] - 2024-08-263 +### Changed +- SSO integration introduced +- Adjusted scripts to support SSO +- Increased efficiency of running scans in parallel + ## [0.9.0] - 2024-08-13 ### Changed - Release of initial version - beta diff --git a/backend/README.md b/backend/README.md index a573037..acd1e34 100644 --- a/backend/README.md +++ b/backend/README.md @@ -11,3 +11,9 @@ ## First login `admin:admin` - then forced change + + +### debug postgresql +```shell +docker run --name flow_db -e POSTGRES_PASSWORD=flow_pass -e POSTGRES_USER=flow_user -e POSTGRES_DB=flow -p 5433:5432 -v pgdata:/var/lib/postgresql/data -d postgres +``` \ No newline at end of file diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index e3ca3a3..abe7a79 100644 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -1,5 +1,22 @@ #!/bin/bash +# Ensure required environment variables are set when SSO is enabled +if [ "$(echo $SSO | tr '[:upper:]' '[:lower:]')" = "true" ]; then + : "${SSO_CLIENT_ID:?SSO_CLIENT_ID is required when SSO is true}" + : "${SSO_CLIENT_SECRET:?SSO_CLIENT_SECRET is required when SSO is true}" + : "${SSO_REDIRECT_URI:?SSO_REDIRECT_URI is required when SSO is true}" + : "${SSO_AUTHORIZATION_URI:?SSO_AUTHORIZATION_URI is required when SSO is true}" + : "${SSO_TOKEN_URI:?SSO_TOKEN_URI is required when SSO is true}" + : "${SSO_USER_INFO_URI:?SSO_USER_INFO_URI is required when SSO is true}" + : "${SSO_JWK_SET_URI:?SSO_JWK_SET_URI is required when SSO is true}" + + # Set the active profile to prodsso + SPRING_PROFILE="prodsso" +else + # Set the default profile + SPRING_PROFILE="prod" +fi + # Start Dependency-Track in the background with 4GB of memory and log output to a file LOG_FILE="/var/log/dtrack.log" echo "Starting Dependency-Track..." @@ -72,11 +89,11 @@ if [ "$(echo $SSL | tr '[:upper:]' '[:lower:]')" = "true" ]; then git config --global https.proxy http://$PROXY_HOST:$PROXY_PORT fi - echo "Proceeding to run the application with SSL..." + echo "Proceeding to run the application with SSL and profile $SPRING_PROFILE..." if [ -n "$PROXY_HOST" ] && [ -n "$PROXY_PORT" ]; then - java -Dspring.profiles.active=prod -Dserver.ssl.key-store=/etc/pki/certificate.p12 -Dserver.ssl.key-store-password=$P12PASS -Dserver.ssl.key-alias=flow -Dproxy.host=$PROXY_HOST -Dproxy.port=$PROXY_PORT -jar /app/flowapi.jar + java -Dspring.profiles.active=$SPRING_PROFILE -Dserver.ssl.key-store=/etc/pki/certificate.p12 -Dserver.ssl.key-store-password=$P12PASS -Dserver.ssl.key-alias=flow -Dproxy.host=$PROXY_HOST -Dproxy.port=$PROXY_PORT -jar /app/flowapi.jar else - java -Dspring.profiles.active=prod -Dserver.ssl.key-store=/etc/pki/certificate.p12 -Dserver.ssl.key-store-password=$P12PASS -Dserver.ssl.key-alias=flow -jar /app/flowapi.jar + java -Dspring.profiles.active=$SPRING_PROFILE -Dserver.ssl.key-store=/etc/pki/certificate.p12 -Dserver.ssl.key-store-password=$P12PASS -Dserver.ssl.key-alias=flow -jar /app/flowapi.jar fi else echo "SSL is not enabled. Running the application without SSL..." diff --git a/backend/pom.xml b/backend/pom.xml index 734767c..d8d3028 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -131,6 +131,16 @@ junit test + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.keycloak + keycloak-spring-boot-starter + 24.0.3 + diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/api/auth/controller/AuthController.java b/backend/src/main/java/io/mixeway/mixewayflowapi/api/auth/controller/AuthController.java index 1079bf5..27ac123 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/api/auth/controller/AuthController.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/api/auth/controller/AuthController.java @@ -15,6 +15,8 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.springframework.context.annotation.Profile; +import org.springframework.core.env.Environment; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -24,12 +26,14 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import java.io.IOException; import java.security.Principal; @RestController @@ -41,7 +45,7 @@ public class AuthController { private final JwtService jwtService; private final AuthService authService; private final FindUserService findUserService; - + private final Environment environment; @PostMapping("/api/v1/login") public ResponseEntity login(@Valid @RequestBody AuthRequestDTO authRequestDTO, HttpServletRequest request, HttpServletResponse response) { @@ -95,13 +99,29 @@ public ResponseEntity changePassword(@Valid @RequestBody PassRequestD @PreAuthorize("hasAuthority('USER')") @GetMapping("/api/v1/hc") - public ResponseEntity hc() { + public ResponseEntity hc(Principal principal) { + UserInfo userInfo = findUserService.findUser(principal.getName()); try { - return new ResponseEntity<>("", HttpStatus.OK); + return new ResponseEntity<>(new StatusDTO(userInfo.getHighestRole()), HttpStatus.OK); + } catch (Exception e){ + throw new RuntimeException(e); + } + } + + @GetMapping("/api/v1/status") + public ResponseEntity status() { + String[] activeProfiles = environment.getActiveProfiles(); + String profile = ""; + if (activeProfiles.length > 0) { + profile = activeProfiles[0]; + } + try { + return new ResponseEntity<>(new StatusDTO(profile), HttpStatus.OK); } catch (Exception e){ throw new RuntimeException(e); } } + @PreAuthorize("hasAuthority('ADMIN')") @GetMapping("/api/v1/hc/admin") public ResponseEntity hcAdmin() { diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/api/auth/service/CustomOAuth2UserService.java b/backend/src/main/java/io/mixeway/mixewayflowapi/api/auth/service/CustomOAuth2UserService.java new file mode 100644 index 0000000..384ba87 --- /dev/null +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/api/auth/service/CustomOAuth2UserService.java @@ -0,0 +1,43 @@ +package io.mixeway.mixewayflowapi.api.auth.service; + +import lombok.extern.log4j.Log4j2; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Map; + +@Service +@Log4j2 +public class CustomOAuth2UserService implements OAuth2UserService { + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + log.error("XAXAXAXAXAXAXAXAXAXA"); + OAuth2UserService delegate = new DefaultOAuth2UserService(); + OAuth2User oAuth2User = delegate.loadUser(userRequest); + + // Extracting ID Token and User Info attributes + Map attributes = oAuth2User.getAttributes(); + OidcIdToken idToken = new OidcIdToken( + userRequest.getAccessToken().getTokenValue(), + userRequest.getAccessToken().getIssuedAt(), + userRequest.getAccessToken().getExpiresAt(), + attributes + ); + OidcUserInfo userInfo = new OidcUserInfo(attributes); + + // Creating OidcUser using OidcUserAuthority + OidcUserAuthority authority = new OidcUserAuthority(idToken, userInfo); + return new DefaultOidcUser(Collections.singleton(authority), idToken, userInfo); + } +} diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/api/team/controller/TeamController.java b/backend/src/main/java/io/mixeway/mixewayflowapi/api/team/controller/TeamController.java index ef10867..7be5c38 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/api/team/controller/TeamController.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/api/team/controller/TeamController.java @@ -43,7 +43,7 @@ public ResponseEntity createTeam(@Valid @RequestBody CreateTeamReques } } - @PreAuthorize("hasAuthority('TEAM_MANAGER')") + @PreAuthorize("hasAuthority('USER')") @GetMapping(value= "/api/v1/team") public ResponseEntity> getTeams(Principal principal){ try { diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/auth/CustomAuthenticationEntryPoint.java b/backend/src/main/java/io/mixeway/mixewayflowapi/auth/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..7f32a29 --- /dev/null +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/auth/CustomAuthenticationEntryPoint.java @@ -0,0 +1,22 @@ +package io.mixeway.mixewayflowapi.auth; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + if (request.getServletPath().startsWith("/api/")) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied"); + } else { + response.sendRedirect("/oauth2/authorization/sso"); + } + } +} diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/auth/OAuth2LoginSuccessHandler.java b/backend/src/main/java/io/mixeway/mixewayflowapi/auth/OAuth2LoginSuccessHandler.java new file mode 100644 index 0000000..e87bb06 --- /dev/null +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/auth/OAuth2LoginSuccessHandler.java @@ -0,0 +1,61 @@ +package io.mixeway.mixewayflowapi.auth; + +import io.mixeway.mixewayflowapi.api.user.dto.CreateUserRequestDto; +import io.mixeway.mixewayflowapi.auth.jwt.JwtService; +import io.mixeway.mixewayflowapi.db.entity.UserInfo; +import io.mixeway.mixewayflowapi.domain.user.CreateUserService; +import io.mixeway.mixewayflowapi.domain.user.FindUserService; +import io.mixeway.mixewayflowapi.utils.Role; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.ArrayList; + +@Component +@RequiredArgsConstructor +@Log4j2 +public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler { + + private final JwtService jwtService; + private final CreateUserService createUserService; + private final FindUserService findUserService; + @Value("${frontend.url}") + String frontendUrl; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + OidcUser oidcUser = (OidcUser) authentication.getPrincipal(); + String username = oidcUser.getPreferredUsername(); // Adjust based on your Keycloak config + + UserInfo userInfo = findUserService.findUser(username); + if (userInfo == null){ + userInfo = createUserService.createUser(CreateUserRequestDto.of(username, Role.USER, "xxxxxxxxxxxx", new ArrayList<>())); + } + String jwtToken = jwtService.GenerateToken(userInfo.getUsername(), userInfo.getHighestRole()); // Replace "USER_ROLE" with actual role logic + SecurityContextHolder.getContext().setAuthentication(authentication); + // Set the JWT token in an HTTP-only and secure cookie + Cookie cookie = new Cookie("flow-token", jwtToken); + cookie.setHttpOnly(true); + cookie.setSecure(request.isSecure()); + cookie.setPath("/"); + cookie.setMaxAge(7 * 24 * 60 * 60); + + response.addCookie(cookie); + + if (frontendUrl == null) { + throw new IllegalStateException("FRONTEND_URL environment variable must be set when SSO is enabled"); + } + response.sendRedirect(frontendUrl); + } +} diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/auth/jwt/JwtAuthFilter.java b/backend/src/main/java/io/mixeway/mixewayflowapi/auth/jwt/JwtAuthFilter.java index 839240e..0dcedfb 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/auth/jwt/JwtAuthFilter.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/auth/jwt/JwtAuthFilter.java @@ -12,9 +12,11 @@ import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.jwt.JwtValidationException; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; @@ -32,6 +34,8 @@ public class JwtAuthFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + log.debug("JwtAuthFilter: Processing request " + request.getRequestURI()); + String token = null; String username = null; @@ -79,4 +83,13 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse filterChain.doFilter(request, response); } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String path = request.getRequestURI(); + return path.startsWith("/api/v1/sso") || + path.startsWith("/oauth2/") || + path.startsWith("/api/v1/webhook/") || + path.startsWith("/api/v1/status"); + } } diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/config/BCryptEncoderConfig.java b/backend/src/main/java/io/mixeway/mixewayflowapi/config/BCryptEncoderConfig.java new file mode 100644 index 0000000..1e4d4c2 --- /dev/null +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/config/BCryptEncoderConfig.java @@ -0,0 +1,13 @@ +package io.mixeway.mixewayflowapi.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +@Configuration +public class BCryptEncoderConfig { + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/config/Oauth2SecurityConfig.java b/backend/src/main/java/io/mixeway/mixewayflowapi/config/Oauth2SecurityConfig.java new file mode 100644 index 0000000..6dd3ea5 --- /dev/null +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/config/Oauth2SecurityConfig.java @@ -0,0 +1,71 @@ +package io.mixeway.mixewayflowapi.config; + +import io.mixeway.mixewayflowapi.auth.CustomAuthenticationEntryPoint; +import io.mixeway.mixewayflowapi.auth.OAuth2LoginSuccessHandler; +import io.mixeway.mixewayflowapi.auth.jwt.JwtAuthFilter; +import lombok.RequiredArgsConstructor; +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.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +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.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +@Profile("devsso | prodsso") +@RequiredArgsConstructor +public class Oauth2SecurityConfig { + + private final JwtAuthFilter jwtAuthFilter; + private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler; + private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + + + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .authorizeHttpRequests(authorizeRequests -> + authorizeRequests + .requestMatchers("/api/v1/status").permitAll() + .requestMatchers("/api/v1/webhook/**").permitAll() // Public webhook endpoint + .requestMatchers("/api/v1/sso").permitAll() // Ensure the SSO endpoint is public + ) // Ensure the SSO endpoint is public + .exceptionHandling((exception)-> exception.authenticationEntryPoint(new Http403ForbiddenEntryPoint())) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(authorizeRequests -> + authorizeRequests + .requestMatchers("/api/**").authenticated() // All /api/** endpoints require authentication + .anyRequest().authenticated() // Any other request requires authentication + ) + .oauth2Login(oauth2 -> oauth2 + .redirectionEndpoint(redirectionEndpoint -> + redirectionEndpoint.baseUri("/api/v1/sso") + ) + .successHandler(oAuth2LoginSuccessHandler)) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthFilter, OAuth2LoginAuthenticationFilter.class) + .build(); + } + + + @Bean + public OidcUserService oidcUserService() { + return new OidcUserService(); + } + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } +} diff --git a/backend/src/main/java/io/mixeway/mixewayflowapi/config/SecurityConfig.java b/backend/src/main/java/io/mixeway/mixewayflowapi/config/SecurityConfig.java index 6a99a71..9e98c2a 100644 --- a/backend/src/main/java/io/mixeway/mixewayflowapi/config/SecurityConfig.java +++ b/backend/src/main/java/io/mixeway/mixewayflowapi/config/SecurityConfig.java @@ -2,9 +2,11 @@ import io.mixeway.mixewayflowapi.auth.UserDetailsServiceImpl; import io.mixeway.mixewayflowapi.auth.jwt.JwtAuthFilter; +import lombok.RequiredArgsConstructor; 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.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; @@ -28,10 +30,12 @@ @Configuration @EnableWebSecurity @EnableMethodSecurity +@Profile("!prodsso & !devsso") +@RequiredArgsConstructor public class SecurityConfig { - @Autowired - JwtAuthFilter jwtAuthFilter; + private final JwtAuthFilter jwtAuthFilter; + private final BCryptPasswordEncoder bCryptPasswordEncoder; @Bean public UserDetailsService userDetailsService() { @@ -44,6 +48,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(authorize -> authorize .requestMatchers("/api/v1/login").permitAll() + .requestMatchers("/api/v1/status").permitAll() // Ensure the SSO endpoint is public .requestMatchers("/api/v1/webhook/gitlab/push").permitAll() .requestMatchers("/api/v1/webhook/gitlab/merge").permitAll() .anyRequest().authenticated()) @@ -54,17 +59,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .build(); } - - @Bean - public BCryptPasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(userDetailsService()); - authenticationProvider.setPasswordEncoder(passwordEncoder()); + authenticationProvider.setPasswordEncoder(bCryptPasswordEncoder); return authenticationProvider; } @@ -73,16 +72,5 @@ public AuthenticationProvider authenticationProvider() { public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } -// @Bean -// public CorsConfigurationSource corsConfigurationSource() { -// CorsConfiguration configuration = new CorsConfiguration(); -// configuration.setAllowedOrigins(Arrays.asList("*")); -// configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); -// configuration.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type")); -// configuration.setAllowCredentials(true); -// -// UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); -// source.registerCorsConfiguration("/**", configuration); -// return source; -// } + } diff --git a/backend/src/main/resources/application-devsso.properties b/backend/src/main/resources/application-devsso.properties new file mode 100644 index 0000000..3b5b92a --- /dev/null +++ b/backend/src/main/resources/application-devsso.properties @@ -0,0 +1,41 @@ +spring.datasource.url=jdbc:postgresql://flowdb:5432/flow +spring.datasource.username=flow_user +spring.datasource.password=flow_pass +spring.datasource.driver-class-name=org.postgresql.Driver + + +server.port=8888 +# Hibernate settings +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=update + + +# connection timeout +spring.datasource.hikari.connection-timeout=20000 +# min idle connections +spring.datasource.hikari.minimum-idle=5 +# max pool size +spring.datasource.hikari.maximum-pool-size=12 +spring.datasource.hikari.idle-timeout=300000 +spring.datasource.hikari.max-lifetime=1200000 +spring.datasource.hikari.auto-commit=true +#logging.level.root = DEBUG + +spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.sql + + +kics.queries.dir=/opt/tools/kics/assets/queries +bearer.queries.dir= +dependency-track.url=http://127.0.0.1:8080 +spring.jackson.write-nesting-depth=2000 + +spring.security.oauth2.client.registration.sso.client-id=flow +spring.security.oauth2.client.registration.sso.client-secret=ZPTAyJ053tdwqN2hzGCzSJtRD34vomVY +spring.security.oauth2.client.registration.sso.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.sso.redirect-uri=http://localhost:8888/api/v1/sso +spring.security.oauth2.client.registration.sso.scope=openid,profile,email +spring.security.oauth2.client.provider.sso.authorization-uri=http://localhost:8080/realms/test/protocol/openid-connect/auth +spring.security.oauth2.client.provider.sso.token-uri=http://localhost:8080/realms/test/protocol/openid-connect/token +spring.security.oauth2.client.provider.sso.user-info-uri=http://localhost:8080/realms/test/protocol/openid-connect/userinfo +spring.security.oauth2.client.provider.sso.jwk-set-uri=http://localhost:8080/realms/test/protocol/openid-connect/certs +spring.security.oauth2.client.provider.sso.user-name-attribute=preferred_username diff --git a/backend/src/main/resources/application-prodsso.properties b/backend/src/main/resources/application-prodsso.properties new file mode 100644 index 0000000..bf5fbf3 --- /dev/null +++ b/backend/src/main/resources/application-prodsso.properties @@ -0,0 +1,53 @@ +spring.datasource.url=jdbc:postgresql://flowdb:5432/flow +spring.datasource.username=flow_user +spring.datasource.password=flow_pass +spring.datasource.driver-class-name=org.postgresql.Driver + +# Production environment configuration +server.port=8443 +server.ssl.enabled=true +server.ssl.key-store-type=PKCS12 +server.ssl.key-store=/etc/pki/certificate.p12 +server.ssl.key-store-password=${P12PASS} +server.ssl.key-alias=flow + +# Hibernate settings +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=update + + +# connection timeout +spring.datasource.hikari.connection-timeout=20000 +# min idle connections +spring.datasource.hikari.minimum-idle=5 +# max pool size +spring.datasource.hikari.maximum-pool-size=12 +spring.datasource.hikari.idle-timeout=300000 +spring.datasource.hikari.max-lifetime=1200000 +spring.datasource.hikari.auto-commit=true +#logging.level.root = DEBUG + +spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.sql + + +kics.queries.dir=/opt/tools/kics/assets/queries +bearer.queries.dir= +dependency-track.url=http://127.0.0.1:8080 +spring.jackson.write-nesting-depth=2000 + +frontend.url=${FRONTEND_URL} + +# OAuth2 Configuration - SSO +spring.security.oauth2.client.registration.sso.client-id=${SSO_CLIENT_ID} +spring.security.oauth2.client.registration.sso.client-secret=${SSO_CLIENT_SECRET} +spring.security.oauth2.client.registration.sso.redirect-uri=${SSO_REDIRECT_URI} +spring.security.oauth2.client.provider.sso.authorization-uri=${SSO_AUTHORIZATION_URI} +spring.security.oauth2.client.provider.sso.token-uri=${SSO_TOKEN_URI} +spring.security.oauth2.client.provider.sso.user-info-uri=${SSO_USER_INFO_URI} +spring.security.oauth2.client.provider.sso.jwk-set-uri=${SSO_JWK_SET_URI} + +# Constant OAuth2 Properties +spring.security.oauth2.client.provider.sso.user-name-attribute=preferred_username +spring.security.oauth2.client.registration.sso.scope=openid,profile,email +spring.security.oauth2.client.registration.sso.authorization-grant-type=authorization_code + diff --git a/dev/Dockerfile b/dev/Dockerfile new file mode 100644 index 0000000..d30cc32 --- /dev/null +++ b/dev/Dockerfile @@ -0,0 +1,8 @@ +# Use the official Nginx image from the Docker Hub +FROM nginx:alpine + +# Copy the custom Nginx configuration file to the container +COPY nginx-dev.conf /etc/nginx/nginx.conf + +# Expose port 80 to the host +EXPOSE 80 \ No newline at end of file diff --git a/dev/nginx-dev.conf b/dev/nginx-dev.conf new file mode 100644 index 0000000..f3f5969 --- /dev/null +++ b/dev/nginx-dev.conf @@ -0,0 +1,34 @@ +events { } +http { + server { + listen 80; + + server_name localhost; + + # Serve Angular frontend + location / { + proxy_pass http://host.docker.internal:4200; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Proxy requests to Spring Boot backend + location /api/ { + proxy_pass http://host.docker.internal:8888/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /oauth2/ { + proxy_pass http://host.docker.internal:8888/oauth2/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 4eb58df..8e55789 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,16 @@ services: - "8443:8443" environment: SSL: "TRUE" + # Uncomment and set these when using SSO + # SSO: "TRUE" + # SSO_CLIENT_ID: your_client_id + # SSO_CLIENT_SECRET: your_client_secret + # SSO_REDIRECT_URI: http://your-redirect-uri + # SSO_AUTHORIZATION_URI: http://your-authorization-uri + # SSO_TOKEN_URI: http://your-token-uri + # SSO_USER_INFO_URI: http://your-user-info-uri + # SSO_JWK_SET_URI: http://your-jwk-set-uri + # FRONTEND_URL: http://your-frontend-url volumes: - pki_data:/etc/pki - dependency_track_data:/root/.dependency-track diff --git a/frontend/nginx/nginx.conf b/frontend/nginx/nginx.conf index 00f61b3..0274efc 100644 --- a/frontend/nginx/nginx.conf +++ b/frontend/nginx/nginx.conf @@ -33,4 +33,15 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } + location /oauth2/ { + proxy_pass https://backend:8443; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_http_version 1.1; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } } diff --git a/frontend/src/app/service/AuthService.ts b/frontend/src/app/service/AuthService.ts index d5804e8..bce0bdd 100644 --- a/frontend/src/app/service/AuthService.ts +++ b/frontend/src/app/service/AuthService.ts @@ -17,6 +17,9 @@ export class AuthService { hc(): Observable { return this.http.get(this.loginUrl + '/api/v1/hc',{ withCredentials: true }); } + status(): Observable { + return this.http.get(this.loginUrl + '/api/v1/status'); + } hcAdmin(): Observable { return this.http.get(this.loginUrl + '/api/v1/hc/admin',{ withCredentials: true }); } diff --git a/frontend/src/app/views/dashboard/dashboard.component.ts b/frontend/src/app/views/dashboard/dashboard.component.ts index 333c5f1..f0d1d62 100644 --- a/frontend/src/app/views/dashboard/dashboard.component.ts +++ b/frontend/src/app/views/dashboard/dashboard.component.ts @@ -103,6 +103,7 @@ export class DashboardComponent implements OnInit { teams: Team[] = []; widgetStats: any; canManage: boolean = false; + @Output() userRoleSet: EventEmitter = new EventEmitter(); rows: CodeRepo[] = []; @@ -175,13 +176,21 @@ export class DashboardComponent implements OnInit { }); } ngOnInit(): void { + let userRole = localStorage.getItem('userRole'); + this.authService.hc().subscribe({ - next: () => { - const userRole = localStorage.getItem('userRole'); - if (userRole === 'ADMIN' || userRole === 'TEAM_MANAGER'){ - this.canManage = true; - this.loadTeams(); + next: (response) => { + if (!userRole) { + localStorage.setItem('userRole', response.status.replace("ROLE_","")); + location.reload(); } + // localStorage.setItem('userRole', response.status.replace("ROLE_","")); + // const userRole = localStorage.getItem('userRole') || 'USER'; + // this.userRoleSet.emit(userRole); // Emit event after setting userRole + // if (userRole === 'ADMIN' || userRole === 'TEAM_MANAGER'){ + // this.canManage = true; + // this.loadTeams(); + // } }, error: () => { // Health check failed, redirect to login diff --git a/frontend/src/app/views/pages/login/login.component.html b/frontend/src/app/views/pages/login/login.component.html index e862020..a7b5b85 100644 --- a/frontend/src/app/views/pages/login/login.component.html +++ b/frontend/src/app/views/pages/login/login.component.html @@ -3,7 +3,7 @@ - +

Login

@@ -36,6 +36,13 @@

Login

+ + +
+ +
+
+
diff --git a/frontend/src/app/views/pages/login/login.component.scss b/frontend/src/app/views/pages/login/login.component.scss index b49cf05..b623ebe 100644 --- a/frontend/src/app/views/pages/login/login.component.scss +++ b/frontend/src/app/views/pages/login/login.component.scss @@ -10,4 +10,24 @@ max-width: 100%; max-height: 100%; object-fit: contain; -} \ No newline at end of file +} +.button-container { + display: flex; + justify-content: center; + align-items: center; + height: 100%; /* Adjust height as needed */ +} + +.primary-button { + background-color: #007bff; /* Primary color */ + color: white; + border: none; + padding: 10px 20px; + font-size: 16px; + cursor: pointer; + border-radius: 4px; +} + +.primary-button:hover { + background-color: #0056b3; +} diff --git a/frontend/src/app/views/pages/login/login.component.ts b/frontend/src/app/views/pages/login/login.component.ts index 2fd4957..ae8ee54 100644 --- a/frontend/src/app/views/pages/login/login.component.ts +++ b/frontend/src/app/views/pages/login/login.component.ts @@ -42,6 +42,8 @@ import {getNavItems, navItems} from "../../../layout/default-layout/_nav"; }) export class LoginComponent implements OnInit{ loginForm: FormGroup; + password: boolean = true; + sso: boolean = false; constructor(private fb: FormBuilder, private authService: AuthService, private router: Router) { this.loginForm = this.fb.group({ @@ -97,6 +99,7 @@ export class LoginComponent implements OnInit{ } } ngOnInit() { + this.getStatus(); this.authService.hc().subscribe({ next: () => { this.router.navigate(['/dashboard']); @@ -106,5 +109,21 @@ export class LoginComponent implements OnInit{ } }); } + getStatus() { + this.authService.status().subscribe({ + next: (response) => { + if (response.status === 'prodsso' || response.status === 'devsso'){ + this.password = false; + this.sso = true; + } + }, + error: () => { + // Health check failed, stay on login page + } + }); + } + redirectToSSO(): void { + window.location.href = 'http://localhost:8888/oauth2/authorization/sso'; + } }