feat: Add ansible-test and refactor plugins

This commit is contained in:
Jadyn Emma Jaeger 2022-07-29 10:47:49 +02:00
parent 1a302fb74b
commit 52785ab768
No known key found for this signature in database
GPG key ID: 632410170049FDDA
19 changed files with 650 additions and 261 deletions

View file

@ -1,7 +1,31 @@
include:
- project: 'famedly/company/devops/templates/ci-cd'
ref: 'ansible-v1'
file: '/ansible.yml'
stages: stages:
- test
- build - build
- publish - publish
# Debian Stable
sanity 3.9:
variables: { PYTHON_VERSION: "3.9" }
extends: .ansible-test-sanity
# Latest stable Python
sanity 3.10:
variables: { PYTHON_VERSION: "3.10" }
extends: .ansible-test-sanity
unit:
variables: { PYTHON_VERSION: "3.10" }
extends: .ansible-test-units
integration:
variables: { PYTHON_VERSION: "3.10" }
extends: .ansible-test-integration
build: build:
image: docker.io/alpine image: docker.io/alpine
stage: build stage: build

View file

@ -1,93 +1,121 @@
#!/usr/bin/python
# coding: utf-8 # coding: utf-8
# (c) 2021-2022, Famedly GmbH # (c) 2021-2022, Famedly GmbH
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt) # GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
import traceback import traceback
import asyncio
#Check if all required libs can loded from ansible.module_utils.basic import AnsibleModule, missing_required_lib
# Check if all required libs can load
LIB_IMP_ERR = None LIB_IMP_ERR = None
try: try:
from nio import * from nio import AsyncClient, AsyncClientConfig, \
HAS_LIB = True Api, \
LoginResponse, LoginError, \
LogoutResponse, LogoutError, \
RoomGetStateResponse, RoomGetStateError, \
RoomBanResponse, RoomBanError, \
RoomUnbanResponse, RoomUnbanError, \
RoomKickResponse, RoomKickError, \
RoomInviteResponse, RoomInviteError, \
RoomResolveAliasResponse, RoomResolveAliasError, \
JoinedRoomsResponse, JoinedRoomsError
HAS_LIB = True
except ImportError: except ImportError:
LIB_IMP_ERR = traceback.format_exc() LIB_IMP_ERR = traceback.format_exc()
HAS_LIB = False HAS_LIB = False
class AnsibleNioModule():
class AnsibleNioModule:
def __init__(self, def __init__(self,
custom_spec={}, custom_spec=None,
bypass_checks=False, bypass_checks=False,
no_log=False, no_log=False,
mutually_exclusive=[['password', 'token']], mutually_exclusive=None,
required_together=None, required_together=None,
required_one_of=[['password', 'token', 'key']], required_one_of=None,
required_by={'password': 'user_id', 'key': 'user_id'}, required_by=None,
add_file_common_args=False, add_file_common_args=False,
supports_check_mode=True, supports_check_mode=True,
required_if=None, required_if=None,
user_logout=True): user_logout=True):
#If a user/password login is provided, should we logout when exiting? if required_by is None:
self.user_logout=user_logout required_by = {'password': 'user_id'}
#Create the Ansible module if required_one_of is None:
required_one_of = [['password', 'token']]
if mutually_exclusive is None:
mutually_exclusive = [['password', 'token']]
if custom_spec is None:
custom_spec = {}
# If a user/password login is provided, should we logout when exiting?
self.user_logout = user_logout
# Create the Ansible module
self.module = AnsibleModule( self.module = AnsibleModule(
argument_spec = AnsibleNioModule.__common_argument_spec(custom_spec), argument_spec=AnsibleNioModule.__common_argument_spec(custom_spec),
bypass_checks=bypass_checks, bypass_checks=bypass_checks,
no_log=no_log, no_log=no_log,
mutually_exclusive=mutually_exclusive, mutually_exclusive=mutually_exclusive,
required_together=required_together, required_together=required_together,
required_one_of=required_one_of, required_one_of=required_one_of,
add_file_common_args=add_file_common_args, add_file_common_args=add_file_common_args,
supports_check_mode = supports_check_mode, supports_check_mode=supports_check_mode,
required_if=required_if, required_if=required_if,
required_by=required_by required_by=required_by
) )
#Make some values from the module easly accessible # Make some values from the module easly accessible
self.check_mode = self.module.check_mode self.check_mode = self.module.check_mode
self.params = self.module.params self.params = self.module.params
#Fail when matix-nio is not installed # Fail when matix-nio is not installed
#WARNING: We don't perform a version check! # WARNING: We don't perform a version check!
if not HAS_LIB: if not HAS_LIB:
self.module.fail_json(msg=missing_required_lib("matrix-nio")) self.module.fail_json(msg=missing_required_lib("matrix-nio"))
async def matrix_login(self): async def matrix_login(self):
#Login with token or supplied user account # Login with token or supplied user account
if self.module.params['token'] is None: if self.module.params['token'] is None:
self.client = AsyncClient(self.module.params['hs_url'], self.module.params['user_id']) self.client = AsyncClient(self.module.params['hs_url'], self.module.params['user_id'])
login_response = await self.client.login(self.module.params['password']) login_response = await self.client.login(password=self.module.params['password'])
if isinstance(login_response, LoginResponse):
self.access_token = login_response.access_token
self.device_id = login_response.device_id
else:
result['msg'] = login_response.message
result['http_status_code'] = login_response.status_code
module.fail_json(**result)
else: else:
self.client = AsyncClient(self.module.params['hs_url']) self.client = AsyncClient(self.module.params['hs_url'])
self.client.access_token = self.module.params['token'] login_response = await self.client.login(token=self.module.params['token'])
if isinstance(login_response, LoginResponse):
self.access_token = login_response.access_token
self.device_id = login_response.device_id
else:
result = {
'msg': login_response.message,
'http_status_code': login_response.status_code
}
self.module.fail_json(**result)
async def matrix_logout(self): async def matrix_logout(self):
if self.client.logged_in: if self.client.logged_in:
await self.client.logout() request = await self.client.logout()
if isinstance(request, LogoutError):
result = {'msg': request.message}
self.module.fail_json(**result)
async def exit_json(self, **result): async def exit_json(self, **result):
if self.module.params['token'] is None and self.user_logout == True: if self.module.params['token'] is None and self.user_logout is True:
await self.matrix_logout() await self.matrix_logout()
await self.client.close() await self.client.close()
self.module.exit_json(**result) self.module.exit_json(**result)
async def fail_json(self, **result): async def fail_json(self, **result):
if self.module.params['token'] is None and self.user_logout == True: if self.module.params['token'] is None and self.user_logout is True:
await self.matrix_logout() await self.matrix_logout()
await self.client.close() await self.client.close()
self.module.fail_json(**result) self.module.fail_json(**result)
@ -101,4 +129,3 @@ class AnsibleNioModule():
token=dict(type='str', required=False, no_log=True) token=dict(type='str', required=False, no_log=True)
) )
return {**argument_spec, **custom_spec} return {**argument_spec, **custom_spec}

