├── .gitignore ├── NOTICE ├── .github └── PULL_REQUEST_TEMPLATE.md ├── CODE_OF_CONDUCT.md ├── README.md ├── src ├── test │ └── java │ │ └── com │ │ └── amazonaws │ │ └── neptune │ │ ├── integration │ │ ├── NeptuneRdf4JIntegrationTest.java │ │ └── NeptuneJenaIntegrationTest.java │ │ ├── NeptuneSPARQLConnectionIntegrationTestUtil.java │ │ └── client │ │ ├── rdf4j │ │ └── NeptuneSparqlRepositoryTest.java │ │ └── jena │ │ └── AwsSigningHttpClientTest.java └── main │ └── java │ └── com │ └── amazonaws │ └── neptune │ └── client │ ├── rdf4j │ ├── NeptuneRdf4JSigV4Example.java │ └── NeptuneSparqlRepository.java │ └── jena │ ├── NeptuneJenaSigV4Example.java │ └── AwsSigningHttpClient.java ├── CONTRIBUTING.md ├── COPYING ├── LICENSE └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | *.iml 3 | *.idea 4 | 5 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Amazon Neptune Sparql Java Sigv4 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Amazon Neptune Sparql Java Sigv4 2 | 3 | A SPARQL client for [Amazon Neptune](https://aws.amazon.com/neptune) that includes AWS Signature Version 4 signing. Implemented as an RDF4J repository and Jena HTTP Client. This library relies on [amazon-neptune-sigv4-signer](https://github.com/aws/amazon-neptune-sigv4-signer/). 4 | 5 | For example usage refer to: 6 | 7 | - [NeptuneJenaSigV4Example.java](https://github.com/aws/amazon-neptune-sparql-java-sigv4/blob/master/src/main/java/com/amazonaws/neptune/client/jena/NeptuneJenaSigV4Example.java) 8 | - [NeptuneRdf4JSigV4Example.java](https://github.com/aws/amazon-neptune-sparql-java-sigv4/blob/master/src/main/java/com/amazonaws/neptune/client/rdf4j/NeptuneRdf4JSigV4Example.java) 9 | 10 | For the official Amazon Neptune page refer to: https://aws.amazon.com/neptune 11 | 12 | ## License 13 | 14 | This library is licensed under the Apache 2.0 License. 15 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/neptune/integration/NeptuneRdf4JIntegrationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | package com.amazonaws.neptune.integration; 17 | 18 | import com.amazonaws.neptune.client.rdf4j.NeptuneSparqlRepository; 19 | import org.eclipse.rdf4j.query.TupleQuery; 20 | import org.eclipse.rdf4j.query.Update; 21 | import org.eclipse.rdf4j.repository.Repository; 22 | import org.eclipse.rdf4j.repository.RepositoryConnection; 23 | import org.junit.jupiter.api.AfterEach; 24 | import org.junit.jupiter.api.BeforeEach; 25 | import org.junit.jupiter.api.Test; 26 | import org.junit.jupiter.api.condition.EnabledIfSystemProperty; 27 | import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; 28 | 29 | import java.util.UUID; 30 | 31 | import static com.amazonaws.neptune.NeptuneSPARQLConnectionIntegrationTestUtil.getInsertQuery; 32 | import static com.amazonaws.neptune.NeptuneSPARQLConnectionIntegrationTestUtil.getSelectQuery; 33 | import static org.junit.jupiter.api.Assertions.*; 34 | 35 | @EnabledIfSystemProperty(named = "neptune.endpoint", matches = ".*") 36 | class NeptuneRdf4JIntegrationTest { 37 | 38 | private Repository repository; 39 | private String testGraphUri; 40 | String neptuneEndpoint = System.getProperty("neptune.endpoint").concat("/sparql"); 41 | String regionName = System.getProperty("aws.region", "us-west-1"); 42 | 43 | @BeforeEach 44 | void setUp() throws Exception { 45 | 46 | repository = new NeptuneSparqlRepository( 47 | neptuneEndpoint, 48 | DefaultCredentialsProvider.builder().build(), 49 | regionName 50 | ); 51 | repository.init(); 52 | 53 | testGraphUri = "http://neptune.aws.com/ontology/testing/" + UUID.randomUUID(); 54 | } 55 | 56 | @Test 57 | void testInsertAndQueryWithRdf4J(){ 58 | 59 | try (RepositoryConnection conn = repository.getConnection()) { 60 | // Insert test data 61 | Update update = conn.prepareUpdate(getInsertQuery(testGraphUri)); 62 | update.execute(); 63 | System.out.println("✓ RDF4J Insert completed successfully"); 64 | 65 | // Query and verify data 66 | TupleQuery tupleQuery = conn.prepareTupleQuery(getSelectQuery(testGraphUri)); 67 | 68 | long resultCount = tupleQuery.evaluate().stream().count(); 69 | 70 | assertEquals(1, resultCount, "Should find exactly 1 result"); 71 | 72 | } catch (Exception e) { 73 | fail("RDF4J Insert and Query test failed: " + e.getMessage()); 74 | } 75 | } 76 | 77 | @AfterEach 78 | void tearDown() { 79 | String deleteQuery = String.format("DROP GRAPH <%s>", testGraphUri); 80 | 81 | try (RepositoryConnection conn = repository.getConnection()) { 82 | Update update = conn.prepareUpdate(deleteQuery); 83 | update.execute(); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/neptune/NeptuneSPARQLConnectionIntegrationTestUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | 16 | package com.amazonaws.neptune; 17 | 18 | /** 19 | * Base class providing utility methods for Neptune integration tests. 20 | *
21 | * This class contains helper methods for generating common SPARQL queries 22 | * used in integration testing with Amazon Neptune. It provides standardized 23 | * query templates for inserting, selecting, and clearing test data. 24 | *
25 | * The queries use named graphs to isolate test data and avoid conflicts 26 | * between different test runs or test methods. 27 | * 28 | * @author AWS Neptune Team 29 | * @since 1.0 30 | */ 31 | public class NeptuneSPARQLConnectionIntegrationTestUtil { 32 | 33 | /** 34 | * Generates a SPARQL INSERT query for adding test data to a named graph. 35 | *
36 | * Creates a simple triple with test URNs that can be used to verify
37 | * that data insertion operations are working correctly.
38 | *
39 | * @param namedGraph the URI of the named graph where data should be inserted
40 | * @return a SPARQL INSERT DATA query string
41 | */
42 | public static String getInsertQuery(String namedGraph) {
43 | return String.format(
44 | "INSERT DATA {" +
45 | " GRAPH <%s> {" +
46 | "
54 | * Returns all subject-predicate-object triples stored in the specified
55 | * named graph, useful for verifying that data was inserted correctly.
56 | *
57 | * @param namedGraph the URI of the named graph to query
58 | * @return a SPARQL SELECT query string that retrieves all triples from the graph
59 | */
60 | public static String getSelectQuery(String namedGraph) {
61 | return String.format(
62 | "SELECT * {" +
63 | " GRAPH <%s> {" +
64 | " ?s ?p ?o " + // Select all triples in the graph
65 | " }" +
66 | "}", namedGraph);
67 | }
68 |
69 | /**
70 | * Generates a SPARQL CLEAR query for removing all data from a named graph.
71 | *
72 | * Removes all triples from the specified named graph, useful for cleaning
73 | * up test data after test execution to ensure test isolation.
74 | *
75 | * @param namedGraph the URI of the named graph to clear
76 | * @return a SPARQL CLEAR GRAPH query string
77 | */
78 | public static String getClearQuery(String namedGraph) {
79 | return String.format(
80 | "CLEAR GRAPH <%s>", namedGraph); // Remove all triples from the graph
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
4 | documentation, we greatly value feedback and contributions from our community.
5 |
6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
7 | information to effectively respond to your bug report or contribution.
8 |
9 |
10 | ## Reporting Bugs/Feature Requests
11 |
12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features.
13 |
14 | When filing an issue, please check [existing open](https://github.com/aws/amazon-neptune-sparql-java-sigv4/issues), or [recently closed](https://github.com/aws/amazon-neptune-sparql-java-sigv4/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already
15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
16 |
17 | * A reproducible test case or series of steps
18 | * The version of our code being used
19 | * Any modifications you've made relevant to the bug
20 | * Anything unusual about your environment or deployment
21 |
22 |
23 | ## Contributing via Pull Requests
24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
25 |
26 | 1. You are working against the latest source on the *master* branch.
27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
29 |
30 | To send us a pull request, please:
31 |
32 | 1. Fork the repository.
33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
34 | 3. Ensure local tests pass.
35 | 4. Commit to your fork using clear commit messages.
36 | 5. Send us a pull request, answering any default questions in the pull request interface.
37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
38 |
39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
41 |
42 |
43 | ## Finding contributions to work on
44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws/amazon-neptune-sparql-java-sigv4/labels/help%20wanted) issues is a great place to start.
45 |
46 |
47 | ## Code of Conduct
48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
50 | opensource-codeofconduct@amazon.com with any additional questions or comments.
51 |
52 |
53 | ## Security issue notifications
54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
55 |
56 |
57 | ## Licensing
58 |
59 | See the [LICENSE](https://github.com/aws/amazon-neptune-sparql-java-sigv4/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
60 |
61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes.
62 |
--------------------------------------------------------------------------------
/src/main/java/com/amazonaws/neptune/client/rdf4j/NeptuneRdf4JSigV4Example.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License").
5 | * You may not use this file except in compliance with the License.
6 | * A copy of the License is located at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * or in the "license" file accompanying this file. This file is distributed
11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12 | * express or implied. See the License for the specific language governing
13 | * permissions and limitations under the License.
14 | */
15 |
16 | package com.amazonaws.neptune.client.rdf4j;
17 |
18 | import com.amazonaws.neptune.auth.NeptuneSigV4SignerException;
19 | import org.eclipse.rdf4j.query.BindingSet;
20 | import org.eclipse.rdf4j.query.TupleQueryResult;
21 | import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
22 | import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
23 |
24 | /**
25 | * Example demonstrating how to connect to Amazon Neptune using Eclipse RDF4J with AWS Signature V4 authentication.
26 | *
27 | * This example shows how to:
28 | *
34 | * The example uses the default AWS credentials provider, which will automatically
35 | * discover credentials from the environment, AWS profiles, or IAM roles.
36 | *
37 | * @author AWS Neptune Team
38 | * @since 1.0
39 | */
40 | public class NeptuneRdf4JSigV4Example {
41 |
42 | /**
43 | * Main method demonstrating Neptune connection with RDF4J and AWS Signature V4.
44 | *
45 | * Requires two command line arguments: endpoint URL and region name.
46 | * Ensure your AWS credentials are properly configured in your environment.
47 | *
48 | * Example usage:
49 | * {@code java NeptuneRdf4JSigV4Example https://my-cluster.cluster-xyz.us-west-2.neptune.amazonaws.com:8182/sparql us-west-2}
50 | *
51 | * @param args command line arguments: [0] endpoint URL, [1] region name
52 | * @throws NeptuneSigV4SignerException if there's an error with AWS signature signing
53 | */
54 | public static void main(final String[] args) throws NeptuneSigV4SignerException {
55 | if (args.length < 2) {
56 | System.err.println("Usage: java NeptuneRdf4JSigV4Example
30 | * This example shows how to:
31 | *
37 | * The example uses the default AWS credentials provider, which will automatically
38 | * discover credentials from the environment, AWS profiles, or IAM roles.
39 | *
40 | * @author AWS Neptune Team
41 | * @since 1.0
42 | */
43 | public class NeptuneJenaSigV4Example {
44 |
45 | /**
46 | * Main method demonstrating Neptune connection with Jena and AWS Signature V4.
47 | *
48 | * Requires two command line arguments: endpoint URL and region name.
49 | * Ensure your AWS credentials are properly configured in your environment.
50 | *
51 | * Example usage:
52 | * {@code java NeptuneJenaSigV4Example https://my-cluster.cluster-xyz.us-west-2.neptune.amazonaws.com:8182 us-west-2}
53 | *
54 | * @param args command line arguments: [0] endpoint URL, [1] region name
55 | */
56 | public static void main(String... args) {
57 | if (args.length < 2) {
58 | System.err.println("Usage: java NeptuneJenaSigV4Example
31 | * Tests both authenticated and unauthenticated repository configurations.
32 | */
33 | class NeptuneSparqlRepositoryTest {
34 |
35 | @Mock
36 | private AwsCredentialsProvider mockCredentialsProvider;
37 |
38 | private final String testEndpoint = "https://test-cluster.cluster-xyz.us-east-1.neptune.amazonaws.com:8182";
39 | private final String testRegion = "us-east-1";
40 |
41 | @Test
42 | void testAuthenticatedRepositoryCreation() throws NeptuneSigV4SignerException {
43 | MockitoAnnotations.openMocks(this);
44 |
45 | // Mock credentials provider
46 | when(mockCredentialsProvider.resolveCredentials())
47 | .thenReturn(AwsBasicCredentials.create("test-access-key", "test-secret-key"));
48 |
49 | // Test creating repository with authentication
50 | NeptuneSparqlRepository repository = new NeptuneSparqlRepository(
51 | testEndpoint,
52 | mockCredentialsProvider,
53 | testRegion
54 | );
55 |
56 | assertNotNull(repository);
57 | assertEquals(testEndpoint , repository.toString());
58 | }
59 |
60 | @Test
61 | void testNullEndpointThrowsException() {
62 | // Test that null endpoint throws appropriate exception
63 | assertThrows(Exception.class, () -> {
64 | new NeptuneSparqlRepository(null);
65 | });
66 | }
67 |
68 | @Test
69 | void testNullCredentialsProviderThrowsException() {
70 | // Test that null credentials provider throws exception for authenticated repository
71 | assertThrows(Exception.class, () -> {
72 | new NeptuneSparqlRepository(testEndpoint, null, testRegion);
73 | });
74 | }
75 |
76 | @Test
77 | void testNullRegionThrowsException() {
78 | MockitoAnnotations.openMocks(this);
79 |
80 | when(mockCredentialsProvider.resolveCredentials())
81 | .thenReturn(AwsBasicCredentials.create("test-access-key", "test-secret-key"));
82 |
83 | // Test that null region throws exception for authenticated repository
84 | assertThrows(Exception.class, () -> {
85 | new NeptuneSparqlRepository(testEndpoint, mockCredentialsProvider, null);
86 | });
87 | }
88 |
89 | @Test
90 | void testRepositoryConnection() {
91 | // Test that repository can provide connections
92 | NeptuneSparqlRepository repository = new NeptuneSparqlRepository(testEndpoint);
93 |
94 | assertDoesNotThrow(() -> {
95 | // This would normally connect to Neptune, but in unit tests we just verify no exceptions
96 | var connection = repository.getConnection();
97 | assertNotNull(connection);
98 | connection.close();
99 | });
100 | }
101 |
102 | @Test
103 | void testRepositoryInitialization() {
104 | NeptuneSparqlRepository repository = new NeptuneSparqlRepository(testEndpoint);
105 |
106 | // Test repository initialization
107 | assertDoesNotThrow(() -> {
108 | repository.init();
109 | assertTrue(repository.isInitialized());
110 | });
111 | }
112 |
113 | @Test
114 | void testRepositoryShutdown() {
115 | NeptuneSparqlRepository repository = new NeptuneSparqlRepository(testEndpoint);
116 |
117 | // Test repository shutdown
118 | assertDoesNotThrow(() -> {
119 | repository.init();
120 | repository.shutDown();
121 | });
122 | }
123 | }
--------------------------------------------------------------------------------
/src/test/java/com/amazonaws/neptune/client/jena/AwsSigningHttpClientTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License").
5 | * You may not use this file except in compliance with the License.
6 | * A copy of the License is located at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * or in the "license" file accompanying this file. This file is distributed
11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12 | * express or implied. See the License for the specific language governing
13 | * permissions and limitations under the License.
14 | */
15 |
16 | package com.amazonaws.neptune.client.jena;
17 |
18 | import org.junit.jupiter.api.BeforeEach;
19 | import org.junit.jupiter.api.Test;
20 | import org.mockito.Mock;
21 | import org.mockito.MockitoAnnotations;
22 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
23 | import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
24 | import software.amazon.awssdk.regions.Region;
25 |
26 | import java.io.IOException;
27 | import java.net.URI;
28 | import java.net.http.HttpRequest;
29 | import java.net.http.HttpResponse;
30 | import java.time.Duration;
31 |
32 | import static org.junit.jupiter.api.Assertions.*;
33 | import static org.mockito.Mockito.when;
34 |
35 | /**
36 | * Unit tests for AwsSigningHttpClient.
37 | *
38 | * Tests the AWS Signature V4 signing functionality and HTTP client delegation behavior.
39 | */
40 | class AwsSigningHttpClientTest {
41 |
42 | @Mock
43 | private AwsCredentialsProvider mockCredentialsProvider;
44 |
45 | private AwsSigningHttpClient signingClient;
46 | private final String testRequestBody = "SELECT * { ?s ?p ?o } LIMIT 10";
47 | private final Region testRegion = Region.US_EAST_1;
48 | private final String serviceName = "neptune-db";
49 |
50 | @BeforeEach
51 | void setUp() {
52 | MockitoAnnotations.openMocks(this);
53 |
54 | // Mock credentials provider
55 | when(mockCredentialsProvider.resolveCredentials())
56 | .thenReturn(AwsBasicCredentials.create("test-access-key", "test-secret-key"));
57 |
58 | signingClient = new AwsSigningHttpClient(
59 | serviceName,
60 | testRegion,
61 | mockCredentialsProvider,
62 | testRequestBody
63 | );
64 | }
65 |
66 | @Test
67 | void testDelegateMethodsReturnNonNull() {
68 | // Test that delegate methods return expected values
69 | assertNotNull(signingClient.cookieHandler());
70 | assertNotNull(signingClient.followRedirects());
71 | assertNotNull(signingClient.proxy());
72 | assertNotNull(signingClient.sslContext());
73 | assertNotNull(signingClient.sslParameters());
74 | assertNotNull(signingClient.authenticator());
75 | assertNotNull(signingClient.version());
76 | assertNotNull(signingClient.executor());
77 | }
78 |
79 | @Test
80 | void testSendWithValidRequest() throws IOException, InterruptedException {
81 | // Create a simple GET request for testing
82 | HttpRequest request = HttpRequest.newBuilder()
83 | .uri(URI.create("https://example.com/sparql"))
84 | .GET()
85 | .build();
86 |
87 | HttpResponse.BodyHandler
33 | * This repository extends Eclipse RDF4J's SPARQLRepository to provide seamless integration
34 | * with Amazon Neptune databases. It supports both authenticated and unauthenticated connections:
35 | *
42 | * For authenticated connections, the repository automatically signs HTTP requests using
43 | * AWS Signature V4 as described in the
44 | * AWS documentation.
45 | *
46 | * Usage Examples:
47 | *
87 | * Use this constructor for Neptune instances that don't require IAM authentication.
88 | * The repository will connect directly to the Neptune SPARQL endpoint without
89 | * adding any AWS signature headers.
90 | *
91 | * @param endpointUrl the fully qualified Neptune cluster endpoint URL
92 | * (e.g., "https://my-cluster.cluster-xyz.us-east-1.neptune.amazonaws.com:8182/sparql")
93 | */
94 | public NeptuneSparqlRepository(final String endpointUrl) {
95 | super(endpointUrl);
96 | // all the fields below are only relevant for authentication and can be ignored
97 | this.authenticationEnabled = false;
98 | this.awsCredentialsProvider = null; // only needed if auth is enabled
99 | this.regionName = null; // only needed if auth is enabled
100 | }
101 |
102 | /**
103 | * Creates a Neptune SPARQL repository with AWS Signature V4 authentication.
104 | *
105 | * Use this constructor for Neptune instances that require IAM authentication.
106 | * The repository will automatically sign all HTTP requests using AWS Signature V4
107 | * before sending them to Neptune.
108 | *
109 | * @param endpointUrl fully qualified Neptune cluster endpoint URL
110 | * (e.g., "https://my-cluster.cluster-xyz.us-east-1.neptune.amazonaws.com:8182/sparql")
111 | * @param awsCredentialsProvider the AWS credentials provider for obtaining signing credentials
112 | * (e.g., DefaultCredentialsProvider.create())
113 | * @param regionName the AWS region name where the Neptune cluster is located (e.g., "us-east-1")
114 | * @throws NeptuneSigV4SignerException if there's an error initializing the AWS signature signer
115 | */
116 | public NeptuneSparqlRepository(
117 | final String endpointUrl,
118 | final AwsCredentialsProvider awsCredentialsProvider,
119 | final String regionName)
120 | throws NeptuneSigV4SignerException {
121 |
122 | super(endpointUrl);
123 |
124 | this.authenticationEnabled = true;
125 | this.awsCredentialsProvider = awsCredentialsProvider;
126 | this.regionName = regionName;
127 |
128 | initAuthenticatingHttpClient();
129 |
130 | }
131 |
132 | /**
133 | * Initializes the HTTP client with AWS Signature V4 signing capability.
134 | *
135 | * This method configures an Apache HTTP client with a request interceptor that
136 | * automatically signs outgoing requests using AWS Signature V4. The interceptor
137 | * is added last in the chain to ensure it operates on the final request.
138 | *
139 | * @throws NeptuneSigV4SignerException if there's an error initializing the AWS signature signer
140 | */
141 | protected void initAuthenticatingHttpClient() throws NeptuneSigV4SignerException {
142 |
143 | if (!authenticationEnabled) {
144 | return; // Authentication not enabled, skip HTTP client configuration
145 | }
146 |
147 | // Initialize AWS Signature V4 signer for Apache HTTP requests
148 | v4Signer = new NeptuneApacheHttpSigV4Signer(regionName, awsCredentialsProvider);
149 |
150 | /*
151 | * Configure HTTP client with signing interceptor.
152 | * The interceptor is added last to ensure it operates on the final version
153 | * of the request after all other interceptors have processed it.
154 | */
155 | final HttpClient v4SigningClient = HttpClientBuilder
156 | .create()
157 | .addInterceptorLast((HttpRequestInterceptor) (req, ctx) -> {
158 | // Ensure the request is the expected type
159 | if (req instanceof HttpUriRequest httpUriReq) {
160 | try {
161 | // Sign the request using AWS Signature V4
162 | v4Signer.signRequest(httpUriReq);
163 | } catch (NeptuneSigV4SignerException e) {
164 | throw new HttpException("Failed to sign Neptune request with AWS Signature V4: ", e);
165 | }
166 | } else {
167 | // This should never happen with standard HTTP clients
168 | throw new HttpException("Request is not an HttpUriRequest instance");
169 | }
170 | }).build();
171 |
172 | setHttpClient(v4SigningClient);
173 |
174 | }
175 |
176 | }
177 |
178 |
--------------------------------------------------------------------------------
/src/test/java/com/amazonaws/neptune/integration/NeptuneJenaIntegrationTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License").
5 | * You may not use this file except in compliance with the License.
6 | * A copy of the License is located at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * or in the "license" file accompanying this file. This file is distributed
11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12 | * express or implied. See the License for the specific language governing
13 | * permissions and limitations under the License.
14 | */
15 |
16 | package com.amazonaws.neptune.integration;
17 |
18 | import com.amazonaws.neptune.client.jena.AwsSigningHttpClient;
19 | import org.apache.jena.atlas.web.HttpException;
20 | import org.apache.jena.rdfconnection.RDFConnection;
21 | import org.apache.jena.rdfconnection.RDFConnectionRemote;
22 | import org.junit.jupiter.api.AfterEach;
23 | import org.junit.jupiter.api.BeforeEach;
24 | import org.junit.jupiter.api.Test;
25 | import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
26 | import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
27 | import software.amazon.awssdk.regions.Region;
28 |
29 | import java.net.http.HttpClient;
30 | import java.util.UUID;
31 |
32 | import static com.amazonaws.neptune.NeptuneSPARQLConnectionIntegrationTestUtil.*;
33 | import static org.junit.jupiter.api.Assertions.*;
34 |
35 | @EnabledIfSystemProperty(named = "neptune.endpoint", matches = ".*")
36 | class NeptuneJenaIntegrationTest {
37 |
38 | private HttpClient signingClient;
39 | private String testGraphUri;
40 | private String testGraphUri2;
41 | private final String regionName = System.getProperty(
42 | "aws.region",
43 | "us-west-1"
44 | );
45 | private final String neptuneEndpoint = System.getProperty(
46 | "neptune.endpoint"
47 | );
48 | private DefaultCredentialsProvider credentialsProvider;
49 |
50 |
51 | @BeforeEach
52 | void setUp() {
53 |
54 | credentialsProvider = DefaultCredentialsProvider.builder().build();
55 |
56 | testGraphUri = "http://neptune.aws.com/ontology/testing/" + UUID.randomUUID();
57 | testGraphUri2 = "http://neptune.aws.com/ontology/testing/" + UUID.randomUUID();
58 | }
59 |
60 | @AfterEach
61 | void cleanup() {
62 |
63 | String deleteQuery = getClearQuery(testGraphUri);
64 |
65 | signingClient = new AwsSigningHttpClient(
66 | "neptune-db",
67 | Region.of(regionName),
68 | credentialsProvider,
69 | deleteQuery
70 | );
71 |
72 | try (RDFConnection conn = RDFConnectionRemote.create()
73 | .httpClient(signingClient)
74 | .destination(neptuneEndpoint)
75 | .updateEndpoint("sparql")
76 | .build()) {
77 |
78 | conn.update(deleteQuery);
79 | }
80 |
81 | }
82 |
83 | /**
84 | * Tests Neptune SPARQL operations with Jena, demonstrating the critical requirement
85 | * that the request body (SPARQL query) must be provided when creating the signing client.
86 | *
87 | * AWS Signature V4 Body Requirement:
88 | * AWS Signature Version 4 requires the request body to be included in the signature
89 | * calculation to ensure request integrity and prevent tampering. The signature is
90 | * computed using a hash of the request body along with other request components
91 | * (headers, URI, method, etc.). This means:
92 | *
98 | * Why This Matters for SPARQL:
99 | * SPARQL queries are sometimes sent as HTTP POST requests with the query in the request body.
100 | * Since AWS Signature V4 includes the body hash in the signature calculation,
101 | * we must provide the exact SPARQL query text when creating the AwsSigningHttpClient.
102 | * This ensures the signature matches what Neptune expects when it validates the request.
103 | *
104 | * This test verifies that the signing process works correctly by:
105 | *
149 | * CRITICAL: The query string itself must be provided to the signing client because
150 | * AWS Signature V4 requires the request body to be included in signature calculation.
151 | * The signature includes a hash of the request body to ensure integrity.
152 | * Because of this, a new signingClient must be created for every request that contains a body (POST, PUT).
153 | */
154 | @Test
155 | void testSecondInsertRequiresNewClientForCorrectSignatureCreation() {
156 | // Generate the SPARQL INSERT query that will be sent in the request body
157 | String insertQuery = getInsertQuery(testGraphUri);
158 | String insertQuery2 = getInsertQuery(testGraphUri2);
159 |
160 | signingClient = new AwsSigningHttpClient(
161 | "neptune-db",
162 | Region.of(regionName),
163 | credentialsProvider,
164 | insertQuery // This exact query body will be used for signature calculation
165 | );
166 |
167 | try (RDFConnection conn = RDFConnectionRemote.create()
168 | .httpClient(signingClient)
169 | .destination(neptuneEndpoint)
170 | .queryEndpoint("sparql")
171 | .updateEndpoint("sparql")
172 | .build()) {
173 |
174 | // Execute the INSERT - the request body must match what was used for signing
175 | conn.update(insertQuery);
176 | System.out.println("✓ Insert completed successfully");
177 | conn.update(insertQuery2);
178 | fail("Should throw exception");
179 | } catch (HttpException hje) {
180 | assertTrue(hje.getMessage().contains("403 - Forbidden"));
181 | }
182 |
183 | HttpClient signingClient2 = new AwsSigningHttpClient(
184 | "neptune-db",
185 | Region.of(regionName),
186 | credentialsProvider,
187 | insertQuery2
188 | );
189 | try (RDFConnection conn = RDFConnectionRemote.create()
190 | .httpClient(signingClient2)
191 | .destination(neptuneEndpoint)
192 | .queryEndpoint("sparql")
193 | .updateEndpoint("sparql")
194 | .build()) {
195 |
196 | // Execute the INSERT - the request body must match what was used for signing
197 | conn.update(insertQuery2);
198 | System.out.println(
199 | "✓ Insert 2 now completes successfully as new client and " +
200 | "connection as auth header is correctly built using the body from insert 2."
201 | );
202 | }
203 | }
204 |
205 |
206 | }
--------------------------------------------------------------------------------
/src/main/java/com/amazonaws/neptune/client/jena/AwsSigningHttpClient.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License").
5 | * You may not use this file except in compliance with the License.
6 | * A copy of the License is located at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * or in the "license" file accompanying this file. This file is distributed
11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12 | * express or implied. See the License for the specific language governing
13 | * permissions and limitations under the License.
14 | */
15 |
16 | package com.amazonaws.neptune.client.jena;
17 |
18 | import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
19 | import software.amazon.awssdk.auth.signer.Aws4Signer;
20 | import software.amazon.awssdk.auth.signer.params.Aws4SignerParams;
21 | import software.amazon.awssdk.http.SdkHttpFullRequest;
22 | import software.amazon.awssdk.http.SdkHttpMethod;
23 | import software.amazon.awssdk.regions.Region;
24 |
25 | import javax.net.ssl.SSLContext;
26 | import javax.net.ssl.SSLParameters;
27 | import java.io.ByteArrayInputStream;
28 | import java.io.IOException;
29 | import java.net.Authenticator;
30 | import java.net.CookieHandler;
31 | import java.net.ProxySelector;
32 | import java.net.http.HttpClient;
33 | import java.net.http.HttpRequest;
34 | import java.net.http.HttpResponse;
35 | import java.nio.charset.StandardCharsets;
36 | import java.time.Duration;
37 | import java.util.Optional;
38 | import java.util.concurrent.CompletableFuture;
39 | import java.util.concurrent.Executor;
40 |
41 | /**
42 | * AWS Signature Version 4 signing HTTP client for Amazon Neptune SPARQL requests.
43 | *
44 | * This class extends the standard Java HttpClient to automatically sign HTTP requests
45 | * using AWS Signature Version 4 authentication. It's specifically designed for use with
46 | * Amazon Neptune database instances that require IAM authentication.
47 | *
48 | * The client wraps a delegate HttpClient and intercepts requests to add the necessary
49 | * AWS authentication headers (Authorization and x-amz-date) before forwarding them
50 | * to the underlying client.
51 | *
52 | * A new HTTP client and connection must be created every time the request contains a BODY.
53 | *
29 | *
33 | *
32 | *
36 | *
36 | *
41 | * {@code
48 | * // Unauthenticated connection
49 | * NeptuneSparqlRepository repo = new NeptuneSparqlRepository("https://my-neptune-cluster:8182");
50 | *
51 | * // Authenticated connection
52 | * NeptuneSparqlRepository repo = new NeptuneSparqlRepository(
53 | * "https://my-neptune-cluster:8182",
54 | * DefaultCredentialsProvider.create(),
55 | * "us-east-1"
56 | * );
57 | * }
58 | *
59 | * @author AWS Neptune Team
60 | * @since 1.0
61 | */
62 | public class NeptuneSparqlRepository extends SPARQLRepository {
63 |
64 | /**
65 | * The name of the region in which Neptune is running.
66 | */
67 | private final String regionName;
68 |
69 | /**
70 | * Whether authentication is enabled.
71 | */
72 | private final boolean authenticationEnabled;
73 |
74 | /**
75 | * The credentials provider, offering credentials for signing the request.
76 | */
77 | private final AwsCredentialsProvider awsCredentialsProvider;
78 |
79 | /**
80 | * The signature V4 signer used to sign the request.
81 | */
82 | private NeptuneSigV4Signer
93 | *
97 | *
106 | *
110 | */
111 | @Test
112 | void testInsertAndQueryWithJena() {
113 | // Generate the SPARQL INSERT query that will be sent in the request body
114 | String insertQuery = getInsertQuery(testGraphUri);
115 |
116 | signingClient = new AwsSigningHttpClient(
117 | "neptune-db",
118 | Region.of(regionName),
119 | credentialsProvider,
120 | insertQuery // This exact query body will be used for signature calculation
121 | );
122 |
123 | try (RDFConnection conn = RDFConnectionRemote.create()
124 | .httpClient(signingClient)
125 | .destination(neptuneEndpoint)
126 | .queryEndpoint("sparql")
127 | .updateEndpoint("sparql")
128 | .build()) {
129 |
130 | // Execute the INSERT - the request body must match what was used for signing
131 | conn.update(insertQuery);
132 | System.out.println("✓ Insert completed successfully");
133 |
134 | final int[] resultCount = {0};
135 |
136 | // Query the inserted data to verify the operation succeeded
137 | // As this SELECT query uses GET, there is no request body.
138 | conn.querySelect(getSelectQuery(testGraphUri), result -> {
139 | resultCount[0]++;
140 | });
141 |
142 | assertEquals(1, resultCount[0], "Should find exactly 1 result");
143 | }
144 | }
145 |
146 | /**
147 | * Tests that a new signing client must be created for each request with a different body.
148 | *