View a markdown version of this page

Utilisation du protocole Bolt pour envoyer des requêtes openCypher à Neptune - Amazon Neptune

Les traductions sont fournies par des outils de traduction automatique. En cas de conflit entre le contenu d'une traduction et celui de la version originale en anglais, la version anglaise prévaudra.

Utilisation du protocole Bolt pour envoyer des requêtes openCypher à Neptune

Bolt est un client/server protocole orienté instructions initialement développé par Neo4j et distribué sous licence Creative Commons 3.0 Attribution. ShareAlike Il est axé sur le client, ce qui signifie que le client initie toujours les échanges de messages.

Pour vous connecter à Neptune à l'aide des pilotes Bolt de Neo4j, remplacez simplement l'URL et le numéro de port par les points de terminaison de votre cluster à l'aide du schéma d'URI bolt. Si vous n'avez qu'une seule instance Neptune en cours d'exécution, utilisez le point de terminaison read_write. Si plusieurs instances sont en cours d'exécution, deux pilotes sont recommandés, l'un pour l'enregistreur et l'autre pour tous les réplicas en lecture. Si vous ne disposez que des deux points de terminaison par défaut, un pilote read_write et un pilote read_only sont suffisants, mais si vous avez également des points de terminaison personnalisés, envisagez de créer une instance de pilote pour chacun d'eux.

Note

Bien que la spécification Bolt indique que Bolt peut se connecter en utilisant TCP ou, WebSockets Neptune ne prend en charge que les connexions TCP pour Bolt.

Neptune autorise jusqu'à 1 000 connexions Bolt simultanées sur toutes les tailles d'instance, à l'exception de t3.medium et t4g.medium. Sur les instances t3.medium et t4g.medium, seules 512 connexions sont autorisées.

Pour obtenir des exemples de requêtes openCypher dans différents langages utilisant les pilotes Bolt, consultez les guides spécifiques aux pilotes et aux langages de Neo4j.

Important

Les pilotes Neo4j Bolt pour Python JavaScript, .NET et Golang ne prenaient pas initialement en charge le renouvellement automatique des jetons d'authentification AWS Signature v4. Autrement dit, après l'expiration de la signature (souvent au bout de cinq minutes), le pilote ne parvenait pas à s'authentifier et les demandes suivantes échouaient. Les exemples Python JavaScript, .NET et Go ci-dessous étaient tous concernés par ce problème.

Consultez le numéro #834 du pilote Python Neo4j, le numéro #664 du pilote Neo4j .NET, le problème #993 du pilote Neo4j et le numéro #429 JavaScript du pilote Neo4j GoLang pour plus d'informations.

À partir de la version 5.8.0 du pilote, une nouvelle version préliminaire de l'API de réauthentification a été publiée pour le pilote Go (voir v5.8.0 - Feedback wanted on re-authentication).

Utilisation de Bolt pour se connecter à Neptune

Vous pouvez télécharger un pilote pour la version que vous souhaitez utiliser à partir du référentiel Maven MVN, ou ajouter cette dépendance à votre projet :

<dependency> <groupId>org.neo4j.driver</groupId> <artifactId>neo4j-java-driver</artifactId> <version>4.3.3</version> </dependency>

Ensuite, pour vous connecter à Neptune en Java à l'aide de l'un de ces pilotes Bolt, créez une instance de pilote pour l' primary/writer instance de votre cluster à l'aide d'un code tel que celui-ci :

import org.neo4j.driver.Driver; import org.neo4j.driver.GraphDatabase; final Driver driver = GraphDatabase.driver("bolt://(your cluster endpoint URL):(your cluster port)", AuthTokens.none(), Config.builder().withEncryption() .withTrustStrategy(TrustStrategy.trustSystemCertificates()) .build());

Si vous possédez un ou plusieurs réplicas de lecteurs, vous pouvez également créer une instance de pilote pour ceux-ci à l'aide d'un code similaire à ce qui suit :

final Driver read_only_driver = // (without connection timeout) GraphDatabase.driver("bolt://(your cluster endpoint URL):(your cluster port)", Config.builder().withEncryption() .withTrustStrategy(TrustStrategy.trustSystemCertificates()) .build());

Ou, avec un délai d'expiration :

final Driver read_only_timeout_driver = // (with connection timeout) GraphDatabase.driver("bolt://(your cluster endpoint URL):(your cluster port)", Config.builder().withConnectionTimeout(30, TimeUnit.SECONDS) .withEncryption() .withTrustStrategy(TrustStrategy.trustSystemCertificates()) .build());