View file

@ -1,4 +1,3 @@
#!/usr/bin/python3
# coding: utf-8 # coding: utf-8
# (c) 2021, Famedly GmbH # (c) 2021, Famedly GmbH
@ -8,9 +7,19 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
from typing import Union, Type import traceback
import requests
import urllib.parse import urllib.parse
from types import SimpleNamespace
# Check if all required libs can load
LIB_IMP_ERR = None
try:
import requests
HAS_REQUESTS = True
except ImportError:
REQUESTS_IMPORT_ERROR = traceback.format_exc()
HAS_REQUESTS = False
requests = SimpleNamespace(Response=None)
class AdminApi: class AdminApi:
@ -31,39 +40,39 @@ class AdminApi:
# Make API request # Make API request
def get(self, path: str) -> requests.Response: def get(self, path: str) -> requests.Response:
response = requests.get(url=urllib.parse.urljoin(self.api_url, path), response = requests.get(url=urllib.parse.urljoin(self.api_url, path),
headers={"Authorization": "Bearer {}".format(self.access_token)}) headers={"Authorization": f"Bearer {self.access_token}"})
if response.status_code == 200: if response.status_code == 200:
return response return response
if response.status_code == 500: if response.status_code == 500:
raise Exceptions.MatrixException( raise Exceptions.MatrixException(
"Matrix Error\nHTTP-Code: {}\n Response: {}".format(response.status_code, response.text)) f"Matrix Error\nHTTP-Code: {response.status_code}\n Response: {response.text}")
else: else:
raise Exceptions.HTTPException( raise Exceptions.HTTPException(
"Unexpected return code\nHTTP-Code: {}\n Response: {}".format(response.status_code, response.text)) f"Unexpected return code\nHTTP-Code: {response.status_code}\n Response: {response.text}")
def post(self, path: str, **kwargs) -> requests.Response: def post(self, path: str, **kwargs) -> requests.Response:
response = requests.post(url=urllib.parse.urljoin(self.api_url, path), response = requests.post(url=urllib.parse.urljoin(self.api_url, path),
headers={"Authorization": "Bearer {}".format(self.access_token)}, **kwargs) headers={"Authorization": f"Bearer {self.access_token}"}, **kwargs)
if response.status_code == 200: if response.status_code == 200:
return response return response
if response.status_code == 500: if response.status_code == 500:
raise Exceptions.MatrixException( raise Exceptions.MatrixException(
"Matrix Error\nHTTP-Code: {}\n Response: {}".format(response.status_code, response.text)) f"Matrix Error\nHTTP-Code: {response.status_code}\n Response: {response.text}")
else: else:
raise Exceptions.HTTPException( raise Exceptions.HTTPException(
"Unexpected return code\nHTTP-Code: {}\n Response: {}".format(response.status_code, response.text)) f"Unexpected return code\nHTTP-Code: {response.status_code}\n Response: {response.text}")
def delete(self, path: str) -> requests.Response: def delete(self, path: str) -> requests.Response:
response = requests.delete(url=urllib.parse.urljoin(self.api_url, path), response = requests.delete(url=urllib.parse.urljoin(self.api_url, path),
headers={"Authorization": "Bearer {}".format(self.access_token)}) headers={"Authorization": f"Bearer {self.access_token}"})
if response.status_code == 200: if response.status_code == 200:
return response return response
if response.status_code == 500: if response.status_code == 500:
raise Exceptions.MatrixException( raise Exceptions.MatrixException(
"Matrix Error\nHTTP-Code: {}\n Response: {}".format(response.status_code, response.text)) f"Matrix Error\nHTTP-Code: {response.status_code}\n Response: {response.text}")
else: else:
raise Exceptions.HTTPException( raise Exceptions.HTTPException(
"Unexpected return code\nHTTP-Code: {}\n Response: {}".format(response.status_code, response.text)) f"Unexpected return code\nHTTP-Code: {response.status_code}\n Response: {response.text}")
@staticmethod @staticmethod
def url_encode(string: str) -> str: def url_encode(string: str) -> str:
@ -83,8 +92,8 @@ class AdminApi:
def set(self, user_id: str, messages_per_second: int = 0, burst_count: int = 0) -> dict: def set(self, user_id: str, messages_per_second: int = 0, burst_count: int = 0) -> dict:
user_id = AdminApi.url_encode(user_id) user_id = AdminApi.url_encode(user_id)
return self.__parent.post( return self.__parent.post(
self.API_PATH.format(user_id=user_id), json={"messages_per_second": messages_per_second, self.API_PATH.format(user_id=user_id), json={"messages_per_second": messages_per_second,
"burst_count": burst_count}).json() "burst_count": burst_count}).json()
def delete(self, user_id: str) -> dict: def delete(self, user_id: str) -> dict:
user_id = AdminApi.url_encode(user_id) user_id = AdminApi.url_encode(user_id)

View file

