Skip to content

Commit

Permalink
Authenticate using Snowflake authentication mechanism (#4315)
Browse files Browse the repository at this point in the history

---------

Signed-off-by: Iliyan Velichkov <[email protected]>
  • Loading branch information
iliyan-velichkov authored Oct 4, 2024
1 parent 9db3de6 commit e6c4481
Show file tree
Hide file tree
Showing 16 changed files with 515 additions and 35 deletions.
4 changes: 4 additions & 0 deletions build/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@
<groupId>org.eclipse.dirigible</groupId>
<artifactId>dirigible-components-security-keycloak</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.dirigible</groupId>
<artifactId>dirigible-components-security-snowflake</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.dirigible</groupId>
<artifactId>dirigible-components-security-oauth2</artifactId>
Expand Down
2 changes: 2 additions & 0 deletions build/application/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,6 @@
<logger name="org.eclipse.dirigible.components.tenants.tenant" level="INFO"/>
<logger name="org.springframework.context.support" level="INFO"/>

<logger name="net.snowflake.client" level="ERROR"/>

</configuration>
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -43,9 +40,6 @@ class AdminUserInitializer implements ApplicationListener<ApplicationReadyEvent>
/** 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;

Expand All @@ -66,7 +60,6 @@ class AdminUserInitializer implements ApplicationListener<ApplicationReadyEvent>
this.userService = userService;
this.defaultTenant = defaultTenant;
this.roleService = roleService;
this.base64Decoder = Base64.getDecoder();
}

/**
Expand Down Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -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"))
Expand All @@ -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();
}
}
6 changes: 6 additions & 0 deletions components/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<module>security/security-basic</module>
<module>security/security-keycloak</module>
<module>security/security-oauth2</module>
<module>security/security-snowflake</module>

<!-- Data -->
<module>data/data-structures</module>
Expand Down Expand Up @@ -382,6 +383,11 @@
<artifactId>dirigible-components-security-keycloak</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.dirigible</groupId>
<artifactId>dirigible-components-security-snowflake</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.dirigible</groupId>
<artifactId>dirigible-components-security-oauth2</artifactId>
Expand Down
43 changes: 43 additions & 0 deletions components/security/security-snowflake/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<parent>
<artifactId>dirigible-components-parent</artifactId>
<groupId>org.eclipse.dirigible</groupId>
<version>11.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

<name>Components - Security - Snowflake</name>
<artifactId>dirigible-components-security-snowflake</artifactId>
<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>org.eclipse.dirigible</groupId>
<artifactId>dirigible-components-core-base</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.dirigible</groupId>
<artifactId>dirigible-components-core-tenants</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>

<properties>
<license.header.location>../../../licensing-header.txt</license.header.location>
<parent.pom.folder>../../../</parent.pom.folder>
</properties>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.eclipse.dirigible.components.security.snowflake;

public class InvalidSecurityContextException extends RuntimeException {

public InvalidSecurityContextException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -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<ApplicationReadyEvent> {

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<String> optionalUsername = getSnowflakeAdminUsername();
if (optionalUsername.isEmpty()) {
LOGGER.warn("Admin user will not be initialized");
return;
}
String username = optionalUsername.get();

Optional<User> 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<String> 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);
}

}
Loading

0 comments on commit e6c4481

Please sign in to comment.