Skip to content

Commit 0dbce33

Browse files
authored
Basic user management operations (#25)
1 parent 75d1989 commit 0dbce33

File tree

4 files changed

+246
-0
lines changed

4 files changed

+246
-0
lines changed

arangoasync/database.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
DatabaseDeleteError,
1818
DatabaseListError,
1919
ServerStatusError,
20+
UserCreateError,
21+
UserDeleteError,
22+
UserGetError,
23+
UserListError,
2024
)
2125
from arangoasync.executor import ApiExecutor, DefaultApiExecutor
2226
from arangoasync.request import Method, Request
@@ -553,6 +557,164 @@ def response_handler(resp: Response) -> bool:
553557

554558
return await self._executor.execute(request, response_handler)
555559

560+
async def has_user(self, username: str) -> Result[bool]:
561+
"""Check if a user exists.
562+
563+
Args:
564+
username (str): Username.
565+
566+
Returns:
567+
bool: True if the user exists, False otherwise.
568+
569+
Raises:
570+
UserListError: If the operation fails.
571+
"""
572+
request = Request(method=Method.GET, endpoint=f"/_api/user/{username}")
573+
574+
def response_handler(resp: Response) -> bool:
575+
if resp.is_success:
576+
return True
577+
if resp.status_code == HTTP_NOT_FOUND:
578+
return False
579+
raise UserListError(resp, request)
580+
581+
return await self._executor.execute(request, response_handler)
582+
583+
async def user(self, username: str) -> Result[UserInfo]:
584+
"""Fetches data about a user.
585+
586+
Args:
587+
username (str): Username.
588+
589+
Returns:
590+
UserInfo: User details.
591+
592+
Raises:
593+
UserGetError: If the operation fails.
594+
595+
References:
596+
- `get-a-user` <https://docs.arangodb.com/stable/develop/http-api/users/#get-a-user>`__
597+
""" # noqa: E501
598+
request = Request(method=Method.GET, endpoint=f"/_api/user/{username}")
599+
600+
def response_handler(resp: Response) -> UserInfo:
601+
if not resp.is_success:
602+
raise UserGetError(resp, request)
603+
body = self.deserializer.loads(resp.raw_body)
604+
return UserInfo(
605+
user=body["user"],
606+
active=cast(bool, body.get("active")),
607+
extra=body.get("extra"),
608+
)
609+
610+
return await self._executor.execute(request, response_handler)
611+
612+
async def users(self) -> Result[Sequence[UserInfo]]:
613+
"""Fetches data about all users.
614+
615+
Without the necessary permissions, you might only get data about the
616+
current user.
617+
618+
Returns:
619+
list: User information.
620+
621+
Raises:
622+
UserListError: If the operation fails.
623+
624+
References:
625+
- `list-available-users <https://docs.arangodb.com/stable/develop/http-api/users/#list-available-users>`__
626+
""" # noqa: E501
627+
request = Request(method=Method.GET, endpoint="/_api/user")
628+
629+
def response_handler(resp: Response) -> Sequence[UserInfo]:
630+
if not resp.is_success:
631+
raise UserListError(resp, request)
632+
body = self.deserializer.loads(resp.raw_body)
633+
return [
634+
UserInfo(user=u["user"], active=u.get("active"), extra=u.get("extra"))
635+
for u in body["result"]
636+
]
637+
638+
return await self._executor.execute(request, response_handler)
639+
640+
async def create_user(
641+
self,
642+
user: UserInfo,
643+
) -> Result[UserInfo]:
644+
"""Create a new user.
645+
646+
Args:
647+
user (UserInfo): User information.
648+
649+
Returns:
650+
UserInfo: New user details.
651+
652+
Raises:
653+
ValueError: If the username is missing.
654+
UserCreateError: If the operation fails.
655+
656+
Example:
657+
.. code-block:: python
658+
659+
await db.create_user(UserInfo(user="john", password="secret"))
660+
661+
References:
662+
- `create-a-user <https://docs.arangodb.com/stable/develop/http-api/users/#create-a-user>`__
663+
""" # noqa: E501
664+
if not user.user:
665+
raise ValueError("Username is required.")
666+
667+
data: Json = user.to_dict()
668+
request = Request(
669+
method=Method.POST,
670+
endpoint="/_api/user",
671+
data=self.serializer.dumps(data),
672+
)
673+
674+
def response_handler(resp: Response) -> UserInfo:
675+
if not resp.is_success:
676+
raise UserCreateError(resp, request)
677+
body = self.deserializer.loads(resp.raw_body)
678+
return UserInfo(
679+
user=body["user"],
680+
active=cast(bool, body.get("active")),
681+
extra=body.get("extra"),
682+
)
683+
684+
return await self._executor.execute(request, response_handler)
685+
686+
async def delete_user(
687+
self,
688+
username: str,
689+
ignore_missing: bool = False,
690+
) -> Result[bool]:
691+
"""Delete a user.
692+
693+
Args:
694+
username (str): Username.
695+
ignore_missing (bool): Do not raise an exception on missing user.
696+
697+
Returns:
698+
bool: True if the user was deleted successfully, `False` if the user was
699+
not found but **ignore_missing** was set to `True`.
700+
701+
Raises:
702+
UserDeleteError: If the operation fails.
703+
704+
References:
705+
- `remove-a-user <https://docs.arangodb.com/stable/develop/http-api/users/#remove-a-user>`__
706+
""" # noqa: E501
707+
request = Request(method=Method.DELETE, endpoint=f"/_api/user/{username}")
708+
709+
def response_handler(resp: Response) -> bool:
710+
if resp.is_success:
711+
return True
712+
if resp.status_code == HTTP_NOT_FOUND and ignore_missing:
713+
return False
714+
raise UserDeleteError(resp, request)
715+
716+
return await self._executor.execute(request, response_handler)
717+
556718

