diff --git a/ballerina/types.bal b/ballerina/types.bal index c1214f7..c3252b5 100644 --- a/ballerina/types.bal +++ b/ballerina/types.bal @@ -33,13 +33,13 @@ public type ConnectionConfig record {| # + resultCode - The operation status of the response # + diagnosticMessage - The diagnostic message from the response # + operationType - The protocol operation type -# + refferal - The referral URIs +# + referral - The referral URIs public type LdapResponse record {| - string matchedDN; + string? matchedDN; Status resultCode; - string diagnosticMessage; - string operationType; - string[] refferal; + string? diagnosticMessage; + string? operationType; + string[]? referral; |}; # LDAP search result type. diff --git a/build.gradle b/build.gradle index 0ba3fa9..25ff235 100644 --- a/build.gradle +++ b/build.gradle @@ -80,6 +80,7 @@ subprojects { task build { dependsOn(":ldap-native:build") dependsOn(":ldap-ballerina:build") + dependsOn(":ldap-examples:build") } def moduleVersion = project.version.replace("-SNAPSHOT", "") diff --git a/examples/access-directory-server/.github/README.md b/examples/access-directory-server/.github/README.md new file mode 120000 index 0000000..dcd056d --- /dev/null +++ b/examples/access-directory-server/.github/README.md @@ -0,0 +1 @@ +../access-directory-server.md \ No newline at end of file diff --git a/examples/access-directory-server/Ballerina.toml b/examples/access-directory-server/Ballerina.toml new file mode 100644 index 0000000..3e3cce5 --- /dev/null +++ b/examples/access-directory-server/Ballerina.toml @@ -0,0 +1,8 @@ +[package] +org = "ballerina" +name = "access_directory_server" +version = "0.1.0" +distribution = "2201.8.0" + +[build-options] +observabilityIncluded = true diff --git a/examples/access-directory-server/access-directory-server.md b/examples/access-directory-server/access-directory-server.md new file mode 100644 index 0000000..011fb38 --- /dev/null +++ b/examples/access-directory-server/access-directory-server.md @@ -0,0 +1,30 @@ +# Employee Directory and Authentication System + +This example demonstrates using the Ballerina LDAP module to integrate with a directory server to manage employees in a corporation. The functionalities include authenticating with the directory server, adding a new user, searching for users, updating user details, and deleting a user. + +## Running an Example + +### 1. Start the directory server + +Run the following docker command to start the LDAP server. + +```sh +cd resources +docker compose up +``` + +### 2. Run the Ballerina project + +Execute the following commands to build an example from the source: + +* To build an example: + + ```bash + bal build + ``` + +* To run an example: + + ```bash + bal run + ``` diff --git a/examples/access-directory-server/main.bal b/examples/access-directory-server/main.bal new file mode 100644 index 0000000..b8e8523 --- /dev/null +++ b/examples/access-directory-server/main.bal @@ -0,0 +1,59 @@ +// 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. + +import ballerina/ldap; +import ballerina/io; + +configurable string hostName = ?; +configurable int port = ?; +configurable string domainName = ?; +configurable string password = ?; +configurable string userDN = ?; + +type Employee record { + string[] objectClass; + string sn; +}; + +public function main() returns error? { + ldap:Client ldapClient = check new ({ + hostName, + port, + domainName, + password + }); + + ldap:Entry employee = { + "objectClass": ["top", "person"], + "sn": "New User" + }; + + // Add a new employee to the directory server. + ldap:LdapResponse addResponse = check ldapClient->add("cn=User,dc=mycompany,dc=com", employee); + io:println(addResponse); + + // Search for the employee. + Employee[] result = check ldapClient->searchWithType("dc=mycompany,dc=com", "(sn=New User)", ldap:SUB); + io:println(result); + + // Update the employee. + ldap:LdapResponse updateResponse = check ldapClient->modify("cn=User,dc=mycompany,dc=com", { "sn": "Updated User" }); + io:println(updateResponse); + + // Delete the employee. + ldap:LdapResponse response = check ldapClient->delete("CN=User,dc=mycompany,dc=com"); + io:println(response); +} diff --git a/examples/access-directory-server/resources/docker-compose.yml b/examples/access-directory-server/resources/docker-compose.yml new file mode 100644 index 0000000..4b78124 --- /dev/null +++ b/examples/access-directory-server/resources/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.7' + +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/examples/build.gradle b/examples/build.gradle new file mode 100644 index 0000000..37acf23 --- /dev/null +++ b/examples/build.gradle @@ -0,0 +1,94 @@ +/* + * 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. + */ + +import org.apache.tools.ant.taskdefs.condition.Os + +description = 'Ballerina - LDAP Examples' + +def ballerinaDist = "${project.rootDir}/target/ballerina-runtime" +def examples = ["access-directory-server"] +def graalvmFlag = "" + +tasks.register('clean') { + examples.forEach { example -> + delete "${projectDir}/${example}/target" + delete "${projectDir}/${example}/Dependencies.toml" + } +} + +task testExamples { + if (project.hasProperty("balGraalVMTest")) { + graalvmFlag = "--graalvm" + } + doLast { + examples.each { example -> + try { + exec { + workingDir project.projectDir + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + commandLine 'cmd', '/c', "${ballerinaDist}/bin/bal.bat test ${graalvmFlag} ${example} && exit %%ERRORLEVEL%%" + } else { + commandLine 'sh', '-c', "${ballerinaDist}/bin/bal test ${graalvmFlag} ${example}" + } + } + } catch (Exception e) { + println("Example '${example}' Build failed: " + e.message) + throw e + } + } + } +} + +task buildExamples { + gradle.taskGraph.whenReady { graph -> + if (graph.hasTask(":ldap-examples:test")) { + buildExamples.enabled = false + } else { + testExamples.enabled = false + } + } + doLast { + examples.each { example -> + try { + exec { + workingDir project.projectDir + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + commandLine 'cmd', '/c', "${ballerinaDist}/bin/bal.bat build ${example} && exit %%ERRORLEVEL%%" + } else { + commandLine 'sh', '-c', "${ballerinaDist}/bin/bal build ${example}" + } + } + } catch (Exception e) { + println("Example '${example}' Build failed: " + e.message) + throw e + } + } + } +} + +task build { + dependsOn buildExamples + +} + +task test { + dependsOn testExamples +} + +buildExamples.dependsOn ":ldap-ballerina:build" +testExamples.dependsOn ":ldap-ballerina:build" diff --git a/examples/library-managment-system/.github/README.md b/examples/library-managment-system/.github/README.md new file mode 120000 index 0000000..db6b39b --- /dev/null +++ b/examples/library-managment-system/.github/README.md @@ -0,0 +1 @@ +../library-managment-system.md \ No newline at end of file diff --git a/examples/library-managment-system/Ballerina.toml b/examples/library-managment-system/Ballerina.toml new file mode 100644 index 0000000..fa7ba38 --- /dev/null +++ b/examples/library-managment-system/Ballerina.toml @@ -0,0 +1,5 @@ +[package] +org = "ballerina" +name = "library_managment_system" +version = "0.1.0" +distribution = "2201.8.0" diff --git a/examples/library-managment-system/library-managment-system.md b/examples/library-managment-system/library-managment-system.md new file mode 100644 index 0000000..48d7655 --- /dev/null +++ b/examples/library-managment-system/library-managment-system.md @@ -0,0 +1,30 @@ +# Employee Directory and Authentication System + +This example demonstrates using the Ballerina LDAP module to integrate with a directory server for managing users in a library system. The functionalities include adding new users, searching for books, and updating books. + +## Running an Example + +### 1. Start the directory server + +Run the following docker command to start the LDAP server. + +```sh +cd resources +docker-compose up +``` + +### 2. Run the Ballerina project + +Execute the following commands to build an example from the source: + +* To build an example: + + ```bash + bal build + ``` + +* To run an example: + + ```bash + bal run + ``` diff --git a/examples/library-managment-system/main.bal b/examples/library-managment-system/main.bal new file mode 100644 index 0000000..a3e2c30 --- /dev/null +++ b/examples/library-managment-system/main.bal @@ -0,0 +1,57 @@ +// 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. + +import ballerina/ldap; +import ballerina/io; + +configurable string hostName = ?; +configurable int port = ?; +configurable string domainName = ?; +configurable string password = ?; + +public function main() returns error? { + ldap:Client ldapClient = check new ({ + hostName, + port, + domainName, + password + }); + + ldap:Entry user = { + "objectClass": "inetOrgPerson", + "sn": "Alice", + "cn": "Alice", + "uid": "alice", + "displayName": "Alice Parker" + }; + + // Add a new user to the library system. + _ = check ldapClient->add("uid=alice,ou=Users,dc=library,dc=org", user); + + // Search for a book. + ldap:SearchResult result = check ldapClient->search("ou=Books,dc=library,dc=org", "(cn=Dracula)", ldap:SUB); + io:println(result.entries); + + ldap:Entry updateBook = { + "member": "uid=alice,ou=Users,dc=library,dc=org" + }; + + // Updates the book. + _ = check ldapClient->modify("cn=Dracula,ou=Books,dc=library,dc=org", updateBook); + + result = check ldapClient->search("ou=Books,dc=library,dc=org", "(cn=Dracula)", ldap:SUB); + io:println(result.entries); +} diff --git a/examples/library-managment-system/resources/bootstrap.ldif b/examples/library-managment-system/resources/bootstrap.ldif new file mode 100644 index 0000000..924de65 --- /dev/null +++ b/examples/library-managment-system/resources/bootstrap.ldif @@ -0,0 +1,43 @@ +dn: ou=Users,dc=library,dc=org +changetype: add +objectClass: organizationalUnit +ou: Users + +dn: ou=Groups,dc=library,dc=org +changetype: add +objectClass: organizationalUnit +ou: Groups + +dn: ou=Books,dc=library,dc=org +changetype: add +objectClass: organizationalUnit +ou: Books + +dn: uid=john,ou=Users,dc=library,dc=org +changetype: add +objectClass: inetOrgPerson +cn: John +givenName: John +sn: John +uid: john +displayName: John Doe +mail: johndoe@gmail.com +userPassword: johndoe@123 + +dn: cn=Users,ou=Groups,dc=library,dc=org +changetype: add +cn: Users +objectClass: groupOfNames +member: uid=john,ou=Users,dc=library,dc=org + +dn: cn=Books,ou=Groups,dc=library,dc=org +changetype: add +cn: Books +objectClass: groupOfNames +member: uid=john,ou=Users,dc=library,dc=org + +dn: cn=Dracula,ou=Books,dc=library,dc=org +changetype: add +objectClass: groupOfNames +cn: Dracula +member: uid=john,ou=Users,dc=library,dc=org diff --git a/examples/library-managment-system/resources/docker-compose.yml b/examples/library-managment-system/resources/docker-compose.yml new file mode 100644 index 0000000..c57735a --- /dev/null +++ b/examples/library-managment-system/resources/docker-compose.yml @@ -0,0 +1,16 @@ +version: '3.7' + +services: + ldap_server: + image: osixia/openldap:latest + container_name: my-openldap-container + environment: + LDAP_ORGANISATION: "Library" + LDAP_DOMAIN: "library.org" + LDAP_ADMIN_PASSWORD: "adminpassword" + ports: + - "389:389" + - "636:636" + command: --copy-service + volumes: + - ./bootstrap.ldif:/container/service/slapd/assets/config/bootstrap/ldif/50-bootstrap.ldif 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 4723e85..e08cded 100644 --- a/native/src/main/java/io/ballerina/lib/ldap/Client.java +++ b/native/src/main/java/io/ballerina/lib/ldap/Client.java @@ -62,6 +62,7 @@ import static io.ballerina.lib.ldap.Utils.LDAP_CONNECTION_CLOSED_ERROR; import static io.ballerina.lib.ldap.Utils.convertObjectGUIDToString; import static io.ballerina.lib.ldap.Utils.convertObjectSidToString; +import static io.ballerina.lib.ldap.Utils.convertToBArray; import static io.ballerina.lib.ldap.Utils.convertToStringArray; import static io.ballerina.lib.ldap.Utils.getSearchScope; @@ -312,7 +313,7 @@ public static BMap generateLdapResponse(LDAPResult ldapResult) response.put(RESULT_STATUS, StringUtils.fromString(ldapResult.getResultCode().getName().toUpperCase(Locale.ROOT))); response.put(DIAGNOSTIC_MESSAGE, StringUtils.fromString(ldapResult.getDiagnosticMessage())); - response.put(REFERRAL, convertToStringArray(ldapResult.getReferralURLs())); + response.put(REFERRAL, convertToBArray(ldapResult.getReferralURLs())); response.put(OPERATION_TYPE, StringUtils.fromString(ldapResult.getOperationType().name())); return response; } diff --git a/native/src/main/java/io/ballerina/lib/ldap/Utils.java b/native/src/main/java/io/ballerina/lib/ldap/Utils.java index 34b1dd5..81857fb 100644 --- a/native/src/main/java/io/ballerina/lib/ldap/Utils.java +++ b/native/src/main/java/io/ballerina/lib/ldap/Utils.java @@ -155,6 +155,17 @@ public static String[] convertToStringArray(Object[] objectArray) { .toArray(String[]::new); } + public static BArray convertToBArray(Object[] objectArray) { + if (Objects.isNull(objectArray)) { + return ValueCreator.createArrayValue(new BString[]{}); + } + BString[] array = Arrays.stream(objectArray) + .filter(Objects::nonNull) + .map(object -> StringUtils.fromString(object.toString())) + .toArray(BString[]::new); + return ValueCreator.createArrayValue(array); + } + public static SearchScope getSearchScope(BString scope) { return switch (scope.getValue()) { case "SUB" -> SearchScope.SUB; diff --git a/settings.gradle b/settings.gradle index 87a72f2..3c21644 100644 --- a/settings.gradle +++ b/settings.gradle @@ -36,10 +36,12 @@ rootProject.name = 'module-ballerina-ldap' include ":checkstyle" include ":${projectName}-native" include ":${projectName}-ballerina" +include ":${projectName}-examples" project(':checkstyle').projectDir = file("build-config${File.separator}checkstyle") project(":${projectName}-native").projectDir = file('native') project(":${projectName}-ballerina").projectDir = file('ballerina') +project(":${projectName}-examples").projectDir = file('examples') gradleEnterprise { buildScan {