feat: clean code for nextcloud process (#138)

code_spécifique_watcha
KevICO 4 years ago committed by c-cal
parent f2640d510b
commit 7def699a1a
Signed by: watcha
GPG Key ID: 87DD78E7F7A1581D
  1. 31
      synapse/api/errors.py
  2. 148
      synapse/handlers/watcha_nextcloud.py
  3. 12
      synapse/handlers/watcha_partner.py
  4. 104
      synapse/http/watcha_nextcloud_client.py
  5. 74
      synapse/rest/client/v1/room.py
  6. 17
      synapse/rest/client/v1/watcha.py
  7. 12
      synapse/storage/databases/main/schema/delta/59/watcha_01drop_directory_path_column.sql
  8. 48
      synapse/storage/databases/main/watcha_nextcloud.py
  9. 224
      tests/handlers/test_watcha_nextcloud.py
  10. 45
      tests/rest/client/v1/test_watcha_nextcloud.py
  11. 64
      tests/storage/test_watcha_nextcloud.py

@ -77,18 +77,10 @@ class Codes:
USER_DEACTIVATED = "M_USER_DEACTIVATED"
BAD_ALIAS = "M_BAD_ALIAS"
# watcha+
NEXTCLOUD_USER_DOES_NOT_EXIST = "W_NEXTCLOUD_USER_DOES_NOT_EXIST"
NEXTCLOUD_CAN_NOT_CREATE_USER = "W_NEXTCLOUD_CAN_NOT_CREATE_USER"
NEXTCLOUD_CAN_NOT_DELETE_USER = "W_NEXTCLOUD_CAN_NOT_DELETE_USER"
NEXTCLOUD_CAN_NOT_ADD_MEMBERS_TO_GROUP = "W_NEXTCLOUD_CAN_NOT_ADD_MEMBERS_TO_GROUP"
NEXTCLOUD_CAN_NOT_CREATE_GROUP = "W_NEXTCLOUD_CAN_NOT_CREATE_GROUP"
NEXTCLOUD_CAN_NOT_DELETE_GROUP = "W_NEXTCLOUD_CAN_NOT_DELETE_GROUP"
NEXTCLOUD_CAN_NOT_ADD_USER_TO_GROUP = "W_NEXTCLOUD_CAN_NOT_ADD_USER_TO_GROUP"
NEXTCLOUD_CAN_NOT_REMOVE_USER_FROM_GROUP = (
"W_NEXTCLOUD_CAN_NOT_REMOVE_USER_FROM_GROUP"
)
NEXTCLOUD_CAN_NOT_GET_SHARES = "W_NEXTCLOUD_CAN_NOT_GET_SHARES"
NEXTCLOUD_CAN_NOT_SHARE = "W_NEXTCLOUD_CAN_NOT_SHARE"
NEXTCLOUD_CAN_NOT_UNSHARE = "W_NEXTCLOUD_CAN_NOT_UNSHARE"
# +watcha
@ -159,6 +151,25 @@ class SynapseError(CodeMessageException):
return cs_error(self.msg, self.errcode)
# watcha+
class NextcloudError(CodeMessageException):
"""A base exception type for Nextcloud errors which have an error code and error
message (corresponding to status code and status message from Nextcloud API documentation).
"""
def __init__(self, code: int, msg: str):
"""Constructs a Nextcloud error.
Args:
code: The integer error code (corresponding to status code in response body)
msg: The human-readable error message.
"""
super().__init__(code, msg)
# +watcha
class ProxiedRequestError(SynapseError):
"""An error from a general matrix endpoint, eg. from a proxied Matrix API call.

@ -1,10 +1,13 @@
import logging
from pathlib import Path
from jsonschema.exceptions import SchemaError, ValidationError
from synapse.api.constants import EventTypes
from synapse.api.errors import Codes, SynapseError
from synapse.api.errors import (
Codes,
HttpResponseException,
NextcloudError,
SynapseError,
)
from ._base import BaseHandler
@ -12,54 +15,70 @@ logger = logging.getLogger(__name__)
# echo -n watcha | md5sum | head -c 10
NEXTCLOUD_GROUP_NAME_PREFIX = "c4d96a06b7_"
NEXTCLOUD_CLIENT_ERRORS = (
NextcloudError,
SchemaError,
ValidationError,
HttpResponseException,
)
class NextcloudHandler(BaseHandler):
def __init__(self, hs):
def __init__(self, hs: "Homeserver"):
self.store = hs.get_datastore()
self.event_creation_handler = hs.get_event_creation_handler()
self.keycloak_client = hs.get_keycloak_client()
self.nextcloud_client = hs.get_nextcloud_client()
async def unbind(self, room_id):
async def unbind(self, room_id: str):
"""Unbind a Nextcloud folder from a room.
Args :
room_id: the id of the room to bind.
"""
group_name = NEXTCLOUD_GROUP_NAME_PREFIX + room_id
try:
await self.nextcloud_client.delete_group(group_name)
except NEXTCLOUD_CLIENT_ERRORS as error:
logger.error(
f"[watcha] delete nextcloud group {group_name} - failed: {error}"
)
await self.nextcloud_client.delete_group(NEXTCLOUD_GROUP_NAME_PREFIX + room_id)
await self.store.unbind(room_id)
await self.store.delete_share(room_id)
async def bind(self, user_id, room_id, path):
"""Bind a Nextcloud folder with a room.
async def bind(self, requester_id: str, room_id: str, path: str):
"""Bind a Nextcloud folder with a room in three steps :
1 - create a new Nextcloud group
2 - add all room members in the new group
3 - create a share on folder for the new group
Args :
user_id: the matrix user id of the requester.
requester_id: the mxid of the requester.
room_id: the id of the room to bind.
path: the path of the Nextcloud folder to bind.
"""
group_name = NEXTCLOUD_GROUP_NAME_PREFIX + room_id
nextcloud_username = await self.store.get_nextcloud_username(user_id)
try:
await self.nextcloud_client.add_group(group_name)
await self.add_room_users_to_nextcloud_group(room_id)
old_share_id = await self.store.get_nextcloud_share_id_from_room_id(room_id)
if old_share_id:
await self.nextcloud_client.unshare(nextcloud_username, old_share_id)
new_share_id = await self.nextcloud_client.share(
nextcloud_username, path, group_name
except NEXTCLOUD_CLIENT_ERRORS as error:
# Do not raise error if Nextcloud group already exist
if isinstance(error, NextcloudError) and error.code == 102:
logger.warn(
f"[watcha] add nextcloud group {group_name} - failed: the group already exists"
)
else:
raise SynapseError(
500,
f"[watcha] add nextcloud group {group_name} - failed: {error}",
Codes.NEXTCLOUD_CAN_NOT_CREATE_GROUP,
)
await self.store.bind(room_id, path, new_share_id)
await self.add_room_members_to_group(room_id)
await self.create_share(requester_id, room_id, path)
async def add_room_users_to_nextcloud_group(self, room_id):
"""Add all users of a room to a Nextcloud.
async def add_room_members_to_group(self, room_id: str):
"""Add all members of a room to a Nextcloud group.
Args:
room_id: the id of the room which the Nextcloud group name is infered from.
@ -68,42 +87,91 @@ class NextcloudHandler(BaseHandler):
user_ids = await self.store.get_users_in_room(room_id)
for user_id in user_ids:
nextcloud_username = await self.store.get_nextcloud_username(user_id)
nextcloud_username = await self.store.get_username(user_id)
try:
await self.nextcloud_client.add_user_to_group(
nextcloud_username, group_name
)
except (SynapseError, ValidationError, SchemaError) as error:
logger.warn(
"Unable to add the user {} to the Nextcloud group {}.".format(
user_id, group_name
except NEXTCLOUD_CLIENT_ERRORS as error:
# Do not raise error if some users can not be added to group
if isinstance(error, NextcloudError) and (error.code in (103, 105)):
logger.error(
f"[watcha] add user {user_id} to group {group_name} - failed: {error}"
)
else:
raise SynapseError(
500,
f"[watcha] add members of room {room_id} to group {group_name} - failed: {error}",
Codes.NEXTCLOUD_CAN_NOT_ADD_MEMBERS_TO_GROUP,
)
async def update_share(self, user_id, room_id, membership):
async def create_share(self, requester_id: str, room_id: str, path: str):
"""Create a new share on folder for a specific Nextcloud group.
Before that, delete old existing share for this group if it exist.
Args:
requester_id: the mxid of the requester.
room_id: the id of the room to bind.
path: the path of the Nextcloud folder to bind.
"""
group_name = NEXTCLOUD_GROUP_NAME_PREFIX + room_id
nextcloud_username = await self.store.get_username(requester_id)
old_share_id = await self.store.get_share_id(room_id)
if old_share_id:
try:
await self.nextcloud_client.unshare(nextcloud_username, old_share_id)
except NEXTCLOUD_CLIENT_ERRORS as error:
logger.error(f"[watcha] unshare {old_share_id} - failed: {error}")
try:
new_share_id = await self.nextcloud_client.share(
nextcloud_username, path, group_name
)
except NEXTCLOUD_CLIENT_ERRORS as error:
await self.unbind(room_id)
# raise 404 error if folder to share do not exist
http_code = (
error.code
if isinstance(error, NextcloudError) and error.code == 404
else 500
)
raise SynapseError(
http_code,
f"[watcha] share folder {path} with group {group_name} - failed: {error}",
Codes.NEXTCLOUD_CAN_NOT_SHARE,
)
await self.store.register_share(room_id, new_share_id)
async def update_group(self, user_id: str, room_id: str, membership: str):
"""Update a Nextcloud group by adding or removing users.
If membership is 'join' or 'invite', the user is add to the Nextcloud group infered from the room.
Else, the users is removed from the group.
Args:
user_id: mxid of the user concerned by the membership event
room_id: the id of the room where the membership event was sent
membership: membership event. Can be 'invite', 'join', 'kick' or 'leave'
"""
group_name = NEXTCLOUD_GROUP_NAME_PREFIX + room_id
nextcloud_username = await self.store.get_nextcloud_username(user_id)
nextcloud_username = await self.store.get_username(user_id)
if membership in ("invite", "join"):
try:
await self.nextcloud_client.add_user_to_group(
nextcloud_username, group_name
)
except (SynapseError, ValidationError, SchemaError):
except NEXTCLOUD_CLIENT_ERRORS as error:
logger.warn(
"Unable to add the user {} to the Nextcloud group {}.".format(
user_id, group_name
),
f"[watcha] add user {user_id} to group {group_name} - failed: {error}"
)
else:
try:
await self.nextcloud_client.remove_user_from_group(
nextcloud_username, group_name
)
except (SynapseError, ValidationError, SchemaError):
except NEXTCLOUD_CLIENT_ERRORS as error:
logger.warn(
"Unable to remove the user {} from the Nextcloud group {}.".format(
user_id, group_name
),
f"[watcha] remove user {user_id} from group {group_name} - failed: {error}"
)

@ -2,7 +2,7 @@ import logging
from jsonschema.exceptions import SchemaError, ValidationError
from synapse.api.errors import HttpResponseException, SynapseError
from synapse.api.errors import HttpResponseException, NextcloudError
from synapse.config.emailconfig import ThreepidBehaviour
from synapse.push.mailer import Mailer
from synapse.util.watcha import Secrets
@ -48,7 +48,15 @@ class PartnerHandler(BaseHandler):
try:
await self.nextcloud_client.add_user(keycloak_user_id)
except (SynapseError, HttpResponseException, ValidationError, SchemaError):
except (
NextcloudError,
HttpResponseException,
ValidationError,
SchemaError,
) as error:
if isinstance(error, NextcloudError) and error.code == 102:
logger.warn(f"[watcha] add user {keycloak_user_id} - failed: {error}")
else:
await self.keycloak_client.delete_user(keycloak_user_id)
raise

@ -3,7 +3,7 @@ from base64 import b64encode
from jsonschema import validate
from typing import Any, List
from synapse.api.errors import Codes, SynapseError
from synapse.api.errors import Codes, NextcloudError
from synapse.http.client import SimpleHttpClient
logger = logging.getLogger(__name__)
@ -64,7 +64,7 @@ class NextcloudClient(SimpleHttpClient):
https://docs.nextcloud.com/server/latest/developer_manual/client_apis/index.html
"""
def __init__(self, hs):
def __init__(self, hs: "Homeserver"):
super().__init__(hs)
self.nextcloud_url = hs.config.nextcloud_url
@ -79,21 +79,16 @@ class NextcloudClient(SimpleHttpClient):
"Authorization": [
"Basic "
+ b64encode(
"{}:{}".format(
self.service_account_name, self.service_account_password
).encode()
f"{self.service_account_name}:{self.service_account_password}".encode()
).decode()
],
}
def _raise_for_status(self, meta: List[Any], errcode: Codes):
if meta["status"] == "failure":
raise SynapseError(
400,
"OCS error : status code {status_code} - message {msg}".format(
status_code=meta["statuscode"], msg=meta["message"]
),
errcode,
raise NextcloudError(
meta["statuscode"],
meta["message"],
)
async def add_user(self, username: str, displayname: str = None):
@ -101,7 +96,7 @@ class NextcloudClient(SimpleHttpClient):
Args:
username: the username of the user to create.
displayname: displayname of invitee. Defaults to user keycloak id.
displayname: displayname of the user. Defaults to user keycloak id.
Status codes:
100 - successful
@ -120,20 +115,13 @@ class NextcloudClient(SimpleHttpClient):
payload["displayName"] = displayname
response = await self.post_json_get_json(
uri="{}/ocs/v1.php/cloud/users".format(self.nextcloud_url),
uri=f"{self.nextcloud_url}/ocs/v1.php/cloud/users",
post_json=payload,
headers=self._headers,
)
validate(response, WITH_DATA_SCHEMA)
meta = response["ocs"]["meta"]
if meta["statuscode"] == 102:
logger.info(
"User '{}' already exists on the Nextcloud server.".format(username)
)
else:
self._raise_for_status(meta, Codes.NEXTCLOUD_CAN_NOT_CREATE_USER)
self._raise_for_status(response["ocs"]["meta"])
async def delete_user(self, username: str):
"""Delete an existing user.
@ -145,17 +133,13 @@ class NextcloudClient(SimpleHttpClient):
100 - successful
101 - failure
"""
response = await self.delete_get_json(
uri="{}/ocs/v1.php/cloud/users/{}".format(self.nextcloud_url, username),
uri=f"{self.nextcloud_url}/ocs/v1.php/cloud/users/{username}",
headers=self._headers,
)
validate(response, WITHOUT_DATA_SCHEMA)
self._raise_for_status(
response["ocs"]["meta"], Codes.NEXTCLOUD_CAN_NOT_DELETE_USER
)
self._raise_for_status(response["ocs"]["meta"])
async def add_group(self, group_name: str):
"""Adds a new Nextcloud group.
@ -169,20 +153,14 @@ class NextcloudClient(SimpleHttpClient):
102: group already exists
103: failed to add the group
"""
response = await self.post_json_get_json(
uri="{}/ocs/v1.php/cloud/groups".format(self.nextcloud_url),
uri=f"{self.nextcloud_url}/ocs/v1.php/cloud/groups",
post_json={"groupid": group_name},
headers=self._headers,
)
validate(response, WITHOUT_DATA_SCHEMA)
meta = response["ocs"]["meta"]
if meta["statuscode"] == 102:
logger.info("Nextcloud group {} already exists.".format(group_name))
else:
self._raise_for_status(meta, Codes.NEXTCLOUD_CAN_NOT_CREATE_GROUP)
self._raise_for_status(response["ocs"]["meta"])
async def delete_group(self, group_name: str):
"""Removes a existing Nextcloud group.
@ -195,17 +173,13 @@ class NextcloudClient(SimpleHttpClient):
101: group does not exist
102: failed to delete group
"""
response = await self.delete_get_json(
uri="{}/ocs/v1.php/cloud/groups/{}".format(self.nextcloud_url, group_name),
uri=f"{self.nextcloud_url}/ocs/v1.php/cloud/groups/{group_name}",
headers=self._headers,
)
validate(response, WITHOUT_DATA_SCHEMA)
self._raise_for_status(
response["ocs"]["meta"], Codes.NEXTCLOUD_CAN_NOT_DELETE_GROUP
)
self._raise_for_status(response["ocs"]["meta"])
async def add_user_to_group(self, username: str, group_name: str):
"""Add user to the Nextcloud group.
@ -222,25 +196,14 @@ class NextcloudClient(SimpleHttpClient):
104: insufficient privileges
105: failed to add user to group
"""
response = await self.post_json_get_json(
uri="{}/ocs/v1.php/cloud/users/{}/groups".format(
self.nextcloud_url, username
),
uri=f"{self.nextcloud_url}/ocs/v1.php/cloud/users/{username}/groups",
post_json={"groupid": group_name},
headers=self._headers,
)
validate(response, WITHOUT_DATA_SCHEMA)
meta = response["ocs"]["meta"]
errcode = (
Codes.NEXTCLOUD_USER_DOES_NOT_EXIST
if meta["statuscode"] == 103
else Codes.NEXTCLOUD_CAN_NOT_ADD_USER_TO_GROUP
)
self._raise_for_status(meta, errcode)
self._raise_for_status(response["ocs"]["meta"])
async def remove_user_from_group(self, username: str, group_name: str):
"""Removes the specified user from the specified group.
@ -257,32 +220,21 @@ class NextcloudClient(SimpleHttpClient):
104: insufficient privileges
105: failed to remove user from group
"""
response = await self.delete_get_json(
uri="{}/ocs/v1.php/cloud/users/{}/groups".format(
self.nextcloud_url, username
),
uri=f"{self.nextcloud_url}/ocs/v1.php/cloud/users/{username}/groups",
headers=self._headers,
json_body={"groupid": group_name},
)
validate(response, WITHOUT_DATA_SCHEMA)
meta = response["ocs"]["meta"]
errcode = (
Codes.NEXTCLOUD_USER_DOES_NOT_EXIST
if meta["statuscode"] == 103
else Codes.NEXTCLOUD_CAN_NOT_REMOVE_USER_FROM_GROUP
)
self._raise_for_status(meta, errcode)
self._raise_for_status(response["ocs"]["meta"])
async def share(self, requester: str, path: str, group_name: str):
"""Share an existing file or folder with all permissions for a group.
Args:
requester: the user who want to create the share
path: the path of folder to share
path: the path of the folder to share
group_name: the name of group which will share the folder
Payload:
@ -310,11 +262,8 @@ class NextcloudClient(SimpleHttpClient):
Returns:
the id of Nextcloud share
"""
response = await self.post_json_get_json(
uri="{}/ocs/v2.php/apps/watcha_integrator/api/v1/shares".format(
self.nextcloud_url
),
uri=f"{self.nextcloud_url}/ocs/v2.php/apps/watcha_integrator/api/v1/shares",
headers=self._headers,
post_json={
"path": path,
@ -326,8 +275,7 @@ class NextcloudClient(SimpleHttpClient):
)
validate(response, WITH_DATA_SCHEMA)
self._raise_for_status(response["ocs"]["meta"], Codes.NEXTCLOUD_CAN_NOT_SHARE)
self._raise_for_status(response["ocs"]["meta"])
return response["ocs"]["data"]["id"]
@ -342,15 +290,11 @@ class NextcloudClient(SimpleHttpClient):
100: successful
404: Share couldnt be deleted.
"""
response = await self.delete_get_json(
uri="{}/ocs/v2.php/apps/watcha_integrator/api/v1/shares/{}".format(
self.nextcloud_url, share_id
),
uri=f"{self.nextcloud_url}/ocs/v2.php/apps/watcha_integrator/api/v1/shares/{share_id}",
headers=self._headers,
json_body={"requester": requester},
)
validate(response, WITHOUT_DATA_SCHEMA)
self._raise_for_status(response["ocs"]["meta"], Codes.NEXTCLOUD_CAN_NOT_UNSHARE)
self._raise_for_status(response["ocs"]["meta"])

@ -86,10 +86,12 @@ class RoomCreateRestServlet(TransactionRestServlet):
return self.txns.fetch_or_execute_request(request, self.on_POST, request)
async def on_POST(self, request):
""" watcha!
"""watcha!
requester = await self.auth.get_user_by_req(request)
!watcha """
requester = await self.auth.get_user_by_req(request, allow_partner=False) # watcha+
!watcha"""
# watcha+
requester = await self.auth.get_user_by_req(request, allow_partner=False)
# +watcha
info, _ = await self._room_creation_handler.create_room(
requester, self.get_room_config(request)
@ -208,10 +210,8 @@ class RoomStateEventRestServlet(TransactionRestServlet):
content=content,
)
# watcha+
mapped_directory = await self.store.get_path_from_room_id(
room_id
)
if mapped_directory and membership in [
share_id = await self.store.get_share_id(room_id)
if share_id and membership in [
"invite",
"join",
"kick",
@ -222,25 +222,19 @@ class RoomStateEventRestServlet(TransactionRestServlet):
if membership in ["join", "leave"]
else state_key
)
await self.nextcloud_handler.update_share(
user, room_id, membership
)
await self.nextcloud_handler.update_group(user, room_id, membership)
# +watcha
else:
# watcha+
if event_type == EventTypes.VectorSetting:
if "nextcloudShare" not in content:
raise SynapseError(
400, "VectorSetting is only used for Nextcloud integration."
)
if (
event_type == EventTypes.VectorSetting
and "nextcloudShare" in content
):
nextcloud_url = content["nextcloudShare"]
requester_id = requester.user.to_string()
if not nextcloud_url:
await self.nextcloud_handler.unbind(
room_id
)
await self.nextcloud_handler.unbind(room_id)
else:
url_query = urlparse.parse_qs(
urlparse.urlparse(nextcloud_url).query
@ -249,13 +243,13 @@ class RoomStateEventRestServlet(TransactionRestServlet):
if "dir" not in url_query:
raise SynapseError(
400,
"The url doesn't point to a valid nextcloud directory path.",
"[watcha] binding Nextcloud folder with room - failed: wrong folder path",
)
nextcloud_directory_path = url_query["dir"][0]
nextcloud_folder_path = url_query["dir"][0]
await self.nextcloud_handler.bind(
requester_id, room_id, nextcloud_directory_path
requester_id, room_id, nextcloud_folder_path
)
# +watcha
(
@ -380,11 +374,9 @@ class JoinRoomAliasServlet(TransactionRestServlet):
)
# watcha+
mapped_directory = await self.store.get_path_from_room_id(
room_id
)
if mapped_directory:
await self.nextcloud_handler.update_share(
share_id = await self.store.get_share_id(room_id)
if share_id:
await self.nextcloud_handler.update_group(
requester.user.to_string(), room_id, "join"
)
# +watcha
@ -412,10 +404,14 @@ class PublicRoomListRestServlet(TransactionRestServlet):
server = parse_string(request, "server", default=None)
try:
""" watcha!
"""watcha!
await self.auth.get_user_by_req(request, allow_guest=True)
!watcha """
await self.auth.get_user_by_req(request, allow_guest=True, allow_partner=False) # watcha+
!watcha"""
# watcha+
await self.auth.get_user_by_req(
request, allow_guest=True, allow_partner=False
)
# +watcha
except InvalidClientCredentialsError as e:
# Option to allow servers to require auth when accessing
# /publicRooms via CS API. This is especially helpful in private
@ -464,10 +460,12 @@ class PublicRoomListRestServlet(TransactionRestServlet):
return 200, data
async def on_POST(self, request):
""" watcha!
"""watcha!
await self.auth.get_user_by_req(request, allow_guest=True)
!watcha """
await self.auth.get_user_by_req(request, allow_guest=True, allow_partner=False) # watcha+
!watcha"""
# watcha+
await self.auth.get_user_by_req(request, allow_guest=True, allow_partner=False)
# +watcha
server = parse_string(request, "server", default=None)
content = parse_json_object_from_request(request)
@ -901,10 +899,8 @@ class RoomMembershipRestServlet(TransactionRestServlet):
pass
# watcha+
mapped_directory = await self.store.get_path_from_room_id(
room_id
)
if mapped_directory and membership_action in [
share_id = await self.store.get_share_id(room_id)
if share_id and membership_action in [
"invite",
"join",
"kick",
@ -915,9 +911,7 @@ class RoomMembershipRestServlet(TransactionRestServlet):
if membership_action in ["join", "leave"]
else content["user_id"]
)
await self.nextcloud_handler.update_share(
user, room_id, membership_action
)
await self.nextcloud_handler.update_group(user, room_id, membership_action)
# +watcha
return_value = {}

@ -2,7 +2,12 @@ import logging
from jsonschema.exceptions import SchemaError, ValidationError
from synapse.api.errors import AuthError, HttpResponseException, SynapseError
from synapse.api.errors import (
AuthError,
HttpResponseException,
SynapseError,
NextcloudError,
)
from synapse.config.emailconfig import ThreepidBehaviour
from synapse.http.servlet import RestServlet, parse_json_object_from_request
from synapse.push.mailer import Mailer
@ -153,7 +158,15 @@ class WatchaRegisterRestServlet(RestServlet):
try:
await self.nextcloud_client.add_user(keycloak_user_id, displayname)
except (SynapseError, HttpResponseException, ValidationError, SchemaError):
except (
NextcloudError,
HttpResponseException,
ValidationError,
SchemaError,
) as error:
if isinstance(error, NextcloudError) and error.code == 102:
logger.warn(f"[watcha] add user {keycloak_user_id} - failed: {error}")
else:
await self.keycloak_client.delete_user(keycloak_user_id)
raise

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS watcha_nextcloud_shares (
room_id TEXT NOT NULL PRIMARY KEY,
share_id INTEGER
);
INSERT INTO watcha_nextcloud_shares
SELECT DISTINCT
room_id,
share_id
FROM room_nextcloud_mapping;
DROP TABLE room_nextcloud_mapping;

@ -3,55 +3,53 @@ from synapse.storage.database import DatabasePool
class NextcloudStore(SQLBaseStore):
def __init__(self, database: DatabasePool, db_conn, hs):
def __init__(self, database: DatabasePool, db_conn, hs: "Homeserver"):
super().__init__(database, db_conn, hs)
async def get_path_from_room_id(self, room_id):
"""Get the Nextcloud folder path which is bound with room_id."""
return await self.db_pool.simple_select_one_onecol(
table="room_nextcloud_mapping",
keyvalues={"room_id": room_id},
retcol="directory_path",
allow_none=True,
desc="get_path_from_room_id",
)
async def get_nextcloud_share_id_from_room_id(self, room_id):
"""Get Nextcloud share id of the room id."""
async def get_share_id(self, room_id: str):
"""Get Nextcloud share id of a room.
Args:
room_id: id of the room
"""
return await self.db_pool.simple_select_one_onecol(
table="room_nextcloud_mapping",
table="watcha_nextcloud_shares",
keyvalues={"room_id": room_id},
retcol="share_id",
allow_none=True,
desc="get_nextcloud_share_id_from_room_id",
desc="get_share_id",
)
async def bind(self, room_id, path, share_id):
"""Bind a room with a Nextcloud folder."""
async def register_share(self, room_id: str, share_id: str):
"""Register a share between a room and a Nextcloud folder
Args:
room_id: id of the room
share_id: id of the Nextcloud share
"""
await self.db_pool.simple_upsert(
table="room_nextcloud_mapping",
table="watcha_nextcloud_shares",
keyvalues={"room_id": room_id},
values={
"room_id": room_id,
"directory_path": path,
"share_id": share_id,
},
desc="bind",
)
async def unbind(self, room_id):
"""Delete mapping between Watcha room and Nextcloud directory for room_id."""
async def delete_share(self, room_id: str):
"""Delete an existing share of a room
Args:
room_id: id of the room where the share is associated
"""
await self.db_pool.simple_delete(
table="room_nextcloud_mapping",
table="watcha_nextcloud_shares",
keyvalues={"room_id": room_id},
desc="unbind",
)
async def get_nextcloud_username(self, user_id):
async def get_username(self, user_id: str):
"""Look up a Nextcloud username by their user_id
Args:
@ -65,5 +63,5 @@ class NextcloudStore(SQLBaseStore):
keyvalues={"user_id": user_id},
retcol="nextcloud_username",
allow_none=True,
desc="get_nextcloud_username",
desc="get_username",
)

@ -1,6 +1,6 @@
from mock import AsyncMock
from synapse.api.errors import Codes, SynapseError
from synapse.api.errors import SynapseError, NextcloudError
from synapse.rest.client.v1 import login, room
from synapse.rest import admin
from tests.unittest import HomeserverTestCase
@ -18,158 +18,172 @@ class NextcloudHandlerTestCase(HomeserverTestCase):
def prepare(self, reactor, clock, hs):
self.store = hs.get_datastore()
self.nextcloud_handler = hs.get_nextcloud_handler()
self.keycloak_client = self.nextcloud_handler.keycloak_client
self.nextcloud_client = self.nextcloud_handler.nextcloud_client
# Create a room with two users :
self.creator = self.register_user("creator", "pass", admin=True)
self.creator_tok = self.login("creator", "pass")
self.inviter = self.register_user("inviter", "pass")
inviter_tok = self.login("inviter", "pass")
self.room_id = self.helper.create_room_as(self.creator, tok=self.creator_tok)
self.helper.invite(
self.room_id, src=self.creator, targ=self.inviter, tok=self.creator_tok
)
self.helper.join(self.room_id, self.inviter, tok=inviter_tok)
self.group_name = NEXTCLOUD_GROUP_NAME_PREFIX + self.room_id
# Mock Nextcloud client functions :
self.nextcloud_client.add_group = AsyncMock()
self.nextcloud_client.delete_group = AsyncMock()
self.nextcloud_client.add_user_to_group = AsyncMock()
self.nextcloud_client.remove_user_from_group = AsyncMock()
self.nextcloud_client.unshare = AsyncMock()
self.nextcloud_client.share = AsyncMock(return_value=1)
self.nextcloud_client.share = AsyncMock(return_value="share_1")
def test_set_a_new_bind(self):
self.get_success(
self.nextcloud_handler.bind(self.creator, self.room_id, "/directory")
self.nextcloud_handler.bind(self.creator, self.room_id, "/folder")
)
self.nextcloud_client.add_group.reset_mock()
self.nextcloud_client.add_user_to_group.reset_mock()
self.nextcloud_client.share.reset_mock()
def test_unbind(self):
self.get_success(self.nextcloud_handler.unbind(self.room_id))
share_id = self.get_success(self.store.get_share_id(self.room_id))
mapped_directory = self.get_success(
self.store.get_path_from_room_id(self.room_id)
self.nextcloud_client.delete_group.assert_called()
self.assertIsNone(share_id)
def test_unbind_with_unexisting_group(self):
self.nextcloud_client.delete_group = AsyncMock(
side_effect=NextcloudError(code=101, msg="")
)
with self.assertLogs("synapse.handlers.watcha_nextcloud", level="WARN") as cm:
self.get_success(self.nextcloud_handler.unbind(self.room_id))
share_id = self.get_success(self.store.get_share_id(self.room_id))
share_id = self.get_success(
self.store.get_nextcloud_share_id_from_room_id(self.room_id)
self.assertIn(
f"[watcha] delete nextcloud group {self.group_name} - failed:",
cm.output[0],
)
self.nextcloud_client.delete_group.assert_called()
self.assertIsNone(share_id)
# Verify that mocked functions are called once
def test_update_bind(self):
old_share_id = self.get_success(self.store.get_share_id(self.room_id))
self.nextcloud_client.share = AsyncMock(return_value="share_2")
self.get_success(
self.nextcloud_handler.bind(self.creator, self.room_id, "/new_folder")
)
share_id = self.get_success(self.store.get_share_id(self.room_id))
self.assertEqual(old_share_id, "share_1")
self.nextcloud_client.add_group.assert_called_once()
self.nextcloud_client.share.assert_called_once()
# Verify that mocked functions are called twice
self.assertEquals(self.nextcloud_client.add_user_to_group.call_count, 2)
self.nextcloud_client.unshare.assert_called()
self.assertEquals(share_id, "share_2")
# Verify that mocked functions are not called
self.nextcloud_client.unshare.assert_not_called()
self.assertEqual(mapped_directory, "/directory")
def test_update_an_existing_bind(self):
self.get_success(self.store.bind(self.room_id, "/directory", 2))
old_mapped_directory = self.get_success(
self.store.get_path_from_room_id(self.room_id)
)
old_share_id = self.get_success(
self.store.get_nextcloud_share_id_from_room_id(self.room_id)
def test_update_bind_with_existing_group(self):
self.nextcloud_client.add_group = AsyncMock(
side_effect=NextcloudError(code=102, msg="")
)
self.assertEqual(old_mapped_directory, "/directory")
self.assertEqual(old_share_id, 2)
with self.assertLogs("synapse.handlers.watcha_nextcloud", level="WARN") as cm:
self.get_success(
self.nextcloud_handler.bind(self.creator, self.room_id, "/directory2")
self.nextcloud_handler.bind(self.creator, self.room_id, "/new_folder")
)
mapped_directory = self.get_success(
self.store.get_path_from_room_id(self.room_id)
self.assertIn(
f"[watcha] add nextcloud group {self.group_name} - failed: the group already exists",
cm.output[0],
)
new_share_id = self.get_success(
self.store.get_nextcloud_share_id_from_room_id(self.room_id)
def test_update_bind_with_invalid_input_data(self):
self.nextcloud_client.add_group = AsyncMock(
side_effect=NextcloudError(code=101, msg="")
)
# Verify that mocked functions has called :
self.nextcloud_client.unshare.assert_called()
self.assertEqual(mapped_directory, "/directory2")
self.assertEqual(new_share_id, 1)
def test_delete_an_existing_bind(self):
self.get_success(self.store.bind(self.room_id, "/directory", 2))
self.get_success(self.nextcloud_handler.unbind(self.room_id))
mapped_directory = self.get_success(
self.store.get_path_from_room_id(self.room_id)
)
share_id = self.get_success(
self.store.get_nextcloud_share_id_from_room_id(self.room_id)
self.get_failure(
self.nextcloud_handler.bind(self.creator, self.room_id, "/new_folder"),
SynapseError,
)
self.nextcloud_client.delete_group.assert_called()
self.assertIsNone(mapped_directory)
self.assertIsNone(share_id)
def test_add_user_to_nextcloud_group_without_nextcloud_account(self):
def test_add_user_to_group_without_account(self):
self.nextcloud_client.add_user_to_group = AsyncMock(
side_effect=SynapseError(code=400, msg="")
side_effect=NextcloudError(code=103, msg="")
)
with self.assertLogs("synapse.handlers.watcha_nextcloud", level="WARN") as cm:
self.get_success(
self.nextcloud_handler.add_room_users_to_nextcloud_group(self.room_id)
self.nextcloud_handler.add_room_members_to_group(self.room_id)
)
self.assertIn(
"Unable to add the user {} to the Nextcloud group {}".format(
self.creator,
NEXTCLOUD_GROUP_NAME_PREFIX + self.room_id,
),
f"[watcha] add user {self.creator} to group {self.group_name} - failed",
cm.output[0],
)
self.assertIn(
"Unable to add the user {} to the Nextcloud group {}".format(
self.inviter,
NEXTCLOUD_GROUP_NAME_PREFIX + self.room_id,
),
f"[watcha] add user {self.inviter} to group {self.group_name} - failed",
cm.output[1],
)
def test_add_user_to_nextcloud_group_with_exception(self):
group_name = NEXTCLOUD_GROUP_NAME_PREFIX + self.room_id
self.nextcloud_client.add_user_to_group = AsyncMock(
side_effect=SynapseError(code=400, msg="")
def test_create_share_with_unexisting_folder(self):
old_share_id = self.get_success(self.store.get_share_id(self.room_id))
self.nextcloud_client.share = AsyncMock(
side_effect=NextcloudError(code=404, msg="")
)
self.nextcloud_client.unshare = AsyncMock(
side_effect=NextcloudError(code=404, msg="")
)
self.nextcloud_handler.unbind = AsyncMock()
with self.assertLogs("synapse.handlers.watcha_nextcloud", level="WARN") as cm:
self.get_success(
self.nextcloud_handler.add_room_users_to_nextcloud_group(self.room_id)
error = self.get_failure(
self.nextcloud_handler.create_share(
self.creator, self.room_id, "/new_folder"
),
SynapseError,
)
self.assertIn(
"Unable to add the user {} to the Nextcloud group {}.".format(
self.creator, group_name
),
cm.output[0],
self.assertEquals(error.value.code, 404)
self.nextcloud_handler.unbind.assert_called_once()
self.assertIn(f"[watcha] unshare {old_share_id} - failed", cm.output[0])
def test_create_share_with_other_exceptions(self):
old_share_id = self.get_success(self.store.get_share_id(self.room_id))
self.nextcloud_client.share = AsyncMock(
side_effect=NextcloudError(code=400, msg="")
)
self.nextcloud_client.unshare = AsyncMock(
side_effect=NextcloudError(code=404, msg="")
)
self.nextcloud_handler.unbind = AsyncMock()
self.assertIn(
"Unable to add the user {} to the Nextcloud group {}.".format(
self.inviter, group_name
with self.assertLogs("synapse.handlers.watcha_nextcloud", level="WARN") as cm:
error = self.get_failure(
self.nextcloud_handler.create_share(
self.creator, self.room_id, "/new_folder"
),
cm.output[1],
SynapseError,
)
self.assertEquals(error.value.code, 500)
self.nextcloud_handler.unbind.assert_called_once()
self.assertIn(f"[watcha] unshare {old_share_id} - failed", cm.output[0])
def test_add_user_to_unexisting_group(self):
self.nextcloud_client.add_user_to_group = AsyncMock(
side_effect=NextcloudError(code=102, msg="")
)
self.get_failure(
self.nextcloud_handler.add_room_members_to_group(self.room_id),
SynapseError,
)
def test_update_existing_nextcloud_share_on_invite_membership(self):
def test_update_existing_group_on_invite_membership(self):
self.get_success(
self.nextcloud_handler.update_share(
self.nextcloud_handler.update_group(
"@second_inviter:test", self.room_id, "invite"
)
)
@ -177,9 +191,9 @@ class NextcloudHandlerTestCase(HomeserverTestCase):
self.nextcloud_client.add_user_to_group.assert_called_once()
self.nextcloud_client.remove_user_from_group.assert_not_called()
def test_update_existing_nextcloud_share_on_join_membership(self):
def test_update_existing_group_on_join_membership(self):
self.get_success(
self.nextcloud_handler.update_share(
self.nextcloud_handler.update_group(
"@second_inviter:test", self.room_id, "join"
)
)
@ -187,9 +201,9 @@ class NextcloudHandlerTestCase(HomeserverTestCase):
self.nextcloud_client.add_user_to_group.assert_called_once()
self.nextcloud_client.remove_user_from_group.assert_not_called()
def test_update_existing_nextcloud_share_on_leave_membership(self):
def test_update_existing_group_on_leave_membership(self):
self.get_success(
self.nextcloud_handler.update_share(
self.nextcloud_handler.update_group(
"@second_inviter:test", self.room_id, "leave"
)
)
@ -197,9 +211,9 @@ class NextcloudHandlerTestCase(HomeserverTestCase):
self.nextcloud_client.remove_user_from_group.assert_called_once()
self.nextcloud_client.add_user_to_group.assert_not_called()
def test_update_existing_nextcloud_share_on_kick_membership(self):
def test_update_existing_group_on_kick_membership(self):
self.get_success(
self.nextcloud_handler.update_share(
self.nextcloud_handler.update_group(
"@second_inviter:test", self.room_id, "kick"
)
)
@ -207,42 +221,36 @@ class NextcloudHandlerTestCase(HomeserverTestCase):
self.nextcloud_client.remove_user_from_group.assert_called_once()
self.nextcloud_client.add_user_to_group.assert_not_called()
def test_update_existing_nextcloud_share_on_invite_membership_with_exception(self):
def test_update_existing_group_on_invite_membership_with_exception(self):
self.nextcloud_client.add_user_to_group = AsyncMock(
side_effect=SynapseError(code=400, msg="")
side_effect=NextcloudError(code=103, msg="")
)
second_inviter = "@second_inviter:test"
with self.assertLogs("synapse.handlers.watcha_nextcloud", level="WARN") as cm:
self.get_success(
self.nextcloud_handler.update_share(
self.nextcloud_handler.update_group(
second_inviter, self.room_id, "invite"
)
)
self.assertIn(
"Unable to add the user {} to the Nextcloud group {}.".format(
second_inviter, NEXTCLOUD_GROUP_NAME_PREFIX + self.room_id
),
f"[watcha] add user {second_inviter} to group {self.group_name} - failed",
cm.output[0],
)
def test_update_existing_nextcloud_share_on_leave_membership_with_exception(self):
def test_update_existing_group_on_leave_membership_with_exception(self):
self.nextcloud_client.remove_user_from_group = AsyncMock(
side_effect=SynapseError(code=400, msg="")
side_effect=NextcloudError(code=103, msg="")
)
second_inviter = "@second_inviter:test"
with self.assertLogs("synapse.handlers.watcha_nextcloud", level="WARN") as cm:
self.get_success(
self.nextcloud_handler.update_share(
self.nextcloud_handler.update_group(
second_inviter, self.room_id, "leave"
)
)
self.assertIn(
"Unable to remove the user {} from the Nextcloud group {}.".format(
second_inviter, NEXTCLOUD_GROUP_NAME_PREFIX + self.room_id
),
f"[watcha] remove user {second_inviter} from group {self.group_name} - failed",
cm.output[0],
)

@ -3,8 +3,7 @@ from mock import AsyncMock
from synapse.api.errors import SynapseError
from synapse.rest import admin
from synapse.rest.client.v1 import login, room, watcha
from synapse.types import UserID, create_requester
from synapse.rest.client.v1 import login, room
from tests import unittest
@ -20,22 +19,17 @@ class NextcloudShareTestCase(unittest.HomeserverTestCase):
def prepare(self, reactor, clock, hs):
self.store = hs.get_datastore()
self.nextcloud_handler = hs.get_nextcloud_handler()
self.keycloak_client = self.nextcloud_handler.keycloak_client
self.nextcloud_client = self.nextcloud_handler.nextcloud_client
self.creator = self.register_user("creator", "pass")
self.creator_tok = self.login("creator", "pass")
self.inviter = self.register_user("inviter", "pass")
self.inviter_tok = self.login("inviter", "pass")
self.room_id = self.helper.create_room_as(self.creator, tok=self.creator_tok)
self.get_success(self.store.register_share(self.room_id, 1))
# map a room with a Nextcloud directory :
self.get_success(self.store.bind(self.room_id, "/directory", 1))
# mock some functions of WatchaRoomNextcloudMappingHandler
self.nextcloud_handler = hs.get_nextcloud_handler()
self.keycloak_client = self.nextcloud_handler.keycloak_client
self.nextcloud_client = self.nextcloud_handler.nextcloud_client
self.nextcloud_handler.bind = AsyncMock()
self.nextcloud_handler.unbind = AsyncMock()
self.nextcloud_directory_url = (
@ -51,7 +45,7 @@ class NextcloudShareTestCase(unittest.HomeserverTestCase):
def send_room_nextcloud_mapping_event(self, request_content):
channel = self.make_request(
"PUT",
"/rooms/{}/state/im.vector.web.settings".format(self.room_id),
f"/rooms/{self.room_id}/state/im.vector.web.settings",
content=json.dumps(request_content),
access_token=self.creator_tok,
)
@ -70,11 +64,10 @@ class NextcloudShareTestCase(unittest.HomeserverTestCase):
self.send_room_nextcloud_mapping_event(
{"nextcloudShare": self.nextcloud_directory_url}
)
self.assertTrue(self.nextcloud_handler.bind.called)
channel = self.send_room_nextcloud_mapping_event({"nextcloudShare": ""})
self.assertTrue(self.nextcloud_handler.unbind.called)
self.assertTrue(self.nextcloud_handler.bind.called)
self.assertTrue(self.nextcloud_handler.unbind.called)
self.assertEquals(200, channel.code)
def test_update_existing_room_nextcloud_mapping(self):
@ -94,12 +87,7 @@ class NextcloudShareTestCase(unittest.HomeserverTestCase):
)
self.assertFalse(self.nextcloud_handler.bind.called)
self.assertRaises(SynapseError)
self.assertEquals(400, channel.code)
self.assertEquals(
"VectorSetting is only used for Nextcloud integration.",
json.loads(channel.result["body"])["error"],
)
self.assertEquals(200, channel.code)
def test_create_new_room_nextcloud_mapping_with_wrong_url(self):
channel = self.send_room_nextcloud_mapping_event(
@ -110,7 +98,7 @@ class NextcloudShareTestCase(unittest.HomeserverTestCase):
self.assertRaises(SynapseError)
self.assertEquals(400, channel.code)
self.assertEquals(
"The url doesn't point to a valid nextcloud directory path.",
"[watcha] binding Nextcloud folder with room - failed: wrong folder path",
json.loads(channel.result["body"])["error"],
)
@ -118,10 +106,9 @@ class NextcloudShareTestCase(unittest.HomeserverTestCase):
self.helper.invite(
self.room_id, self.creator, self.inviter, tok=self.creator_tok
)
channel = self.make_request(
"POST",
"/_matrix/client/r0/rooms/{}/join".format(self.room_id),
f"/_matrix/client/r0/rooms/{self.room_id}/join",
access_token=self.inviter_tok,
)
@ -132,10 +119,9 @@ class NextcloudShareTestCase(unittest.HomeserverTestCase):
self.helper.invite(
self.room_id, self.creator, self.inviter, tok=self.creator_tok
)
channel = self.make_request(
"POST",
"/_matrix/client/r0/rooms/{}/leave".format(self.room_id),
f"/_matrix/client/r0/rooms/{self.room_id}/leave",
access_token=self.inviter_tok,
)
@ -148,10 +134,9 @@ class NextcloudShareTestCase(unittest.HomeserverTestCase):
self.room_id, self.creator, self.inviter, tok=self.creator_tok
)
self.helper.join(self.room_id, user=self.inviter, tok=self.inviter_tok)
channel = self.make_request(
"POST",
"/_matrix/client/r0/rooms/{}/kick".format(self.room_id),
f"/_matrix/client/r0/rooms/{self.room_id}/kick",
content={"user_id": self.inviter},
access_token=self.inviter_tok,
)
@ -162,9 +147,7 @@ class NextcloudShareTestCase(unittest.HomeserverTestCase):
def test_update_nextcloud_share_with_an_unmapped_room(self):
self.nextcloud_handler.update_existing_nextcloud_share_for_user = AsyncMock()
room_id = self.helper.create_room_as(self.creator, tok=self.creator_tok)
self.helper.invite(room_id, self.creator, self.inviter, tok=self.creator_tok)
self.nextcloud_handler.update_existing_nextcloud_share_for_user.assert_not_called()

@ -1,72 +1,28 @@
from twisted.internet import defer
from tests import unittest
from tests.utils import setup_test_homeserver
class NextcloudStorageTestCase(unittest.HomeserverTestCase):
@defer.inlineCallbacks
def setUp(self):
hs = setup_test_homeserver(self.addCleanup)
def prepare(self, reactor, clock, hs):
self.store = hs.get_datastore()
self.room_id = "room1"
self.directory_path = "/directory"
self.share_id = 1
# Set mapping between a room and a nextcloud directory :
yield defer.ensureDeferred(
self.store.bind(
self.room_id, self.directory_path, self.share_id
)
)
@defer.inlineCallbacks
def test_get_room_mapping_with_nextcloud_directory(self):
mapped_directory = yield defer.ensureDeferred(
self.store.get_path_from_room_id(self.room_id)
)
share_id = yield defer.ensureDeferred(
self.store.get_nextcloud_share_id_from_room_id(self.room_id)
)
self.get_success(self.store.register_share(self.room_id, self.share_id))
self.assertEquals(mapped_directory, self.directory_path)
def test_get_share_id(self):
share_id = self.get_success(self.store.get_share_id(self.room_id))
self.assertEquals(share_id, self.share_id)
@defer.inlineCallbacks
def test_delete_room_nextcloud_mapping(self):
yield defer.ensureDeferred(
self.store.unbind(self.room_id)
)
mapped_directory = yield defer.ensureDeferred(
self.store.get_path_from_room_id(self.room_id)
)
self.assertIsNone(mapped_directory)
share_id = yield defer.ensureDeferred(
self.store.get_nextcloud_share_id_from_room_id(self.room_id)
)
def test_delete_share(self):
self.get_success(self.store.delete_share(self.room_id))
share_id = self.get_success(self.store.get_share_id(self.room_id))
self.assertIsNone(share_id)
@defer.inlineCallbacks
def test_update_room_mapping_with_nextcloud_directory(self):
new_directory_path = "/directory2"
def test_update_group(self):
new_share_id = 2
yield defer.ensureDeferred(
self.store.bind(
self.room_id, new_directory_path, new_share_id
)
)
mapped_directory = yield defer.ensureDeferred(
self.store.get_path_from_room_id(self.room_id)
)
self.assertEquals(mapped_directory, new_directory_path)
share_id = yield defer.ensureDeferred(
self.store.get_nextcloud_share_id_from_room_id(self.room_id)
)
self.get_success(self.store.register_share(self.room_id, new_share_id))
share_id = self.get_success(self.store.get_share_id(self.room_id))
self.assertEquals(share_id, new_share_id)

Loading…
Cancel
Save