Skip to content

Commit 7cb09b2

Browse files
Feature/add mis endpoints in authentication (#705)
1 parent 24845a3 commit 7cb09b2

File tree

10 files changed

+577
-1
lines changed

10 files changed

+577
-1
lines changed

v2/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Add missing endpoints from administration to v2
77
- Add missing endpoints from cluster to v2
88
- Add missing endpoints from security to v2
9+
- Add missing endpoints from authentication to v2
910

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

v2/arangodb/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ type Client interface {
3838
ClientFoxx
3939
ClientTasks
4040
ClientReplication
41+
ClientAccessTokens
4142
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//
2+
// DISCLAIMER
3+
//
4+
// Copyright 2023-2024 ArangoDB GmbH, Cologne, Germany
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
// Copyright holder is ArangoDB GmbH, Cologne, Germany
19+
//
20+
21+
package arangodb
22+
23+
import "context"
24+
25+
// ClientAccessTokens defines access token API methods.
26+
type ClientAccessTokens interface {
27+
// CreateAccessToken creates a new access token for the specified user.
28+
// Permissions:
29+
// - You can always create an access token for yourself.
30+
// - To create a token for another user, you need admin access
31+
// to the _system database.
32+
CreateAccessToken(ctx context.Context, user *string, req AccessTokenRequest) (CreateAccessTokenResponse, error)
33+
34+
// DeleteAccessToken deletes a specific access token for a given user.
35+
DeleteAccessToken(ctx context.Context, user *string, tokenId *int) error
36+
37+
// GetAllAccessToken retrieves all access tokens for a given user.
38+
GetAllAccessToken(ctx context.Context, user *string) (AccessTokenResponse, error)
39+
}
40+
41+
// AccessTokenRequest represents the input required to create a new access token.
42+
type AccessTokenRequest struct {
43+
// Name is a descriptive name for the access token (e.g., "Token for Service A").
44+
// This helps identify the token later. Required field.
45+
Name *string `json:"name,omitempty"`
46+
47+
// ValidUntil is the Unix timestamp (seconds since epoch) until which the token remains valid.
48+
// Required field. After this time, the token will automatically expire.
49+
ValidUntil *int64 `json:"valid_until,omitempty"`
50+
}
51+
52+
// AccessTokenInfo contains metadata about an access token.
53+
// Embeds AccessTokenRequest to include the name and validity period in the response.
54+
type AccessTokenInfo struct {
55+
// Id is the unique identifier for the access token.
56+
Id *int `json:"id,omitempty"`
57+
58+
// Embed the AccessTokenRequest fields (Name and ValidUntil) for reference.
59+
AccessTokenRequest
60+
61+
// CreatedAt is the Unix timestamp when the token was created.
62+
CreatedAt *int64 `json:"created_at,omitempty"`
63+
64+
// Fingerprint is a unique string associated with the token,
65+
// useful for tracking or verifying the token without revealing it.
66+
Fingerprint *string `json:"fingerprint,omitempty"`
67+
68+
// Active indicates whether the token is currently active or has been revoked/expired.
69+
Active *bool `json:"active,omitempty"`
70+
}
71+
72+
// CreateAccessTokenResponse represents the response returned when creating a new access token.
73+
type CreateAccessTokenResponse struct {
74+
// Embed the AccessTokenInfo metadata.
75+
AccessTokenInfo
76+
77+
// Token is the actual access token string.
78+
// It is only returned once at creation and should be stored securely.
79+
Token *string `json:"token,omitempty"`
80+
}
81+
82+
// AccessTokenResponse represents a list of access tokens for a user.
83+
type AccessTokenResponse struct {
84+
// Tokens is a list of all access tokens associated with a user.
85+
Tokens []AccessTokenInfo `json:"tokens,omitempty"`
86+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
//
2+
// DISCLAIMER
3+
//
4+
// Copyright 2023-2024 ArangoDB GmbH, Cologne, Germany
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
// Copyright holder is ArangoDB GmbH, Cologne, Germany
19+
//
20+
21+
package arangodb
22+
23+
import (
24+
"context"
25+
"net/http"
26+
"net/url"
27+
"strconv"
28+
29+
"github.com/arangodb/go-driver/v2/arangodb/shared"
30+
"github.com/arangodb/go-driver/v2/connection"
31+
"github.com/pkg/errors"
32+
)
33+
34+
type clientAccessTokens struct {
35+
client *client
36+
}
37+
38+
func newClientAccessTokens(client *client) *clientAccessTokens {
39+
return &clientAccessTokens{
40+
client: client,
41+
}
42+
}
43+
44+
var _ ClientAccessTokens = &clientAccessTokens{}
45+
46+
func validateAccessTokenReqParams(req AccessTokenRequest) (map[string]interface{}, error) {
47+
reqParams := make(map[string]interface{})
48+
if req.Name == nil {
49+
return nil, RequiredFieldError("name")
50+
}
51+
if req.ValidUntil == nil {
52+
return nil, RequiredFieldError("valid_until")
53+
}
54+
reqParams["name"] = *req.Name
55+
reqParams["valid_until"] = *req.ValidUntil
56+
return reqParams, nil
57+
}
58+
59+
// CreateAccessToken creates a new access token for the specified user.
60+
// Permissions:
61+
// - You can always create an access token for yourself.
62+
// - To create a token for another user, you need admin access
63+
// to the _system database.
64+
func (c *clientAccessTokens) CreateAccessToken(ctx context.Context, user *string, req AccessTokenRequest) (CreateAccessTokenResponse, error) {
65+
if user == nil {
66+
return CreateAccessTokenResponse{}, RequiredFieldError("user")
67+
}
68+
// Build the URL for the access token endpoint, safely escaping the username
69+
url := connection.NewUrl("_api", "token", url.PathEscape(*user))
70+
71+
var response struct {
72+
shared.ResponseStruct `json:",inline"`
73+
CreateAccessTokenResponse `json:",inline"`
74+
}
75+
76+
reqParams, err := validateAccessTokenReqParams(req)
77+
if err != nil {
78+
return CreateAccessTokenResponse{}, errors.WithStack(err)
79+
}
80+
81+
resp, err := connection.CallPost(ctx, c.client.connection, url, &response, reqParams)
82+
if err != nil {
83+
return CreateAccessTokenResponse{}, errors.WithStack(err)
84+
}
85+
86+
switch code := resp.Code(); code {
87+
case http.StatusOK:
88+
return response.CreateAccessTokenResponse, nil
89+
default:
90+
return CreateAccessTokenResponse{}, response.AsArangoErrorWithCode(code)
91+
}
92+
}
93+
94+
// DeleteAccessToken deletes a specific access token for a given user.
95+
func (c *clientAccessTokens) DeleteAccessToken(ctx context.Context, user *string, tokenId *int) error {
96+
if user == nil {
97+
return RequiredFieldError("user")
98+
}
99+
if tokenId == nil {
100+
return RequiredFieldError("token-id")
101+
}
102+
// Build the URL for the access token endpoint, safely escaping the username
103+
url := connection.NewUrl("_api", "token", url.PathEscape(*user), url.PathEscape(strconv.Itoa(*tokenId)))
104+
105+
resp, err := connection.CallDelete(ctx, c.client.connection, url, nil)
106+
if err != nil {
107+
return errors.WithStack(err)
108+
}
109+
110+
switch code := resp.Code(); code {
111+
case http.StatusOK:
112+
return nil
113+
default:
114+
return (&shared.ResponseStruct{}).AsArangoErrorWithCode(resp.Code())
115+
}
116+
}
117+
118+
// GetAllAccessToken retrieves all access tokens for a given user.
119+
func (c *clientAccessTokens) GetAllAccessToken(ctx context.Context, user *string) (AccessTokenResponse, error) {
120+
if user == nil {
121+
return AccessTokenResponse{}, RequiredFieldError("user")
122+
}
123+
// Build the URL for the access token endpoint, safely escaping the username
124+
url := connection.NewUrl("_api", "token", url.PathEscape(*user))
125+
126+
var response struct {
127+
shared.ResponseStruct `json:",inline"`
128+
AccessTokenResponse `json:",inline"`
129+
}
130+
131+
resp, err := connection.CallGet(ctx, c.client.connection, url, &response)
132+
if err != nil {
133+
return AccessTokenResponse{}, errors.WithStack(err)
134+
}
135+
136+
switch code := resp.Code(); code {
137+
case http.StatusOK:
138+
return response.AccessTokenResponse, nil
139+
default:
140+
return AccessTokenResponse{}, response.AsArangoErrorWithCode(code)
141+
}
142+
}

v2/arangodb/client_admin.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ type ClientAdmin interface {
8585
// the --rocksdb.encryption-keyfolder and re-encrypts the internal encryption key.
8686
// Requires superuser rights and is not available on Coordinators.
8787
RotateEncryptionAtRestKey(ctx context.Context) ([]EncryptionKey, error)
88+
// GetJWTSecrets retrieves information about the currently loaded JWT secrets
89+
// for a given database.
90+
// Requires a superuser JWT for authorization.
91+
GetJWTSecrets(ctx context.Context, dbName string) (JWTSecretsResult, error)
92+
93+
// ReloadJWTSecrets forces the server to reload the JWT secrets from disk.
94+
// Requires a superuser JWT for authorization.
95+
ReloadJWTSecrets(ctx context.Context) (JWTSecretsResult, error)
8896
}
8997

9098
type ClientAdminLog interface {
@@ -588,3 +596,14 @@ type EncryptionKey struct {
588596
// This is used to uniquely identify which key is active/available.
589597
SHA256 *string `json:"sha256,omitempty"`
590598
}
599+
600+
// JWTSecretsResult contains the active and passive JWT secrets
601+
type JWTSecretsResult struct {
602+
Active *JWTSecret `json:"active,omitempty"` // The currently active JWT secret
603+
Passive []JWTSecret `json:"passive,omitempty"` // List of passive JWT secrets (may be empty)
604+
}
605+
606+
// JWTSecret represents a single JWT secret's SHA-256 hash
607+
type JWTSecret struct {
608+
SHA256 *string `json:"sha256,omitempty"` // SHA-256 hash of the JWT secret
609+
}

v2/arangodb/client_admin_impl.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,3 +436,52 @@ func (c *clientAdmin) RotateEncryptionAtRestKey(ctx context.Context) ([]Encrypti
436436
return nil, response.AsArangoErrorWithCode(code)
437437
}
438438
}
439+
440+
// GetJWTSecrets retrieves information about the currently loaded JWT secrets
441+
// for a given database.
442+
// Requires a superuser JWT for authorization.
443+
func (c *clientAdmin) GetJWTSecrets(ctx context.Context, dbName string) (JWTSecretsResult, error) {
444+
// Build the URL for the JWT secrets endpoint, safely escaping the database name
445+
url := connection.NewUrl("_db", url.PathEscape(dbName), "_admin", "server", "jwt")
446+
447+
var response struct {
448+
shared.ResponseStruct `json:",inline"` // Common fields: error, code, etc.
449+
Result JWTSecretsResult `json:"result"` // Contains active and passive JWT secrets
450+
}
451+
452+
resp, err := connection.CallGet(ctx, c.client.connection, url, &response)
453+
if err != nil {
454+
return JWTSecretsResult{}, errors.WithStack(err)
455+
}
456+
457+
switch code := resp.Code(); code {
458+
case http.StatusOK:
459+
return response.Result, nil
460+
default:
461+
return JWTSecretsResult{}, response.AsArangoErrorWithCode(code)
462+
}
463+
}
464+
465+
// ReloadJWTSecrets forces the server to reload the JWT secrets from disk.
466+
// Requires a superuser JWT for authorization.
467+
func (c *clientAdmin) ReloadJWTSecrets(ctx context.Context) (JWTSecretsResult, error) {
468+
// Build the URL for the JWT secrets endpoint, safely escaping the database name
469+
url := connection.NewUrl("_admin", "server", "jwt")
470+
471+
var response struct {
472+
shared.ResponseStruct `json:",inline"` // Common fields: error, code, etc.
473+
Result JWTSecretsResult `json:"result"` // Contains active and passive JWT secrets
474+
}
475+
476+
resp, err := connection.CallPost(ctx, c.client.connection, url, &response, nil)
477+
if err != nil {
478+
return JWTSecretsResult{}, errors.WithStack(err)
479+
}
480+
481+
switch code := resp.Code(); code {
482+
case http.StatusOK:
483+
return response.Result, nil
484+
default:
485+
return JWTSecretsResult{}, response.AsArangoErrorWithCode(code)
486+
}
487+
}

v2/arangodb/client_impl.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func newClient(connection connection.Connection) *client {
4141
c.clientFoxx = newClientFoxx(c)
4242
c.clientTask = newClientTask(c)
4343
c.clientReplication = newClientReplication(c)
44+
c.clientAccessTokens = newClientAccessTokens(c)
4445

4546
c.Requests = NewRequests(connection)
4647

@@ -60,6 +61,7 @@ type client struct {
6061
*clientFoxx
6162
*clientTask
6263
*clientReplication
64+
*clientAccessTokens
6365

6466
Requests
6567
}

v2/arangodb/utils.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"encoding/json"
2727
"fmt"
2828
"io"
29+
"net/http"
2930
"reflect"
3031

3132
"github.com/pkg/errors"
@@ -110,6 +111,18 @@ func CreateDocuments(ctx context.Context, col Collection, docCount int, generato
110111
return err
111112
}
112113

114+
type ClientError struct {
115+
Code int
116+
Message string
117+
}
118+
119+
func (e *ClientError) Error() string {
120+
return e.Message
121+
}
122+
113123
func RequiredFieldError(field string) error {
114-
return fmt.Errorf("%s field must be set", field)
124+
return &ClientError{
125+
Code: http.StatusBadRequest,
126+
Message: fmt.Sprintf("%s field must be set", field),
127+
}
115128
}

0 commit comments

Comments
 (0)