Skip to content

Commit c60f4ea

Browse files
authored
Access Tokens Support (#383)
* Access tokens * Bump driver version
1 parent 0d8e6ca commit c60f4ea

File tree

5 files changed

+168
-2
lines changed

5 files changed

+168
-2
lines changed

arango/database.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
from arango.connection import Connection
2020
from arango.errno import HTTP_NOT_FOUND
2121
from arango.exceptions import (
22+
AccessTokenCreateError,
23+
AccessTokenDeleteError,
24+
AccessTokenListError,
2225
AnalyzerCreateError,
2326
AnalyzerDeleteError,
2427
AnalyzerGetError,
@@ -1158,6 +1161,89 @@ def response_handler(resp: Response) -> Json:
11581161

11591162
return self._execute(request, response_handler)
11601163

1164+
def create_access_token(
1165+
self,
1166+
user: str,
1167+
name: str,
1168+
valid_until: int,
1169+
) -> Result[Json]:
1170+
"""Create an access token for the given user.
1171+
1172+
:param user: The name of the user.
1173+
:type user: str
1174+
:param name: A name for the access token to make identification easier,
1175+
like a short description.
1176+
:type name: str
1177+
:param valid_until: A Unix timestamp in seconds to set the expiration
1178+
date and time.
1179+
:type valid_until: int
1180+
1181+
:return: Information about the created access token, including the token itself.
1182+
:rtype: dict
1183+
1184+
:raise arango.exceptions.AccessTokenCreateError: If the operations fails.
1185+
"""
1186+
data: Json = {
1187+
"name": name,
1188+
"valid_until": valid_until,
1189+
}
1190+
1191+
request = Request(
1192+
method="post",
1193+
endpoint=f"/_api/token/{user}",
1194+
data=data,
1195+
)
1196+
1197+
def response_handler(resp: Response) -> Json:
1198+
if not resp.is_success:
1199+
raise AccessTokenCreateError(resp, request)
1200+
result: Json = resp.body
1201+
return result
1202+
1203+
return self._executor.execute(request, response_handler)
1204+
1205+
def delete_access_token(self, user: str, token_id: int) -> Result[None]:
1206+
"""Delete an access token for the given user.
1207+
1208+
:param user: The name of the user.
1209+
:type user: str
1210+
:param token_id: The ID of the access token to delete.
1211+
:type token_id: int
1212+
1213+
:raise arango.exceptions.AccessTokenDeleteError: If the operation fails.
1214+
"""
1215+
request = Request(
1216+
method="delete",
1217+
endpoint=f"/_api/token/{user}/{token_id}",
1218+
)
1219+
1220+
def response_handler(resp: Response) -> None:
1221+
if not resp.is_success:
1222+
raise AccessTokenDeleteError(resp, request)
1223+
1224+
return self._executor.execute(request, response_handler)
1225+
1226+
def list_access_tokens(self, user: str) -> Result[Jsons]:
1227+
"""List all access tokens for the given user.
1228+
1229+
:param user: The name of the user.
1230+
:type user: str
1231+
1232+
:return: List of access tokens for the user.
1233+
:rtype: list
1234+
1235+
:raise arango.exceptions.AccessTokenListError: If the operation fails.
1236+
"""
1237+
request = Request(method="get", endpoint=f"/_api/token/{user}")
1238+
1239+
def response_handler(resp: Response) -> Jsons:
1240+
if not resp.is_success:
1241+
raise AccessTokenListError(resp, request)
1242+
result: Jsons = resp.body["tokens"]
1243+
return result
1244+
1245+
return self._executor.execute(request, response_handler)
1246+
11611247
def tls(self) -> Result[Json]:
11621248
"""Return TLS data (server key, client-auth CA).
11631249

arango/exceptions.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,23 @@ class AQLQueryRulesGetError(ArangoServerError):
161161
"""Failed to retrieve AQL query rules."""
162162

163163

164+
#######################
165+
# Access Token Errors #
166+
#######################
167+
168+
169+
class AccessTokenCreateError(ArangoServerError):
170+
"""Failed to create an access token."""
171+
172+
173+
class AccessTokenDeleteError(ArangoServerError):
174+
"""Failed to delete an access token."""
175+
176+
177+
class AccessTokenListError(ArangoServerError):
178+
"""Failed to retrieve access tokens."""
179+
180+
164181
##############################
165182
# Async Execution Exceptions #
166183
##############################

arango/request.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def normalize_headers(
1212
if driver_flags is not None:
1313
for flag in driver_flags:
1414
flags = flags + flag + ";"
15-
driver_version = "8.2.4"
15+
driver_version = "8.2.5"
1616
driver_header = "python-arango/" + driver_version + " (" + flags + ")"
1717
normalized_headers: Headers = {
1818
"charset": "utf-8",

tests/helpers.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ def generate_service_mount():
108108
return f"/test_{uuid4().hex}"
109109

110110

111+
def generate_token_name():
112+
"""Generate and return a random token name.
113+
114+
:return: Random token name.
115+
:rtype: str
116+
"""
117+
return f"test_token_{uuid4().hex}"
118+
119+
111120
def generate_jwt(secret, exp=3600):
112121
"""Generate and return a JWT.
113122

tests/test_auth.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
import time
2+
13
from arango.connection import BasicConnection, JwtConnection, JwtSuperuserConnection
24
from arango.errno import FORBIDDEN, HTTP_UNAUTHORIZED
35
from arango.exceptions import (
6+
AccessTokenCreateError,
7+
AccessTokenDeleteError,
8+
AccessTokenListError,
49
JWTAuthError,
510
JWTExpiredError,
611
JWTSecretListError,
@@ -11,7 +16,12 @@
1116
ServerTLSReloadError,
1217
ServerVersionError,
1318
)
14-
from tests.helpers import assert_raises, generate_jwt, generate_string
19+
from tests.helpers import (
20+
assert_raises,
21+
generate_jwt,
22+
generate_string,
23+
generate_token_name,
24+
)
1525

1626

1727
def test_auth_invalid_method(client, db_name, username, password):
@@ -155,3 +165,47 @@ def test_auth_jwt_expiry(client, db_name, root_password, secret):
155165
db = client.db("_system", user_token=valid_token)
156166
with assert_raises(JWTExpiredError) as err:
157167
db.conn.set_token(expired_token)
168+
169+
170+
def test_auth_access_token(client, db_name, username, password, bad_db):
171+
# Login with basic auth
172+
db_auth_basic = client.db(
173+
name=db_name,
174+
username=username,
175+
password=password,
176+
verify=True,
177+
auth_method="basic",
178+
)
179+
180+
# Create an access token
181+
token_name = generate_token_name()
182+
token = db_auth_basic.create_access_token(
183+
user=username, name=token_name, valid_until=int(time.time() + 3600)
184+
)
185+
assert token["active"] is True
186+
187+
# Cannot create a token with the same name
188+
with assert_raises(AccessTokenCreateError):
189+
db_auth_basic.create_access_token(
190+
user=username, name=token_name, valid_until=int(time.time() + 3600)
191+
)
192+
193+
# Authenticate with the created token
194+
access_token_db = client.db(
195+
name=db_name,
196+
username=username,
197+
password=token["token"],
198+
verify=True,
199+
auth_method="basic",
200+
)
201+
202+
# List access tokens
203+
tokens = access_token_db.list_access_tokens(username)
204+
assert isinstance(tokens, list)
205+
with assert_raises(AccessTokenListError):
206+
bad_db.list_access_tokens(username)
207+
208+
# Clean up
209+
access_token_db.delete_access_token(username, token["id"])
210+
with assert_raises(AccessTokenDeleteError):
211+
access_token_db.delete_access_token(username, token["id"])

0 commit comments

Comments
 (0)