feat: Self-signed certificates for REST APIs (#11043)

* feat: Self-signed certificates for REST APIs

* Changed scope for netty dep
This commit is contained in:
Nidhi 2022-02-15 12:24:26 +05:30 committed by GitHub
parent 00a7647590
commit 1868675349
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 209 additions and 37 deletions

View File

@ -8,6 +8,11 @@ export enum AuthType {
bearerToken = "bearerToken",
}
export enum SSLType {
DEFAULT = "DEFAULT",
SELF_SIGNED_CERTIFICATE = "SELF_SIGNED_CERTIFICATE",
}
export enum ApiKeyAuthType {
QueryParams = "queryParams",
Header = "header",
@ -25,6 +30,20 @@ export type Authentication =
| ApiKey
| BearerToken;
export interface Connection {
ssl: SSL;
}
export interface SSL {
authType: SSLType;
certificateFile: Certificate;
}
export interface Certificate {
name: string;
base64Content: string | ArrayBuffer | null;
}
export interface ApiDatasourceForm {
datasourceId: string;
pluginId: string;
@ -37,6 +56,7 @@ export interface ApiDatasourceForm {
sessionSignatureKey: string;
authType: AuthType;
authentication?: Authentication;
connection?: Connection;
}
export interface Oauth2Common {

View File

@ -38,6 +38,7 @@ import {
ApiKeyAuthType,
AuthType,
GrantType,
SSLType,
} from "entities/Datasource/RestAPIForm";
import {
createMessage,
@ -479,10 +480,48 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
)}
</FormInputContainer>
{this.renderAuthFields()}
<FormInputContainer data-replay-id={btoa("ssl")}>
{this.renderDropdownControlViaFormControl(
"connection.ssl.authType",
[
{
label: "No",
value: "DEFAULT",
},
{
label: "Yes",
value: "SELF_SIGNED_CERTIFICATE",
},
],
"Use Self-signed certificate",
"",
true,
"",
"DEFAULT",
)}
</FormInputContainer>
{this.renderSelfSignedCertificateFields()}
</>
);
};
renderSelfSignedCertificateFields = () => {
const { connection } = this.props.formData;
if (connection?.ssl.authType === SSLType.SELF_SIGNED_CERTIFICATE) {
return (
<Collapsible defaultIsOpen title="Certificate Details">
{this.renderFilePickerControlViaFormControl(
"connection.ssl.certificateFile",
"Upload Certificate",
"",
false,
true,
)}
</Collapsible>
);
}
};
renderAuthFields = () => {
const { authType } = this.props.formData;
@ -954,6 +993,7 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
placeholderText: string,
isRequired: boolean,
subtitle?: string,
initialValue?: any,
) {
const config = {
id: "",
@ -967,6 +1007,7 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
conditionals: {},
placeholderText: placeholderText,
formName: DATASOURCE_REST_API_FORM,
initialValue: initialValue,
};
return (
<FormControl
@ -1002,6 +1043,34 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
/>
);
}
renderFilePickerControlViaFormControl(
configProperty: string,
label: string,
placeholderText: string,
isRequired: boolean,
encrypted: boolean,
) {
const config = {
id: "",
configProperty: configProperty,
isValid: false,
controlType: "FILE_PICKER",
placeholderText: placeholderText,
encrypted: encrypted,
label: label,
conditionals: {},
formName: DATASOURCE_REST_API_FORM,
isRequired: isRequired,
};
return (
<FormControl
config={config}
formName={DATASOURCE_REST_API_FORM}
multipleConfig={[]}
/>
);
}
}
const mapStateToProps = (state: AppState, props: any) => {

View File

@ -11,6 +11,7 @@ import {
Basic,
ApiKey,
BearerToken,
SSLType,
} from "entities/Datasource/RestAPIForm";
import _ from "lodash";
@ -22,6 +23,11 @@ export const datasourceToFormValues = (
"datasourceConfiguration.authentication.authenticationType",
AuthType.NONE,
);
const connection = _.get(datasource, "datasourceConfiguration.connection", {
ssl: {
authType: SSLType.DEFAULT,
},
});
const authentication = datasourceToFormAuthentication(authType, datasource);
const isSendSessionEnabled =
_.get(datasource, "datasourceConfiguration.properties[0].value", "N") ===
@ -43,6 +49,7 @@ export const datasourceToFormValues = (
sessionSignatureKey: sessionSignatureKey,
authType: authType,
authentication: authentication,
connection: connection,
};
};
@ -69,6 +76,7 @@ export const formValuesToDatasource = (
{ key: "sessionSignatureKey", value: form.sessionSignatureKey },
],
authentication: authentication,
connection: form.connection,
},
} as Datasource;
};

View File

@ -0,0 +1,14 @@
package com.appsmith.external.helpers;
public class ExceptionHelper {
public static Throwable getRootCause(Throwable e) {
Throwable cause = null;
Throwable result = e;
while (null != (cause = result.getCause()) && (result != cause)) {
result = cause;
}
return result;
}
}

View File