Si vous avez des points de terminaison personnalisés, il peut également être intéressant de créer une instance de pilote pour chacun d'entre eux.

Exemple de requête openCypher en Python avec Bolt

Voici comment créer une requête openCypher en Python avec Bolt :

python -m pip install neo4j
from neo4j import GraphDatabase uri = "bolt://(your cluster endpoint URL):(your cluster port)" driver = GraphDatabase.driver(uri, auth=("username", "password"), encrypted=True)

Notez que les paramètres auth sont ignorés.

Exemple de requête openCypher dans NET avec Bolt

Pour effectuer une requête OpenCypher dans .NET à l'aide de Bolt, la première étape consiste à installer le pilote Neo4j à l'aide de. NuHet Pour passer des appels synchrones, utilisez la version .Simple comme suit :

Install-Package Neo4j.Driver.Simple-4.3.0
using Neo4j.Driver; namespace hello { // This example creates a node and reads a node in a Neptune // Cluster where IAM Authentication is not enabled. public class HelloWorldExample : IDisposable { private bool _disposed = false; private readonly IDriver _driver; private static string url = "bolt://(your cluster endpoint URL):(your cluster port)"; private static string createNodeQuery = "CREATE (a:Greeting) SET a.message = 'HelloWorldExample'"; private static string readNodeQuery = "MATCH(n:Greeting) RETURN n.message"; ~HelloWorldExample() => Dispose(false); public HelloWorldExample(string uri) { _driver = GraphDatabase.Driver(uri, AuthTokens.None, o => o.WithEncryptionLevel(EncryptionLevel.Encrypted)); } public void createNode() { // Open a session using (var session = _driver.Session()) { // Run the query in a write transaction var greeting = session.WriteTransaction(tx => { var result = tx.Run(createNodeQuery); // Consume the result return result.Consume(); }); // The output will look like this: // ResultSummary{Query=`CREATE (a:Greeting) SET a.message = 'HelloWorldExample"..... Console.WriteLine(greeting); } } public void retrieveNode() { // Open a session using (var session = _driver.Session()) { // Run the query in a read transaction var greeting = session.ReadTransaction(tx => { var result = tx.Run(readNodeQuery); // Consume the result. Read the single node // created in a previous step. return result.Single()[0].As<string>(); }); // The output will look like this: // HelloWorldExample Console.WriteLine(greeting); } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _driver?.Dispose(); } _disposed = true; } public static void Main() { using (var apiCaller = new HelloWorldExample(url)) { apiCaller.createNode(); apiCaller.retrieveNode(); } } } }

Exemple de requête Java openCypher à l'aide de Bolt avec l'authentification IAM

Le code Java ci-dessous montre comment créer des requêtes openCypher en Java à l'aide de Bolt avec l'authentification IAM. Le JavaDoc commentaire décrit son utilisation. Une fois qu'une instance de pilote est disponible, vous pouvez l'utiliser pour effectuer plusieurs demandes authentifiées.

package software.amazon.neptune.bolt; import com.amazonaws.DefaultRequest; import com.amazonaws.Request; import com.amazonaws.auth.AWS4Signer; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.http.HttpMethodName; import com.google.gson.Gson; import lombok.Builder; import lombok.Getter; import lombok.NonNull; import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.internal.security.InternalAuthToken; import org.neo4j.driver.internal.value.StringValue; import java.net.URI; import java.util.Collections; import java.util.HashMap; import java.util.Map; import static com.amazonaws.auth.internal.SignerConstants.AUTHORIZATION; import static com.amazonaws.auth.internal.SignerConstants.HOST; import static com.amazonaws.auth.internal.SignerConstants.X_AMZ_DATE; import static com.amazonaws.auth.internal.SignerConstants.X_AMZ_SECURITY_TOKEN; /** * Use this class instead of `AuthTokens.basic` when working with an IAM * auth-enabled server. It works the same as `AuthTokens.basic` when using * static credentials, and avoids making requests with an expired signature * when using temporary credentials. Internally, it generates a new signature * on every invocation (this may change in a future implementation). * * Note that authentication happens only the first time for a pooled connection. * * Typical usage: * * NeptuneAuthToken authToken = NeptuneAuthToken.builder() * .credentialsProvider(credentialsProvider) * .region("aws region") * .url("cluster endpoint url") * .build(); * * Driver driver = GraphDatabase.driver( * authToken.getUrl(), * authToken, * config * ); */ public class NeptuneAuthToken extends InternalAuthToken { private static final String SCHEME = "basic"; private static final String REALM = "realm"; private static final String SERVICE_NAME = "neptune-db"; private static final String HTTP_METHOD_HDR = "HttpMethod"; private static final String DUMMY_USERNAME = "username"; @NonNull private final String region; @NonNull @Getter private final String url; @NonNull private final AWSCredentialsProvider credentialsProvider; private final Gson gson = new Gson(); @Builder private NeptuneAuthToken( @NonNull final String region, @NonNull final String url, @NonNull final AWSCredentialsProvider credentialsProvider ) { // The superclass caches the result of toMap(), which we don't want super(Collections.emptyMap()); this.region = region; this.url = url; this.credentialsProvider = credentialsProvider; } @Override public Map<String, Value> toMap() { final Map<String, Value> map = new HashMap<>(); map.put(SCHEME_KEY, Values.value(SCHEME)); map.put(PRINCIPAL_KEY, Values.value(DUMMY_USERNAME)); map.put(CREDENTIALS_KEY, new StringValue(getSignedHeader())); map.put(REALM_KEY, Values.value(REALM)); return map; } private String getSignedHeader() { final Request<Void> request = new DefaultRequest<>(SERVICE_NAME); request.setHttpMethod(HttpMethodName.GET); request.setEndpoint(URI.create(url)); // Comment out the following line if you're using an engine version older than 1.2.0.0 request.setResourcePath("/opencypher"); final AWS4Signer signer = new AWS4Signer(); signer.setRegionName(region); signer.setServiceName(request.getServiceName()); signer.sign(request, credentialsProvider.getCredentials()); return getAuthInfoJson(request); } private String getAuthInfoJson(final Request<Void> request) { final Map<String, Object> obj = new HashMap<>(); obj.put(AUTHORIZATION, request.getHeaders().get(AUTHORIZATION)); obj.put(HTTP_METHOD_HDR, request.getHttpMethod()); obj.put(X_AMZ_DATE, request.getHeaders().get(X_AMZ_DATE)); obj.put(HOST, request.getHeaders().get(HOST)); obj.put(X_AMZ_SECURITY_TOKEN, request.getHeaders().get(X_AMZ_SECURITY_TOKEN)); return gson.toJson(obj); } }

Exemple de requête Python openCypher à l'aide de Bolt avec l'authentification IAM

La classe Python ci-dessous vous permet d'effectuer des requêtes openCypher en Python à l'aide de Bolt avec l'authentification IAM :

import json from neo4j import Auth from botocore.awsrequest import AWSRequest from botocore.credentials import Credentials from botocore.auth import ( SigV4Auth, _host_from_url, ) SCHEME = "basic" REALM = "realm" SERVICE_NAME = "neptune-db" DUMMY_USERNAME = "username" HTTP_METHOD_HDR = "HttpMethod" HTTP_METHOD = "GET" AUTHORIZATION = "Authorization" X_AMZ_DATE = "X-Amz-Date" X_AMZ_SECURITY_TOKEN = "X-Amz-Security-Token" HOST = "Host" class NeptuneAuthToken(Auth): def __init__( self, credentials: Credentials, region: str, url: str, **parameters ): # Do NOT add "/opencypher" in the line below if you're using an engine version older than 1.2.0.0 request = AWSRequest(method=HTTP_METHOD, url=url + "/opencypher") request.headers.add_header("Host", _host_from_url(request.url)) sigv4 = SigV4Auth(credentials, SERVICE_NAME, region) sigv4.add_auth(request) auth_obj = { hdr: request.headers[hdr] for hdr in [AUTHORIZATION, X_AMZ_DATE, X_AMZ_SECURITY_TOKEN, HOST] } auth_obj[HTTP_METHOD_HDR] = request.method creds: str = json.dumps(auth_obj) super().__init__(SCHEME, DUMMY_USERNAME, creds, REALM, **parameters)

Utilisez cette classe pour créer un pilote comme suit :

authToken = NeptuneAuthToken(creds, REGION, URL) driver = GraphDatabase.driver(URL, auth=authToken, encrypted=True)

Exemple Node.js utilisant l'authentification IAM et Bolt

Le code Node.js ci-dessous utilise le AWS SDK de la JavaScript version 3 et la ES6 syntaxe pour créer un pilote qui authentifie les demandes :

import neo4j from "neo4j-driver"; import { HttpRequest } from "@smithy/protocol-http"; import { defaultProvider } from "@aws-sdk/credential-provider-node"; import { SignatureV4 } from "@smithy/signature-v4"; import crypto from "@aws-crypto/sha256-js"; const { Sha256 } = crypto; import assert from "node:assert"; const region = "us-west-2"; const serviceName = "neptune-db"; const host = "(your cluster endpoint URL)"; const port = 8182; const protocol = "bolt"; const hostPort = host + ":" + port; const url = protocol + "://" + hostPort; const createQuery = "CREATE (n:Greeting {message: 'Hello'}) RETURN ID(n)"; const readQuery = "MATCH(n:Greeting) WHERE ID(n) = $id RETURN n.message"; async function signedHeader() { const req = new HttpRequest({ method: "GET", protocol: protocol, hostname: host, port: port, // Comment out the following line if you're using an engine version older than 1.2.0.0 path: "/opencypher", headers: { host: hostPort } }); const signer = new SignatureV4({ credentials: defaultProvider(), region: region, service: serviceName, sha256: Sha256 }); return signer.sign(req, { unsignableHeaders: new Set(["x-amz-content-sha256"]) }) .then((signedRequest) => { const authInfo = { "Authorization": signedRequest.headers["authorization"], "HttpMethod": signedRequest.method, "X-Amz-Date": signedRequest.headers["x-amz-date"], "Host": signedRequest.headers["host"], "X-Amz-Security-Token": signedRequest.headers["x-amz-security-token"] }; return JSON.stringify(authInfo); }); } async function createDriver() { let authToken = { scheme: "basic", realm: "realm", principal: "username", credentials: await signedHeader() }; return neo4j.driver(url, authToken, { encrypted: "ENCRYPTION_ON", trust: "TRUST_SYSTEM_CA_SIGNED_CERTIFICATES", maxConnectionPoolSize: 1, // logging: neo4j.logging.console("debug") } ); } async function unmanagedTxn(driver) { const session = driver.session(); const tx = session.beginTransaction(); try { const created = await tx.run(createQuery); const matched = await tx.run(readQuery, { id: created.records[0].get(0) }); const msg = matched.records[0].get("n.message"); assert.equal(msg, "Hello"); await tx.commit(); } catch (err) { // The transaction will be rolled back, now handle the error. console.log(err); } finally { await session.close(); } } const driver = await createDriver(); try { await unmanagedTxn(driver); } catch (err) { console.log(err); } finally { await driver.close(); }

Exemple de requête .NET openCypher à l'aide de Bolt avec l'authentification IAM

Pour activer l'authentification IAM en .NET, vous devez signer une demande lors de l'établissement de la connexion. L'exemple ci-dessous montre comment créer un assistant NeptuneAuthToken pour générer un jeton d'authentification :

using Amazon.Runtime; using Amazon.Util; using Neo4j.Driver; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Web; namespace Hello { /* * Use this class instead of `AuthTokens.None` when working with an IAM-auth-enabled server. * * Note that authentication happens only the first time for a pooled connection. * * Typical usage: * * var authToken = new NeptuneAuthToken(AccessKey, SecretKey, Region).GetAuthToken(Host); * _driver = GraphDatabase.Driver(Url, authToken, o => o.WithEncryptionLevel(EncryptionLevel.Encrypted)); */ public class NeptuneAuthToken { private const string ServiceName = "neptune-db"; private const string Scheme = "basic"; private const string Realm = "realm"; private const string DummyUserName = "username"; private const string Algorithm = "AWS4-HMAC-SHA256"; private const string AWSRequest = "aws4_request"; private readonly string _accessKey; private readonly string _secretKey; private readonly string _region; private readonly string _emptyPayloadHash; private readonly SHA256 _sha256; public NeptuneAuthToken(string awsKey = null, string secretKey = null, string region = null) { var awsCredentials = awsKey == null || secretKey == null ? FallbackCredentialsFactory.GetCredentials().GetCredentials() : null; _accessKey = awsKey ?? awsCredentials.AccessKey; _secretKey = secretKey ?? awsCredentials.SecretKey; _region = region ?? FallbackRegionFactory.GetRegionEndpoint().SystemName; //ex: us-east-1 _sha256 = SHA256.Create(); _emptyPayloadHash = Hash(Array.Empty<byte>()); } public IAuthToken GetAuthToken(string url) { return AuthTokens.Custom(DummyUserName, GetCredentials(url), Realm, Scheme); } /******************** AWS SIGNING FUNCTIONS *********************/ private string Hash(byte[] bytesToHash) { return ToHexString(_sha256.ComputeHash(bytesToHash)); } private static byte[] HmacSHA256(byte[] key, string data) { return new HMACSHA256(key).ComputeHash(Encoding.UTF8.GetBytes(data)); } private byte[] GetSignatureKey(string dateStamp) { var kSecret = Encoding.UTF8.GetBytes($"AWS4{_secretKey}"); var kDate = HmacSHA256(kSecret, dateStamp); var kRegion = HmacSHA256(kDate, _region); var kService = HmacSHA256(kRegion, ServiceName); return HmacSHA256(kService, AWSRequest); } private static string ToHexString(byte[] array) { return Convert.ToHexString(array).ToLowerInvariant(); } private string GetCredentials(string url) { var request = new HttpRequestMessage { Method = HttpMethod.Get, RequestUri = new Uri($"https://{url}/opencypher") }; var signedrequest = Sign(request); var headers = new Dictionary<string, object> { [HeaderKeys.AuthorizationHeader] = signedrequest.Headers.GetValues(HeaderKeys.AuthorizationHeader).FirstOrDefault(), ["HttpMethod"] = HttpMethod.Get.ToString(), [HeaderKeys.XAmzDateHeader] = signedrequest.Headers.GetValues(HeaderKeys.XAmzDateHeader).FirstOrDefault(), // Host should be capitalized, not like in Amazon.Util.HeaderKeys.HostHeader ["Host"] = signedrequest.Headers.GetValues(HeaderKeys.HostHeader).FirstOrDefault(), }; return JsonSerializer.Serialize(headers); } private HttpRequestMessage Sign(HttpRequestMessage request) { var now = DateTimeOffset.UtcNow; var amzdate = now.ToString("yyyyMMddTHHmmssZ"); var datestamp = now.ToString("yyyyMMdd"); if (request.Headers.Host == null) { request.Headers.Host = $"{request.RequestUri.Host}:{request.RequestUri.Port}"; } request.Headers.Add(HeaderKeys.XAmzDateHeader, amzdate); var canonicalQueryParams = GetCanonicalQueryParams(request); var canonicalRequest = new StringBuilder(); canonicalRequest.Append(request.Method + "\n"); canonicalRequest.Append(request.RequestUri.AbsolutePath + "\n"); canonicalRequest.Append(canonicalQueryParams + "\n"); var signedHeadersList = new List<string>(); foreach (var header in request.Headers.OrderBy(a => a.Key.ToLowerInvariant())) { canonicalRequest.Append(header.Key.ToLowerInvariant()); canonicalRequest.Append(':'); canonicalRequest.Append(string.Join(",", header.Value.Select(s => s.Trim()))); canonicalRequest.Append('\n'); signedHeadersList.Add(header.Key.ToLowerInvariant()); } canonicalRequest.Append('\n'); var signedHeaders = string.Join(";", signedHeadersList); canonicalRequest.Append(signedHeaders + "\n"); canonicalRequest.Append(_emptyPayloadHash); var credentialScope = $"{datestamp}/{_region}/{ServiceName}/{AWSRequest}"; var stringToSign = $"{Algorithm}\n{amzdate}\n{credentialScope}\n" + Hash(Encoding.UTF8.GetBytes(canonicalRequest.ToString())); var signing_key = GetSignatureKey(datestamp); var signature = ToHexString(HmacSHA256(signing_key, stringToSign)); request.Headers.TryAddWithoutValidation(HeaderKeys.AuthorizationHeader, $"{Algorithm} Credential={_accessKey}/{credentialScope}, SignedHeaders={signedHeaders}, Signature={signature}"); return request; } private static string GetCanonicalQueryParams(HttpRequestMessage request) { var querystring = HttpUtility.ParseQueryString(request.RequestUri.Query); // Query params must be escaped in upper case (i.e. "%2C", not "%2c"). var queryParams = querystring.AllKeys.OrderBy(a => a) .Select(key => $"{key}={Uri.EscapeDataString(querystring[key])}"); return string.Join("&", queryParams); } } }

Voici comment créer une requête openCypher en .NET à l'aide de Bolt avec l'authentification IAM. L'exemple ci-dessous utilise l'assistant NeptuneAuthToken :

using Neo4j.Driver; namespace Hello { public class HelloWorldExample { private const string Host = "(your hostname):8182"; private const string Url = $"bolt://{Host}"; private const string CreateNodeQuery = "CREATE (a:Greeting) SET a.message = 'HelloWorldExample'"; private const string ReadNodeQuery = "MATCH(n:Greeting) RETURN n.message"; private const string AccessKey = "(your access key)"; private const string SecretKey = "(your secret key)"; private const string Region = "(your AWS region)"; // e.g. "us-west-2" private readonly IDriver _driver; public HelloWorldExample() { var authToken = new NeptuneAuthToken(AccessKey, SecretKey, Region).GetAuthToken(Host); // Note that when the connection is reinitialized after max connection lifetime // has been reached, the signature token could have already been expired (usually 5 min) // You can face exceptions like: // `Unexpected server exception 'Signature expired: XXXX is now earlier than YYYY (ZZZZ - 5 min.)` _driver = GraphDatabase.Driver(Url, authToken, o => o.WithMaxConnectionLifetime(TimeSpan.FromMinutes(60)).WithEncryptionLevel(EncryptionLevel.Encrypted)); } public async Task CreateNode() { // Open a session using (var session = _driver.AsyncSession()) { // Run the query in a write transaction var greeting = await session.WriteTransactionAsync(async tx => { var result = await tx.RunAsync(CreateNodeQuery); // Consume the result return await result.ConsumeAsync(); }); // The output will look like this: // ResultSummary{Query=`CREATE (a:Greeting) SET a.message = 'HelloWorldExample"..... Console.WriteLine(greeting.Query); } } public async Task RetrieveNode() { // Open a session using (var session = _driver.AsyncSession()) { // Run the query in a read transaction var greeting = await session.ReadTransactionAsync(async tx => { var result = await tx.RunAsync(ReadNodeQuery); var records = await result.ToListAsync(); // Consume the result. Read the single node // created in a previous step. return records[0].Values.First().Value; }); // The output will look like this: // HelloWorldExample Console.WriteLine(greeting); } } } }

Cet exemple peut être lancé en exécutant le code ci-dessous sur .NET 6 ou .NET 7 avec les packages suivants :

  • Neo4j.Driver=4.3.0

  • AWSSDK.Core=3.7.102.1

namespace Hello { class Program { static async Task Main() { var apiCaller = new HelloWorldExample(); await apiCaller.CreateNode(); await apiCaller.RetrieveNode(); } } }

Exemple de requête Golang openCypher à l'aide de Bolt avec l'authentification IAM

L'exemple suivant montre comment effectuer des requêtes OpenCypher dans Go à l'aide du protocole Bolt avec authentification IAM. Il utilise le SDK AWS pour Go v2 pour la signature Sigv4 et le pilote Neo4j Go v5 avec AuthTokenManager une structure qui implémente l'interface de gestion de jetons du pilote Neo4j Go github.com/neo4j/neo4j-go-driver/v5/neo4j/auth.TokenManager () pour actualiser automatiquement les informations d'identification avant leur expiration.

Créez d'abord un fichier AuthTokenManager qui génère des jetons signés SIGv4. Enregistrez ceci sous le nom auth_token_manager.go :

// AuthTokenManager for Amazon Neptune IAM authentication via the Bolt protocol. // Provides SigV4-signed credentials to the Neo4j driver's auth interface. package main import ( "context" "encoding/json" "fmt" "net/http" "reflect" "sync" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws/signer/v4" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/neo4j/neo4j-go-driver/v5/neo4j" "github.com/neo4j/neo4j-go-driver/v5/neo4j/db" ) const ( serviceName = "neptune-db" emptyPayloadHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" ) // AuthTokenManager manages SigV4-signed authentication tokens for Neptune with automatic refresh. type AuthTokenManager struct { region string endpoint string refreshInterval time.Duration credentials aws.CredentialsProvider mutex sync.Mutex cachedToken neo4j.AuthToken tokenTime time.Time } // NewAuthTokenManager creates a new AuthTokenManager. // // Parameters: // - region: AWS region (e.g., "us-east-1") // - endpoint: Neptune endpoint with port (e.g., "cluster.region.neptune.amazonaws.com:8182") // - profile: AWS profile name (optional, pass "" to use default) // - roleArn: AWS role ARN to assume (optional, pass "" to skip) // - refreshInterval: Token refresh interval func NewAuthTokenManager(region, endpoint, profile, roleArn string, refreshInterval time.Duration) (*AuthTokenManager, error) { credentials, err := createCredentialsProvider(region, profile, roleArn) if err != nil { return nil, err } return &AuthTokenManager{ region: region, endpoint: endpoint, refreshInterval: refreshInterval, credentials: credentials, }, nil } // createCredentialsProvider builds an AWS credentials provider, optionally using // a named profile and/or assuming a role. func createCredentialsProvider(region, profile, roleArn string) (aws.CredentialsProvider, error) { ctx := context.Background() var opts []func(*config.LoadOptions) error opts = append(opts, config.WithRegion(region)) if profile != "" { opts = append(opts, config.WithSharedConfigProfile(profile)) } cfg, err := config.LoadDefaultConfig(ctx, opts...) if err != nil { return nil, fmt.Errorf("failed to load AWS config: %w", err) } var credentials aws.CredentialsProvider = cfg.Credentials if roleArn != "" { stsClient := sts.NewFromConfig(cfg) credentials = stscreds.NewAssumeRoleProvider(stsClient, roleArn, func(o *stscreds.AssumeRoleOptions) { o.RoleSessionName = "NeptuneAuthSession" o.Duration = 900 * time.Second }) } return credentials, nil } // GetAuthToken returns a valid authentication token, using cached token if still valid. func (m *AuthTokenManager) GetAuthToken(ctx context.Context) (neo4j.AuthToken, error) { m.mutex.Lock() defer m.mutex.Unlock() if time.Since(m.tokenTime) < m.refreshInterval && m.cachedToken.Tokens != nil { return m.cachedToken, nil } token, err := m.generateToken(ctx) if err != nil { return neo4j.AuthToken{}, err } m.cachedToken = token m.tokenTime = time.Now() return token, nil } // HandleSecurityException handles security exceptions by invalidating the cached // token (if it matches the token that caused the error) and returning true so // the driver retries with a fresh token. The comparison prevents a concurrent // retry from unnecessarily invalidating a freshly generated token. func (m *AuthTokenManager) HandleSecurityException(ctx context.Context, token neo4j.AuthToken, err *db.Neo4jError) (bool, error) { m.mutex.Lock() defer m.mutex.Unlock() if reflect.DeepEqual(m.cachedToken.Tokens, token.Tokens) { m.tokenTime = time.Time{} m.cachedToken = neo4j.AuthToken{} } return true, nil } // generateToken generates a new SigV4-signed authentication token for Neptune. func (m *AuthTokenManager) generateToken(ctx context.Context) (neo4j.AuthToken, error) { req, err := http.NewRequest(http.MethodGet, "https://"+m.endpoint+"/opencypher", nil) if err != nil { return neo4j.AuthToken{}, err } req.Host = m.endpoint creds, err := m.credentials.Retrieve(ctx) if err != nil { return neo4j.AuthToken{}, err } signer := v4.NewSigner() err = signer.SignHTTP(ctx, creds, req, emptyPayloadHash, serviceName, m.region, time.Now()) if err != nil { return neo4j.AuthToken{}, err } authData := map[string]string{ "Authorization": req.Header.Get("Authorization"), "X-Amz-Date": req.Header.Get("X-Amz-Date"), "Host": m.endpoint, "HttpMethod": req.Method, } if st := req.Header.Get("X-Amz-Security-Token"); st != "" { authData["X-Amz-Security-Token"] = st } authJSON, err := json.Marshal(authData) if err != nil { return neo4j.AuthToken{}, err } return neo4j.BasicAuth("username", string(authJSON), ""), nil }

Utilisez ensuite le gestionnaire de jetons pour créer un pilote et rechercher un nœud par identifiant. Notez l'utilisation d'une requête paramétrée ($nodeId) au lieu d'une interpolation de chaîne :

package main import ( "context" "fmt" "log" "time" "github.com/neo4j/neo4j-go-driver/v5/neo4j" ) func findNode(ctx context.Context, driver neo4j.DriverWithContext, nodeId string) (string, error) { session := driver.NewSession(ctx, neo4j.SessionConfig{ AccessMode: neo4j.AccessModeRead, }) defer session.Close(ctx) // Use parameterized queries to prevent injection and enable query plan caching. result, err := session.Run(ctx, "MATCH (n) WHERE ID(n) = $nodeId RETURN n", map[string]any{"nodeId": nodeId}, ) if err != nil { return "", fmt.Errorf("error running query: %v", err) } if !result.Next(ctx) { if err = result.Err(); err != nil { return "", fmt.Errorf("error fetching result: %v", err) } return "", fmt.Errorf("node not found") } n, found := result.Record().Get("n") if !found { return "", fmt.Errorf("node not found") } return fmt.Sprintf("%+v", n), nil } func main() { region := "(your AWS region)" // e.g. "us-east-1" endpoint := "(your Neptune endpoint):8182" // e.g. "cluster.xxx.us-east-1.neptune.amazonaws.com:8182" ctx := context.Background() // Pass a profile name for local development, or a role ARN for cross-account access authManager, err := NewAuthTokenManager(region, endpoint, "", "", 4*time.Minute) // Refresh before the 5-minute SigV4 signature expiry if err != nil { log.Fatalf("auth manager error: %v", err) } // bolt+s:// enables TLS with full certificate verification. // If you're developing on macOS, use bolt+ssc:// instead. Go on macOS uses the // system TLS verifier, which requires Certificate Transparency compliance that // Neptune endpoints don't support. In production (typically Linux), bolt+s:// // works with system CA certificates. driver, err := neo4j.NewDriverWithContext("bolt+s://"+endpoint, authManager) if err != nil { log.Fatalf("driver error: %v", err) } defer driver.Close(ctx) if err = driver.VerifyConnectivity(ctx); err != nil { log.Fatalf("connectivity error: %v", err) } // Neptune assigns UUID-format node IDs if a user does not supply their own node IDs res, err := findNode(ctx, driver, "72c2e8c1-7d5f-5f30-10ca-9d2bb8c4afbc") if err != nil { log.Fatal(err) } fmt.Println(res) }
Note

L'auth.TokenManagerinterface (github.com/neo4j/neo4j-go-driver/v5/neo4j/auth) utilisée dans cet exemple est devenue généralement disponible dans le pilote Neo4j Go v5.14.0. Cette interface permet l'actualisation automatique des informations d'identification, ce qui est nécessaire pour l'authentification Neptune IAM car les signatures SigV4 ne sont valides que pendant une courte période et doivent être régénérées lorsque le pilote établit de nouvelles connexions.

Cet exemple a été validé à l'aide des modules Go suivants :

require ( github.com/aws/aws-sdk-go-v2 v1.30.3 github.com/aws/aws-sdk-go-v2/config v1.27.27 github.com/aws/aws-sdk-go-v2/credentials v1.17.27 github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 github.com/neo4j/neo4j-go-driver/v5 v5.22.0 )

Comportement des connexions Bolt dans Neptune

Voici quelques éléments à garder à l'esprit concernant les connexions Neptune Bolt :

  • Comme les connexions Bolt sont créées au niveau de la couche TCP, vous ne pouvez pas utiliser un Application Load Balancer devant elles, comme c'est le cas avec un point de terminaison HTTP.

  • Le port que Neptune utilise pour les connexions Bolt est le port de votre cluster de bases de données.

  • Sur la base du préambule Bolt qui lui a été transmis, le serveur Neptune sélectionne la version Bolt la plus appropriée (1, 2, 3 ou 4.0).

  • Le nombre maximum de connexions au serveur Neptune qu'un client peut ouvrir à tout moment est de 1 000.

  • Si le client ne ferme pas la connexion après une requête, celle-ci peut être utilisée pour exécuter la requête suivante.

  • Toutefois, si une connexion est inactive pendant 20 minutes, le serveur la ferme automatiquement.

  • Si l'authentification IAM n'est pas activée, vous pouvez utiliser AuthTokens.none() au lieu de les fournir un nom d'utilisateur et un mot de passe factices. Par exemple, en Java :

    GraphDatabase.driver("bolt://(your cluster endpoint URL):(your cluster port)", AuthTokens.none(), Config.builder().withEncryption().withTrustStrategy(TrustStrategy.trustSystemCertificates()).build());
  • Lorsque l'authentification IAM est activée, une connexion Bolt est toujours déconnectée quelques minutes de plus que 10 jours après son établissement si elle n'a pas déjà été fermée pour une autre raison.

  • Si le client envoie une requête à exécuter via une connexion sans avoir consommé les résultats d'une requête précédente, la nouvelle requête est supprimée. Pour ignorer les résultats précédents, le client doit envoyer un message de réinitialisation via la connexion.

  • Une seule transaction à la fois peut être créée sur une connexion donnée.

  • Si une exception se produit au cours d'une transaction, le serveur Neptune annule cette transaction et ferme la connexion. Dans ce cas, le pilote crée une autre connexion pour la prochaine requête.

  • Sachez que les sessions ne sont pas adaptées aux threads. Diverses opérations parallèles doivent utiliser diverses sessions distinctes.