Skip to content

Commit 2feb93c

Browse files
committed
Add v2 endpoint for access management
1 parent 054023a commit 2feb93c

File tree

2 files changed

+152
-9
lines changed

2 files changed

+152
-9
lines changed

mergin/client.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from .version import __version__
3737

3838
this_dir = os.path.dirname(os.path.realpath(__file__))
39+
json_headers = {"Content-Type": "application/json"}
3940

4041

4142
class TokenError(Exception):
@@ -244,6 +245,11 @@ def patch(self, path, data=None, headers={}):
244245
request = urllib.request.Request(url, data, headers, method="PATCH")
245246
return self._do_request(request)
246247

248+
def delete(self, path, data=None, headers={}):
249+
url = urllib.parse.urljoin(self.url, urllib.parse.quote(path))
250+
request = urllib.request.Request(url, data, headers, method="DELETE")
251+
return self._do_request(request)
252+
247253
def login(self, login, password):
248254
"""
249255
Authenticate login credentials and store session token
@@ -1205,7 +1211,8 @@ def reset_local_changes(self, directory: str, files_to_reset: typing.List[str] =
12051211
self.download_files(directory, files_download, version=current_version)
12061212

12071213
def download_files(
1208-
self, project_dir: str, file_paths: typing.List[str], output_paths: typing.List[str] = None, version: str = None
1214+
self, project_dir: str, file_paths: typing.List[str], output_paths: typing.List[str] = None,
1215+
version: str = None
12091216
):
12101217
"""
12111218
Download project files at specified version. Get the latest if no version specified.
@@ -1228,3 +1235,84 @@ def has_editor_support(self):
12281235
Returns whether the server version is acceptable for editor support.
12291236
"""
12301237
return is_version_acceptable(self.server_version(), "2024.4.0")
1238+
1239+
def create_user(self, email: str, password: str, workspace_id: int, workspace_role: str, notify_user: bool = False):
1240+
"""
1241+
Create a new user in a workspace. The username is generated from the email address.
1242+
"""
1243+
params = {
1244+
"email": email,
1245+
"password": password,
1246+
"workspace_id": workspace_id,
1247+
"role": workspace_role,
1248+
"notify_user": notify_user,
1249+
}
1250+
try:
1251+
self.post("v2/users", params, json_headers)
1252+
except ClientError as e:
1253+
e.extra = f"Email: {email}"
1254+
raise e
1255+
1256+
def get_workspace_member(self, workspace_id: int, user_id: int):
1257+
"""
1258+
Get a workspace member detail
1259+
"""
1260+
resp = self.get(f"v2/workspaces/{workspace_id}/members/{user_id}")
1261+
return json.load(resp)
1262+
1263+
def list_workspace_members(self, workspace_id: int):
1264+
"""
1265+
Get a list of workspace members
1266+
"""
1267+
resp = self.get(f"v2/workspaces/{workspace_id}/members")
1268+
return json.load(resp)
1269+
1270+
def update_workspace_member(self, workspace_id: int, user_id: int, workspace_role: str,
1271+
reset_projects_roles: bool = False):
1272+
"""
1273+
Update workspace role of a workspace member, optionally resets the projects role
1274+
"""
1275+
params = {
1276+
"reset_projects_roles": reset_projects_roles,
1277+
"workspace_role": workspace_role,
1278+
}
1279+
resp = self.patch(f"v2/workspaces/{workspace_id}/members/{user_id}", params, json_headers)
1280+
return json.load(resp)
1281+
1282+
def remove_workspace_member(self, workspace_id: int, user_id: int):
1283+
"""
1284+
Remove a user from workspace members
1285+
"""
1286+
self.delete(f"v2/workspaces/{workspace_id}/members/{user_id}")
1287+
1288+
def list_project_collaborators(self, project_id: int):
1289+
"""
1290+
Get a list of project collaborators
1291+
"""
1292+
project_collaborators = self.get(f"v2/projects/{project_id}/collaborators")
1293+
return json.load(project_collaborators)
1294+
1295+
def add_project_collaborator(self, project_id: int, user: str, project_role: str):
1296+
"""
1297+
Add a user to project collaborators and grant them a project role
1298+
"""
1299+
params = {
1300+
"role": project_role,
1301+
"user": user
1302+
}
1303+
project_collaborator = self.post(f"v2/projects/{project_id}/collaborators", params, json_headers)
1304+
return json.load(project_collaborator)
1305+
1306+
def update_project_collaborator(self, project_id: int, user_id: int, project_role: str):
1307+
"""
1308+
Update project role of the existing project collaborator
1309+
"""
1310+
params = {"role": project_role}
1311+
project_collaborator = self.patch(f"v2/projects/{project_id}/collaborators/{user_id}", params, json_headers)
1312+
return json.load(project_collaborator)
1313+
1314+
def remove_project_collaborator(self, project_id: int, user_id: int):
1315+
"""
1316+
Remove a user from project collaborators
1317+
"""
1318+
self.delete(f"v2/projects/{project_id}/collaborators/{user_id}")

mergin/test/test_client.py

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import logging
33
import os
4+
import random
45
import tempfile
56
import subprocess
67
import shutil
@@ -19,7 +20,6 @@
1920
decode_token_data,
2021
TokenError,
2122
ServerType,
22-
ErrorCode,
2323
)
2424
from ..client_push import push_project_async, push_project_cancel
2525
from ..client_pull import (
@@ -39,7 +39,7 @@
3939
from ..merginproject import pygeodiff
4040
from ..report import create_report
4141
from ..editor import EDITOR_ROLE_NAME, filter_changes, is_editor_enabled
42-
42+
from ..common import ErrorCode
4343

4444
SERVER_URL = os.environ.get("TEST_MERGIN_URL")
4545
API_USER = os.environ.get("TEST_API_USERNAME")
@@ -355,8 +355,8 @@ def test_push_pull_changes(mc):
355355
assert os.path.exists(os.path.join(project_dir_2, "renamed.txt"))
356356
assert os.path.exists(os.path.join(project_dir_2, conflicted_copy_file_name(f_updated, API_USER, 1)))
357357
assert (
358-
generate_checksum(os.path.join(project_dir_2, conflicted_copy_file_name(f_updated, API_USER, 1)))
359-
== f_conflict_checksum
358+
generate_checksum(os.path.join(project_dir_2, conflicted_copy_file_name(f_updated, API_USER, 1)))
359+
== f_conflict_checksum
360360
)
361361
assert generate_checksum(os.path.join(project_dir_2, f_updated)) == f_remote_checksum
362362

@@ -2459,8 +2459,8 @@ def test_project_rename(mc: MerginClient):
24592459

24602460
# cannot rename with full project name
24612461
with pytest.raises(
2462-
ClientError,
2463-
match="Project's new name should be without workspace specification",
2462+
ClientError,
2463+
match="Project's new name should be without workspace specification",
24642464
):
24652465
mc.rename_project(project, "workspace" + "/" + test_project_renamed)
24662466

@@ -2711,8 +2711,8 @@ def test_error_projects_limit_hit(mcStorage: MerginClient):
27112711
mcStorage.create_project_and_push(test_project_fullname, project_dir)
27122712
assert e.value.server_code == ErrorCode.ProjectsLimitHit.value
27132713
assert (
2714-
e.value.detail
2715-
== "Maximum number of projects is reached. Please upgrade your subscription to create new projects (ProjectsLimitHit)"
2714+
e.value.detail
2715+
== "Maximum number of projects is reached. Please upgrade your subscription to create new projects (ProjectsLimitHit)"
27162716
)
27172717
assert e.value.http_error == 422
27182718
assert e.value.http_method == "POST"
@@ -2742,3 +2742,58 @@ def test_workspace_requests(mc2: MerginClient):
27422742
assert service["plan"]["product_id"] == None
27432743
assert service["plan"]["type"] == "custom"
27442744
assert service["subscription"] == None
2745+
2746+
2747+
def test_access_management(mc: MerginClient):
2748+
# create a user in a workspace
2749+
workspace_id = None
2750+
for workspace in mc.workspaces_list():
2751+
if workspace["name"] == mc.username():
2752+
workspace_id = workspace["id"]
2753+
break
2754+
email = "create_user" + str(random.randint(1000, 9999)) + "@client.py"
2755+
password = "Il0vemergin"
2756+
role = "writer"
2757+
mc.create_user(email, password, workspace_id, role)
2758+
workspace_members = mc.list_workspace_members(workspace_id)
2759+
new_user = next((m for m in workspace_members if m["email"] == email))
2760+
assert new_user
2761+
assert new_user["workspace_role"] == role
2762+
# test get workspace member
2763+
ws_member = mc.get_workspace_member(workspace_id, new_user["id"])
2764+
assert ws_member["email"] == email
2765+
assert ws_member["workspace_role"] == role
2766+
updated_role = "admin"
2767+
# test update workspace member
2768+
mc.update_workspace_member(workspace_id, new_user["id"], updated_role)
2769+
updated_user = mc.get_workspace_member(workspace_id, new_user["id"])
2770+
assert updated_user["workspace_role"] == updated_role
2771+
# test remove workspace member
2772+
mc.remove_workspace_member(workspace_id, new_user["id"])
2773+
workspace_members = mc.list_workspace_members(workspace_id)
2774+
assert not any(m["id"] == new_user["id"] for m in workspace_members)
2775+
# add project
2776+
test_project_name = "test_collaborators"
2777+
test_project_fullname = API_USER + "/" + test_project_name
2778+
project_dir = os.path.join(TMP_DIR, test_project_name, API_USER)
2779+
cleanup(mc, test_project_fullname, [project_dir])
2780+
mc.create_project(test_project_name)
2781+
project_info = get_project_info(mc, API_USER, test_project_name)
2782+
test_project_id = project_info["id"]
2783+
project_role = "reader"
2784+
# test add project collaborator
2785+
mc.add_project_collaborator(test_project_id, new_user["email"], project_role)
2786+
collaborators = mc.list_project_collaborators(test_project_id)
2787+
new_collaborator = next((c for c in collaborators if c["id"] == new_user["id"]))
2788+
assert new_collaborator
2789+
assert new_collaborator["project_role"] == project_role
2790+
updated_role = "owner"
2791+
# test update project collaborator
2792+
mc.update_project_collaborator(test_project_id, new_user["id"], updated_role)
2793+
collaborators = mc.list_project_collaborators(test_project_id)
2794+
updated_collaborator = next((c for c in collaborators if c["id"] == new_user["id"]))
2795+
assert updated_collaborator["project_role"] == updated_role
2796+
# test remove project collaborator
2797+
mc.remove_project_collaborator(test_project_id, new_user["id"])
2798+
collaborators = mc.list_project_collaborators(test_project_id)
2799+
assert not any(c["id"] == new_user["id"] for c in collaborators)

0 commit comments

Comments
 (0)