mirror of
https://github.com/famedly/ansible-collection-matrix
synced 2024-12-04 00:29:12 +00:00
feat(matrix_member): support force join for local users
Requires synapse as homeserver.
This commit is contained in:
parent
a46d21e9df
commit
9dbd99a305
3 changed files with 256 additions and 6 deletions
|
@ -36,6 +36,7 @@ class AdminApi:
|
|||
|
||||
# init subclasses
|
||||
self.ratelimit = self.__Ratelimit(self)
|
||||
self.join = self.__Join(self)
|
||||
|
||||
# Make API request
|
||||
def get(self, path: str) -> requests.Response:
|
||||
|
@ -118,6 +119,19 @@ class AdminApi:
|
|||
user_id = AdminApi.url_encode(user_id)
|
||||
return self.__parent.delete(self.API_PATH.format(user_id=user_id)).json()
|
||||
|
||||
class __Join:
|
||||
API_PATH = "v1/join/{room_id}"
|
||||
|
||||
def __init__(self, parent):
|
||||
self.__parent = parent
|
||||
|
||||
def join(self, room_id: str, user_id: str) -> dict:
|
||||
room_id = AdminApi.url_encode(room_id)
|
||||
return self.__parent.post(
|
||||
self.API_PATH.format(room_id=room_id),
|
||||
json={"user_id": user_id},
|
||||
).json()
|
||||
|
||||
|
||||
class Exceptions:
|
||||
class HTTPException(BaseException):
|
||||
|
|
|
@ -68,8 +68,18 @@ options:
|
|||
default: false
|
||||
type: bool
|
||||
required: false
|
||||
force_join:
|
||||
description:
|
||||
- If state=member, the module force joins the user by calling the
|
||||
`/_synapse/admin/v1/join/{room-id}` endpoint of synapses admin API.
|
||||
- Only works for synapse homeservers and if the provided credentials have admin privileges.
|
||||
- Only works for local users.
|
||||
default: false
|
||||
type: bool
|
||||
required: false
|
||||
requirements:
|
||||
- matrix-nio (Python library)
|
||||
- requests (Python library)
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
|
@ -99,6 +109,10 @@ try:
|
|||
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import (
|
||||
AnsibleNioModule,
|
||||
)
|
||||
from ansible_collections.famedly.matrix.plugins.module_utils.synapse import (
|
||||
AdminApi,
|
||||
Exceptions,
|
||||
)
|
||||
from nio import (
|
||||
RoomGetStateError,
|
||||
RoomBanError,
|
||||
|
@ -176,12 +190,19 @@ async def invite_to_room(client, room_id, user_id, res):
|
|||
res["invited"].append(user_id)
|
||||
|
||||
|
||||
async def force_join_into_room(admin_client, room_id, user_id, res):
|
||||
admin_client.join.join(room_id, user_id)
|
||||
res["changed"] = True
|
||||
res["joined"].append(user_id)
|
||||
|
||||
|
||||
async def run_module():
|
||||
module_args = dict(
|
||||
state=dict(choices=["member", "kicked", "banned"], required=True),
|
||||
room_id=dict(type="str", required=True),
|
||||
user_ids=dict(type="list", required=True, elements="str"),
|
||||
exclusive=dict(type="bool", required=False, default=False),
|
||||
force_join=dict(type="bool", required=False, default=False),
|
||||
)
|
||||
|
||||
result = dict(
|
||||
|
@ -190,6 +211,7 @@ async def run_module():
|
|||
unbanned=[],
|
||||
kicked=[],
|
||||
invited=[],
|
||||
joined=[],
|
||||
members=[],
|
||||
msg="",
|
||||
)
|
||||
|
@ -202,10 +224,13 @@ async def run_module():
|
|||
action = module.params["state"]
|
||||
room_id = module.params["room_id"]
|
||||
user_ids = module.params["user_ids"]
|
||||
force_join = module.params["force_join"]
|
||||
|
||||
# Check for valid parameter combination
|
||||
if module.params["exclusive"] and action != "member":
|
||||
await module.fail_json(msg="exclusive=True can only be used with state=member")
|
||||
if module.params["force_join"] and action != "member":
|
||||
await module.fail_json(msg="force_join=True can only be used with state=member")
|
||||
|
||||
# Handle ansible check mode
|
||||
if module.check_mode:
|
||||
|
@ -215,6 +240,9 @@ async def run_module():
|
|||
|
||||
# Create client object
|
||||
client = module.client
|
||||
admin_client = AdminApi(
|
||||
home_server=module.params["hs_url"], access_token=module.params["token"]
|
||||
)
|
||||
|
||||
try:
|
||||
# Query all room members (invited users count as member, as they _can_ be in the room)
|
||||
|
@ -233,7 +261,12 @@ async def run_module():
|
|||
if action == "member" and user_id not in present_members:
|
||||
if user_id in banned_members:
|
||||
await unban_from_room(client, room_id, user_id, result)
|
||||
await invite_to_room(client, room_id, user_id, result)
|
||||
if force_join:
|
||||
await force_join_into_room(
|
||||
admin_client, room_id, user_id, result
|
||||
)
|
||||
else:
|
||||
await invite_to_room(client, room_id, user_id, result)
|
||||
elif action == "kicked" and user_id in present_members:
|
||||
await kick_from_room(client, room_id, user_id, result)
|
||||
elif action == "kicked" and user_id in banned_members:
|
||||
|
@ -242,6 +275,9 @@ async def run_module():
|
|||
await ban_from_room(client, room_id, user_id, result)
|
||||
except NioOperationError:
|
||||
await module.fail_json(**result)
|
||||
except (Exceptions.HTTPException, Exceptions.MatrixException) as e:
|
||||
result["msg"] = str(e)
|
||||
await module.fail_json(**result)
|
||||
else:
|
||||
# Handle exclusive mode: get state and make lists of users to be kicked or invited
|
||||
to_invite = list(filter(lambda m: m not in present_members, user_ids))
|
||||
|
@ -249,11 +285,17 @@ async def run_module():
|
|||
|
||||
try:
|
||||
for user_id in to_invite:
|
||||
await invite_to_room(client, room_id, user_id, result)
|
||||
if force_join:
|
||||
await force_join_into_room(admin_client, room_id, user_id, result)
|
||||
else:
|
||||
await invite_to_room(client, room_id, user_id, result)
|
||||
for user_id in to_kick:
|
||||
await kick_from_room(client, room_id, user_id, result)
|
||||
except NioOperationError:
|
||||
await module.fail_json(**result)
|
||||
except (Exceptions.HTTPException, Exceptions.MatrixException) as e:
|
||||
result["msg"] = str(e)
|
||||
await module.fail_json(**result)
|
||||
|
||||
# Get all current members from the room
|
||||
try:
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import absolute_import, division, print_function, annotations
|
||||
|
||||
import json
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
@ -19,6 +20,11 @@ from ansible_collections.famedly.matrix.tests.unit.mock_nio.utils.RoomSimulator
|
|||
)
|
||||
from ansible_collections.famedly.matrix.tests.unit.mock_nio.room import failure
|
||||
|
||||
from ansible_collections.famedly.matrix.plugins.module_utils import synapse
|
||||
from ansible_collections.famedly.matrix.tests.unit.mock_synapse.requests.RequestsBase import (
|
||||
RequestsBase,
|
||||
)
|
||||
|
||||
from ansible_collections.famedly.matrix.tests.unit.utils import (
|
||||
AnsibleExitJson,
|
||||
AnsibleFailJson,
|
||||
|
@ -71,6 +77,20 @@ class TestAnsibleModuleMatrixMember:
|
|||
)
|
||||
monkeypatch.setenv("ROOM_SIMULATOR", simulator.export())
|
||||
|
||||
@staticmethod
|
||||
def patchAdminApiModule(
|
||||
monkeypatch: MonkeyPatch, target_module, mock_class: type(RequestsBase)
|
||||
):
|
||||
# Mock Admin API
|
||||
for method in RequestsBase.__dict__:
|
||||
if isinstance(
|
||||
getattr(mock_class, method),
|
||||
(types.FunctionType, types.BuiltinFunctionType),
|
||||
):
|
||||
monkeypatch.setattr(
|
||||
synapse.requests, method, getattr(mock_class, method)
|
||||
)
|
||||
|
||||
def test_check_mode(self, monkeypatch):
|
||||
self.patchAnsibleNioModule(monkeypatch, MatrixNioSuccess)
|
||||
set_module_args(
|
||||
|
@ -82,7 +102,7 @@ class TestAnsibleModuleMatrixMember:
|
|||
"state": "member",
|
||||
"user_ids": ["@user1:matrix.example.tld", "@user2:matrix.example.tld"],
|
||||
},
|
||||
check_mode=True
|
||||
check_mode=True,
|
||||
)
|
||||
with pytest.raises(AnsibleExitJson) as result:
|
||||
matrix_member.main()
|
||||
|
@ -242,7 +262,7 @@ class TestAnsibleModuleMatrixMember:
|
|||
"room_id": "!myroomid:matrix.example.tld",
|
||||
"state": "member",
|
||||
"exclusive": True,
|
||||
"user_ids": ["@user2:matrix.example.tld"],
|
||||
"user_ids": ["@user2:matrix.example.tld", "@user3:matrix.example.tld"],
|
||||
}
|
||||
)
|
||||
with pytest.raises(AnsibleExitJson) as result:
|
||||
|
@ -252,9 +272,132 @@ class TestAnsibleModuleMatrixMember:
|
|||
assert_expression(ansible_result["banned"] == [])
|
||||
assert_expression(ansible_result["unbanned"] == [])
|
||||
assert_expression(ansible_result["kicked"] == ["@user1:matrix.example.tld"])
|
||||
assert_expression(ansible_result["invited"] == [])
|
||||
assert_expression(ansible_result["invited"] == ["@user3:matrix.example.tld"])
|
||||
assert_expression(
|
||||
list(ansible_result["members"]) == ["@user2:matrix.example.tld"]
|
||||
list(ansible_result["members"])
|
||||
== ["@user2:matrix.example.tld", "@user3:matrix.example.tld"]
|
||||
)
|
||||
|
||||
def test_force_join(self, monkeypatch):
|
||||
self.patchAnsibleNioModule(monkeypatch, MatrixNioSuccess)
|
||||
self.patchAdminApiModule(monkeypatch, matrix_member, RequestsBase)
|
||||
set_module_args(
|
||||
{
|
||||
"hs_url": "matrix.example.tld",
|
||||
"token": "supersecrettoken",
|
||||
"room_id": "!myroomid:matrix.example.tld",
|
||||
"state": "member",
|
||||
"force_join": True,
|
||||
"user_ids": ["@user3:matrix.example.tld"],
|
||||
}
|
||||
)
|
||||
response = {
|
||||
"_synapse/admin/v1/join/%21myroomid%3Amatrix.example.tld": {
|
||||
"status": 200,
|
||||
"content": '{"room_id": "!myroomid:matrix.example.tld"}',
|
||||
}
|
||||
}
|
||||
monkeypatch.setenv("REQUESTS_POST_RESPONSE", json.dumps(response))
|
||||
with pytest.raises(AnsibleExitJson) as result:
|
||||
matrix_member.main()
|
||||
ansible_result = result.value.result
|
||||
print(ansible_result)
|
||||
assert_expression(ansible_result["changed"] is True)
|
||||
assert_expression(ansible_result["banned"] == [])
|
||||
assert_expression(ansible_result["unbanned"] == [])
|
||||
assert_expression(ansible_result["kicked"] == [])
|
||||
assert_expression(ansible_result["invited"] == [])
|
||||
assert_expression(ansible_result["joined"] == ["@user3:matrix.example.tld"])
|
||||
# TODO: make this assertion work. Needs proper mocking of Admin API that
|
||||
# calls the mocked nio in order to have the members changed
|
||||
# assert_expression(
|
||||
# list(ansible_result["members"])
|
||||
# == [
|
||||
# "@user1:matrix.example.tld",
|
||||
# "@user2:matrix.example.tld",
|
||||
# "@user3:matrix.example.tld",
|
||||
# ]
|
||||
# )
|
||||
|
||||
def test_exclusive_force_join(self, monkeypatch):
|
||||
self.patchAnsibleNioModule(monkeypatch, MatrixNioSuccess)
|
||||
self.patchAdminApiModule(monkeypatch, matrix_member, RequestsBase)
|
||||
set_module_args(
|
||||
{
|
||||
"hs_url": "matrix.example.tld",
|
||||
"token": "supersecrettoken",
|
||||
"room_id": "!myroomid:matrix.example.tld",
|
||||
"state": "member",
|
||||
"force_join": True,
|
||||
"exclusive": True,
|
||||
"user_ids": ["@user3:matrix.example.tld"],
|
||||
}
|
||||
)
|
||||
response = {
|
||||
"_synapse/admin/v1/join/%21myroomid%3Amatrix.example.tld": {
|
||||
"status": 200,
|
||||
"content": '{"room_id": "!myroomid:matrix.example.tld"}',
|
||||
}
|
||||
}
|
||||
monkeypatch.setenv("REQUESTS_POST_RESPONSE", json.dumps(response))
|
||||
with pytest.raises(AnsibleExitJson) as result:
|
||||
matrix_member.main()
|
||||
ansible_result = result.value.result
|
||||
print(ansible_result)
|
||||
assert_expression(ansible_result["changed"] is True)
|
||||
assert_expression(ansible_result["banned"] == [])
|
||||
assert_expression(ansible_result["unbanned"] == [])
|
||||
assert_expression(
|
||||
ansible_result["kicked"]
|
||||
== ["@user1:matrix.example.tld", "@user2:matrix.example.tld"]
|
||||
)
|
||||
assert_expression(ansible_result["invited"] == [])
|
||||
assert_expression(ansible_result["joined"] == ["@user3:matrix.example.tld"])
|
||||
# TODO: make this assertion work. Needs proper mocking of Admin API that
|
||||
# calls the mocked nio in order to have the members changed
|
||||
# assert_expression(
|
||||
# list(ansible_result["members"])
|
||||
# == [
|
||||
# "@user3:matrix.example.tld",
|
||||
# ]
|
||||
# )
|
||||
|
||||
def test_exclusive_kick_fail(self, monkeypatch):
|
||||
self.patchAnsibleNioModule(monkeypatch, MatrixNioSuccess)
|
||||
set_module_args(
|
||||
{
|
||||
"hs_url": "matrix.example.tld",
|
||||
"token": "supersecrettoken",
|
||||
"room_id": "!myroomid:matrix.example.tld",
|
||||
"state": "kicked",
|
||||
"user_ids": ["@user1:matrix.example.tld"],
|
||||
"exclusive": True,
|
||||
},
|
||||
)
|
||||
with pytest.raises(AnsibleFailJson) as result:
|
||||
matrix_member.main()
|
||||
assert_expression(
|
||||
"exclusive=True can only be used with state=member"
|
||||
in result.value.result["msg"]
|
||||
)
|
||||
|
||||
def test_force_join_kick_fail(self, monkeypatch):
|
||||
self.patchAnsibleNioModule(monkeypatch, MatrixNioSuccess)
|
||||
set_module_args(
|
||||
{
|
||||
"hs_url": "matrix.example.tld",
|
||||
"token": "supersecrettoken",
|
||||
"room_id": "!myroomid:matrix.example.tld",
|
||||
"state": "kicked",
|
||||
"user_ids": ["@user1:matrix.example.tld"],
|
||||
"force_join": True,
|
||||
},
|
||||
)
|
||||
with pytest.raises(AnsibleFailJson) as result:
|
||||
matrix_member.main()
|
||||
assert_expression(
|
||||
"force_join=True can only be used with state=member"
|
||||
in result.value.result["msg"]
|
||||
)
|
||||
|
||||
def test_add_user_fail(self, monkeypatch):
|
||||
|
@ -350,3 +493,54 @@ class TestAnsibleModuleMatrixMember:
|
|||
)
|
||||
with pytest.raises(AnsibleFailJson) as result:
|
||||
matrix_member.main()
|
||||
|
||||
def test_force_join_fail_privileges(self, monkeypatch):
|
||||
self.patchAnsibleNioModule(monkeypatch, MatrixNioSuccess)
|
||||
self.patchAdminApiModule(monkeypatch, matrix_member, RequestsBase)
|
||||
set_module_args(
|
||||
{
|
||||
"hs_url": "matrix.example.tld",
|
||||
"token": "notaserveradmintoken",
|
||||
"room_id": "!myroomid:matrix.example.tld",
|
||||
"state": "member",
|
||||
"force_join": True,
|
||||
"user_ids": ["@user3:matrix.example.tld"],
|
||||
}
|
||||
)
|
||||
response = {
|
||||
"_synapse/admin/v1/join/%21myroomid%3Amatrix.example.tld": {
|
||||
"status": 403,
|
||||
"content": '{"errcode": "M_FORBIDDEN", "error": "You are not a server admin"}',
|
||||
}
|
||||
}
|
||||
monkeypatch.setenv("REQUESTS_POST_RESPONSE", json.dumps(response))
|
||||
with pytest.raises(AnsibleFailJson) as result:
|
||||
matrix_member.main()
|
||||
print(result.value.result["msg"])
|
||||
assert_expression("M_FORBIDDEN" in result.value.result["msg"])
|
||||
|
||||
def test_exclusive_force_join_fail_privileges(self, monkeypatch):
|
||||
self.patchAnsibleNioModule(monkeypatch, MatrixNioSuccess)
|
||||
self.patchAdminApiModule(monkeypatch, matrix_member, RequestsBase)
|
||||
set_module_args(
|
||||
{
|
||||
"hs_url": "matrix.example.tld",
|
||||
"token": "notaserveradmintoken",
|
||||
"room_id": "!myroomid:matrix.example.tld",
|
||||
"state": "member",
|
||||
"force_join": True,
|
||||
"exclusive": True,
|
||||
"user_ids": ["@user3:matrix.example.tld"],
|
||||
}
|
||||
)
|
||||
response = {
|
||||
"_synapse/admin/v1/join/%21myroomid%3Amatrix.example.tld": {
|
||||
"status": 403,
|
||||
"content": '{"errcode": "M_FORBIDDEN", "error": "You are not a server admin"}',
|
||||
}
|
||||
}
|
||||
monkeypatch.setenv("REQUESTS_POST_RESPONSE", json.dumps(response))
|
||||
with pytest.raises(AnsibleFailJson) as result:
|
||||
matrix_member.main()
|
||||
print(result.value.result["msg"])
|
||||
assert_expression("M_FORBIDDEN" in result.value.result["msg"])
|
||||
|
|
Loading…
Reference in a new issue