diff --git a/.github/workflows/stale_check.yml b/.github/workflows/stale_check.yml deleted file mode 100644 index 8763360..0000000 --- a/.github/workflows/stale_check.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: 'Close stale pull requests' - -on: - schedule: - - cron: '30 19 * * *' - workflow_dispatch: - -jobs: - stale: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v3 - with: - stale-pr-message: 'This PR has been open for more than 15 days with no activity. This will be closed in 3 days unless the `stale` label is removed or commented.' - close-pr-message: 'Closed PR due to inactivity for more than 18 days.' - days-before-pr-stale: 15 - days-before-pr-close: 3 - days-before-issue-stale: -1 - days-before-issue-close: -1 diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 64c2700..1a36948 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,13 +1,13 @@ [package] org = "ballerina" name = "ldap" -version = "1.0.1" +version = "1.1.0" authors = ["Ballerina"] export=["ldap"] keywords = ["ldap"] repository = "https://github.com/ballerina-platform/module-ballerina-ldap" license = ["Apache-2.0"] -distribution = "2201.8.0" +distribution = "2201.9.0" [platform.java21] graalvmCompatible = true @@ -15,8 +15,8 @@ graalvmCompatible = true [[platform.java21.dependency]] groupId = "io.ballerina.lib" artifactId = "ldap-native" -version = "1.0.1" -path = "../native/build/libs/ldap-native-1.0.1.jar" +version = "1.1.0-SNAPSHOT" +path = "../native/build/libs/ldap-native-1.1.0-SNAPSHOT.jar" [[platform.java21.dependency]] groupId = "com.unboundid" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index a69236b..8594c70 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -5,7 +5,19 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.8.0" +distribution-version = "2201.11.0-20241112-214900-6b80ab87" + +[[package]] +org = "ballerina" +name = "crypto" +version = "2.7.3" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] +modules = [ + {org = "ballerina", packageName = "crypto", moduleName = "crypto"} +] [[package]] org = "ballerina" @@ -15,6 +27,26 @@ modules = [ {org = "ballerina", packageName = "jballerina.java", moduleName = "jballerina.java"} ] +[[package]] +org = "ballerina" +name = "lang.__internal" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] + [[package]] org = "ballerina" name = "lang.error" @@ -24,11 +56,18 @@ dependencies = [ {org = "ballerina", name = "jballerina.java"} ] +[[package]] +org = "ballerina" +name = "lang.object" +version = "0.0.0" +scope = "testOnly" + [[package]] org = "ballerina" name = "ldap" -version = "1.0.1" +version = "1.1.0" dependencies = [ + {org = "ballerina", name = "crypto"}, {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "test"} ] @@ -43,9 +82,18 @@ version = "0.0.0" scope = "testOnly" dependencies = [ {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "lang.error"} ] modules = [ {org = "ballerina", packageName = "test", moduleName = "test"} ] +[[package]] +org = "ballerina" +name = "time" +version = "2.6.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + diff --git a/ballerina/build.gradle b/ballerina/build.gradle index 48b2d23..68e0729 100644 --- a/ballerina/build.gradle +++ b/ballerina/build.gradle @@ -104,7 +104,7 @@ task startLdapServer() { if (!stdOut.toString().contains("my-openldap-container")) { println "Starting LDAP server." exec { - commandLine 'sh', '-c', "docker compose -f $project.projectDir/tests/resources/openldap/compose.yml up -d" + commandLine 'sh', '-c', "docker compose -f $project.projectDir/tests/resources/server/compose.yml up -d" standardOutput = stdOut } println stdOut.toString() diff --git a/ballerina/tests/resources/openldap/compose.yml b/ballerina/tests/resources/openldap/compose.yml deleted file mode 100644 index 28059ed..0000000 --- a/ballerina/tests/resources/openldap/compose.yml +++ /dev/null @@ -1,12 +0,0 @@ -services: - ldap_server: - image: osixia/openldap:latest - container_name: my-openldap-container - environment: - LDAP_ORGANISATION: "My Company" - LDAP_DOMAIN: "mycompany.com" - LDAP_ADMIN_PASSWORD: "adminpassword" - ports: - - "389:389" - - "636:636" - command: --copy-service diff --git a/ballerina/tests/resources/openldap/bootstrap.ldif b/ballerina/tests/resources/server/bootstrap.ldif similarity index 100% rename from ballerina/tests/resources/openldap/bootstrap.ldif rename to ballerina/tests/resources/server/bootstrap.ldif diff --git a/ballerina/tests/resources/server/certs/ca.crt b/ballerina/tests/resources/server/certs/ca.crt new file mode 100644 index 0000000..688bcc2 --- /dev/null +++ b/ballerina/tests/resources/server/certs/ca.crt @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIB1TCCAXqgAwIBAgIUBRWemdC/fCcrBTHz38nqiMyh5EkwCgYIKoZIzj0EAwIw +SDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp +c2NvMRQwEgYDVQQDEwtleGFtcGxlLm5ldDAeFw0yMjA3MjYxMTAwMDBaFw0yNzA3 +MjUxMTAwMDBaMEgxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMN +U2FuIEZyYW5jaXNjbzEUMBIGA1UEAxMLZXhhbXBsZS5uZXQwWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARkG6xkTUGFjSyTJCo1Ioq+ESJwuxBCvPFxz2hjYB/rOinH +rdZ/hXvNtylbzxO4KWQPxIMVAARTDE6AXqxffimno0IwQDAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU/XH2rrWqxj17DMkRe213pKgf +EkgwCgYIKoZIzj0EAwIDSQAwRgIhALfiAHDDTAMMWXBdkksB9Vwww8vY4ocnX1gY +TKVyQIhvAiEArQS/Vc+WP3dpXfoBCBatPzCuQakAu4QeWe9WH36OJq4= +-----END CERTIFICATE----- diff --git a/ballerina/tests/resources/server/certs/invalid.crt b/ballerina/tests/resources/server/certs/invalid.crt new file mode 100644 index 0000000..40594fc --- /dev/null +++ b/ballerina/tests/resources/server/certs/invalid.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFdDCCA1wCCQDmVAk1spgkaTANBgkqhkiG9w0BAQUFADB8MQswCQYDVQQGEwJM +SzENMAsGA1UECAwEVGVzdDENMAsGA1UEBwwEVGVzdDENMAsGA1UECgwEVGVzdDEN +MAsGA1UECwwEVGVzdDESMBAGA1UEAwwJbG9jYWxob3N0MR0wGwYJKoZIhvcNAQkB +Fg50ZXN0QGxvY2FsaG9zdDAeFw0yMTA3MTUwNDU1NDVaFw0zMTA3MTMwNDU1NDVa +MHwxCzAJBgNVBAYTAkxLMQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQHDARUZXN0MQ0w +CwYDVQQKDARUZXN0MQ0wCwYDVQQLDARUZXN0MRIwEAYDVQQDDAlsb2NhbGhvc3Qx +HTAbBgkqhkiG9w0BCQEWDnRlc3RAbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAxBw4LW+IX7mb5H9TPoSKh+pBX5vmcBOnyFBTd1Rdli3B +AIBb6BgUTQIQWYcmGuQIt94OiyyDOujTrJmQLoeMfOLIQ/UiXWg0TmD/dn8vRJDB +AoDVKQRpeGVy88J8gaPs7siGnGxHdRBgrINpjLlC+iec8JATlescXKdx0vW2ZNht +t4vpj+QM54QofAE0X0dwi4Y+zfeMV/RFF70kCX85HoPrla2yvJywMsdc7AoS/OKZ +/i65yLr91hAaCEXkU0VDgz9sX7bqBPt/7u7R3VrvbvEmu7H7x+ozrddNIsA3PrU6 +29nraxR89vTXnSxj8yUXWEMsc/j+zYNhod+8oupAXpqLW6naDQ7hpA8M+dGPiyTj +8dpbkeVbAXmJy7iwGIgUGntBVXPhOvYOFwTurxl51OPReioQ+L05sFf0nEmLL4uW +RSENAEYYexMBqCynoi3qvF4Qqq8bhbjKsq/d7U9RlCtLE/0/aVYri4Odk6vOcTGw +yWlXiGNSlEjWxx5YzlDCPYECWc5qu94RfFsotqVd1YSaWAtacLra913XHBuzkXxC +cnbAFmCQXXW3Ef4Aymf5mnLwArM376F1txkbMtj+SwTYsu9cxS/oM4tHxVMfjUHS +xAJ1q9s/yDLA9L3Ny+b2r6NxkYlCOdz7Gz7cDM3r3hKXwyknfIXHMI1rgtcYpycC +AwEAATANBgkqhkiG9w0BAQUFAAOCAgEAOPjaVDyZX/gocSS1ehHxYFLG+WOCVpaN +/k8+YY1lRQDjDS2yxOJ8AC++lV66fY7Uyqwdh4vhnN5TPY3Zx2LHQoyK9eqGRS5y +zSuyweBz8yRgEyHX6I/FGR8Pjal+60/kYNQ7IQfEvjrOTZm3ixxUVyWpcHYqNRyt +bkL2umCSRRDEAQuahV9mSuW/X0qtZcleXpGqF+tNdjxWyVWBQ1BuVHYe6NuyvY2P +ClBuoB5ci2Yl7BQYjbESZSOzxHFExjn13k2aCKTStyUu6eJgttnHSbbXbkWbfQ0I +CKVw8utNppqp8WnPyAN1DexwibfHSd3NIBBIbRE99cBGp0qlm4gaAFz/Br5WvcCB +1I0g5CVzCHVbcGUrju+XTpu5Q1XpANCPbbypvfNm7BRFpqPEKbIT8+RZggreRsDn +SsBRfyQ6We1Y32dxIH7zXkegSZ56BwoJK0NAnRY4jGiFWAfvmJGfOsPR2UvwIh+s +CobykJ1ElgV84aGfeOjyz8PZ2px0VwlQ+1EDPNCn4dAkbcnRFK4Gtr6kZzYLOCVQ +eWZDsQ/5+PZxkzCq3D6PnFEO0tKVODIuEXjPNYzqjhR0GqYCJPdCe10snIzu497H +ZBYJqdpwTbFxyHrk+9+IWtosMZhbfPuR2JDw9EPRvHCwf7gBPSO4HBatRwsUSnpb +BXtJUH8iMO4= +-----END CERTIFICATE----- diff --git a/ballerina/tests/resources/server/certs/server.crt b/ballerina/tests/resources/server/certs/server.crt new file mode 100644 index 0000000..579e474 --- /dev/null +++ b/ballerina/tests/resources/server/certs/server.crt @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB8zCCAZigAwIBAgIUG+X4UsMlTRYdOr5qhM5r42NMdpIwCgYIKoZIzj0EAwIw +SDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp +c2NvMRQwEgYDVQQDEwtleGFtcGxlLm5ldDAeFw0yMjA3MjYxMTA5MDBaFw0zMjA3 +MjMxMTA5MDBaMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASWwlOAru9lwJti +94TSukA4m+T/lmCldTDp+eDQFkuau+troON4kZ1RHxH017csGa3Vm00j5UR6O+SU +43FzAzhvo4GnMIGkMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcD +ATAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQRCBy4ZEhht96frjrz/UXep3oG3jAf +BgNVHSMEGDAWgBT9cfautarGPXsMyRF7bXekqB8SSDAvBgNVHREBAf8EJTAjghBs +ZGFwLmV4YW1wbGUub3Jngglsb2NhbGhvc3SHBH8AAAEwCgYIKoZIzj0EAwIDSQAw +RgIhAKZNvMbRuI6V0g5rMdHjTHc4+toPn3VjkGYMpIr34AUZAiEA3dTYcKVL0Wc+ +4OZhAcfSe2PLDHL/Z7MeM6f/mrMJ8gI= +-----END CERTIFICATE----- diff --git a/ballerina/tests/resources/server/certs/server.key b/ballerina/tests/resources/server/certs/server.key new file mode 100644 index 0000000..35c6525 --- /dev/null +++ b/ballerina/tests/resources/server/certs/server.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIFExcME6ak1+/RxHbHuDQ80y57bXRvB+YFpfXE3fwkKOoAoGCCqGSM49 +AwEHoUQDQgAElsJTgK7vZcCbYveE0rpAOJvk/5ZgpXUw6fng0BZLmrvra6DjeJGd +UR8R9Ne3LBmt1ZtNI+VEejvklONxcwM4bw== +-----END EC PRIVATE KEY----- diff --git a/ballerina/tests/resources/server/certs/truststore.p12 b/ballerina/tests/resources/server/certs/truststore.p12 new file mode 100644 index 0000000..f36d5df Binary files /dev/null and b/ballerina/tests/resources/server/certs/truststore.p12 differ diff --git a/ballerina/tests/resources/server/compose.yml b/ballerina/tests/resources/server/compose.yml new file mode 100644 index 0000000..37e1fe9 --- /dev/null +++ b/ballerina/tests/resources/server/compose.yml @@ -0,0 +1,18 @@ +services: + ldap_server: + image: osixia/openldap:latest + container_name: my-openldap-container + environment: + LDAP_ORGANISATION: "My Company" + LDAP_DOMAIN: "mycompany.com" + LDAP_ADMIN_PASSWORD: "adminpassword" + LDAP_TLS_CRT_FILENAME: "server.crt" + LDAP_TLS_KEY_FILENAME: "server.key" + LDAP_TLS_CA_CRT_FILENAME: "ca.crt" + LDAP_TLS_VERIFY_CLIENT: try + ports: + - "389:389" + - "636:636" + volumes: + - ./certs:/container/service/slapd/assets/certs + command: --copy-service diff --git a/ballerina/tests/test.bal b/ballerina/tests/test.bal index e60647e..d690cb8 100644 --- a/ballerina/tests/test.bal +++ b/ballerina/tests/test.bal @@ -280,3 +280,60 @@ public function testSearchWithInvalidType() returns error? { LdapResponse delete = check ldapClient->delete("CN=Test User1,dc=mycompany,dc=com"); test:assertEquals(delete.resultCode, SUCCESS); } + +@test:Config{} +public function testTlsConnection() returns error? { + ClientSecureSocket clientSecureSocket = { + cert: "tests/resources/server/certs/server.crt", + enable: true + }; + + Client ldapClient = check new ({ + port: 636, + hostName, + password, + domainName, + clientSecureSocket} + ); + + ldapClient->close(); +} + +@test:Config{} +public function testTlsConnectionWithInvalidCert() returns error? { + ClientSecureSocket clientSecureSocket = { + cert: "tests/resources/server/certs/invalid.crt", + enable: true + }; + + Client|Error ldapClient = new ({ + port: 636, + hostName, + password, + domainName, + clientSecureSocket} + ); + + test:assertTrue(ldapClient is Error); +} + +@test:Config{} +public function testTlsConnectionWithTrustStore() returns error? { + ClientSecureSocket clientSecureSocket = { + cert: { + path: "tests/resources/server/certs/truststore.p12", + password: "password" + } + }; + + Client ldapClient = check new ({ + port: 636, + hostName, + password, + domainName, + clientSecureSocket} + ); + + ldapClient->close(); +} + diff --git a/ballerina/types.bal b/ballerina/types.bal index 241356b..37f948f 100644 --- a/ballerina/types.bal +++ b/ballerina/types.bal @@ -14,17 +14,35 @@ // specific language governing permissions and limitations // under the License. +import ballerina/crypto; + # Provides a set of configurations to connect with a directory server. # # + hostName - The host name of the Active Directory server # + port - The port of the Active Directory server # + domainName - The domain name of the Active Directory # + password - The password of the Active Directory +# + clientSecureSocket - Client secure socket configurations public type ConnectionConfig record {| string hostName; int port; string domainName; string password; + ClientSecureSocket clientSecureSocket?; +|}; + + +# Provides configurations for facilitating secure communication with a remote ldap server. +# +# + enable - Enable SSL validation +# + cert - Configurations associated with `crypto:TrustStore` or single certificate file that the client trusts +# + verifyHostName - Enable/disable host name verification +# + tlsVersions - The TLS versions to be used +public type ClientSecureSocket record {| + boolean enable = true; + crypto:TrustStore|string cert?; + boolean verifyHostName = true; + string[] tlsVersions = []; |}; # LDAP response type. diff --git a/build-config/resources/Ballerina.toml b/build-config/resources/Ballerina.toml index 0391423..4673384 100644 --- a/build-config/resources/Ballerina.toml +++ b/build-config/resources/Ballerina.toml @@ -7,7 +7,7 @@ export=["ldap"] keywords = ["ldap"] repository = "https://github.com/ballerina-platform/module-ballerina-ldap" license = ["Apache-2.0"] -distribution = "2201.8.0" +distribution = "2201.9.0" [platform.java21] graalvmCompatible = true diff --git a/build.gradle b/build.gradle index f380c9b..b2fec17 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,8 @@ plugins { id "net.researchgate.release" } +ext.stdlibCryptoVersion = project.stdlibCryptoVersion + description = 'Ballerina - LDAP' allprojects { @@ -74,8 +76,11 @@ subprojects { jbalTools ("org.ballerinalang:jballerina-tools:${ballerinaLangVersion}") { transitive = false } + /* Standard libraries */ ballerinaStdLibs "io.ballerina.stdlib:io-ballerina:${stdlibIoVersion}" + ballerinaStdLibs "io.ballerina.stdlib:crypto-ballerina:${stdlibCryptoVersion}" + ballerinaStdLibs "io.ballerina.stdlib:time-ballerina:${stdlibTimeVersion}" } } diff --git a/gradle.properties b/gradle.properties index 10e70fc..a5faf9e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ org.gradle.caching=true group=io.ballerina.lib -version=1.0.2-SNAPSHOT -ballerinaLangVersion=2201.10.0-20241011-161100-51978649 +version=1.1.0-SNAPSHOT +ballerinaLangVersion=2201.11.0-20241112-214900-6b80ab87 checkstylePluginVersion=10.12.0 spotbugsPluginVersion=6.0.18 @@ -10,5 +10,8 @@ downloadPluginVersion=5.4.0 releasePluginVersion=2.8.0 ballerinaGradlePluginVersion=2.2.4 -stdlibIoVersion=1.6.2-20240928-084100-656404f unboundIdLdapVersion=7.0.0 + +stdlibIoVersion=1.6.2-20240928-084100-656404f +stdlibCryptoVersion=2.7.3-20241113-081400-d015a39 +stdlibTimeVersion=2.6.0-20241113-073800-201b904 diff --git a/native/src/main/java/io/ballerina/lib/ldap/Client.java b/native/src/main/java/io/ballerina/lib/ldap/Client.java index 1eb4d24..886411f 100644 --- a/native/src/main/java/io/ballerina/lib/ldap/Client.java +++ b/native/src/main/java/io/ballerina/lib/ldap/Client.java @@ -24,6 +24,7 @@ import com.unboundid.ldap.sdk.DeleteRequest; import com.unboundid.ldap.sdk.Entry; import com.unboundid.ldap.sdk.LDAPConnection; +import com.unboundid.ldap.sdk.LDAPConnectionOptions; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPResult; import com.unboundid.ldap.sdk.Modification; @@ -36,9 +37,16 @@ import com.unboundid.ldap.sdk.SearchResultListener; import com.unboundid.ldap.sdk.SearchScope; import com.unboundid.util.Base64; +import com.unboundid.util.ssl.AggregateTrustManager; +import com.unboundid.util.ssl.HostNameSSLSocketVerifier; +import com.unboundid.util.ssl.JVMDefaultTrustManager; +import com.unboundid.util.ssl.PEMFileTrustManager; +import com.unboundid.util.ssl.SSLUtil; +import com.unboundid.util.ssl.TrustStoreTrustManager; +import io.ballerina.lib.ldap.ssl.SSLConfig; import io.ballerina.runtime.api.Environment; -import io.ballerina.runtime.api.TypeTags; import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.types.TypeTags; import io.ballerina.runtime.api.utils.StringUtils; import io.ballerina.runtime.api.utils.TypeUtils; import io.ballerina.runtime.api.utils.ValueUtils; @@ -49,6 +57,8 @@ import io.ballerina.runtime.api.values.BString; import io.ballerina.runtime.api.values.BTypedesc; +import java.security.GeneralSecurityException; +import java.security.KeyStoreException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -77,6 +87,7 @@ public final class Client { public static final BString PORT = StringUtils.fromString("port"); public static final BString DOMAIN_NAME = StringUtils.fromString("domainName"); public static final BString PASSWORD = StringUtils.fromString("password"); + public static final BString CLIENT_SECURE_SOCKET = StringUtils.fromString("clientSecureSocket"); public static final String NATIVE_CLIENT = "client"; public static final String LDAP_RESPONSE = "LdapResponse"; public static final BString REFERRAL = StringUtils.fromString("referral"); @@ -84,6 +95,21 @@ public final class Client { public static final String OBJECT_GUID = "objectGUID"; public static final String OBJECT_SID = "objectSid"; + //Socket config + private static final BString SECURE_SOCKET_CONFIG_ENABLE_TLS = StringUtils.fromString("enable"); + private static final BString VERIFY_HOSTNAME = StringUtils.fromString("verifyHostName"); + private static final BString TLS_VERSIONS = StringUtils.fromString("tlsVersions"); + private static final BString SECURE_SOCKET_CONFIG_TRUSTSTORE_FILE_PATH = StringUtils.fromString("path"); + private static final BString SECURE_SOCKET_CONFIG_TRUSTSTORE_PASSWORD = StringUtils.fromString("password"); + private static final BString SECURE_SOCKET_CONFIG_CERT = StringUtils.fromString("cert"); + public static final String PKCS_12 = "PKCS12"; + public static final String PEM = "PEM"; + public static final String TRUST_STORE_INITIALIZATION_ERROR = "Error occurred while initializing trust store"; + public static final String UNSUPPORTED_TRUST_STORE_TYPE_ERROR = "Unsupported trust store type"; + public static final String EMPTY_TRUST_STORE_FILE_PATH_ERROR = "Truststore file path cannot be empty"; + public static final String EMPTY_TRUST_STORE_PASSWORD_ERROR = "Truststore password cannot be empty"; + public static final String EMPTY_CERTIFICATE_FILE_PATH_ERROR = "Certificate file path cannot be empty"; + private Client() { } @@ -92,15 +118,110 @@ public static BError initLdapConnection(BObject ldapClient, BMap secureSocketConfig = (BMap) config + .getMapValue(CLIENT_SECURE_SOCKET); try { - LDAPConnection ldapConnection = new LDAPConnection(hostName, port, domainName, password); - ldapClient.addNativeData(NATIVE_CLIENT, ldapConnection); - } catch (LDAPException e) { + if (Objects.nonNull(secureSocketConfig) && isClientSecurityConfigured(secureSocketConfig)) { + SSLConfig sslConfig = populateSSLConfig(secureSocketConfig); + AggregateTrustManager trustManager = buildAggregatedTrustManager(sslConfig); + + SSLUtil sslUtil = new SSLUtil(trustManager); + + if (sslConfig.getTLSVersions().isEmpty()) { + SSLUtil.setDefaultSSLProtocol(SSLUtil.SSL_PROTOCOL_TLS_1_2); + } else { + SSLUtil.setEnabledSSLProtocols(sslConfig.getTLSVersions()); + } + + LDAPConnectionOptions connectionOptions = new LDAPConnectionOptions(); + + connectionOptions.setSSLSocketVerifier( + new HostNameSSLSocketVerifier(sslConfig.getVerifyHostnames())); + + LDAPConnection ldapConnection = new LDAPConnection(sslUtil.createSSLSocketFactory(), + connectionOptions, hostName, port, domainName, password); + + ldapClient.addNativeData(NATIVE_CLIENT, ldapConnection); + + } else { + LDAPConnection ldapConnection = new LDAPConnection(hostName, port, domainName, password); + ldapClient.addNativeData(NATIVE_CLIENT, ldapConnection); + } + } catch (LDAPException | GeneralSecurityException e) { return Utils.createError(e.getMessage(), e); } return null; } + private static SSLConfig populateSSLConfig(BMap secureSocketConfig) { + SSLConfig sslConfig = new SSLConfig(); + + Object cert = secureSocketConfig.get(SECURE_SOCKET_CONFIG_CERT); + evaluateCertField(cert, sslConfig); + + sslConfig.setVerifyHostnames(secureSocketConfig.getBooleanValue(VERIFY_HOSTNAME)); + + BArray tlsVersions = (BArray) secureSocketConfig.get(TLS_VERSIONS); + + if (Objects.nonNull(tlsVersions)) { + List tlsVersionsList = Arrays.stream(tlsVersions.getStringArray()) + .collect(ArrayList::new, ArrayList::add, ArrayList::addAll); + sslConfig.setTLSVersions(tlsVersionsList); + } + return sslConfig; + } + + private static boolean isClientSecurityConfigured(BMap secureSocketConfig) { + return secureSocketConfig.get(Client.SECURE_SOCKET_CONFIG_ENABLE_TLS) != null; + } + + private static void evaluateCertField(Object cert, SSLConfig sslConfiguration) { + if (cert instanceof BMap) { + BMap trustStore = (BMap) cert; + String trustStoreFile = trustStore.getStringValue(SECURE_SOCKET_CONFIG_TRUSTSTORE_FILE_PATH).getValue(); + String trustStorePassword = trustStore.getStringValue(SECURE_SOCKET_CONFIG_TRUSTSTORE_PASSWORD).getValue(); + if (trustStoreFile.isBlank()) { + throw new IllegalArgumentException(EMPTY_TRUST_STORE_FILE_PATH_ERROR); + } + if (trustStorePassword.isBlank()) { + throw new IllegalArgumentException(EMPTY_TRUST_STORE_PASSWORD_ERROR); + } + sslConfiguration.setTrustStoreFile(trustStoreFile); + sslConfiguration.setTrustStorePass(trustStorePassword); + sslConfiguration.setTLSStoreType(PKCS_12); + } else { + String certFile = ((BString) cert).getValue(); + if (certFile.isBlank()) { + throw new IllegalArgumentException(EMPTY_CERTIFICATE_FILE_PATH_ERROR); + } + sslConfiguration.setTrustStoreFile(certFile); + sslConfiguration.setTLSStoreType(PEM); + } + } + + private static AggregateTrustManager buildAggregatedTrustManager(SSLConfig sslConfiguration) { + if (sslConfiguration.getTLSStoreType().equals(PEM)) { + try { + PEMFileTrustManager pemFileTrustManager = new PEMFileTrustManager( + sslConfiguration.getTrustStore()); + return new AggregateTrustManager(false, + JVMDefaultTrustManager.getInstance(), + pemFileTrustManager); + } catch (KeyStoreException e) { + throw new IllegalArgumentException(TRUST_STORE_INITIALIZATION_ERROR + e.getMessage()); + } + } else if (sslConfiguration.getTLSStoreType().equals(PKCS_12)) { + TrustStoreTrustManager trustStoreManager = new TrustStoreTrustManager(sslConfiguration.getTrustStore(), + sslConfiguration.getTrustStorePass().toCharArray(), + sslConfiguration.getTLSStoreType(), true); + return new AggregateTrustManager(false, + JVMDefaultTrustManager.getInstance(), + trustStoreManager); + } else { + throw new IllegalArgumentException(UNSUPPORTED_TRUST_STORE_TYPE_ERROR); + } + } + public static Object add(Environment env, BObject ldapClient, BString dN, BMap entry) { return env.yieldAndRun(() -> { CompletableFuture future = new CompletableFuture<>(); diff --git a/native/src/main/java/io/ballerina/lib/ldap/ssl/SSLConfig.java b/native/src/main/java/io/ballerina/lib/ldap/ssl/SSLConfig.java new file mode 100644 index 0000000..328ff01 --- /dev/null +++ b/native/src/main/java/io/ballerina/lib/ldap/ssl/SSLConfig.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com) + * + * WSO2 LLC. licenses this file to you 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. + */ +package io.ballerina.lib.ldap.ssl; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A class that encapsulates SSLContext configuration. + */ + +public class SSLConfig { + + private File trustStore; + private String trustStorePass; + private String tlsStoreType; + private Boolean verifyHostnames; + private List tlsVersions; + + public SSLConfig() {} + + public File getTrustStore() { + return trustStore; + } + + public SSLConfig setTrustStore(File trustStore) { + this.trustStore = trustStore; + return this; + } + + public void setTrustStoreFile(String trustStoreFile) { + this.trustStore = new File(trustStoreFile); + } + + public String getTrustStorePass() { + return trustStorePass; + } + + public SSLConfig setTrustStorePass(String trustStorePass) { + this.trustStorePass = trustStorePass; + return this; + } + + public String getTLSStoreType() { + return tlsStoreType; + } + + public void setTLSStoreType(String tlsStoreType) { + this.tlsStoreType = tlsStoreType; + } + + public Boolean getVerifyHostnames() { + return verifyHostnames; + } + + public void setVerifyHostnames(Boolean verifyHostnames) { + this.verifyHostnames = verifyHostnames; + } + + public List getTLSVersions() { + return Collections.unmodifiableList((List) tlsVersions); + } + + public void setTLSVersions(List tlsVersions) { + this.tlsVersions = new ArrayList<>(tlsVersions); + } +} +