Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sso): support saml integration #3783

Open
wants to merge 2 commits into
base: dev/4.3.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@
<jwt.version>3.9.0</jwt.version>
<okhttp.version>4.11.0</okhttp.version>
<jgit.version>5.13.3.202401111512-r</jgit.version>
<bouncycastle.jdk18.version>1.76</bouncycastle.jdk18.version>
<bouncycastle.jdk15.version>1.70</bouncycastle.jdk15.version>
<commons-codec.version>1.15</commons-codec.version>
</properties>

<dependencyManagement>
Expand All @@ -170,6 +173,11 @@
<artifactId>groovy-all</artifactId>
<version>${groovy-all.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
Expand Down Expand Up @@ -436,6 +444,12 @@
<artifactId>spring-security-oauth2-client</artifactId>
<version>${spring-security.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-saml2-service-provider</artifactId>
<version>${spring-security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
Expand All @@ -459,7 +473,7 @@
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>${bcpkix-jdk15on.version}</version>
<version>${bouncycastle.jdk15.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
Expand All @@ -476,6 +490,21 @@
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>${spring-cloud-starter-bootstrap.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.jdk18.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.jdk18.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcutil-jdk18on</artifactId>
<version>${bouncycastle.jdk18.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
Expand Down Expand Up @@ -955,9 +984,7 @@
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<!-- original version = 1.62 -->
<!-- upgrade to avoid vulnerability issues, version >= 1.67 -->
<version>1.70</version>
<version>${bouncycastle.jdk15.version}</version>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2024 OceanBase.
*
* 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.
*/

alter table `integration_integration` modify column `secret` mediumtext DEFAULT null;
4 changes: 4 additions & 0 deletions server/odc-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

import com.oceanbase.odc.service.common.response.Responses;
import com.oceanbase.odc.service.common.response.SuccessResponse;
import com.oceanbase.odc.service.integration.IntegrationService;
import com.oceanbase.odc.service.integration.SSOCredential;
import com.oceanbase.odc.service.integration.model.IntegrationConfig;
import com.oceanbase.odc.service.integration.oauth2.Oauth2StateManager;
import com.oceanbase.odc.service.integration.oauth2.SSOTestInfo;
Expand All @@ -44,6 +46,9 @@ public class SSOController {
@Autowired
private Oauth2StateManager oauth2StateManager;

@Autowired
private IntegrationService integrationService;

@PostMapping(value = "/test/start")
public SuccessResponse<SSOTestInfo> addTestClientRegistration(@RequestBody IntegrationConfig config,
@RequestParam(required = false) String type) {
Expand All @@ -68,4 +73,9 @@ public SuccessResponse<Map<String, String>> getTestClientInfo(@RequestParam Stri
return Responses.ok(oauth2StateManager.getStateParameters(state));
}

@GetMapping("/credential")
public SuccessResponse<SSOCredential> generateSSOCredential() {
return Responses.ok(integrationService.generateSSOCredential());
}

}
18 changes: 17 additions & 1 deletion server/odc-service/pom.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?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">
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>
<groupId>com.oceanbase</groupId>
Expand Down Expand Up @@ -108,6 +108,18 @@
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcutil-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
Expand All @@ -133,6 +145,10 @@
<artifactId>spring-security-oauth2-jose</artifactId>
<version>5.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-saml2-service-provider</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,29 +36,68 @@
import com.oceanbase.odc.service.integration.model.IntegrationConfig;
import com.oceanbase.odc.service.integration.model.SSOIntegrationConfig;
import com.oceanbase.odc.service.integration.model.SqlInterceptorProperties;
import com.oceanbase.odc.service.integration.saml.SamlCredentialManager;
import com.oceanbase.odc.service.integration.saml.SamlParameter;
import com.oceanbase.odc.service.integration.saml.SamlParameter.SecretInfo;

/**
* @author gaoda.xy
* @date 2023/3/29 20:07
*/
@Component
@Validated
public class IntegrationConfigurationValidator {
public class IntegrationConfigurationProcessor {

@Autowired
private IntegrationRepository integrationRepository;

@Autowired
private SamlCredentialManager samlCredentialManager;

public void check(@NotNull @Valid ApprovalProperties properties) {}

public void check(@NotNull @Valid SqlInterceptorProperties properties) {}

public void checkAndFillConfig(@NotNull @Valid IntegrationConfig config, Long organizationId, Boolean enabled,
public void checkAndFillConfig(@NotNull @Valid IntegrationConfig config, @Nullable IntegrationConfig savedConfig,
Long organizationId, Boolean enabled,
@Nullable Long integrationId) {
SSOIntegrationConfig ssoIntegrationConfig = SSOIntegrationConfig.of(config, organizationId);
fillSamlSecret(config, savedConfig, organizationId, ssoIntegrationConfig);
config.setConfiguration(JsonUtils.toJson(ssoIntegrationConfig));
checkNotEnabledInDbBeforeSave(enabled, organizationId, integrationId);
}

private void fillSamlSecret(IntegrationConfig config, IntegrationConfig savedConfig, Long organizationId,
SSOIntegrationConfig ssoIntegrationConfig) {
if ("SAML".equals(ssoIntegrationConfig.getType()) && config.getEncryption().getSecret() == null) {
SecretInfo secretInfo = new SecretInfo();
SamlParameter newParameter = (SamlParameter) ssoIntegrationConfig.getSsoParameter();
if (savedConfig != null) {
SecretInfo savedSecretInfo =
JsonUtils.fromJson(savedConfig.getEncryption().getSecret(), SecretInfo.class);
SSOIntegrationConfig savedSsoConfig = SSOIntegrationConfig.of(savedConfig, organizationId);
SamlParameter savedParameter = (SamlParameter) savedSsoConfig.getSsoParameter();
if (Objects.equals(savedParameter.getSigning().getCertificate(),
newParameter.getSigning().getCertificate())) {
secretInfo.setSigningPrivateKey(savedSecretInfo.getSigningPrivateKey());
}
if (Objects.equals(savedParameter.getDecryption().getCertificate(),
newParameter.getDecryption().getCertificate())) {
secretInfo.setDecryptionPrivateKey(savedSecretInfo.getDecryptionPrivateKey());
}
}
String certificate = newParameter.getSigning().getCertificate();
if (secretInfo.getSigningPrivateKey() == null && certificate != null) {
secretInfo.setSigningPrivateKey(samlCredentialManager.getPrivateKeyByCert(certificate));
}
String decryptionCertificate = newParameter.getDecryption().getCertificate();
if (secretInfo.getDecryptionPrivateKey() == null && decryptionCertificate != null) {
secretInfo.setDecryptionPrivateKey(samlCredentialManager.getPrivateKeyByCert(decryptionCertificate));
}
config.getEncryption().setSecret(JsonUtils.toJson(secretInfo));
}
}

public void checkNotEnabledInDbBeforeSave(Boolean enabled, Long organizationId, @Nullable Long integrationId) {
if (Boolean.TRUE.equals(enabled)) {
List<IntegrationEntity> dbSSO = integrationRepository.findByTypeAndOrganizationId(SSO,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
*/
package com.oceanbase.odc.service.integration;

import java.util.Optional;

import javax.annotation.Nullable;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

Expand All @@ -25,22 +29,24 @@
import com.oceanbase.odc.service.integration.model.SqlInterceptorProperties;

@Component
public class IntegrationConfigurationValidatorDelegate {
public class IntegrationConfigurationProcessorDelegate {

@Autowired
private IntegrationConfigurationValidator configurationValidator;
private IntegrationConfigurationProcessor configurationValidator;

@Autowired
private AuthenticationFacade authenticationFacade;

public void preProcessConfig(IntegrationConfig config) {
if (config.getType() == IntegrationType.APPROVAL) {
configurationValidator.check(ApprovalProperties.from(config));
} else if (config.getType() == IntegrationType.SQL_INTERCEPTOR) {
configurationValidator.check(SqlInterceptorProperties.from(config));
} else if (config.getType() == IntegrationType.SSO) {
configurationValidator.checkAndFillConfig(config, authenticationFacade.currentOrganizationId(),
config.getEnabled(), null);
public void preProcessConfig(IntegrationConfig newConfig, @Nullable IntegrationConfig savedConfig) {
if (newConfig.getType() == IntegrationType.APPROVAL) {
configurationValidator.check(ApprovalProperties.from(newConfig));
} else if (newConfig.getType() == IntegrationType.SQL_INTERCEPTOR) {
configurationValidator.check(SqlInterceptorProperties.from(newConfig));
} else if (newConfig.getType() == IntegrationType.SSO) {
configurationValidator.checkAndFillConfig(newConfig, savedConfig,
authenticationFacade.currentOrganizationId(),
newConfig.getEnabled(),
Optional.ofNullable(savedConfig).map(IntegrationConfig::getId).orElse(null));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
import com.oceanbase.odc.service.integration.model.QueryIntegrationParams;
import com.oceanbase.odc.service.integration.model.SSOIntegrationConfig;
import com.oceanbase.odc.service.integration.model.SqlInterceptorProperties;
import com.oceanbase.odc.service.integration.saml.SamlCredentialManager;

import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -91,7 +92,7 @@ public class IntegrationService {
private AuthenticationFacade authenticationFacade;

@Autowired
private IntegrationConfigurationValidatorDelegate integrationConfigurationValidatorDelegate;
private IntegrationConfigurationProcessorDelegate integrationConfigurationProcessorDelegate;

@Autowired
private HorizontalDataPermissionValidator permissionValidator;
Expand All @@ -117,6 +118,9 @@ public class IntegrationService {
@Autowired
private CacheManager defaultCacheManager;

@Autowired
private SamlCredentialManager samlCredentialManager;

@PreAuthenticate(actions = "create", resourceType = "ODC_INTEGRATION", isForAll = true)
public Boolean exists(@NotBlank String name, @NotNull IntegrationType type) {
Long organizationId = authenticationFacade.currentOrganizationId();
Expand All @@ -132,7 +136,7 @@ public IntegrationConfig create(@NotNull @Valid IntegrationConfig config) {
.findByNameAndTypeAndOrganizationId(config.getName(), config.getType(), organizationId);
PreConditions.validNoDuplicated(ResourceType.ODC_EXTERNAL_APPROVAL, "name", config.getName(),
existsEntity::isPresent);
integrationConfigurationValidatorDelegate.preProcessConfig(config);
integrationConfigurationProcessorDelegate.preProcessConfig(config, null);
Encryption encryption = config.getEncryption();
encryption.check();
applicationContext.publishEvent(IntegrationEvent.createPreCreate(config));
Expand Down Expand Up @@ -214,12 +218,13 @@ public IntegrationConfig delete(@NotNull Long id) {
@PreAuthenticate(actions = "update", resourceType = "ODC_INTEGRATION", indexOfIdParam = 0)
public IntegrationConfig update(@NotNull Long id, @NotNull @Valid IntegrationConfig config) {
IntegrationEntity entity = nullSafeGet(id);
permissionValidator.checkCurrentOrganization(new IntegrationConfig(entity));
IntegrationConfig saveConfig = getDecodeConfig(entity);
permissionValidator.checkCurrentOrganization(saveConfig);
if (Boolean.TRUE.equals(entity.getBuiltin())) {
throw new UnsupportedException(ErrorCodes.IllegalOperation, new Object[] {"builtin integration"},
"Operation on builtin integration is not allowed");
}
integrationConfigurationValidatorDelegate.preProcessConfig(config);
integrationConfigurationProcessorDelegate.preProcessConfig(config, saveConfig);
Encryption encryption = config.getEncryption();
applicationContext.publishEvent(
IntegrationEvent.createPreUpdate(config, new IntegrationConfig(entity), entity.getSalt()));
Expand All @@ -239,6 +244,13 @@ public IntegrationConfig update(@NotNull Long id, @NotNull @Valid IntegrationCon
return new IntegrationConfig(entity);
}

private IntegrationConfig getDecodeConfig(IntegrationEntity entity) {
IntegrationConfig integrationConfig = new IntegrationConfig(entity);
String secret = decodeSecret(entity.getSecret(), entity.getSalt(), entity.getOrganizationId());
integrationConfig.getEncryption().setSecret(secret);
return integrationConfig;
}

@Transactional(rollbackFor = Exception.class)
@PreAuthenticate(actions = "update", resourceType = "ODC_INTEGRATION", indexOfIdParam = 0)
public IntegrationConfig setEnabled(@NotNull Long id, @NotNull Boolean enabled) {
Expand Down Expand Up @@ -343,7 +355,6 @@ public SSOIntegrationConfig getSSOIntegrationConfig(IntegrationEntity integratio
return ssoIntegrationConfig;
}


private String encodeSecret(String plainSecret, String salt, Long organizationId) {
if (plainSecret == null) {
return null;
Expand All @@ -352,7 +363,6 @@ private String encodeSecret(String plainSecret, String salt, Long organizationId
return encryptor.encrypt(plainSecret);
}


@SkipAuthorize("odc internal usage")
public String decodeSecret(String encryptedSecret, String salt, Long organizationId) {
if (encryptedSecret == null) {
Expand All @@ -369,4 +379,8 @@ private void updateCache(Long key) {
}
}

public SSOCredential generateSSOCredential() {
return new SSOCredential(samlCredentialManager.generateCertWithCachedPrivateKey());
}

}
Loading