Skip to content

Commit 24845a3

Browse files
Feature/add mis endpoints in security (#704)
1 parent f545158 commit 24845a3

File tree

4 files changed

+279
-0
lines changed

4 files changed

+279
-0
lines changed

v2/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Add missing endpoints from monitoring to v2
66
- Add missing endpoints from administration to v2
77
- Add missing endpoints from cluster to v2
8+
- Add missing endpoints from security to v2
89

910
## [2.1.5](https://github.com/arangodb/go-driver/tree/v2.1.5) (2025-08-31)
1011
- Add tasks endpoints to v2

v2/arangodb/client_admin.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,20 @@ type ClientAdmin interface {
7171
// by compacting the entire database system data.
7272
// The endpoint requires superuser access.
7373
CompactDatabases(ctx context.Context, opts *CompactOpts) (map[string]interface{}, error)
74+
75+
// GetTLSData returns information about the server's TLS configuration.
76+
// This call requires authentication.
77+
GetTLSData(ctx context.Context, dbName string) (TLSDataResponse, error)
78+
79+
// ReloadTLSData triggers a reload of all TLS data (server key, client-auth CA)
80+
// and returns the updated TLS configuration summary.
81+
// Requires superuser rights.
82+
ReloadTLSData(ctx context.Context) (TLSDataResponse, error)
83+
84+
// RotateEncryptionAtRestKey reloads the user-supplied encryption key from
85+
// the --rocksdb.encryption-keyfolder and re-encrypts the internal encryption key.
86+
// Requires superuser rights and is not available on Coordinators.
87+
RotateEncryptionAtRestKey(ctx context.Context) ([]EncryptionKey, error)
7488
}
7589

7690
type ClientAdminLog interface {
@@ -541,3 +555,36 @@ type CompactOpts struct {
541555
// Whether or not to compact the bottommost level of data.
542556
CompactBottomMostLevel *bool `json:"compactBottomMostLevel,omitempty"`
543557
}
558+
559+
// ServerName represents the hostname used in SNI configuration.
560+
type ServerName string
561+
562+
// TLSConfigObject describes the details of a TLS keyfile or CA file.
563+
type TLSDataObject struct {
564+
// SHA-256 hash of the whole input file (certificate or CA file).
565+
Sha256 *string `json:"sha256,omitempty"`
566+
// Public certificates in the chain, in PEM format.
567+
Certificates []string `json:"certificates,omitempty"`
568+
// SHA-256 hash of the private key (only present for keyfile).
569+
PrivateKeySha256 *string `json:"privateKeySha256,omitempty"`
570+
}
571+
572+
// TLSConfigResponse represents the response of the TLS configuration endpoint.
573+
type TLSDataResponse struct {
574+
// Information about the server TLS keyfile (certificate + private key).
575+
Keyfile *TLSDataObject `json:"keyfile,omitempty"`
576+
// Information about the CA certificates used for client verification.
577+
ClientCA *TLSDataObject `json:"clientCA,omitempty"`
578+
// Optional mapping of server names (via SNI) to their respective TLS configurations.
579+
SNI map[ServerName]TLSDataObject `json:"sni,omitempty"`
580+
}
581+
582+
// EncryptionKey represents metadata about an encryption key used for
583+
// RocksDB encryption-at-rest in ArangoDB.
584+
// The server exposes only the SHA-256 hash of the key for identification.
585+
// The actual key material is never returned for security reasons.
586+
type EncryptionKey struct {
587+
// SHA256 is the SHA-256 hash of the encryption key, encoded as a hex string.
588+
// This is used to uniquely identify which key is active/available.
589+
SHA256 *string `json:"sha256,omitempty"`
590+
}

v2/arangodb/client_admin_impl.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,3 +364,75 @@ func (c *clientAdmin) CompactDatabases(ctx context.Context, opts *CompactOpts) (
364364
return nil, (&shared.ResponseStruct{}).AsArangoErrorWithCode(code)
365365
}
366366
}
367+
368+
// GetTLSData returns information about the server's TLS configuration.
369+
// This call requires authentication.
370+
func (c *clientAdmin) GetTLSData(ctx context.Context, dbName string) (TLSDataResponse, error) {
371+
url := connection.NewUrl("_db", url.PathEscape(dbName), "_admin", "server", "tls")
372+
373+
var response struct {
374+
shared.ResponseStruct `json:",inline"`
375+
Result TLSDataResponse `json:"result,omitempty"`
376+
}
377+
378+
resp, err := connection.CallGet(ctx, c.client.connection, url, &response)
379+
if err != nil {
380+
return TLSDataResponse{}, errors.WithStack(err)
381+
}
382+
383+
switch code := resp.Code(); code {
384+
case http.StatusOK:
385+
return response.Result, nil
386+
default:
387+
return TLSDataResponse{}, response.AsArangoErrorWithCode(code)
388+
}
389+
}
390+
391+
// ReloadTLSData triggers a reload of all TLS data (server key, client-auth CA)
392+
// and returns the updated TLS configuration summary.
393+
// Requires superuser rights.
394+
func (c *clientAdmin) ReloadTLSData(ctx context.Context) (TLSDataResponse, error) {
395+
url := connection.NewUrl("_admin", "server", "tls")
396+
397+
var response struct {
398+
shared.ResponseStruct `json:",inline"`
399+
Result TLSDataResponse `json:"result,omitempty"`
400+
}
401+
402+
// POST request, no body required
403+
resp, err := connection.CallPost(ctx, c.client.connection, url, &response, nil)
404+
if err != nil {
405+
return TLSDataResponse{}, errors.WithStack(err)
406+
}
407+
switch code := resp.Code(); code {
408+
case http.StatusOK:
409+
return response.Result, nil
410+
// Requires superuser rights, otherwise returns 403 Forbidden
411+
default:
412+
return TLSDataResponse{}, response.AsArangoErrorWithCode(code)
413+
}
414+
}
415+
416+
// RotateEncryptionAtRestKey reloads the user-supplied encryption key from
417+
// the --rocksdb.encryption-keyfolder and re-encrypts the internal encryption key.
418+
// Requires superuser rights and is not available on Coordinators.
419+
func (c *clientAdmin) RotateEncryptionAtRestKey(ctx context.Context) ([]EncryptionKey, error) {
420+
url := connection.NewUrl("_admin", "server", "encryption")
421+
422+
var response struct {
423+
shared.ResponseStruct `json:",inline"`
424+
Result []EncryptionKey `json:"result,omitempty"`
425+
}
426+
427+
// POST request, no body required
428+
resp, err := connection.CallPost(ctx, c.client.connection, url, &response, nil)
429+
if err != nil {
430+
return nil, errors.WithStack(err)
431+
}
432+
switch code := resp.Code(); code {
433+
case http.StatusOK:
434+
return response.Result, nil
435+
default:
436+
return nil, response.AsArangoErrorWithCode(code)
437+
}
438+
}

v2/tests/admin_test.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"context"
2424
"errors"
2525
"net/http"
26+
"strings"
2627
"testing"
2728
"time"
2829

@@ -309,3 +310,161 @@ func Test_CompactDatabases(t *testing.T) {
309310
})
310311
})
311312
}
313+
314+
// Test_GetTLSData checks that TLS configuration data is available and valid, skipping if not configured.
315+
func Test_GetTLSData(t *testing.T) {
316+
Wrap(t, func(t *testing.T, client arangodb.Client) {
317+
withContextT(t, time.Minute, func(ctx context.Context, t testing.TB) {
318+
db, err := client.GetDatabase(ctx, "_system", nil)
319+
require.NoError(t, err)
320+
321+
// Get TLS data using the client (which embeds ClientAdmin)
322+
tlsResp, err := client.GetTLSData(ctx, db.Name())
323+
if err != nil {
324+
var arangoErr shared.ArangoError
325+
if errors.As(err, &arangoErr) {
326+
t.Logf("GetTLSData failed with ArangoDB error code: %d", arangoErr.Code)
327+
switch arangoErr.Code {
328+
case 403:
329+
t.Skip("Skipping TLS get test - authentication/permission denied (HTTP 403)")
330+
default:
331+
t.Logf("Unexpected ArangoDB error code: %d, message: %s", arangoErr.Code, arangoErr.ErrorMessage)
332+
}
333+
return
334+
}
335+
// Skip for any other error (TLS not configured, network issues, etc.)
336+
t.Logf("GetTLSData failed: %v", err)
337+
t.Skip("Skipping TLS get test - likely TLS not configured or other server issue")
338+
}
339+
340+
// Success! Validate response structure
341+
t.Logf("TLS data retrieved successfully")
342+
343+
// Validate TLS response data
344+
validateTLSResponse(t, tlsResp, "Retrieved")
345+
})
346+
})
347+
}
348+
349+
// validateTLSResponse is a helper function to validate TLS response data
350+
func validateTLSResponse(t testing.TB, tlsResp arangodb.TLSDataResponse, operation string) {
351+
// Basic validation - at least one field should be populated
352+
hasData := false
353+
if tlsResp.Keyfile != nil {
354+
if tlsResp.Keyfile.Sha256 != nil && *tlsResp.Keyfile.Sha256 != "" {
355+
t.Logf("%s keyfile SHA256: %s", operation, *tlsResp.Keyfile.Sha256)
356+
hasData = true
357+
}
358+
if len(tlsResp.Keyfile.Certificates) > 0 {
359+
t.Logf("%s keyfile contains %d certificates", operation, len(tlsResp.Keyfile.Certificates))
360+
hasData = true
361+
362+
// Validate certificate content (basic PEM format check)
363+
for i, cert := range tlsResp.Keyfile.Certificates {
364+
require.NotEmpty(t, cert, "Certificate %d should not be empty", i)
365+
// Basic PEM format validation
366+
if !strings.Contains(cert, "-----BEGIN CERTIFICATE-----") {
367+
t.Logf("Warning: Certificate %d may not be in PEM format", i)
368+
} else {
369+
t.Logf("Certificate %d appears to be valid PEM format", i)
370+
}
371+
}
372+
}
373+
if tlsResp.Keyfile.PrivateKeySha256 != nil && *tlsResp.Keyfile.PrivateKeySha256 != "" {
374+
t.Logf("%s keyfile private key SHA256: %s", operation, *tlsResp.Keyfile.PrivateKeySha256)
375+
hasData = true
376+
}
377+
}
378+
if tlsResp.ClientCA != nil && tlsResp.ClientCA.Sha256 != nil && *tlsResp.ClientCA.Sha256 != "" {
379+
t.Logf("%s client CA SHA256: %s", operation, *tlsResp.ClientCA.Sha256)
380+
hasData = true
381+
}
382+
if len(tlsResp.SNI) > 0 {
383+
t.Logf("%s SNI configurations found: %d", operation, len(tlsResp.SNI))
384+
hasData = true
385+
}
386+
if hasData {
387+
t.Logf("TLS configuration data validated successfully")
388+
} else {
389+
t.Logf("TLS endpoint accessible but no TLS data returned - server may not have TLS configured")
390+
}
391+
}
392+
393+
// Test_ReloadTLSData tests TLS certificate reload functionality, skipping if superuser rights unavailable.
394+
func Test_ReloadTLSData(t *testing.T) {
395+
Wrap(t, func(t *testing.T, client arangodb.Client) {
396+
withContextT(t, time.Minute, func(ctx context.Context, t testing.TB) {
397+
// Reload TLS data - requires superuser rights
398+
tlsResp, err := client.ReloadTLSData(ctx)
399+
if err != nil {
400+
var arangoErr shared.ArangoError
401+
if errors.As(err, &arangoErr) {
402+
t.Logf("ReloadTLSData failed with ArangoDB error code: %d", arangoErr.Code)
403+
switch arangoErr.Code {
404+
case 403:
405+
t.Skip("Skipping TLS reload test - superuser rights required (HTTP 403)")
406+
default:
407+
t.Logf("Unexpected ArangoDB error code: %d, message: %s", arangoErr.Code, arangoErr.ErrorMessage)
408+
}
409+
return
410+
}
411+
// Skip for any other error (TLS not configured, network issues, etc.)
412+
t.Logf("ReloadTLSData failed: %v", err)
413+
t.Skip("Skipping TLS reload test - likely TLS not configured or other server issue")
414+
}
415+
416+
// Success! Validate response structure
417+
t.Logf("TLS data reloaded successfully")
418+
419+
// Validate TLS response data
420+
validateTLSResponse(t, tlsResp, "Reloaded")
421+
})
422+
})
423+
}
424+
425+
// Test_RotateEncryptionAtRestKey verifies that the encryption key rotation endpoint works as expected.
426+
// The test is skipped if superuser rights are missing or the feature is disabled/not configured.
427+
func Test_RotateEncryptionAtRestKey(t *testing.T) {
428+
Wrap(t, func(t *testing.T, client arangodb.Client) {
429+
withContextT(t, time.Minute, func(ctx context.Context, t testing.TB) {
430+
431+
// Attempt to rotate encryption at rest key - requires superuser rights
432+
resp, err := client.RotateEncryptionAtRestKey(ctx)
433+
if err != nil {
434+
var arangoErr shared.ArangoError
435+
if errors.As(err, &arangoErr) {
436+
t.Logf("RotateEncryptionAtRestKey failed with ArangoDB error code: %d", arangoErr.Code)
437+
switch arangoErr.Code {
438+
case 403:
439+
t.Skip("Skipping RotateEncryptionAtRestKey test - superuser rights required (HTTP 403)")
440+
case 404:
441+
t.Skip("Skipping RotateEncryptionAtRestKey test - encryption key rotation disabled (HTTP 404)")
442+
default:
443+
t.Logf("Unexpected ArangoDB error code: %d, message: %s", arangoErr.Code, arangoErr.ErrorMessage)
444+
t.FailNow()
445+
}
446+
} else {
447+
t.Fatalf("RotateEncryptionAtRestKey failed with unexpected error: %v", err)
448+
}
449+
return
450+
}
451+
452+
// Convert response to JSON for logging
453+
encryptionRespJson, err := utils.ToJSONString(resp)
454+
require.NoError(t, err)
455+
t.Logf("RotateEncryptionAtRestKey response: %s", encryptionRespJson)
456+
457+
// Validate the response is not nil
458+
require.NotNil(t, resp, "Expected non-nil response")
459+
t.Logf("RotateEncryptionAtRestKey succeeded with %d encryption keys", len(resp))
460+
461+
// Validate each encryption key
462+
for i, key := range resp {
463+
// Explicit nil check for pointer
464+
require.NotNil(t, key.SHA256, "Expected encryption key %d SHA256 not to be nil", i)
465+
require.NotEmpty(t, *key.SHA256, "Expected encryption key %d SHA256 not to be empty", i)
466+
t.Logf("Encryption key %d SHA256: %s", i, *key.SHA256)
467+
}
468+
})
469+
})
470+
}

0 commit comments

Comments
 (0)