diff --git a/build/application/pom.xml b/build/application/pom.xml index 54fa1bdd5cf..2464ce055fd 100644 --- a/build/application/pom.xml +++ b/build/application/pom.xml @@ -139,6 +139,10 @@ org.eclipse.dirigible dirigible-components-security-keycloak + + org.eclipse.dirigible + dirigible-components-security-snowflake + org.eclipse.dirigible dirigible-components-security-oauth2 diff --git a/build/application/src/main/resources/logback.xml b/build/application/src/main/resources/logback.xml index 6c6157e57ef..2ba7a4f5cab 100644 --- a/build/application/src/main/resources/logback.xml +++ b/build/application/src/main/resources/logback.xml @@ -75,4 +75,6 @@ + + diff --git a/components/core/core-base/src/main/java/org/eclipse/dirigible/components/base/http/access/CorsConfigurationSourceProvider.java b/components/core/core-base/src/main/java/org/eclipse/dirigible/components/base/http/access/CorsConfigurationSourceProvider.java new file mode 100644 index 00000000000..7ab0f8a3e55 --- /dev/null +++ b/components/core/core-base/src/main/java/org/eclipse/dirigible/components/base/http/access/CorsConfigurationSourceProvider.java @@ -0,0 +1,29 @@ +package org.eclipse.dirigible.components.base.http.access; + +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; + +public class CorsConfigurationSourceProvider { + + public static CorsConfigurationSource get() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(List.of("*")); + configuration.setAllowCredentials(true); + configuration.setAllowedHeaders( + Arrays.asList("Access-Control-Allow-Headers", "Access-Control-Allow-Origin", "Access-Control-Request-Method", + "Access-Control-Request-Headers", "Origin", "Cache-Control", "Content-Type", "Authorization")); + configuration.setExposedHeaders( + Arrays.asList("Access-Control-Allow-Headers", "Access-Control-Allow-Origin", "Access-Control-Request-Method", + "Access-Control-Request-Headers", "Origin", "Cache-Control", "Content-Type", "Authorization")); + configuration.setAllowedMethods(Arrays.asList("HEAD", "DELETE", "GET", "POST", "PATCH", "PUT")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + + return source; + } +} diff --git a/components/core/core-tenants/src/main/java/org/eclipse/dirigible/components/tenants/init/AdminUserInitializer.java b/components/core/core-tenants/src/main/java/org/eclipse/dirigible/components/tenants/init/AdminUserInitializer.java index 7b59cde866f..4a1b14b83f5 100644 --- a/components/core/core-tenants/src/main/java/org/eclipse/dirigible/components/tenants/init/AdminUserInitializer.java +++ b/components/core/core-tenants/src/main/java/org/eclipse/dirigible/components/tenants/init/AdminUserInitializer.java @@ -27,9 +27,6 @@ import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.Base64.Decoder; import java.util.Optional; /** @@ -43,9 +40,6 @@ class AdminUserInitializer implements ApplicationListener /** The Constant LOGGER. */ private static final Logger LOGGER = LoggerFactory.getLogger(AdminUserInitializer.class); - /** The base 64 decoder. */ - private final Decoder base64Decoder; - /** The user service. */ private final UserService userService; @@ -66,7 +60,6 @@ class AdminUserInitializer implements ApplicationListener this.userService = userService; this.defaultTenant = defaultTenant; this.roleService = roleService; - this.base64Decoder = Base64.getDecoder(); } /** @@ -120,15 +113,4 @@ private void assignRole(User user, Roles predefinedRole) { .getId()); } - /** - * Decode. - * - * @param base64String the base 64 string - * @return the string - */ - private String decode(String base64String) { - byte[] decodedValue = base64Decoder.decode(base64String); - return new String(decodedValue, StandardCharsets.UTF_8); - } - } diff --git a/components/core/core-tenants/src/main/java/org/eclipse/dirigible/components/tenants/security/BasicSecurityConfig.java b/components/core/core-tenants/src/main/java/org/eclipse/dirigible/components/tenants/security/BasicSecurityConfig.java index fe87d62f2c3..408c8c06b36 100644 --- a/components/core/core-tenants/src/main/java/org/eclipse/dirigible/components/tenants/security/BasicSecurityConfig.java +++ b/components/core/core-tenants/src/main/java/org/eclipse/dirigible/components/tenants/security/BasicSecurityConfig.java @@ -9,7 +9,7 @@ */ package org.eclipse.dirigible.components.tenants.security; -import java.util.Arrays; +import org.eclipse.dirigible.components.base.http.access.CorsConfigurationSourceProvider; import org.eclipse.dirigible.components.base.http.access.HttpSecurityURIConfigurator; import org.eclipse.dirigible.components.tenants.tenant.TenantContextInitFilter; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -20,9 +20,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; /** * The Class WebSecurityConfig. @@ -44,7 +42,7 @@ public class BasicSecurityConfig { SecurityFilterChain filterChain(HttpSecurity http, TenantContextInitFilter tenantContextInitFilter) throws Exception { http.cors(Customizer.withDefaults()) .httpBasic(Customizer.withDefaults()) - .csrf(csrf -> csrf.disable()) + .csrf(csrf -> csrf.disable())// if enabled, some functionalities will not work - like creating a project .addFilterBefore(tenantContextInitFilter, UsernamePasswordAuthenticationFilter.class) .formLogin(Customizer.withDefaults()) .logout(logout -> logout.deleteCookies("JSESSIONID")) @@ -62,18 +60,6 @@ SecurityFilterChain filterChain(HttpSecurity http, TenantContextInitFilter tenan */ @Bean CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOriginPatterns(Arrays.asList("*")); - configuration.setAllowCredentials(true); - configuration.setAllowedHeaders( - Arrays.asList("Access-Control-Allow-Headers", "Access-Control-Allow-Origin", "Access-Control-Request-Method", - "Access-Control-Request-Headers", "Origin", "Cache-Control", "Content-Type", "Authorization")); - configuration.setExposedHeaders( - Arrays.asList("Access-Control-Allow-Headers", "Access-Control-Allow-Origin", "Access-Control-Request-Method", - "Access-Control-Request-Headers", "Origin", "Cache-Control", "Content-Type", "Authorization")); - configuration.setAllowedMethods(Arrays.asList("HEAD", "DELETE", "GET", "POST", "PATCH", "PUT")); - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - return source; + return CorsConfigurationSourceProvider.get(); } } diff --git a/components/pom.xml b/components/pom.xml index 71ef5c83adc..4286b4c067f 100644 --- a/components/pom.xml +++ b/components/pom.xml @@ -33,6 +33,7 @@ security/security-basic security/security-keycloak security/security-oauth2 + security/security-snowflake data/data-structures @@ -382,6 +383,11 @@ dirigible-components-security-keycloak ${project.version} + + org.eclipse.dirigible + dirigible-components-security-snowflake + ${project.version} + org.eclipse.dirigible dirigible-components-security-oauth2 diff --git a/components/security/security-snowflake/pom.xml b/components/security/security-snowflake/pom.xml new file mode 100644 index 00000000000..721cb206087 --- /dev/null +++ b/components/security/security-snowflake/pom.xml @@ -0,0 +1,43 @@ + + + + 4.0.0 + + + dirigible-components-parent + org.eclipse.dirigible + 11.0.0-SNAPSHOT + ../../pom.xml + + + Components - Security - Snowflake + dirigible-components-security-snowflake + jar + + + + org.eclipse.dirigible + dirigible-components-core-base + + + org.eclipse.dirigible + dirigible-components-core-tenants + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot + spring-boot-starter-security + + + + + ../../../licensing-header.txt + ../../../ + + + \ No newline at end of file diff --git a/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/InvalidSecurityContextException.java b/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/InvalidSecurityContextException.java new file mode 100644 index 00000000000..35b56716cc8 --- /dev/null +++ b/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/InvalidSecurityContextException.java @@ -0,0 +1,8 @@ +package org.eclipse.dirigible.components.security.snowflake; + +public class InvalidSecurityContextException extends RuntimeException { + + public InvalidSecurityContextException(String message) { + super(message); + } +} diff --git a/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeAdminUserInitializer.java b/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeAdminUserInitializer.java new file mode 100644 index 00000000000..7371b53fe6a --- /dev/null +++ b/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeAdminUserInitializer.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2024 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.components.security.snowflake; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.dirigible.commons.config.DirigibleConfig; +import org.eclipse.dirigible.components.base.ApplicationListenersOrder.ApplicationReadyEventListeners; +import org.eclipse.dirigible.components.base.http.roles.Roles; +import org.eclipse.dirigible.components.base.tenant.DefaultTenant; +import org.eclipse.dirigible.components.base.tenant.Tenant; +import org.eclipse.dirigible.components.tenants.domain.User; +import org.eclipse.dirigible.components.tenants.domain.UserRoleAssignment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Profile; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Profile("snowflake") +@Order(ApplicationReadyEventListeners.ADMIN_USER_INITIALIZER) +@Component +class SnowflakeAdminUserInitializer implements ApplicationListener { + + private static final Logger LOGGER = LoggerFactory.getLogger(SnowflakeAdminUserInitializer.class); + + private final Tenant defaultTenant; + + private final SnowflakeUserManager snowflakeUserManager; + + SnowflakeAdminUserInitializer(@DefaultTenant Tenant defaultTenant, SnowflakeUserManager snowflakeUserManager) { + this.defaultTenant = defaultTenant; + this.snowflakeUserManager = snowflakeUserManager; + } + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + LOGGER.info("Executing..."); + initAdminUser(); + LOGGER.info("Completed."); + + } + + private void initAdminUser() { + Optional optionalUsername = getSnowflakeAdminUsername(); + if (optionalUsername.isEmpty()) { + LOGGER.warn("Admin user will not be initialized"); + return; + } + String username = optionalUsername.get(); + + Optional existingUser = snowflakeUserManager.findUserByUsernameAndTenantId(username, defaultTenant.getId()); + if (existingUser.isPresent()) { + LOGGER.info("A user with username [{}] for tenant [{}] already exists. Skipping its initialization.", username, + defaultTenant.getId()); + return; + } + User adminUser = snowflakeUserManager.createNewUser(username, defaultTenant.getId()); + LOGGER.info("Created admin user with username [{}] for tenant with id [{}]", adminUser.getUsername(), adminUser.getTenant() + .getId()); + for (Roles predefinedRole : Roles.values()) { + assignRole(adminUser, predefinedRole); + } + } + + /** + * Assign role. + * + * @param user the user + * @param predefinedRole the predefined role + */ + private void assignRole(User user, Roles predefinedRole) { + UserRoleAssignment assignment = new UserRoleAssignment(); + assignment.setUser(user); + + String roleName = predefinedRole.getRoleName(); + snowflakeUserManager.assignUserRoles(user, roleName); + + LOGGER.info("Assigned role [{}] to user [{}] in tenant [{}]", roleName, user.getUsername(), user.getTenant() + .getId()); + } + + private Optional getSnowflakeAdminUsername() { + String username = DirigibleConfig.SNOWFLAKE_ADMIN_USERNAME.getStringValue(); + if (StringUtils.isBlank(username)) { + LOGGER.warn("Missing snowflake admin username in configuration [{}].", DirigibleConfig.SNOWFLAKE_ADMIN_USERNAME.getKey()); + return Optional.empty(); + } + return Optional.of(username); + } + +} diff --git a/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeAuthFilter.java b/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeAuthFilter.java new file mode 100644 index 00000000000..640ce5bcd3a --- /dev/null +++ b/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeAuthFilter.java @@ -0,0 +1,88 @@ +package org.eclipse.dirigible.components.security.snowflake; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.lang.NonNull; +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.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Objects; + +@Profile("snowflake") +@Component +class SnowflakeAuthFilter extends OncePerRequestFilter { + private static final Logger LOGGER = LoggerFactory.getLogger(SnowflakeAuthFilter.class); + + private static final String SNOWFLAKE_USER_HEADER = "Sf-Context-Current-User"; + + private final SnowflakeUserDetailsService userDetailsService; + + public SnowflakeAuthFilter(SnowflakeUserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + String currentUser = request.getHeader(SNOWFLAKE_USER_HEADER); + + if (currentUser == null) { + LOGGER.debug("Missing user header with name [{}]. Forwarding the request further", SNOWFLAKE_USER_HEADER); + + HttpSession session = request.getSession(false); + if (null != session) { + session.invalidate(); + } + SecurityContextHolder.clearContext(); + filterChain.doFilter(request, response); + return; + } + + Authentication authentication = SecurityContextHolder.getContext() + .getAuthentication(); + + if (authentication == null) { + loginCurrentUser(request, currentUser); + } else { + validateSecurityContext(authentication, currentUser); + } + + filterChain.doFilter(request, response); + } + + private void validateSecurityContext(Authentication authentication, String currentUser) { + Object principal = authentication.getPrincipal(); + if (principal instanceof UserDetails userDetails) { + String loggedInUsername = userDetails.getUsername(); + if (!Objects.equals(currentUser, loggedInUsername)) { + String errMessage = "Current user [" + currentUser + "] doesn't match the one in the security context: " + loggedInUsername; + throw new InvalidSecurityContextException(errMessage); + } + } else { + throw new InvalidSecurityContextException("Unexpected type [" + principal.getClass() + "] for principal"); + } + } + + private void loginCurrentUser(HttpServletRequest request, String currentUser) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(currentUser); + + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext() + .setAuthentication(authToken); + } +} diff --git a/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeLogoutHandler.java b/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeLogoutHandler.java new file mode 100644 index 00000000000..d5092c24bde --- /dev/null +++ b/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeLogoutHandler.java @@ -0,0 +1,34 @@ +package org.eclipse.dirigible.components.security.snowflake; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.stereotype.Component; + +@Component +class SnowflakeLogoutHandler implements LogoutHandler { + + private static final String SNOWFLAKE_AUTH_COOKIE_PREFIX = "sfc-ss-ingress-auth-v1-"; + private static final String SNOWFLAKE_AUTH_COOKIE_INVALIDATED_VALUE_PATTERN = + SNOWFLAKE_AUTH_COOKIE_PREFIX + "%s=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; domain=.%s; Secure; HttpOnly"; + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + String hostHeader = request.getHeader(HttpHeaders.HOST); + String subdomain = extractSubdomain(hostHeader); + + String invalidatedAuthCookie = String.format(SNOWFLAKE_AUTH_COOKIE_INVALIDATED_VALUE_PATTERN, subdomain, hostHeader); + + // set the cookie as header since addCookie cannot be used + // due to an RFC restriction + response.addHeader(HttpHeaders.SET_COOKIE, invalidatedAuthCookie); + } + + private static String extractSubdomain(String host) { + int dotIdx = host.indexOf("."); + return dotIdx == -1 ? host : host.substring(0, dotIdx); + } + +} diff --git a/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeSecurityConfig.java b/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeSecurityConfig.java new file mode 100644 index 00000000000..e0feae9dd5d --- /dev/null +++ b/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeSecurityConfig.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.components.security.snowflake; + +import org.eclipse.dirigible.components.base.http.access.CorsConfigurationSourceProvider; +import org.eclipse.dirigible.components.base.http.access.HttpSecurityURIConfigurator; +import org.eclipse.dirigible.components.tenants.tenant.TenantContextInitFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfigurationSource; + +@Profile("snowflake") +@Configuration +@EnableWebSecurity +class SnowflakeSecurityConfig { + + private static final Logger LOGGER = LoggerFactory.getLogger(SnowflakeSecurityConfig.class); + + private final SnowflakeAuthFilter snowflakeAuthFilter; + private final SnowflakeLogoutHandler snowflakeLogoutHandler; + + SnowflakeSecurityConfig(SnowflakeAuthFilter snowflakeAuthFilter, SnowflakeLogoutHandler snowflakeLogoutHandler) { + this.snowflakeAuthFilter = snowflakeAuthFilter; + this.snowflakeLogoutHandler = snowflakeLogoutHandler; + } + + @Bean + SecurityFilterChain filterChain(HttpSecurity http, TenantContextInitFilter tenantContextInitFilter) throws Exception { + LOGGER.info("Configure snowflake security configurations"); + http.cors(Customizer.withDefaults()) + .csrf(csrf -> csrf.disable()) // if enabled, some functionalities will not work - like creating a project + .logout(logout -> logout.deleteCookies("JSESSIONID") + // consider redirect to reserved path "/sfc-endpoint/logout" and snowflakeLogoutHandler removal + .addLogoutHandler(snowflakeLogoutHandler) + .logoutSuccessUrl("/") + .invalidateHttpSession(true)) + .headers(headers -> headers.frameOptions(frameOpts -> frameOpts.disable())) + .sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) + .addFilterBefore(tenantContextInitFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterAt(snowflakeAuthFilter, UsernamePasswordAuthenticationFilter.class); + + HttpSecurityURIConfigurator.configure(http); + + return http.build(); + } + + @Bean + CorsConfigurationSource corsConfigurationSource() { + return CorsConfigurationSourceProvider.get(); + } + +} diff --git a/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeUserDetailsService.java b/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeUserDetailsService.java new file mode 100644 index 00000000000..cc7e32e5c2d --- /dev/null +++ b/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeUserDetailsService.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.components.security.snowflake; + +import org.eclipse.dirigible.components.base.util.AuthoritiesUtil; +import org.eclipse.dirigible.components.tenants.domain.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Service; + +import java.util.Set; + +@Profile("snowflake") +@Service +class SnowflakeUserDetailsService implements UserDetailsService { + + private static final Logger LOGGER = LoggerFactory.getLogger(SnowflakeUserDetailsService.class); + + private final SnowflakeUserManager userManager; + + SnowflakeUserDetailsService(SnowflakeUserManager userManager) { + this.userManager = userManager; + } + + @Override + public UserDetails loadUserByUsername(String username) { + LOGGER.debug("Loading user with username [{}]...", username); + User user = userManager.findUserByUsername(username) + .orElseGet(() -> userManager.createNewUser(username)); + + LOGGER.debug("Logged in user with username [{}] in tenant [{}]", user.getUsername(), user.getTenant()); + Set userRoles = userManager.getUserRoleNames(user); + LOGGER.debug("User [{}] has assigned roles [{}]", user, userRoles); + + Set auths = AuthoritiesUtil.toAuthorities(userRoles); + + return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), auths); + } + +} diff --git a/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeUserManager.java b/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeUserManager.java new file mode 100644 index 00000000000..7cec82363d8 --- /dev/null +++ b/components/security/security-snowflake/src/main/java/org/eclipse/dirigible/components/security/snowflake/SnowflakeUserManager.java @@ -0,0 +1,62 @@ +package org.eclipse.dirigible.components.security.snowflake; + +import org.eclipse.dirigible.components.base.tenant.TenantContext; +import org.eclipse.dirigible.components.security.domain.Role; +import org.eclipse.dirigible.components.security.service.RoleService; +import org.eclipse.dirigible.components.tenants.domain.User; +import org.eclipse.dirigible.components.tenants.service.UserService; +import org.springframework.stereotype.Component; + +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +@Component +class SnowflakeUserManager { + + private final UserService userService; + private final RoleService roleService; + private final TenantContext tenantContext; + + SnowflakeUserManager(UserService userService, RoleService roleService, TenantContext tenantContext) { + this.userService = userService; + this.roleService = roleService; + this.tenantContext = tenantContext; + } + + public Optional findUserByUsername(String username) { + String currentTenantId = tenantContext.getCurrentTenant() + .getId(); + return findUserByUsernameAndTenantId(username, currentTenantId); + } + + public Optional findUserByUsernameAndTenantId(String username, String tenantId) { + return userService.findUserByUsernameAndTenantId(toSnowflakeUsername(username), tenantId); + } + + private String toSnowflakeUsername(String username) { + // usernames are passed in uppercase to the applications via header Sf-Context-Current-User + return username.toUpperCase(); + } + + public User createNewUser(String username) { + String currentTenantId = tenantContext.getCurrentTenant() + .getId(); + return createNewUser(username, currentTenantId); + } + + public User createNewUser(String username, String tenantId) { + String password = UUID.randomUUID() + .toString();// password not used in the Snowflake scenario + return userService.createNewUser(toSnowflakeUsername(username), password, tenantId); + } + + public void assignUserRoles(User user, String roleName) { + Role role = roleService.findByName(roleName); + userService.assignUserRoles(user, role); + } + + public Set getUserRoleNames(User user) { + return userService.getUserRoleNames(user); + } +} diff --git a/components/security/security-snowflake/src/main/resources/application-snowflake.properties b/components/security/security-snowflake/src/main/resources/application-snowflake.properties new file mode 100644 index 00000000000..a89760437eb --- /dev/null +++ b/components/security/security-snowflake/src/main/resources/application-snowflake.properties @@ -0,0 +1,13 @@ +# +# Copyright (c) 2010-2024 Eclipse Dirigible contributors +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v2.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v20.html +# +# SPDX-FileCopyrightText: Eclipse Dirigible contributors +# SPDX-License-Identifier: EPL-2.0 +# + +basic.enabled=false diff --git a/modules/commons/commons-config/src/main/java/org/eclipse/dirigible/commons/config/DirigibleConfig.java b/modules/commons/commons-config/src/main/java/org/eclipse/dirigible/commons/config/DirigibleConfig.java index 14ad922d883..aa83324a2e9 100644 --- a/modules/commons/commons-config/src/main/java/org/eclipse/dirigible/commons/config/DirigibleConfig.java +++ b/modules/commons/commons-config/src/main/java/org/eclipse/dirigible/commons/config/DirigibleConfig.java @@ -52,6 +52,8 @@ public enum DirigibleConfig { /** The tenant subdomain regex. */ TENANT_SUBDOMAIN_REGEX("DIRIGIBLE_TENANT_SUBDOMAIN_REGEX", "^([^\\.]+)\\..+$"), + SNOWFLAKE_ADMIN_USERNAME("DIRIGIBLE_SNOWFLAKE_ADMIN_USERNAME", null), + /** The basic admin username. */ BASIC_ADMIN_USERNAME("DIRIGIBLE_BASIC_USERNAME", toBase64("admin")),