557719
class StandardDatabase(Database):
558720
"""Standard database API wrapper."""

arangoasync/exceptions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,19 @@ class ServerConnectionError(ArangoServerError):
141141

142142
class ServerStatusError(ArangoServerError):
143143
"""Failed to retrieve server status."""
144+
145+
146+
class UserCreateError(ArangoServerError):
147+
"""Failed to create user."""
148+
149+
150+
class UserDeleteError(ArangoServerError):
151+
"""Failed to delete user."""
152+
153+
154+
class UserGetError(ArangoServerError):
155+
"""Failed to retrieve user details."""
156+
157+
158+
class UserListError(ArangoServerError):
159+
"""Failed to retrieve users."""

tests/helpers.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,21 @@ def generate_col_name():
1717
str: Random collection name.
1818
"""
1919
return f"test_collection_{uuid4().hex}"
20+
21+
22+
def generate_username():
23+
"""Generate and return a random username.
24+
25+
Returns:
26+
str: Random username.
27+
"""
28+
return f"test_user_{uuid4().hex}"
29+
30+
31+
def generate_string():
32+
"""Generate and return a random unique string.
33+
34+
Returns:
35+
str: Random unique string.
36+
"""
37+
return uuid4().hex

tests/test_user.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import pytest
2+
3+
from arangoasync.auth import Auth
4+
from arangoasync.client import ArangoClient
5+
from arangoasync.typings import UserInfo
6+
from tests.helpers import generate_string, generate_username
7+
8+
9+
@pytest.mark.asyncio
10+
async def test_user_management(url, sys_db_name, root, password):
11+
auth = Auth(username=root, password=password)
12+
13+
# TODO also handle exceptions
14+
async with ArangoClient(hosts=url) as client:
15+
db = await client.db(sys_db_name, auth_method="basic", auth=auth, verify=True)
16+
17+
# Create a user
18+
username = generate_username()
19+
password = generate_string()
20+
users = await db.users()
21+
assert not any(user.user == username for user in users)
22+
assert await db.has_user(username) is False
23+
24+
# Verify user creation
25+
new_user = await db.create_user(
26+
UserInfo(
27+
user=username,
28+
password=password,
29+
active=True,
30+
extra={"foo": "bar"},
31+
)
32+
)
33+
assert new_user.user == username
34+
assert new_user.active is True
35+
assert new_user.extra == {"foo": "bar"}
36+
users = await db.users()
37+
assert sum(user.user == username for user in users) == 1
38+
assert await db.has_user(username) is True
39+
user = await db.user(username)
40+
assert user.user == username
41+
assert user.active is True
42+
43+
# Delete the newly created user
44+
assert await db.delete_user(username) is True
45+
users = await db.users()
46+
assert not any(user.user == username for user in users)
47+
assert await db.has_user(username) is False
48+
49+
# Ignore missing user
50+
assert await db.delete_user(username, ignore_missing=True) is False

0 commit comments

Comments
 (0)