mirror of
https://github.com/famedly/ansible-collection-matrix
synced 2024-09-20 05:51:57 +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
|
# init subclasses
|
||||||
self.ratelimit = self.__Ratelimit(self)
|
self.ratelimit = self.__Ratelimit(self)
|
||||||
|
self.join = self.__Join(self)
|
||||||
|
|
||||||
# Make API request
|
# Make API request
|
||||||
def get(self, path: str) -> requests.Response:
|
def get(self, path: str) -> requests.Response:
|
||||||
|
@ -118,6 +119,19 @@ class AdminApi:
|
||||||
user_id = AdminApi.url_encode(user_id)
|
user_id = AdminApi.url_encode(user_id)
|
||||||
return self.__parent.delete(self.API_PATH.format(user_id=user_id)).json()
|
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 Exceptions:
|
||||||
class HTTPException(BaseException):
|
class HTTPException(BaseException):
|
||||||
|
|
|
@ -68,8 +68,18 @@ options:
|
||||||
default: false
|
default: false
|
||||||
type: bool
|
type: bool
|
||||||
required: false
|
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:
|
requirements:
|
||||||
- matrix-nio (Python library)
|
- matrix-nio (Python library)
|
||||||
|
- requests (Python library)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
EXAMPLES = """
|
EXAMPLES = """
|
||||||
|
@ -99,6 +109,10 @@ try:
|
||||||
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import (
|
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import (
|
||||||
AnsibleNioModule,
|
AnsibleNioModule,
|
||||||
)
|
)
|
||||||
|
from ansible_collections.famedly.matrix.plugins.module_utils.synapse import (
|
||||||
|
AdminApi,
|
||||||
|
Exceptions,
|
||||||
|
)
|
||||||
from nio import (
|
from nio import (
|
||||||
RoomGetStateError,
|
RoomGetStateError,
|
||||||
RoomBanError,
|
RoomBanError,
|
||||||
|
@ -176,12 +190,19 @@ async def invite_to_room(client, room_id, user_id, res):
|
||||||
res["invited"].append(user_id)
|
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():
|
async def run_module():
|
||||||
module_args = dict(
|
module_args = dict(
|
||||||
state=dict(choices=["member", "kicked", "banned"], required=True),
|
state=dict(choices=["member", "kicked", "banned"], required=True),
|
||||||
room_id=dict(type="str", required=True),
|
room_id=dict(type="str", required=True),
|
||||||
user_ids=dict(type="list", required=True, elements="str"),
|
user_ids=dict(type="list", required=True, elements="str"),
|
||||||
exclusive=dict(type="bool", required=False, default=False),
|
exclusive=dict(type="bool", required=False, default=False),
|
||||||
|
force_join=dict(type="bool", required=False, default=False),
|
||||||
)
|
)
|
||||||
|
|
||||||
result = dict(
|
result = dict(
|
||||||
|
@ -190,6 +211,7 @@ async def run_module():
|
||||||
unbanned=[],
|
unbanned=[],
|
||||||
kicked=[],
|
kicked=[],
|
||||||
invited=[],
|
invited=[],
|
||||||
|
joined=[],
|
||||||
members=[],
|
members=[],
|
||||||
msg="",
|
msg="",
|
||||||
)
|
)
|
||||||
|
@ -202,10 +224,13 @@ async def run_module():
|
||||||
action = module.params["state"]
|
action = module.params["state"]
|
||||||
room_id = module.params["room_id"]
|
room_id = module.params["room_id"]
|
||||||
user_ids = module.params["user_ids"]
|
user_ids = module.params["user_ids"]
|
||||||
|
force_join = module.params["force_join"]
|
||||||
|
|
||||||
# Check for valid parameter combination
|
# Check for valid parameter combination
|
||||||
if module.params["exclusive"] and action != "member":
|
if module.params["exclusive"] and action != "member":
|
||||||
await module.fail_json(msg="exclusive=True can only be used with state=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
|
# Handle ansible check mode
|
||||||
if module.check_mode:
|
if module.check_mode:
|
||||||
|
@ -215,6 +240,9 @@ async def run_module():
|
||||||
|
|
||||||
# Create client object
|
# Create client object
|
||||||
client = module.client
|
client = module.client
|
||||||
|
admin_client = AdminApi(
|
||||||
|
home_server=module.params["hs_url"], access_token=module.params["token"]
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Query all room members (invited users count as member, as they _can_ be in the room)
|
# 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 action == "member" and user_id not in present_members:
|
||||||
if user_id in banned_members:
|
if user_id in banned_members:
|
||||||
await unban_from_room(client, room_id, user_id, result)
|
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:
|
elif action == "kicked" and user_id in present_members:
|
||||||
await kick_from_room(client, room_id, user_id, result)
|
await kick_from_room(client, room_id, user_id, result)
|
||||||
elif action == "kicked" and user_id in banned_members:
|
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)
|
await ban_from_room(client, room_id, user_id, result)
|
||||||
except NioOperationError:
|
except NioOperationError:
|
||||||
await module.fail_json(**result)
|
await module.fail_json(**result)
|
||||||
|
except (Exceptions.HTTPException, Exceptions.MatrixException) as e:
|
||||||
|
result["msg"] = str(e)
|
||||||
|
await module.fail_json(**result)
|
||||||
else:
|
else:
|
||||||
# Handle exclusive mode: get state and make lists of users to be kicked or invited
|
# 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))
|
to_invite = list(filter(lambda m: m not in present_members, user_ids))
|
||||||
|
@ -249,11 +285,17 @@ async def run_module():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for user_id in to_invite:
|
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:
|
for user_id in to_kick:
|
||||||
await kick_from_room(client, room_id, user_id, result)
|
await kick_from_room(client, room_id, user_id, result)
|
||||||
except NioOperationError:
|
except NioOperationError:
|
||||||
await module.fail_json(**result)
|
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
|
# Get all current members from the room
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import absolute_import, division, print_function, annotations
|
from __future__ import absolute_import, division, print_function, annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import types
|
import types
|
||||||
|
|
||||||
import pytest
|
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.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 (
|
from ansible_collections.famedly.matrix.tests.unit.utils import (
|
||||||
AnsibleExitJson,
|
AnsibleExitJson,
|
||||||
AnsibleFailJson,
|
AnsibleFailJson,
|
||||||
|
@ -71,6 +77,20 @@ class TestAnsibleModuleMatrixMember:
|
||||||
)
|
)
|
||||||
monkeypatch.setenv("ROOM_SIMULATOR", simulator.export())
|
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):
|
def test_check_mode(self, monkeypatch):
|
||||||
self.patchAnsibleNioModule(monkeypatch, MatrixNioSuccess)
|
self.patchAnsibleNioModule(monkeypatch, MatrixNioSuccess)
|
||||||
set_module_args(
|
set_module_args(
|
||||||
|
@ -82,7 +102,7 @@ class TestAnsibleModuleMatrixMember:
|
||||||
"state": "member",
|
"state": "member",
|
||||||
"user_ids": ["@user1:matrix.example.tld", "@user2:matrix.example.tld"],
|
"user_ids": ["@user1:matrix.example.tld", "@user2:matrix.example.tld"],
|
||||||
},
|
},
|
||||||
check_mode=True
|
check_mode=True,
|
||||||
)
|
)
|
||||||
with pytest.raises(AnsibleExitJson) as result:
|
with pytest.raises(AnsibleExitJson) as result:
|
||||||
matrix_member.main()
|
matrix_member.main()
|
||||||
|
@ -242,7 +262,7 @@ class TestAnsibleModuleMatrixMember:
|
||||||
"room_id": "!myroomid:matrix.example.tld",
|
"room_id": "!myroomid:matrix.example.tld",
|
||||||
"state": "member",
|
"state": "member",
|
||||||
"exclusive": True,
|
"exclusive": True,
|
||||||
"user_ids": ["@user2:matrix.example.tld"],
|
"user_ids": ["@user2:matrix.example.tld", "@user3:matrix.example.tld"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
with pytest.raises(AnsibleExitJson) as result:
|
with pytest.raises(AnsibleExitJson) as result:
|
||||||
|
@ -252,9 +272,132 @@ class TestAnsibleModuleMatrixMember:
|
||||||
assert_expression(ansible_result["banned"] == [])
|
assert_expression(ansible_result["banned"] == [])
|
||||||
assert_expression(ansible_result["unbanned"] == [])
|
assert_expression(ansible_result["unbanned"] == [])
|
||||||
assert_expression(ansible_result["kicked"] == ["@user1:matrix.example.tld"])
|
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(
|
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):
|
def test_add_user_fail(self, monkeypatch):
|
||||||
|
@ -350,3 +493,54 @@ class TestAnsibleModuleMatrixMember:
|
||||||
)
|
)
|
||||||
with pytest.raises(AnsibleFailJson) as result:
|
with pytest.raises(AnsibleFailJson) as result:
|
||||||
matrix_member.main()
|
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