From e5544f999a8c2535fec7ae07b87f859f47093970 Mon Sep 17 00:00:00 2001 From: jzonthemtn Date: Fri, 17 Nov 2023 09:31:51 -0500 Subject: [PATCH] Initial commit. --- .github/workflows/build.yaml | 18 + .github/workflows/deploy.yaml | 27 ++ .gitignore | 15 + LICENSE | 202 +++++++++++ README.md | 49 +++ pom.xml | 204 +++++++++++ .../ai/philterd/airlock/AbstractClient.java | 8 + .../ai/philterd/airlock/AirlockClient.java | 333 ++++++++++++++++++ .../philterd/airlock/model/ApplyResponse.java | 90 +++++ .../philterd/airlock/model/Explanation.java | 49 +++ .../philterd/airlock/model/FilteredSpan.java | 111 ++++++ .../java/ai/philterd/airlock/model/Span.java | 158 +++++++++ .../airlock/model/StatusResponse.java | 50 +++ .../model/exceptions/ClientException.java | 9 + .../ServiceUnavailableException.java | 9 + .../exceptions/UnauthorizedException.java | 9 + .../airlock/services/AirlockService.java | 50 +++ .../test/philter/AirlockClientTest.java | 181 ++++++++++ src/test/resources/default2.json | 15 + src/test/resources/log4j2.xml | 13 + 20 files changed, 1600 insertions(+) create mode 100644 .github/workflows/build.yaml create mode 100644 .github/workflows/deploy.yaml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/ai/philterd/airlock/AbstractClient.java create mode 100644 src/main/java/ai/philterd/airlock/AirlockClient.java create mode 100644 src/main/java/ai/philterd/airlock/model/ApplyResponse.java create mode 100644 src/main/java/ai/philterd/airlock/model/Explanation.java create mode 100644 src/main/java/ai/philterd/airlock/model/FilteredSpan.java create mode 100644 src/main/java/ai/philterd/airlock/model/Span.java create mode 100644 src/main/java/ai/philterd/airlock/model/StatusResponse.java create mode 100644 src/main/java/ai/philterd/airlock/model/exceptions/ClientException.java create mode 100644 src/main/java/ai/philterd/airlock/model/exceptions/ServiceUnavailableException.java create mode 100644 src/main/java/ai/philterd/airlock/model/exceptions/UnauthorizedException.java create mode 100644 src/main/java/ai/philterd/airlock/services/AirlockService.java create mode 100644 src/test/java/com/mtnfog/test/philter/AirlockClientTest.java create mode 100644 src/test/resources/default2.json create mode 100644 src/test/resources/log4j2.xml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..1c94449 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,18 @@ +name: Build Artifacts +on: [push, workflow_dispatch] +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: 11 + distribution: adopt + architecture: x64 + - name: Build + run: mvn --batch-mode --update-snapshots package \ No newline at end of file diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..e830b18 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,27 @@ +name: Deploy Artifacts +on: + push: + branches: + - main +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: 11 + distribution: adopt + architecture: x64 + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + - name: Build and Publish to Maven Central + run: mvn --batch-mode --update-snapshots package deploy + env: + MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b03a1a --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.metadata +.cache +.checkstyle +bin/ +.settings/ +target/ +.classpath +.project +.cache-main +.cache-tests +*.log +.idea/ +*.iml +obj/ +.vscode/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a2dd2b2 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# Airlock SDK for Java + +The **Airlock SDK for Java** is an API client for [Airlock](https://www.philterd.ai/airlock). Airlock is an AI policy layer to prevent the disclosure of sensitive information, such as PII and PHI, in your AI applications. + +Refer to the [Airlock API](https://docs.philterd.ai/airlock/latest/api.html) documentation for details on the methods available. + +## Example Usage + +To apply a policy to text: + +``` +AirlockClient client = new AirlockClient.AirlockClientBuilder().withEndpoint("https://127.0.0.1:8080").build(); +ApplyResponse applyResponse = client.apply(text); +``` + +## Dependency + +Release dependencies are available in Maven Central. + +``` + + com.mtnfog + airlock-sdk-java + 1.0.0 + +``` + +Snapshot dependencies are available in the Maven Central Snapshot Repository by adding the repository to your `pom.xml`: + +``` + + snapshots + https://s01.oss.sonatype.org/content/repositories/snapshots + false + true + +``` + +## Release History + +* 1.0.0: + * Initial release. + +## License + +This project is licensed under the Apache License, version 2.0. + +Copyright 2023 Philterd, LLC. +Philter is a registered trademark of Philterd, LLC. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..9cef6a1 --- /dev/null +++ b/pom.xml @@ -0,0 +1,204 @@ + + + 4.0.0 + ai.philterd + airlock-sdk-java + airlock-sdk-java + 1.0.0-SNAPSHOT + jar + Java SDK client for Airlock + https://github.com/philterd/airlock-sdk-java + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + scm:git:https://github.com/philterd/airlock-sdk-java.git + scm:git:git@github.com:philterd/airlock-sdk-java.git + https://github.com/philterd/airlock-sdk-java.git + HEAD + + + + Apache License, Version 2 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + Philterd + support@philterd.ai + Philterd, LLC + https://www.philterd.ai + + + + 1.15 + 4.4 + 2.8.0 + 3.11 + 4.13.2 + 4.5.13 + 2.17.2 + 3.14.9 + 2.9.0 + 5.3.0 + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + false + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-site-plugin + 3.8.2 + + + + + ${project.basedir}/src/test/resources + + + + + + release + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + ${javadoc.opts} + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.6 + true + + ossrh + https://oss.sonatype.org/ + false + + + + + + + + + com.squareup.retrofit2 + retrofit + ${retrofit.version} + + + com.squareup.retrofit2 + converter-scalars + ${retrofit.version} + + + org.apache.commons + commons-lang3 + ${commons.lang3.version} + + + commons-codec + commons-codec + ${commons.codec.version} + + + org.apache.httpcomponents + httpclient + ${httpclient.version} + + + org.apache.commons + commons-collections4 + ${commons.collections.version} + + + commons-io + commons-io + ${commons.io.version} + + + io.github.hakky54 + sslcontext-kickstart + ${sslcontext.version} + + + com.squareup.retrofit2 + converter-gson + ${retrofit.version} + + + com.squareup.okhttp3 + okhttp + ${okhttp.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + test + + + junit + junit + ${junit.version} + test + + + diff --git a/src/main/java/ai/philterd/airlock/AbstractClient.java b/src/main/java/ai/philterd/airlock/AbstractClient.java new file mode 100644 index 0000000..8f7ab3f --- /dev/null +++ b/src/main/java/ai/philterd/airlock/AbstractClient.java @@ -0,0 +1,8 @@ +package ai.philterd.airlock; + +public abstract class AbstractClient { + + public static final String UNAUTHORIZED = "Unauthorized"; + public static final String SERVICE_UNAVAILABLE = "Service unavailable"; + +} diff --git a/src/main/java/ai/philterd/airlock/AirlockClient.java b/src/main/java/ai/philterd/airlock/AirlockClient.java new file mode 100644 index 0000000..655b9d0 --- /dev/null +++ b/src/main/java/ai/philterd/airlock/AirlockClient.java @@ -0,0 +1,333 @@ +/******************************************************************************* + * Copyright 2023 Philterd, LLC + * + * 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. + ******************************************************************************/ +package ai.philterd.airlock; + +import ai.philterd.airlock.model.*; +import ai.philterd.airlock.model.exceptions.ClientException; +import ai.philterd.airlock.model.exceptions.ServiceUnavailableException; +import ai.philterd.airlock.model.exceptions.UnauthorizedException; +import ai.philterd.airlock.services.AirlockService; +import nl.altindag.sslcontext.SSLFactory; +import okhttp3.*; +import org.apache.commons.lang3.StringUtils; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.converter.scalars.ScalarsConverterFactory; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Client class for Airlock's API. See https://www.philterd.ai. + */ +public class AirlockClient extends AbstractClient { + + public static final int DEFAULT_TIMEOUT_SEC = 30; + public static final int DEFAULT_MAX_IDLE_CONNECTIONS = 20; + public static final int DEFAULT_KEEP_ALIVE_DURATION_MS = 30 * 1000; + + private AirlockService service; + + public static class AirlockClientBuilder { + + private String endpoint; + private OkHttpClient.Builder okHttpClientBuilder; + private long timeout = DEFAULT_TIMEOUT_SEC; + private int maxIdleConnections = DEFAULT_MAX_IDLE_CONNECTIONS; + private int keepAliveDurationMs = DEFAULT_KEEP_ALIVE_DURATION_MS; + private String keystore; + private String keystorePassword; + private String truststore; + private String truststorePassword; + + public AirlockClientBuilder withEndpoint(String endpoint) { + this.endpoint = endpoint; + return this; + } + + public AirlockClientBuilder withOkHttpClientBuilder(OkHttpClient.Builder okHttpClientBuilder) { + this.okHttpClientBuilder = okHttpClientBuilder; + return this; + } + + public AirlockClientBuilder withTimeout(long timeout) { + this.timeout = timeout; + return this; + } + + public AirlockClientBuilder withMaxIdleConnections(int maxIdleConnections) { + this.maxIdleConnections = maxIdleConnections; + return this; + } + + public AirlockClientBuilder withKeepAliveDurationMs(int keepAliveDurationMs) { + this.keepAliveDurationMs = keepAliveDurationMs; + return this; + } + + public AirlockClientBuilder withSslConfiguration(String keystore, String keystorePassword, String truststore, String truststorePassword) { + this.keystore = keystore; + this.keystorePassword = keystorePassword; + this.truststore = truststore; + this.truststorePassword = truststorePassword; + return this; + } + + public AirlockClient build() throws Exception { + return new AirlockClient(endpoint, okHttpClientBuilder, timeout, maxIdleConnections, keepAliveDurationMs, keystore, + keystorePassword, truststore, truststorePassword); + } + + } + + private AirlockClient(String endpoint, OkHttpClient.Builder okHttpClientBuilder, long timeout, int maxIdleConnections, int keepAliveDurationMs, + String keystore, String keystorePassword, String truststore, String truststorePassword) { + + if(okHttpClientBuilder == null) { + + okHttpClientBuilder = new OkHttpClient.Builder() + .connectTimeout(timeout, TimeUnit.SECONDS) + .writeTimeout(timeout, TimeUnit.SECONDS) + .readTimeout(timeout, TimeUnit.SECONDS) + .connectionPool(new ConnectionPool(maxIdleConnections, keepAliveDurationMs, TimeUnit.MILLISECONDS)); + + } + + if(StringUtils.isNotEmpty(keystore)) { + configureSSL(okHttpClientBuilder, keystore, keystorePassword, truststore, truststorePassword); + } + + final OkHttpClient okHttpClient = okHttpClientBuilder.build(); + + final Retrofit.Builder builder = new Retrofit.Builder() + .baseUrl(endpoint) + .client(okHttpClient) + .addConverterFactory(ScalarsConverterFactory.create()) + .addConverterFactory(GsonConverterFactory.create()); + + final Retrofit retrofit = builder.build(); + + service = retrofit.create(AirlockService.class); + + } + + private void configureSSL(final OkHttpClient.Builder okHttpClientBuilder, String keystore, String keystorePassword, + String truststore, String truststorePassword) { + + final SSLFactory sslFactory = SSLFactory.builder() + .withIdentityMaterial(Paths.get(keystore), keystorePassword.toCharArray()) + .withTrustMaterial(Paths.get(truststore), truststorePassword.toCharArray()) + .build(); + + okHttpClientBuilder.sslSocketFactory(sslFactory.getSslSocketFactory(), sslFactory.getTrustManager().get()); + + } + + /** + * Send text to Philter to be filtered and get an explanation. + * @param context The context. Contexts can be used to group text based on some arbitrary property. + * @param documentId The document ID. Leave empty for Philter to assign a document ID to the request. + * @param policyName The name of the policy to apply to the text. + * @param text The text to be filtered. + * @return The filter {@link ApplyResponse}. + * @throws IOException Thrown if the request can not be completed. + */ + public ApplyResponse explain(String context, String documentId, String policyName, String text) throws IOException { + + final Response response = service.apply(context, policyName, text).execute(); + + if(response.isSuccessful()) { + + return response.body(); + + } else { + + if(response.code() == 401) { + + throw new UnauthorizedException(UNAUTHORIZED); + + } else if(response.code() == 503) { + + throw new ServiceUnavailableException(SERVICE_UNAVAILABLE); + + } else { + + throw new ClientException("Unknown error: HTTP " + response.code()); + + } + + } + + } + + /** + * Gets the status of Philter. + * @return A {@link StatusResponse} object. + * @throws IOException Thrown if the request can not be completed. + */ + public StatusResponse status() throws IOException { + + final Response response = service.status().execute(); + + if(response.isSuccessful()) { + + return response.body(); + + } else { + + if(response.code() == 503) { + + throw new ServiceUnavailableException(SERVICE_UNAVAILABLE); + + } else { + + throw new ClientException("Unknown error: HTTP " + response.code()); + + } + + } + + } + + /** + * Gets a list of policy names. + * @return A list of policy names. + * @throws IOException Thrown if the call not be executed. + */ + public List getPolicies() throws IOException { + + final Response> response = service.Policy().execute(); + + if(response.isSuccessful()) { + + return response.body(); + + } else { + + if(response.code() == 401) { + + throw new UnauthorizedException(UNAUTHORIZED); + + } else if(response.code() == 503) { + + throw new ServiceUnavailableException(SERVICE_UNAVAILABLE); + + } else { + + throw new ClientException("Unknown error: HTTP " + response.code()); + + } + + } + + } + + /** + * Gets the content of a policy. + * @param policyName The name of the policy to get. + * @return The content of the policy. + * @throws IOException Thrown if the call not be executed. + */ + public String Policy(String policyName) throws IOException { + + final Response response = service.Policy(policyName).execute(); + + if(response.isSuccessful()) { + + return response.body(); + + } else { + + if(response.code() == 401) { + + throw new UnauthorizedException(UNAUTHORIZED); + + } else if(response.code() == 503) { + + throw new ServiceUnavailableException(SERVICE_UNAVAILABLE); + + } else { + + throw new ClientException("Unknown error: HTTP " + response.code()); + + } + + } + + } + + /** + * Saves (or overwrites) the policy. + * @param json The body of the policy. + * @throws IOException Thrown if the call not be executed. + */ + public void savePolicy(String json) throws IOException { + + final Response response = service.savePolicy(json).execute(); + + if(!response.isSuccessful()) { + + if(response.code() == 401) { + + throw new UnauthorizedException(UNAUTHORIZED); + + } else if(response.code() == 503) { + + throw new ServiceUnavailableException(SERVICE_UNAVAILABLE); + + } else { + + throw new ClientException("Unknown error: HTTP " + response.code()); + + } + + } + + } + + /** + * Deletes a policy. + * @param policyName The name of the policy to delete. + * @throws IOException Thrown if the call not be executed. + */ + public void deletePolicy(String policyName) throws IOException { + + final Response response = service.deletePolicy(policyName).execute(); + + if(!response.isSuccessful()) { + + if(response.code() == 401) { + + throw new UnauthorizedException(UNAUTHORIZED); + + } else if(response.code() == 503) { + + throw new ServiceUnavailableException(SERVICE_UNAVAILABLE); + + } else { + + throw new ClientException("Unknown error: HTTP " + response.code()); + + } + + } + + } + +} diff --git a/src/main/java/ai/philterd/airlock/model/ApplyResponse.java b/src/main/java/ai/philterd/airlock/model/ApplyResponse.java new file mode 100644 index 0000000..cc3586e --- /dev/null +++ b/src/main/java/ai/philterd/airlock/model/ApplyResponse.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright 2023 Philterd, LLC + * + * 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. + ******************************************************************************/ +package ai.philterd.airlock.model; + +import com.google.gson.Gson; +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +/** + * The response from Philter resulting from an explain request. + */ +public class ApplyResponse { + + @Expose + @SerializedName("filteredText") + private String filteredText; + + @Expose + @SerializedName("context") + private String context; + + @Expose + @SerializedName("documentId") + private String documentId; + + @Expose + @SerializedName("explanation") + private Explanation explanation; + + public int hashCode() { + return (new HashCodeBuilder(17, 37)).append(this.filteredText).append(this.context).append(this.documentId).toHashCode(); + } + + public String toString() { + Gson gson = new Gson(); + return gson.toJson(this); + } + + public boolean equals(Object o) { + return EqualsBuilder.reflectionEquals(this, o, new String[0]); + } + + public String getFilteredText() { + return filteredText; + } + + public void setFilteredText(String filteredText) { + this.filteredText = filteredText; + } + + public String getContext() { + return context; + } + + public void setContext(String context) { + this.context = context; + } + + public String getDocumentId() { + return documentId; + } + + public void setDocumentId(String documentId) { + this.documentId = documentId; + } + + public Explanation getExplanation() { + return explanation; + } + + public void setExplanation(Explanation explanation) { + this.explanation = explanation; + } + +} diff --git a/src/main/java/ai/philterd/airlock/model/Explanation.java b/src/main/java/ai/philterd/airlock/model/Explanation.java new file mode 100644 index 0000000..3c60f2a --- /dev/null +++ b/src/main/java/ai/philterd/airlock/model/Explanation.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright 2023 Philterd, LLC + * + * 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. + ******************************************************************************/ +package ai.philterd.airlock.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class Explanation { + + @Expose + @SerializedName("appliedSpans") + private List appliedSpans; + + @Expose + @SerializedName("ignoredSpans") + private List ignoredSpans; + + public List getAppliedSpans() { + return appliedSpans; + } + + public void setAppliedSpans(List appliedSpans) { + this.appliedSpans = appliedSpans; + } + + public List getIgnoredSpans() { + return ignoredSpans; + } + + public void setIgnoredSpans(List ignoredSpans) { + this.ignoredSpans = ignoredSpans; + } + +} diff --git a/src/main/java/ai/philterd/airlock/model/FilteredSpan.java b/src/main/java/ai/philterd/airlock/model/FilteredSpan.java new file mode 100644 index 0000000..03ebcb8 --- /dev/null +++ b/src/main/java/ai/philterd/airlock/model/FilteredSpan.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * Copyright 2023 Philterd, LLC + * + * 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. + ******************************************************************************/ +package ai.philterd.airlock.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +/** + * A section of text that was identified by Philter as sensitive information + * and filtered from the input text. + */ +public class FilteredSpan { + + @Expose + @SerializedName("characterStart") + private int characterStart; + + @Expose + @SerializedName("characterEnd") + private int characterEnd; + + @Expose + @SerializedName("filterType") + private String filterType; + + @Expose + @SerializedName("context") + private String context; + + @Expose + @SerializedName("documentId") + private String documentId; + + @Expose + @SerializedName("confidence") + private double confidence; + + @Expose + @SerializedName("replacement") + private String replacement; + + public int getCharacterStart() { + return characterStart; + } + + public void setCharacterStart(int characterStart) { + this.characterStart = characterStart; + } + + public int getCharacterEnd() { + return characterEnd; + } + + public void setCharacterEnd(int characterEnd) { + this.characterEnd = characterEnd; + } + + public String getFilterType() { + return filterType; + } + + public void setFilterType(String filterType) { + this.filterType = filterType; + } + + public String getContext() { + return context; + } + + public void setContext(String context) { + this.context = context; + } + + public String getDocumentId() { + return documentId; + } + + public void setDocumentId(String documentId) { + this.documentId = documentId; + } + + public double getConfidence() { + return confidence; + } + + public void setConfidence(double confidence) { + this.confidence = confidence; + } + + public String getReplacement() { + return replacement; + } + + public void setReplacement(String replacement) { + this.replacement = replacement; + } + +} diff --git a/src/main/java/ai/philterd/airlock/model/Span.java b/src/main/java/ai/philterd/airlock/model/Span.java new file mode 100644 index 0000000..54450da --- /dev/null +++ b/src/main/java/ai/philterd/airlock/model/Span.java @@ -0,0 +1,158 @@ +/******************************************************************************* + * Copyright 2023 Philterd, LLC + * + * 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. + ******************************************************************************/ +package ai.philterd.airlock.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +/** + * Identifies a section of text that Philter found to contain sensitive information. + */ +public class Span { + + @Expose + @SerializedName("id") + private String id; + + @Expose + @SerializedName("characterStart") + private int characterStart; + + @Expose + @SerializedName("characterEnd") + private int characterEnd; + + @Expose + @SerializedName("filterType") + private String filterType; + + @Expose + @SerializedName("context") + private String context; + + @Expose + @SerializedName("documentId") + private String documentId; + + @Expose + @SerializedName("confidence") + private double confidence; + + @Expose + @SerializedName("text") + private String text; + + @Expose + @SerializedName("replacement") + private String replacement; + + @Expose + @SerializedName("salt") + private String salt; + + @Expose + @SerializedName("ignored") + private boolean ignored; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public int getCharacterStart() { + return characterStart; + } + + public void setCharacterStart(int characterStart) { + this.characterStart = characterStart; + } + + public int getCharacterEnd() { + return characterEnd; + } + + public void setCharacterEnd(int characterEnd) { + this.characterEnd = characterEnd; + } + + public String getFilterType() { + return filterType; + } + + public void setFilterType(String filterType) { + this.filterType = filterType; + } + + public String getContext() { + return context; + } + + public void setContext(String context) { + this.context = context; + } + + public String getDocumentId() { + return documentId; + } + + public void setDocumentId(String documentId) { + this.documentId = documentId; + } + + public double getConfidence() { + return confidence; + } + + public void setConfidence(double confidence) { + this.confidence = confidence; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String getReplacement() { + return replacement; + } + + public void setReplacement(String replacement) { + this.replacement = replacement; + } + + public boolean isIgnored() { + return ignored; + } + + public void setIgnored(boolean ignored) { + this.ignored = ignored; + } + + public String getSalt() { + return salt; + } + + public void setSalt(String salt) { + this.salt = salt; + } + +} diff --git a/src/main/java/ai/philterd/airlock/model/StatusResponse.java b/src/main/java/ai/philterd/airlock/model/StatusResponse.java new file mode 100644 index 0000000..fd00bb0 --- /dev/null +++ b/src/main/java/ai/philterd/airlock/model/StatusResponse.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright 2023 Philterd, LLC + * + * 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. + ******************************************************************************/ +package ai.philterd.airlock.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +/** + * The status of Philter. + */ +public class StatusResponse { + + @Expose + @SerializedName("status") + private String status; + + @Expose + @SerializedName("version") + private String version; + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + +} diff --git a/src/main/java/ai/philterd/airlock/model/exceptions/ClientException.java b/src/main/java/ai/philterd/airlock/model/exceptions/ClientException.java new file mode 100644 index 0000000..90092c7 --- /dev/null +++ b/src/main/java/ai/philterd/airlock/model/exceptions/ClientException.java @@ -0,0 +1,9 @@ +package ai.philterd.airlock.model.exceptions; + +public class ClientException extends RuntimeException { + + public ClientException(String message) { + super(message); + } + +} diff --git a/src/main/java/ai/philterd/airlock/model/exceptions/ServiceUnavailableException.java b/src/main/java/ai/philterd/airlock/model/exceptions/ServiceUnavailableException.java new file mode 100644 index 0000000..fdc7f91 --- /dev/null +++ b/src/main/java/ai/philterd/airlock/model/exceptions/ServiceUnavailableException.java @@ -0,0 +1,9 @@ +package ai.philterd.airlock.model.exceptions; + +public class ServiceUnavailableException extends RuntimeException { + + public ServiceUnavailableException(String message) { + super(message); + } + +} diff --git a/src/main/java/ai/philterd/airlock/model/exceptions/UnauthorizedException.java b/src/main/java/ai/philterd/airlock/model/exceptions/UnauthorizedException.java new file mode 100644 index 0000000..45fd45f --- /dev/null +++ b/src/main/java/ai/philterd/airlock/model/exceptions/UnauthorizedException.java @@ -0,0 +1,9 @@ +package ai.philterd.airlock.model.exceptions; + +public class UnauthorizedException extends RuntimeException { + + public UnauthorizedException(String message) { + super(message); + } + +} diff --git a/src/main/java/ai/philterd/airlock/services/AirlockService.java b/src/main/java/ai/philterd/airlock/services/AirlockService.java new file mode 100644 index 0000000..d8954d6 --- /dev/null +++ b/src/main/java/ai/philterd/airlock/services/AirlockService.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright 2023 Philterd, LLC + * + * 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. + ******************************************************************************/ +package ai.philterd.airlock.services; + +import ai.philterd.airlock.model.ApplyResponse; +import ai.philterd.airlock.model.StatusResponse; +import retrofit2.Call; +import retrofit2.http.*; + +import java.util.List; + +public interface AirlockService { + @GET("/api/status") + Call status(); + + // Policies + + @Headers({"Accept: text/plain", "Content-Type: text/plain"}) + @POST("/api/policies/apply") + Call apply(@Query("c") String context, @Query("p") String policyName, @Body String text); + + @Headers({"Accept: application/json"}) + @GET("/api/policies") + Call> Policy(); + + @Headers({"Accept: text/plain"}) + @GET("/api/policies/{name}") + Call Policy(@Path("name") String policyName); + + @Headers({"Content-Type: application/json"}) + @POST("/api/policies") + Call savePolicy(@Body String json); + + @DELETE("/api/policies/{name}") + Call deletePolicy(@Path("name") String policyName); + +} diff --git a/src/test/java/com/mtnfog/test/philter/AirlockClientTest.java b/src/test/java/com/mtnfog/test/philter/AirlockClientTest.java new file mode 100644 index 0000000..a07e495 --- /dev/null +++ b/src/test/java/com/mtnfog/test/philter/AirlockClientTest.java @@ -0,0 +1,181 @@ +/******************************************************************************* + * Copyright 2023 Philterd, LLC + * + * 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. + ******************************************************************************/ +package com.mtnfog.test.philter; + +import ai.philterd.airlock.AirlockClient; +import ai.philterd.airlock.model.StatusResponse; +import okhttp3.ConnectionPool; +import okhttp3.OkHttpClient; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.nio.charset.Charset; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Ignore +public class AirlockClientTest { + + private static final Logger LOGGER = LogManager.getLogger(AirlockClientTest.class); + + private static final String ENDPOINT = "https://10.0.2.227:8080/"; + + @Test + public void getPolicies() throws Exception { + + final AirlockClient client = new AirlockClient.AirlockClientBuilder() + .withEndpoint(ENDPOINT) + .withOkHttpClientBuilder(getUnsafeOkHttpClientBuilder()) + .build(); + + final List policyNames = client.getPolicies(); + + Assert.assertTrue(policyNames != null); + Assert.assertFalse(policyNames.isEmpty()); + + for(final String name : policyNames) { + LOGGER.info("Policy: {}", name); + } + + } + + @Test(expected = SSLHandshakeException.class) + public void getPoliciesNoCertificate() throws Exception { + + final AirlockClient client = new AirlockClient.AirlockClientBuilder() + .withEndpoint(ENDPOINT) + .build(); + + client.getPolicies(); + + } + + @Test + public void get() throws Exception { + + final AirlockClient client = new AirlockClient.AirlockClientBuilder() + .withEndpoint(ENDPOINT) + .withSslConfiguration("/tmp/client-test.jks", "changeit", + "/tmp/keystore-server.jks", "changeit") + .build(); + + final List policyNames = client.getPolicies(); + + Assert.assertTrue(policyNames != null); + Assert.assertFalse(policyNames.isEmpty()); + + for(final String name : policyNames) { + LOGGER.info("Policy: {}", name); + } + + } + + @Test + public void getByName() throws Exception { + + final AirlockClient client = new AirlockClient.AirlockClientBuilder() + .withEndpoint(ENDPOINT) + .withSslConfiguration("/tmp/client-test.jks", "changeit", + "/tmp/keystore-server.jks", "changeit") + .build(); + + final String filterProfile = client.Policy("default"); + + Assert.assertTrue(filterProfile != null); + Assert.assertTrue(filterProfile.length() > 0); + + LOGGER.info("Policy:\n{}", filterProfile); + + } + + @Test + public void save() throws Exception { + + final AirlockClient client = new AirlockClient.AirlockClientBuilder() + .withEndpoint(ENDPOINT) + .withSslConfiguration("/tmp/client-test.jks", "changeit", + "/tmp/keystore-server.jks", "changeit") + .build(); + + final String json = IOUtils.toString(this.getClass().getResource("/default2.json"), Charset.defaultCharset()); + + client.savePolicy(json); + + } + + @Test + public void status() throws Exception { + + final AirlockClient client = new AirlockClient.AirlockClientBuilder() + .withEndpoint(ENDPOINT) + .withSslConfiguration("/tmp/client-test.jks", "changeit", + "/tmp/keystore-server.jks", "changeit") + .withOkHttpClientBuilder(getUnsafeOkHttpClientBuilder()) + .build(); + + final StatusResponse statusResponse = client.status(); + + Assert.assertTrue(StringUtils.equals("Healthy", statusResponse.getStatus())); + + } + + // This is used to test against Philter running with a self-signed certificate. + private OkHttpClient.Builder getUnsafeOkHttpClientBuilder() throws NoSuchAlgorithmException, KeyManagementException { + + final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { + + @Override + public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) { + } + + @Override + public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) { + } + + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return new java.security.cert.X509Certificate[] {}; + } + + } }; + + final SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); + + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + builder.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0]); + builder.connectTimeout(AirlockClient.DEFAULT_TIMEOUT_SEC, TimeUnit.SECONDS); + builder.writeTimeout(AirlockClient.DEFAULT_TIMEOUT_SEC, TimeUnit.SECONDS); + builder.readTimeout(AirlockClient.DEFAULT_TIMEOUT_SEC, TimeUnit.SECONDS); + builder.connectionPool(new ConnectionPool(AirlockClient.DEFAULT_MAX_IDLE_CONNECTIONS, AirlockClient.DEFAULT_KEEP_ALIVE_DURATION_MS, TimeUnit.MILLISECONDS)); + builder.hostnameVerifier((hostname, session) -> true); + + return builder; + + } + +} diff --git a/src/test/resources/default2.json b/src/test/resources/default2.json new file mode 100644 index 0000000..a5f9eff --- /dev/null +++ b/src/test/resources/default2.json @@ -0,0 +1,15 @@ +{ + "name": "default2", + "ignored": [ + ], + "identifiers": { + "creditCard": { + "creditCardFilterStrategies": [ + { + "strategy": "REDACT", + "redactionFormat": "{{{REDACTED-%t}}}" + } + ] + } + } +} diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml new file mode 100644 index 0000000..98cfd73 --- /dev/null +++ b/src/test/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file