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:
parent
00a7647590
commit
1868675349
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
14
app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/ExceptionHelper.java
vendored
Normal file
14
app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/ExceptionHelper.java
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
53
app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SSLHelper.java
vendored
Normal file
53
app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/SSLHelper.java
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user