From 7a4a31b52dad3e7bd76a45f977671438ddaa7702 Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Wed, 15 Oct 2025 00:05:22 +0200 Subject: [PATCH] fix: Update user/groups API to OCS v2 --- nextcloud_mcp_server/client/users.py | 70 ++++++++++------------------ nextcloud_mcp_server/models/users.py | 10 ++-- tests/server/test_users_api.py | 10 ++-- 3 files changed, 35 insertions(+), 55 deletions(-) diff --git a/nextcloud_mcp_server/client/users.py b/nextcloud_mcp_server/client/users.py index 41ca5c9..210fea7 100644 --- a/nextcloud_mcp_server/client/users.py +++ b/nextcloud_mcp_server/client/users.py @@ -10,7 +10,7 @@ class UsersClient(BaseNextcloudClient): self, additional_headers: Optional[Dict[str, str]] = None ) -> Dict[str, str]: """Get standard headers required for User API calls.""" - headers = {"OCS-APIRequest": "true"} + headers = {"OCS-APIRequest": "true", "Accept": "application/json"} if additional_headers: headers.update(additional_headers) return headers @@ -49,7 +49,7 @@ class UsersClient(BaseNextcloudClient): headers = self._get_user_headers() await self._make_request( - "POST", "/ocs/v1.php/cloud/users", data=data, headers=headers + "POST", "/ocs/v2.php/cloud/users", data=data, headers=headers ) async def search_users( @@ -71,18 +71,11 @@ class UsersClient(BaseNextcloudClient): headers = self._get_user_headers() response = await self._make_request( - "GET", "/ocs/v1.php/cloud/users", params=params, headers=headers + "GET", "/ocs/v2.php/cloud/users", params=params, headers=headers ) - # The API returns XML, which is parsed into a dict. - # The user IDs are under ocs.data.users.element (can be a list or a single string) + # The v2 API returns JSON with users as a direct list under data.users data = response.json()["ocs"]["data"] - if "users" in data and "element" in data["users"]: - elements = data["users"]["element"] - if isinstance(elements, list): - return elements - elif isinstance(elements, str): - return [elements] - return [] + return data.get("users", []) async def get_user_details(self, userid: str) -> UserDetails: """ @@ -90,7 +83,7 @@ class UsersClient(BaseNextcloudClient): """ headers = self._get_user_headers() response = await self._make_request( - "GET", f"/ocs/v1.php/cloud/users/{userid}", headers=headers + "GET", f"/ocs/v2.php/cloud/users/{userid}", headers=headers ) return UserDetails(**response.json()["ocs"]["data"]) @@ -101,7 +94,7 @@ class UsersClient(BaseNextcloudClient): data = {"key": key, "value": value} headers = self._get_user_headers() await self._make_request( - "PUT", f"/ocs/v1.php/cloud/users/{userid}", data=data, headers=headers + "PUT", f"/ocs/v2.php/cloud/users/{userid}", data=data, headers=headers ) async def get_editable_user_fields(self) -> List[str]: @@ -110,16 +103,11 @@ class UsersClient(BaseNextcloudClient): """ headers = self._get_user_headers() response = await self._make_request( - "GET", "/ocs/v1.php/cloud/user/fields", headers=headers + "GET", "/ocs/v2.php/cloud/user/fields", headers=headers ) + # The v2 API returns data as a direct list data = response.json()["ocs"]["data"] - if "element" in data: - elements = data["element"] - if isinstance(elements, list): - return elements - elif isinstance(elements, str): - return [elements] - return [] + return data if isinstance(data, list) else [] async def disable_user(self, userid: str) -> None: """ @@ -127,7 +115,7 @@ class UsersClient(BaseNextcloudClient): """ headers = self._get_user_headers() await self._make_request( - "PUT", f"/ocs/v1.php/cloud/users/{userid}/disable", headers=headers + "PUT", f"/ocs/v2.php/cloud/users/{userid}/disable", headers=headers ) async def enable_user(self, userid: str) -> None: @@ -136,7 +124,7 @@ class UsersClient(BaseNextcloudClient): """ headers = self._get_user_headers() await self._make_request( - "PUT", f"/ocs/v1.php/cloud/users/{userid}/enable", headers=headers + "PUT", f"/ocs/v2.php/cloud/users/{userid}/enable", headers=headers ) async def delete_user(self, userid: str) -> None: @@ -145,7 +133,7 @@ class UsersClient(BaseNextcloudClient): """ headers = self._get_user_headers() await self._make_request( - "DELETE", f"/ocs/v1.php/cloud/users/{userid}", headers=headers + "DELETE", f"/ocs/v2.php/cloud/users/{userid}", headers=headers ) async def get_user_groups(self, userid: str) -> List[str]: @@ -154,16 +142,11 @@ class UsersClient(BaseNextcloudClient): """ headers = self._get_user_headers() response = await self._make_request( - "GET", f"/ocs/v1.php/cloud/users/{userid}/groups", headers=headers + "GET", f"/ocs/v2.php/cloud/users/{userid}/groups", headers=headers ) + # The v2 API returns groups as a direct list under data.groups data = response.json()["ocs"]["data"] - if "groups" in data and "element" in data["groups"]: - elements = data["groups"]["element"] - if isinstance(elements, list): - return elements - elif isinstance(elements, str): - return [elements] - return [] + return data.get("groups", []) async def add_user_to_group(self, userid: str, groupid: str) -> None: """ @@ -173,7 +156,7 @@ class UsersClient(BaseNextcloudClient): headers = self._get_user_headers() await self._make_request( "POST", - f"/ocs/v1.php/cloud/users/{userid}/groups", + f"/ocs/v2.php/cloud/users/{userid}/groups", data=data, headers=headers, ) @@ -186,7 +169,7 @@ class UsersClient(BaseNextcloudClient): headers = self._get_user_headers() await self._make_request( "DELETE", - f"/ocs/v1.php/cloud/users/{userid}/groups", + f"/ocs/v2.php/cloud/users/{userid}/groups", data=data, headers=headers, ) @@ -199,7 +182,7 @@ class UsersClient(BaseNextcloudClient): headers = self._get_user_headers() await self._make_request( "POST", - f"/ocs/v1.php/cloud/users/{userid}/subadmins", + f"/ocs/v2.php/cloud/users/{userid}/subadmins", data=data, headers=headers, ) @@ -212,7 +195,7 @@ class UsersClient(BaseNextcloudClient): headers = self._get_user_headers() await self._make_request( "DELETE", - f"/ocs/v1.php/cloud/users/{userid}/subadmins", + f"/ocs/v2.php/cloud/users/{userid}/subadmins", data=data, headers=headers, ) @@ -223,16 +206,11 @@ class UsersClient(BaseNextcloudClient): """ headers = self._get_user_headers() response = await self._make_request( - "GET", f"/ocs/v1.php/cloud/users/{userid}/subadmins", headers=headers + "GET", f"/ocs/v2.php/cloud/users/{userid}/subadmins", headers=headers ) + # The v2 API returns data as a direct list data = response.json()["ocs"]["data"] - if "element" in data: - elements = data["element"] - if isinstance(elements, list): - return elements - elif isinstance(elements, str): - return [elements] - return [] + return data if isinstance(data, list) else [] async def resend_welcome_email(self, userid: str) -> None: """ @@ -240,5 +218,5 @@ class UsersClient(BaseNextcloudClient): """ headers = self._get_user_headers() await self._make_request( - "POST", f"/ocs/v1.php/cloud/users/{userid}/welcome", headers=headers + "POST", f"/ocs/v2.php/cloud/users/{userid}/welcome", headers=headers ) diff --git a/nextcloud_mcp_server/models/users.py b/nextcloud_mcp_server/models/users.py index de3070b..784254f 100644 --- a/nextcloud_mcp_server/models/users.py +++ b/nextcloud_mcp_server/models/users.py @@ -1,5 +1,5 @@ -from typing import List, Optional -from pydantic import BaseModel, Field +from typing import Any, Dict, List, Optional, Union +from pydantic import BaseModel, ConfigDict, Field class User(BaseModel): @@ -18,10 +18,12 @@ class User(BaseModel): class UserDetails(BaseModel): """Model for retrieving detailed user information.""" + model_config = ConfigDict(populate_by_name=True) + enabled: bool id: str - quota: str - email: str + quota: Union[str, Dict[str, Any]] # Can be string or quota object + email: Optional[str] = None # Can be null displayname: str = Field( alias="display-name" ) # Handle both displayname and display-name diff --git a/tests/server/test_users_api.py b/tests/server/test_users_api.py index 3be5ee8..e3360f6 100644 --- a/tests/server/test_users_api.py +++ b/tests/server/test_users_api.py @@ -5,7 +5,7 @@ from nextcloud_mcp_server.client import NextcloudClient @pytest.mark.asyncio async def test_create_and_delete_user(nc_client: NextcloudClient): userid = "testuser1" - password = "testpassword1" + password = "SecureTestPassword123!" display_name = "Test User One" email = "test1@example.com" @@ -37,7 +37,7 @@ async def test_create_and_delete_user(nc_client: NextcloudClient): @pytest.mark.asyncio async def test_update_user_field(nc_client: NextcloudClient): userid = "testuser2" - password = "testpassword2" + password = "SecureTestPassword123!" display_name = "Test User Two" email = "test2@example.com" @@ -60,7 +60,7 @@ async def test_update_user_field(nc_client: NextcloudClient): @pytest.mark.asyncio async def test_user_groups(nc_client: NextcloudClient): userid = "testuser3" - password = "testpassword3" + password = "SecureTestPassword123!" groupid = "testgroup" await nc_client.users.create_user(userid=userid, password=password) @@ -81,7 +81,7 @@ async def test_user_groups(nc_client: NextcloudClient): @pytest.mark.asyncio async def test_user_subadmins(nc_client: NextcloudClient): userid = "testuser4" - password = "testpassword4" + password = "SecureTestPassword123!" groupid = "subadmingroup" await nc_client.users.create_user(userid=userid, password=password) @@ -102,7 +102,7 @@ async def test_user_subadmins(nc_client: NextcloudClient): @pytest.mark.asyncio async def test_disable_enable_user(nc_client: NextcloudClient): userid = "testuser5" - password = "testpassword5" + password = "SecureTestPassword123!" await nc_client.users.create_user(userid=userid, password=password)