@ -6,6 +6,7 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt) # GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
ANSIBLE_METADATA = { ANSIBLE_METADATA = {
@ -26,14 +27,22 @@ options:
description: description:
- URL of the homeserver, where the CS-API is reachable - URL of the homeserver, where the CS-API is reachable
required: true required: true
type: str
user_id: user_id:
description: description:
- The user id of the user - The user id of the user
required: true required: false
type: str
password: password:
description: description:
- The password to log in with - The password to log in with
required: true required: false
type: str
token:
description:
- Authentication token for the API call
required: false
type: str
requirements: requirements:
- matrix-nio (Python library) - matrix-nio (Python library)
''' '''
@ -56,10 +65,10 @@ device_id:
returned: When login was successful returned: When login was successful
type: str type: str
''' '''
import traceback
import asyncio import asyncio
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import * from ansible_collections.famedly.matrix.plugins.module_utils.matrix import AnsibleNioModule
async def run_module(): async def run_module():
result = dict( result = dict(

View file

@ -6,6 +6,7 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt) # GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
ANSIBLE_METADATA = { ANSIBLE_METADATA = {
@ -26,10 +27,22 @@ options:
description: description:
- URL of the homeserver, where the CS-API is reachable - URL of the homeserver, where the CS-API is reachable
required: true required: true
type: str
user_id:
description:
- The user id of the user
required: false
type: str
password:
description:
- The password to log in with
required: false
type: str
token: token:
description: description:
- Authentication token for the API call - Authentication token for the API call
required: true required: false
type: str
requirements: requirements:
- matrix-nio (Python library) - matrix-nio (Python library)
''' '''
@ -43,10 +56,11 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
''' '''
import traceback
import asyncio import asyncio
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import * from ansible_collections.famedly.matrix.plugins.module_utils.matrix import AnsibleNioModule
async def run_module(): async def run_module():
result = dict( result = dict(
changed=False, changed=False,
@ -61,5 +75,6 @@ async def run_module():
def main(): def main():
asyncio.run(run_module()) asyncio.run(run_module())
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -6,13 +6,10 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt) # GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
ANSIBLE_METADATA = { ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'}
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
@ -20,32 +17,53 @@ author: "Johanna Dorothea Reichmann (@transcaffeine)"
module: matrix_member module: matrix_member
short_description: Manage matrix room membership short_description: Manage matrix room membership
description: description:
- Manage the membership status of a given set of matrix users. Invitations (`state=member`), kicks and bans can be issued. With the `exclusive=True` flag, all other members in the room can be auto-kicked. - Manage the membership status of a given set of matrix users. Invitations (`state=member`),
kicks and bans can be issued. With the `exclusive=True` flag, all other members in the room can be auto-kicked.
options: options:
room_id: hs_url:
description: description:
- ID of the room to manage - URL of the homeserver, where the CS-API is reachable
user_ids:
description:
- List of matrix IDs to set their state
required: true required: true
state: type: str
description:
- In which state all listed members should be: member|kicked|banned
required: true
exclusive:
description:
- If state=member, the module ensure only the specified user_ids are in the room by kicking every other user present in the room.
required: false
token:
description:
- Authentication token for the API call. If provided, user_id and password are not required
user_id: user_id:
description: description:
- The user id of the user - The user id of the user
required: false
type: str
password: password:
description: description:
- The password to log in with - The password to log in with
required: false
type: str
token:
description:
- Authentication token for the API call
required: false
type: str
room_id:
description:
- ID of the room to edit
required: true
type: str
user_ids:
description:
- List of matrix IDs to set their state
type: list
elements: str
required: true
state:
description:
- In which state all listed members should be member|kicked|banned
choices: ['member', 'kicked', 'banned']
type: str
required: true
exclusive:
description:
- If state=member, the module ensure only the specified user_ids are in the room by
kicking every other user present in the room.
default: false
type: bool
required: false
requirements: requirements:
- matrix-nio (Python library) - matrix-nio (Python library)
''' '''
@ -58,83 +76,97 @@ EXAMPLES = '''
room_id: "{{ matrix_room_id }}" room_id: "{{ matrix_room_id }}"
state: member state: member
user_ids: user_ids:
- @user1:matrix.org - "@user1:matrix.org"
- @user2:homeserver.tld - "@user2:homeserver.tld"
''' '''
RETURN = ''' RETURN = '''
members: members:
description: Dictionary of all members in the given room who are either invited or joined description: Dictionary of all members in the given room who are either invited or joined
returned: When auth_token is valid returned: When auth_token is valid
type: dict[str] type: dict
''' '''
import traceback
import asyncio import asyncio
import traceback
from ansible.module_utils.basic import missing_required_lib
LIB_IMP_ERR = None
try:
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import AnsibleNioModule
from nio import RoomGetStateError, \
RoomBanError, RoomUnbanError, \
RoomKickError, RoomInviteError
HAS_LIB = True
except ImportError:
LIB_IMP_ERR = traceback.format_exc()
HAS_LIB = False
class NioOperationError(Exception):
def __init__(self, *args):
super().__init__(*args)
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import *
async def get_room_members(client, room_id, res): async def get_room_members(client, room_id, res):
member_resp = await client.room_get_state(room_id) member_resp = await client.room_get_state(room_id)
if isinstance(member_resp, RoomGetStateError): if isinstance(member_resp, RoomGetStateError):
res['msg'] = "Could not get room state for roomId={0}".format(room_id) res['msg'] = f"Could not get room state for roomId={room_id}"
raise Exception() raise NioOperationError(res['msg'])
else: else:
return dict(list(map(lambda m: (m['state_key'],m['content']['membership']), filter(lambda e: e['type'] == 'm.room.member' and return dict(list(map(lambda m: (m['state_key'], m['content']['membership']), filter(
e['content']['membership'] in ['invite', 'join', 'leave', 'ban'], member_resp.events)))) lambda e: e['type'] == 'm.room.member' and e['content']['membership'] in ['invite', 'join', 'leave', 'ban'],
member_resp.events))))
async def ban_from_room(client, room_id, user_id, res): async def ban_from_room(client, room_id, user_id, res):
ban_resp = await client.room_ban(room_id, user_id) ban_resp = await client.room_ban(room_id, user_id)
if isinstance(ban_resp, RoomBanError): if isinstance(ban_resp, RoomBanError):
res['msg'] = "Could not ban user={0} from roomId={1}".format(user_id, room_id) res['msg'] = f"Could not ban user={user_id} from roomId={room_id}"
raise Exception() raise NioOperationError(res['msg'])
res['changed'] = True res['changed'] = True
res['banned'].append(user_id) res['banned'].append(user_id)
async def unban_from_room(client, room_id, user_id, res): async def unban_from_room(client, room_id, user_id, res):
ban_resp = await client.room_unban(room_id, user_id) ban_resp = await client.room_unban(room_id, user_id)
if isinstance(ban_resp, RoomUnbanError): if isinstance(ban_resp, RoomUnbanError):
res['msg'] = "Could not unban user={0} from roomId={1}".format(user_id, room_id) res['msg'] = f"Could not unban user={user_id} from roomId={room_id}"
raise Exception() raise NioOperationError(res['msg'])
res['changed'] = True res['changed'] = True
res['unbanned'].append(user_id) res['unbanned'].append(user_id)
async def kick_from_room(client, room_id, user_id, res): async def kick_from_room(client, room_id, user_id, res):
kick_resp = await client.room_kick(room_id, user_id) kick_resp = await client.room_kick(room_id, user_id)
if isinstance(kick_resp, RoomKickError): if isinstance(kick_resp, RoomKickError):
res['msg'] = "Could not kick user={0} from roomId={1}".format(user_id, room_id) res['msg'] = f"Could not kick user={user_id} from roomId={room_id}"
raise Exception() raise NioOperationError(res['msg'])
res['changed'] = True res['changed'] = True
res['kicked'].append(user_id) res['kicked'].append(user_id)
async def invite_to_room(client, room_id, user_id, res): async def invite_to_room(client, room_id, user_id, res):
invite_resp = await client.room_invite(room_id, user_id) invite_resp = await client.room_invite(room_id, user_id)
if isinstance(invite_resp, RoomInviteError): if isinstance(invite_resp, RoomInviteError):
res['msg'] = "Could not invite user={0} to roomId={1}".format(user_id, room_id) res['msg'] = f"Could not invite user={user_id} to roomId={room_id}"
raise Exception() raise NioOperationError(res['msg'])
res['changed'] = True res['changed'] = True
res['invited'].append(user_id) res['invited'].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),
)
result = dict( result = dict(changed=False, banned=[], unbanned=[], kicked=[], invited=[], members=[], msg="", )
changed=False,
banned=[],
unbanned=[],
kicked=[],
invited=[],
members=[],
msg="",
)
module = AnsibleNioModule(module_args) module = AnsibleNioModule(module_args)
if not HAS_LIB:
await module.fail_json(msg=missing_required_lib("matrix-nio"))
await module.matrix_login() await module.matrix_login()
if module.check_mode: if module.check_mode:
@ -151,10 +183,13 @@ async def run_module():
# Create client object # Create client object
client = module.client client = module.client
# Query all room members (invited users count as member, as they _can_ be in the room) try:
room_members = await get_room_members(client, room_id, result) # Query all room members (invited users count as member, as they _can_ be in the room)
present_members = {m: s for m, s in room_members.items() if s in ['join', 'invite']}.keys() room_members = await get_room_members(client, room_id, result)
banned_members = {m: s for m, s in room_members.items() if s == 'ban'}.keys() present_members = {m: s for m, s in room_members.items() if s in ['join', 'invite']}.keys()
banned_members = {m: s for m, s in room_members.items() if s == 'ban'}.keys()
except NioOperationError:
await module.fail_json(**result)
if not module.params['exclusive']: if not module.params['exclusive']:
# Handle non-exclusive invite|kick|ban # Handle non-exclusive invite|kick|ban
@ -170,7 +205,7 @@ async def run_module():
await unban_from_room(client, room_id, user_id, result) await unban_from_room(client, room_id, user_id, result)
elif action == 'banned' and user_id not in banned_members: elif action == 'banned' and user_id not in banned_members:
await ban_from_room(client, room_id, user_id, result) await ban_from_room(client, room_id, user_id, result)
except: except NioOperationError:
await module.fail_json(**result) 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
@ -182,18 +217,19 @@ async def run_module():
await invite_to_room(client, room_id, user_id, result) 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: except NioOperationError:
await module.fail_json(**result) await module.fail_json(**result)
# Get all current members from the room # Get all current members from the room
try: try:
room_members_after = await get_room_members(client, room_id, result) room_members_after = await get_room_members(client, room_id, result)
result['members'] = {m: s for m, s in room_members_after.items() if s in ['join', 'invite']}.keys() result['members'] = {m: s for m, s in room_members_after.items() if s in ['join', 'invite']}.keys()
except: except NioOperationError:
pass pass
await module.exit_json(**result) await module.exit_json(**result)
def main(): def main():
asyncio.run(run_module()) asyncio.run(run_module())

View file

@ -6,6 +6,7 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt) # GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
ANSIBLE_METADATA = { ANSIBLE_METADATA = {
@ -21,33 +22,43 @@ module: matrix_notification
short_description: Send notifications to matrix short_description: Send notifications to matrix
description: description:
- This module sends html formatted notifications to matrix rooms. - This module sends html formatted notifications to matrix rooms.
version_added: "2.8" version_added: "2.8.0"
options: options:
msg_plain:
description:
- Plain text form of the message to send to matrix, usually markdown
required: true
msg_html:
description:
- HTML form of the message to send to matrix
required: true
room_id:
description:
- ID of the room to send the notification to
required: true
hs_url: hs_url:
description: description:
- URL of the homeserver, where the CS-API is reachable - URL of the homeserver, where the CS-API is reachable
required: true required: true
token: type: str
description:
- Authentication token for the API call. If provided, user_id and password are not required
user_id: user_id:
description: description:
- The user id of the user - The user id of the user
required: false
type: str
password: password:
description: description:
- The password to log in with - The password to log in with
required: false
type: str
token:
description:
- Authentication token for the API call
required: false
type: str
msg_plain:
description:
- Plain text form of the message to send to matrix, usually markdown
required: true
type: str
msg_html:
description:
- HTML form of the message to send to matrix
required: true
type: str
room_id:
description:
- ID of the room to send the notification to
required: true
type: str
requirements: requirements:
- matrix-nio (Python library) - matrix-nio (Python library)
''' '''
@ -73,16 +84,27 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
''' '''
import traceback
import asyncio import asyncio
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import * import traceback
from ansible.module_utils.basic import missing_required_lib
LIB_IMP_ERR = None
try:
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import AnsibleNioModule
from nio import RoomSendResponse, RoomSendError
HAS_LIB = True
except ImportError:
LIB_IMP_ERR = traceback.format_exc()
HAS_LIB = False
async def run_module(): async def run_module():
module_args = dict( module_args = dict(
msg_plain=dict(type='str', required=True), msg_plain=dict(type='str', required=True),
msg_html=dict(type='str', required=True), msg_html=dict(type='str', required=True),
room_id=dict(type='str', required=True), room_id=dict(type='str', required=True),
) )
result = dict( result = dict(
changed=False, changed=False,
@ -90,6 +112,8 @@ async def run_module():
) )
module = AnsibleNioModule(module_args) module = AnsibleNioModule(module_args)
if not HAS_LIB:
await module.fail_json(msg=missing_required_lib("matrix-nio"))
if module.check_mode: if module.check_mode:
return result return result
@ -98,7 +122,7 @@ async def run_module():
client = module.client client = module.client
# send message # send message
await client.room_send( response = await client.room_send(
room_id=module.params['room_id'], room_id=module.params['room_id'],
message_type="m.room.message", message_type="m.room.message",
content={ content={
@ -108,9 +132,12 @@ async def run_module():
"formatted_body": module.params['msg_html'], "formatted_body": module.params['msg_html'],
} }
) )
if isinstance(response, RoomSendError):
await module.fail_json(**result)
await module.exit_json(**result) await module.exit_json(**result)
def main(): def main():
asyncio.run(run_module()) asyncio.run(run_module())

View file

@ -6,6 +6,7 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt) # GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
ANSIBLE_METADATA = { ANSIBLE_METADATA = {
@ -20,26 +21,37 @@ author: "Jan Christian Grünhage (@jcgruenhage)"
module: matrix_room module: matrix_room
short_description: Join/Create matrix room short_description: Join/Create matrix room
description: description:
- This module takes a room alias and makes sure that the user identified by the access token is in such a room. If that room does not exist, it is created, if it does exist but the user is not in it, it tries to join. If the alias is taken and the user can't join the room, the module will fail. Remote aliases are not supported for creating, but work for joining. - This module takes a room alias and makes sure that the user identified by the access token is in such a room.
If that room does not exist, it is created, if it does exist but the user is not in it, it tries to join.
If the alias is taken and the user can't join the room, the module will fail.
Remote aliases are not supported for creating, but work for joining.
options: options:
alias:
description:
- Alias of the room to join/create
required: true
hs_url: hs_url:
description: description:
- URL of the homeserver, where the CS-API is reachable - URL of the homeserver, where the CS-API is reachable
required: true required: true
token: type: str
description:
- Authentication token for the API call. If provided, user_id and password are not required
user_id: user_id:
description: description:
- The user id of the user - The user id of the user
required: false
type: str
password: password:
description: description:
- The password to log in with - The password to log in with
equirements: required: false
type: str
token:
description:
- Authentication token for the API call
required: false
type: str
alias:
description:
- Alias of the room to join/create
required: true
type: str
requirements:
- matrix-nio (Python library) - matrix-nio (Python library)
''' '''
@ -55,12 +67,28 @@ RETURN = '''
room_id: room_id:
description: ID of the room description: ID of the room
type: str type: str
sample: !asdfbuiarbk213e479asf:server.tld returned: success
sample: "!asdfbuiarbk213e479asf:server.tld"
''' '''
import traceback
import asyncio import asyncio
import re import re
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import * import traceback
from ansible.module_utils.basic import missing_required_lib
LIB_IMP_ERR = None
try:
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import AnsibleNioModule
from nio import RoomCreateResponse, RoomCreateError, \
JoinedRoomsResponse, JoinedRoomsError, \
JoinResponse, JoinError, \
RoomResolveAliasResponse, RoomResolveAliasError
HAS_LIB = True
except ImportError:
LIB_IMP_ERR = traceback.format_exc()
HAS_LIB = False
async def run_module(): async def run_module():
module_args = dict( module_args = dict(
@ -73,6 +101,9 @@ async def run_module():
) )
module = AnsibleNioModule(module_args) module = AnsibleNioModule(module_args)
if not HAS_LIB:
await module.fail_json(msg=missing_required_lib("matrix-nio"))
await module.matrix_login() await module.matrix_login()
client = module.client client = module.client
@ -87,7 +118,7 @@ async def run_module():
rooms_resp = await client.joined_rooms() rooms_resp = await client.joined_rooms()
if isinstance(rooms_resp, JoinedRoomsError): if isinstance(rooms_resp, JoinedRoomsError):
failed = True failed = True
result = {"msg":"Couldn't get joined rooms."} result = {"msg": "Couldn't get joined rooms."}
elif room_id_resp.room_id in rooms_resp.rooms: elif room_id_resp.room_id in rooms_resp.rooms:
result = {"room_id": room_id_resp.room_id, "changed": False} result = {"room_id": room_id_resp.room_id, "changed": False}
else: else:
@ -99,7 +130,7 @@ async def run_module():
result = {"room_id": join_resp.room_id, "changed": True} result = {"room_id": join_resp.room_id, "changed": True}
else: else:
failed = True failed = True
result = {"msg": "Room exists, but couldn't join: {1}".format(join_resp)} result = {"msg": f"Room exists, but couldn't join: {join_resp}"}
else: else:
# Get local part of alias # Get local part of alias
local_part_regex = re.search("#([^:]*):(.*)", module.params['alias']) local_part_regex = re.search("#([^:]*):(.*)", module.params['alias'])
@ -113,13 +144,14 @@ async def run_module():
result = {"room_id": create_room_resp.room_id, "changed": True} result = {"room_id": create_room_resp.room_id, "changed": True}
else: else:
failed = True failed = True
result = {"msg": "Room does not exist but couldn't be created either: {0}".format(create_room_resp)} result = {"msg": f"Room does not exist but couldn't be created either: {create_room_resp}"}
if failed: if failed:
await module.fail_json(**result) await module.fail_json(**result)
else: else:
await module.exit_json(**result) await module.exit_json(**result)
def main(): def main():
asyncio.run(run_module()) asyncio.run(run_module())

View file

@ -1,18 +1,65 @@
#!/bin/python3 #!/usr/bin/python
# Copyright: (c) 2018, Emmanouil Kampitakis <info@kampitakis.de> # Copyright: (c) 2018
# Apache 2.0 # Apache 2.0
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
author: "Emmanouil Kampitakis (@madonius)"
module: matrix_signing_key
short_description: Create a signing key file if not exists
description:
- Create a signing key file if not exists
options:
path:
description:
- Path to the signing key file
required: true
type: str
requirements:
- signedjson (Python library)
'''
EXAMPLES = '''
- name: Create signing key file
matrix_signing_key:
changed: "/path/to/file"
'''
RETURN = '''
'''
import os import os
from ansible.module_utils.basic import AnsibleModule import traceback
from signedjson import key
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
# Check if all required libs can load
LIB_IMP_ERR = None
try:
from signedjson import key
HAS_LIB = True
except ImportError:
LIB_IMP_ERR = traceback.format_exc()
HAS_LIB = False
def write_signing_key(path): def write_signing_key(path):
with open(path,'w') as file: with open(path, 'w') as file:
key.write_signing_keys( key.write_signing_keys(
file, file,
[key.generate_signing_key('first')] [key.generate_signing_key('first')]
) )
def run_module(): def run_module():
module_args = dict( module_args = dict(
path=dict(type='str', required=True), path=dict(type='str', required=True),
@ -29,6 +76,9 @@ def run_module():
supports_check_mode=True supports_check_mode=True
) )
if not HAS_LIB:
module.fail_json(msg=missing_required_lib("signedjson"))
signing_key_path = module.params['path'] signing_key_path = module.params['path']
signing_key_exists = os.path.isfile(signing_key_path) signing_key_exists = os.path.isfile(signing_key_path)
@ -37,12 +87,17 @@ def run_module():
result['changed'] = True result['changed'] = True
if module.check_mode: if module.check_mode:
module.exit_json(**result) module.exit_json(**result)
write_signing_key(signing_key_path) try:
write_signing_key(signing_key_path)
except OSError as e:
module.fail_json(msg=str(e))
module.exit_json(**result) module.exit_json(**result)
def main(): def main():
run_module() run_module()
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -6,6 +6,7 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt) # GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
ANSIBLE_METADATA = { ANSIBLE_METADATA = {
@ -22,36 +23,47 @@ short_description: Set matrix room state
description: description:
- This module sets matrix room state idempotently - This module sets matrix room state idempotently
options: options:
event_type:
description:
- Event type of the state to be set
required: true
state_key:
description:
- State key for the state event to be set
required: true
content:
description:
- The content to set the state to
required: true
room_id:
description:
- ID of the room to set the state for
required: true
hs_url: hs_url:
description: description:
- URL of the homeserver, where the CS-API is reachable - URL of the homeserver, where the CS-API is reachable
required: true required: true
token: type: str
description:
- Authentication token for the API call. If provided, user_id and password are not required
user_id: user_id:
description: description:
- The user id of the user - The user id of the user
required: false
type: str
password: password:
description: description:
- The password to log in with - The password to log in with
equirements: required: false
type: str
token:
description:
- Authentication token for the API call
required: false
type: str
event_type:
description:
- Event type of the state to be set
required: true
type: str
state_key:
description:
- State key for the state event to be set
required: true
type: str
content:
description:
- The content to set the state to
required: true
type: dict
room_id:
description:
- ID of the room to set the state for
required: true
type: str
requirements:
- matrix-client (Python library) - matrix-client (Python library)
''' '''
@ -79,14 +91,27 @@ event_id:
type: str type: str
sample: $Het2Dv7EEDFNJNgY-ehLSUrdqMo8JOxZDCMnuQPSNfo sample: $Het2Dv7EEDFNJNgY-ehLSUrdqMo8JOxZDCMnuQPSNfo
''' '''
import traceback
import asyncio import asyncio
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import * import traceback
from ansible.module_utils.basic import missing_required_lib
LIB_IMP_ERR = None
try:
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import AnsibleNioModule
from nio import JoinedRoomsResponse, JoinedRoomsError, \
RoomGetStateEventResponse, RoomPutStateResponse
HAS_LIB = True
except ImportError:
LIB_IMP_ERR = traceback.format_exc()
HAS_LIB = False
async def run_module(): async def run_module():
module_args = dict( module_args = dict(
event_type=dict(type='str', required=True), event_type=dict(type='str', required=True),
state_key=dict(type='str', required=True), state_key=dict(type='str', required=True, no_log=False),
content=dict(type='dict', required=True), content=dict(type='dict', required=True),
room_id=dict(type='str', required=True) room_id=dict(type='str', required=True)
) )
@ -97,6 +122,8 @@ async def run_module():
) )
module = AnsibleNioModule(module_args) module = AnsibleNioModule(module_args)
if not HAS_LIB:
await module.fail_json(msg=missing_required_lib("matrix-nio"))
await module.matrix_login() await module.matrix_login()
client = module.client client = module.client
@ -110,7 +137,7 @@ async def run_module():
rooms_resp = await client.joined_rooms() rooms_resp = await client.joined_rooms()
if isinstance(rooms_resp, JoinedRoomsError): if isinstance(rooms_resp, JoinedRoomsError):
failed = True failed = True
result = {"msg":"Couldn't get joined rooms."} result = {"msg": "Couldn't get joined rooms."}
elif module.params['room_id'] not in rooms_resp.rooms: elif module.params['room_id'] not in rooms_resp.rooms:
failed = True failed = True
result = {"msg": "Not in the room you're trying to set state for."} result = {"msg": "Not in the room you're trying to set state for."}
@ -138,13 +165,14 @@ async def run_module():
# Else, fail # Else, fail
else: else:
failed = True failed = True
result = {"msg": "Couldn't set state: {error}".format(error=send_resp)} result = {"msg": f"Couldn't set state: {send_resp}"}
if failed: if failed:
await module.fail_json(**result) await module.fail_json(**result)
else: else:
await module.exit_json(**result) await module.exit_json(**result)
def main(): def main():
asyncio.run(run_module()) asyncio.run(run_module())

View file

@ -7,6 +7,7 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt) # GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
ANSIBLE_METADATA = { ANSIBLE_METADATA = {
@ -17,7 +18,7 @@ ANSIBLE_METADATA = {
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
author: "Jan Christian Grünhage" author: "Jan Christian Grünhage (@jcgruenhage)"
module: matrix_token_login module: matrix_token_login
short_description: Use com.famedly.token based logins to obtain an access token short_description: Use com.famedly.token based logins to obtain an access token
description: description:
@ -27,17 +28,30 @@ options:
description: description:
- URL of the homeserver, where the CS-API is reachable - URL of the homeserver, where the CS-API is reachable
required: true required: true
type: str
user_id: user_id:
description: description:
- The user id of the user - The user id of the user
required: true required: false
key: type: str
password:
description: description:
- The key to sign the log in token with - The password to log in with
required: true required: false
type: str
token:
description:
- Authentication token for the API call
required: false
type: str
admin: admin:
description: description:
- Whether to set the user as admin during login - Whether to set the user as admin during login
type: bool
key:
description: Login key to use
type: str
required: true
requirements: requirements:
- matrix-nio (Python library) - matrix-nio (Python library)
- jwcrypto (Python library) - jwcrypto (Python library)
@ -62,18 +76,38 @@ device_id:
returned: When login was successful returned: When login was successful
type: str type: str
''' '''
import traceback
import asyncio import asyncio
import json import json
import base64 import base64
from jwcrypto import jwt, jwk
import time import time
import traceback
from ansible.module_utils.basic import missing_required_lib
# Check if all required libs can load
JWCRYPTO_IMP_ERR = None
try:
from jwcrypto import jwt, jwk
HAS_JWCRYPTO = True
except ImportError:
JWCRYPTO_IMP_ERR = traceback.format_exc()
HAS_JWCRYPTO = False
NIO_IMP_ERR = None
try:
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import AnsibleNioModule
from nio import AsyncClient, Api
HAS_NIO = True
except ImportError:
NIO_IMP_ERR = traceback.format_exc()
HAS_NIO = False
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import *
async def run_module(): async def run_module():
module_args = dict( module_args = dict(
key=dict(type='str', required=True), key=dict(type='str', required=True, no_log=True),
admin=dict(type='bool', required=False), admin=dict(type='bool', required=False),
) )
@ -83,6 +117,10 @@ async def run_module():
) )
module = AnsibleNioModule(module_args, user_logout=False) module = AnsibleNioModule(module_args, user_logout=False)
if not HAS_JWCRYPTO:
await module.fail_json(msg=missing_required_lib("jwcrypto"))
if not HAS_NIO:
await module.fail_json(msg=missing_required_lib("matrix-nio"))
if module.check_mode: if module.check_mode:
return result return result
@ -112,9 +150,9 @@ async def run_module():
} }
key = jwk.JWK(**key) key = jwk.JWK(**key)
claims = { claims = {
"iss": "Matrix UIA Login Ansible Module", "iss": "Matrix UIA Login Ansible Module",
"sub": client.user, "sub": client.user,
"exp": int(time.time()) + 60 * 30, "exp": int(time.time()) + 60 * 30,
} }
if admin is not None: if admin is not None:
@ -126,12 +164,12 @@ async def run_module():
token.make_signed_token(key) token.make_signed_token(key)
auth = { auth = {
"type": "com.famedly.login.token", "type": "com.famedly.login.token",
"identifier" : { "identifier": {
"type": "m.id.user", "type": "m.id.user",
"user": client.user "user": client.user
}, },
"token": token.serialize() "token": token.serialize()
} }
payload = json.dumps(auth) payload = json.dumps(auth)

View file

@ -6,6 +6,7 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt) # GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
ANSIBLE_METADATA = { ANSIBLE_METADATA = {
@ -26,14 +27,22 @@ options:
description: description:
- URL of the homeserver, where the CS-API is reachable - URL of the homeserver, where the CS-API is reachable
required: true required: true
type: str
user_id: user_id:
description: description:
- The user id of the user - The user id of the user
required: true required: false
type: str
password: password:
description: description:
- The password to log in with - The password to log in with
required: true required: false
type: str
token:
description:
- Authentication token for the API call
required: false
type: str
requirements: requirements:
- matrix-nio (Python library) - matrix-nio (Python library)
''' '''
@ -56,14 +65,24 @@ device_id:
returned: When login was successful returned: When login was successful
type: str type: str
''' '''
import traceback
import asyncio import asyncio
from functools import reduce
import json import json
import traceback
from ansible.module_utils.basic import missing_required_lib
LIB_IMP_ERR = None
try:
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import AnsibleNioModule
from nio import AsyncClient, Api
HAS_LIB = True
except ImportError:
LIB_IMP_ERR = traceback.format_exc()
HAS_LIB = False
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import *
log = [] log = []
def get_payload(data): def get_payload(data):
payload = json.dumps(data) payload = json.dumps(data)
payload_length = len(payload) payload_length = len(payload)
@ -73,55 +92,62 @@ def get_payload(data):
} }
return payload, headers return payload, headers
async def do_password_stage(client, session, method, path, password): async def do_password_stage(client, session, method, path, password):
auth = { auth = {
"type": "m.login.password", "type": "m.login.password",
"identifier" : { "identifier": {
"type": "m.id.user", "type": "m.id.user",
"user": client.user "user": client.user
}, },
"session": session, "session": session,
"password": password "password": password
} }
log.append("DEBUG: attempt stage=" + auth['type'] + " for session="+ auth['session'] + " with password="+auth['password'] + ", method=" + method) log.append(f"DEBUG: attempt stage={auth['type']} for session={auth['session']} with password={auth['password']}, method={method}")
payload, headers = get_payload({"auth" : auth })
raw_response = await client.send(method, path, payload, headers)
res = await client.parse_body(raw_response)
log.append("DEBUG: stage=" + auth['type'] + " resulted in status=" + str(raw_response.status))
return raw_response.status, res
async def do_dummy_stage(client, session, method, path, password):
auth = {
"type": "m.login.dummy",
"session": session
}
log.append("DEBUG: attempt stage=" + auth['type'] + " for session="+ auth['session'])
payload, headers = get_payload({"auth": auth}) payload, headers = get_payload({"auth": auth})
raw_response = await client.send(method, path, payload, headers) raw_response = await client.send(method, path, payload, headers)
res = await client.parse_body(raw_response) res = await client.parse_body(raw_response)
log.append("DEBUG: stage=" + auth['type'] + " resulted in status=" + str(raw_response.status)) log.append(f"DEBUG: stage={auth['type']} resulted in status={raw_response.status}")
return raw_response.status, res return raw_response.status, res
async def do_dummy_stage(client, session, method, path, password):
auth = {
"type": "m.login.dummy",
"session": session
}
log.append(f"DEBUG: attempt stage={auth['type']} for session={auth['session']}")
payload, headers = get_payload({"auth": auth})
raw_response = await client.send(method, path, payload, headers)
res = await client.parse_body(raw_response)
log.append(f"DEBUG: stage{auth['type']} resulted in status={raw_response.status}")
return raw_response.status, res
uia_stages = { uia_stages = {
"m.login.password": do_password_stage, "m.login.password": do_password_stage,
"m.login.dummy": do_dummy_stage "m.login.dummy": do_dummy_stage
} }
# Picks the best compatible flow out of an array of flows # Picks the best compatible flow out of an array of flows
def pick_flow(flows): def pick_flow(flows):
supported_stages = uia_stages.keys() supported_stages = uia_stages.keys()
# reduces each flow to a boolean telling filter if the flow consists only out of compatible stages # reduces each flow to a boolean telling filter if the flow consists only out of compatible stages
compatible_flows = [flow for flow in flows if all([stage in supported_stages for stage in flow['stages']])] compatible_flows = [flow for flow in flows if all(stage in supported_stages for stage in flow['stages'])]
# the best flow is the one with the fewest stages, key= takes a function telling min() the weight of an entry # the best flow is the one with the fewest stages, key= takes a function telling min() the weight of an entry
best = min(compatible_flows, key=(lambda flow: len(flow['stages']))) best = min(compatible_flows, key=(lambda flow: len(flow['stages'])))
return best return best
async def run_module(): async def run_module():
result = dict( result = dict(
changed=False, changed=False,
) )
module = AnsibleNioModule(user_logout=False) module = AnsibleNioModule(user_logout=False)
if not HAS_LIB:
await module.fail_json(msg=missing_required_lib("matrix-nio"))
if module.check_mode: if module.check_mode:
return result return result
@ -134,6 +160,7 @@ async def run_module():
# Collect and check login information # Collect and check login information
password = module.params['password'] password = module.params['password']
token = module.params['token']
if password is None and token is None: if password is None and token is None:
await module.fail_json(msg="A PASSWORD has to be provided") await module.fail_json(msg="A PASSWORD has to be provided")
@ -149,21 +176,21 @@ async def run_module():
raw_response = await client.send(method, path, {}) raw_response = await client.send(method, path, {})
res = await client.parse_body(raw_response) res = await client.parse_body(raw_response)
uia_session = res['session'] uia_session = res['session']
log.append("DEBUG: begin UIA for session=" + uia_session) log.append(f"DEBUG: begin UIA for session={uia_session}")
# Figure out best compatible UIA login flow # Figure out best compatible UIA login flow
log.append("INFO: available flows: " + str(res['flows'])) log.append(f"INFO: available flows: {res['flows']}")
flow_to_attempt = pick_flow(res['flows']) flow_to_attempt = pick_flow(res['flows'])
log.append("INFO: picking flow: " + (" -> ".join(flow_to_attempt['stages']))) log.append(f"INFO: picking flow: {' -> '.join(flow_to_attempt['stages'])}")
# Attempt each stage in the flow # Attempt each stage in the flow
for stage in flow_to_attempt['stages']: for stage in flow_to_attempt['stages']:
stage_status, stage_result = await uia_stages[stage](client, uia_session, method, path, password) stage_status, stage_result = await uia_stages[stage](client, uia_session, method, path, password)
if int(stage_status) == 401 and stage != (flow_to_attempt['stages'])[-1]: if int(stage_status) == 401 and stage != (flow_to_attempt['stages'])[-1]:
log.append("INFO: completed stage " + stage) log.append(f"INFO: completed stage {stage}")
completed_stages = stage_result['completed'] completed_stages = stage_result['completed']
elif int(stage_status) == 200 and stage == (flow_to_attempt['stages'])[-1]: elif int(stage_status) == 200 and stage == (flow_to_attempt['stages'])[-1]:
log.append("INFO: final stage completed " + stage) log.append(f"INFO: final stage completed {stage}")
result['token'] = stage_result['access_token'] result['token'] = stage_result['access_token']
result['device_id'] = stage_result['device_id'] result['device_id'] = stage_result['device_id']
failed = False failed = False

View file

@ -16,7 +16,7 @@ ANSIBLE_METADATA = {
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
author: "Jadyn Emma Jäger (@jadyn.dev)" author: "Jadyn Emma Jäger (@jadyndev)"
module: synapse_ratelimit module: synapse_ratelimit
short_description: Change a users rate-limits short_description: Change a users rate-limits
description: description:
@ -26,31 +26,34 @@ options:
description: description:
- URL of the homeserver, where the CS-API is reachable - URL of the homeserver, where the CS-API is reachable
required: true required: true
type: str
access_token: access_token:
description: description:
- Shared secret to authenticate registration request - Shared secret to authenticate registration request
required: true required: true
type: str
user_id: user_id:
description: description:
- The fully qualified MXID of a __local__ user - The fully qualified MXID of a __local__ user
required: true required: true
type: str
action: action:
description: description:
- Which (http) operation should be executed - Which (http) operation should be executed
required: True required: false
type: str type: str
choices: 'get', 'set', 'delete' choices: ['get', 'set', 'delete']
default: 'get' default: 'get'
messages_per_second: messages_per_second:
description: description:
- Set the maximum messages per second (0 = disabled) - Set the maximum messages per second (0 = disabled)
required: False required: false
type: int type: int
default: 0 default: 0
burst_count: burst_count:
description: description:
- Set the maximum message burst (0 = disabled) - Set the maximum message burst (0 = disabled)
required: False required: false
type: int type: int
default: 0 default: 0
requirements: [] requirements: []
@ -69,13 +72,21 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
ratelimit: ratelimit:
- burst_count: 5 description: if a ratelimit is set, otherwise `ratelimit` is empty
- messages_per_second: 10 type: dict
if a ratelimit is set, otherwise `ratelimit` is empty returned: success
sample:
burst_count: 5
messages_per_second: 10
''' '''
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible_collections.famedly.matrix.plugins.module_utils.synapse import * import traceback
try:
from ansible_collections.famedly.matrix.plugins.module_utils.synapse import AdminApi, Exceptions, HAS_REQUESTS
except ImportError:
REQUESTS_IMPORT_ERROR = traceback.format_exc()
def main(): def main():
@ -97,6 +108,9 @@ def main():
supports_check_mode=True supports_check_mode=True
) )
if not HAS_REQUESTS:
module.fail_json(msg=missing_required_lib("requests"))
if module.check_mode: if module.check_mode:
return result return result
@ -117,7 +131,7 @@ def main():
result['ratelimit'] = synapse.ratelimit.delete(module.params['user_id']) result['ratelimit'] = synapse.ratelimit.delete(module.params['user_id'])
result['changed'] = ratelimit != result['ratelimit'] result['changed'] = ratelimit != result['ratelimit']
module.exit_json(**result) module.exit_json(**result)
raise NotImplementedError("action {} is not implemented".format(action)) raise NotImplementedError(f"action {action} is not implemented")
except (Exceptions.HTTPException, Exceptions.MatrixException) as e: except (Exceptions.HTTPException, Exceptions.MatrixException) as e:
result['msg'] = str(e) result['msg'] = str(e)
module.fail_json(**result) module.fail_json(**result)

View file

@ -6,6 +6,7 @@
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt) # GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
ANSIBLE_METADATA = { ANSIBLE_METADATA = {
@ -26,14 +27,17 @@ options:
description: description:
- URL of the homeserver, where the CS-API is reachable - URL of the homeserver, where the CS-API is reachable
required: true required: true
type: str
user_id: user_id:
description: description:
- The user id of the user - The user id of the user
required: true required: true
type: str
password: password:
description: description:
- The password to register with - The password to register with
required: true required: true
type: str
admin: admin:
description: description:
- Whether or not the new user should be an admin - Whether or not the new user should be an admin
@ -44,6 +48,7 @@ options:
description: description:
- Shared secret to authenticate registration request - Shared secret to authenticate registration request
required: true required: true
type: str
requirements: [] requirements: []
''' '''
@ -57,21 +62,28 @@ EXAMPLES = '''
shared_secret: "long secret string" shared_secret: "long secret string"
''' '''
RETURN = '''
'''
import traceback
import asyncio import asyncio
import hmac import hmac
import hashlib import hashlib
import requests import traceback
# Check if all required libs can load
LIB_IMP_ERR = None
try:
import requests
HAS_REQUESTS = True
except ImportError:
REQUESTS_IMPORT_ERROR = traceback.format_exc()
HAS_REQUESTS = False
from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils.basic import AnsibleModule, missing_required_lib
def generate_mac(nonce, shared_secret, user, password, admin=False, user_type=None):
def generate_mac(nonce, shared_secret, user, password, admin=False, user_type=None):
mac = hmac.new( mac = hmac.new(
key=shared_secret.encode('utf8'), key=shared_secret.encode('utf8'),
digestmod=hashlib.sha1, digestmod=hashlib.sha1,
) )
mac.update(nonce.encode('utf8')) mac.update(nonce.encode('utf8'))
@ -87,6 +99,7 @@ def generate_mac(nonce, shared_secret, user, password, admin=False, user_type=No
return mac.hexdigest() return mac.hexdigest()
async def run_module(): async def run_module():
module_args = dict( module_args = dict(
hs_url=dict(type='str', required=True), hs_url=dict(type='str', required=True),
@ -105,18 +118,22 @@ async def run_module():
supports_check_mode=True supports_check_mode=True
) )
if not HAS_REQUESTS:
module.fail_json(msg=missing_required_lib("requests"))
if module.check_mode: if module.check_mode:
return result return result
failed = False failed = False
url = "{}/_synapse/admin/v1/register".format(module.params["hs_url"]) url = f"{module.params['hs_url']}/_synapse/admin/v1/register"
response = requests.get(url) response = requests.get(url)
if response.status_code != 200: if response.status_code != 200:
result["msg"] = response.json()["error"] result["msg"] = response.json()["error"]
module.exit_json(**result) module.exit_json(**result)
nonce = response.json()["nonce"] nonce = response.json()["nonce"]
mac = generate_mac(nonce, module.params["shared_secret"], module.params["user_id"], module.params["password"], module.params["admin"]) mac = generate_mac(nonce, module.params["shared_secret"], module.params["user_id"], module.params["password"],
module.params["admin"])
data = { data = {
"nonce": nonce, "nonce": nonce,

3
requirements.txt Normal file
View file

@ -0,0 +1,3 @@
ansible~=5.2.0
matrix-nio~=0.19.0
signedjson~=1.1.4

View file

@ -0,0 +1,5 @@
#!/usr/bin/env bash
cd ../../
while read -r line; do
find plugins/modules -name "*.py" | xargs -I {} -n 1 printf "{} $line\n"
done <"tests/sanity/ignore.template"

View file

@ -0,0 +1,11 @@
plugins/modules/matrix_signing_key.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/synapse_register.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_notification.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_state.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_room.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_uia_login.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/synapse_ratelimit.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_token_login.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_login.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_logout.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_member.py validate-modules:missing-gplv3-license # ignore license check

View file

@ -0,0 +1,11 @@
plugins/modules/matrix_signing_key.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/synapse_register.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_notification.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_state.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_room.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_uia_login.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/synapse_ratelimit.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_token_login.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_login.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_logout.py validate-modules:missing-gplv3-license # ignore license check
plugins/modules/matrix_member.py validate-modules:missing-gplv3-license # ignore license check

View file

@ -0,0 +1 @@
validate-modules:missing-gplv3-license # ignore license check