@ -0,0 +1,53 @@
package com.appsmith.external.helpers;
import com.appsmith.external.models.UploadedFile;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
public class SSLHelper {
private static final String X_509_TYPE = "X.509";
private static final String CERT_ALIAS = "caCert";
private static final String SSL_PROTOCOL = "TLS";
public static SSLContext getSslContext(UploadedFile certificate)
throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, KeyManagementException {
final TrustManagerFactory trustManagerFactory = getSslTrustManagerFactory(certificate);
SSLContext sslContext = SSLContext.getInstance(SSL_PROTOCOL);
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
return sslContext;
}
public static TrustManagerFactory getSslTrustManagerFactory(UploadedFile certificate)
throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException {
InputStream certificateIs =
new ByteArrayInputStream(certificate.getDecodedContent());
CertificateFactory certificateFactory = CertificateFactory.getInstance(X_509_TYPE);
X509Certificate caCertificate =
(X509Certificate) certificateFactory.generateCertificate(certificateIs);
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
keyStore.setCertificateEntry(CERT_ALIAS, caCertificate);
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
return trustManagerFactory;
}
}

View File

@ -2,6 +2,7 @@ package com.appsmith.external.models;
import com.appsmith.external.exceptions.BaseException;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
import com.appsmith.external.helpers.ExceptionHelper;
import com.appsmith.external.plugins.AppsmithPluginErrorUtils;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
@ -56,6 +57,6 @@ public class ActionExecutionResult {
}
public void setErrorInfo(Throwable error) {
this.setErrorInfo(error, null);
this.setErrorInfo(ExceptionHelper.getRootCause(error), null);
}
}

View File

@ -2,53 +2,20 @@ package com.external.utils;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
import com.appsmith.external.helpers.SSLHelper;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.SSLDetails;
import com.arangodb.ArangoDB.Builder;
import org.pf4j.util.StringUtils;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
public class SSLUtils {
private static final String X_509_TYPE = "X.509";
private static final String CERT_ALIAS = "caCert";
private static final String SSL_PROTOCOL = "TLS";
public static SSLContext getSslContext(DatasourceConfiguration datasourceConfiguration) throws CertificateException
, KeyStoreException, IOException, NoSuchAlgorithmException, KeyManagementException {
InputStream certificateIs =
new ByteArrayInputStream(datasourceConfiguration.getConnection().getSsl()
.getCaCertificateFile().getDecodedContent());
CertificateFactory certificateFactory = CertificateFactory.getInstance(X_509_TYPE);
X509Certificate caCertificate =
(X509Certificate) certificateFactory.generateCertificate(certificateIs);
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
keyStore.setCertificateEntry(CERT_ALIAS, caCertificate);
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
SSLContext sslContext = SSLContext.getInstance(SSL_PROTOCOL);
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
return sslContext;
}
public static boolean isCaCertificateAvailable(DatasourceConfiguration datasourceConfiguration) {
if (datasourceConfiguration.getConnection() != null
&& datasourceConfiguration.getConnection().getSsl() != null
@ -97,7 +64,7 @@ public class SSLUtils {
case FILE:
case BASE64_STRING:
try {
builder.sslContext(getSslContext(datasourceConfiguration));
builder.sslContext(SSLHelper.getSslContext(datasourceConfiguration.getConnection().getSsl().getCaCertificateFile()));
} catch (CertificateException | KeyStoreException | IOException | NoSuchAlgorithmException
| KeyManagementException e) {
throw new AppsmithPluginException(

View File

@ -134,6 +134,11 @@
<version>5.2.3.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty-http</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>

View File

@ -5,6 +5,7 @@ import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
import com.appsmith.external.helpers.DataTypeStringUtils;
import com.appsmith.external.helpers.MustacheHelper;
import com.appsmith.external.helpers.SSLHelper;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionExecutionRequest;
import com.appsmith.external.models.ActionExecutionResult;
@ -13,6 +14,8 @@ import com.appsmith.external.models.DatasourceTestResult;
import com.appsmith.external.models.PaginationField;
import com.appsmith.external.models.PaginationType;
import com.appsmith.external.models.Property;
import com.appsmith.external.models.SSLDetails;
import com.appsmith.external.models.UploadedFile;
import com.appsmith.external.plugins.BasePlugin;
import com.appsmith.external.plugins.PluginExecutor;
import com.appsmith.external.plugins.SmartSubstitutionInterface;
@ -40,6 +43,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.client.ClientResponse;
@ -48,6 +52,9 @@ import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.Exceptions;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;
import reactor.netty.tcp.DefaultSslContextSpec;
import reactor.util.function.Tuple2;
import javax.crypto.SecretKey;
@ -59,6 +66,9 @@ import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
@ -281,7 +291,32 @@ public class RestApiPlugin extends BasePlugin {
}
// Initializing webClient to be used for http call
WebClient.Builder webClientBuilder = WebClient.builder();
final ConnectionProvider provider = ConnectionProvider
.builder("rest-api-provider")
.build();
HttpClient httpClient = HttpClient.create(provider)
.secure(sslContextSpec -> {
final DefaultSslContextSpec sslContextSpec1 = DefaultSslContextSpec.forClient();
if (datasourceConfiguration.getConnection() != null &&
datasourceConfiguration.getConnection().getSsl() != null &&
datasourceConfiguration.getConnection().getSsl().getAuthType() == SSLDetails.AuthType.SELF_SIGNED_CERTIFICATE) {
sslContextSpec1.configure(sslContextBuilder -> {
try {
final UploadedFile certificateFile = datasourceConfiguration.getConnection().getSsl().getCertificateFile();
sslContextBuilder.trustManager(SSLHelper.getSslTrustManagerFactory(certificateFile));
} catch (CertificateException | KeyStoreException | IOException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
});
}
sslContextSpec.sslContext(sslContextSpec1);
});
WebClient.Builder webClientBuilder = WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient));
// Adding headers from datasource
if (datasourceConfiguration.getHeaders() != null) {