Merge branch 'ts3_reserved_groups' into 'master'

Implement reserved group names in Teamspeak3 service module.

See merge request allianceauth/allianceauth!1380
This commit is contained in:
Ariel Rin 2022-01-27 05:10:22 +00:00
commit d8c6035405
5 changed files with 169 additions and 31 deletions

View File

@ -1,7 +1,8 @@
from django.contrib import admin
from .models import AuthTS, Teamspeak3User, StateGroup
from django.contrib.auth.models import Group
from .models import AuthTS, Teamspeak3User, StateGroup, TSgroup
from ...admin import ServicesUserAdmin
from allianceauth.groupmanagement.models import ReservedGroupName
@admin.register(Teamspeak3User)
@ -25,6 +26,16 @@ class AuthTSgroupAdmin(admin.ModelAdmin):
fields = ('auth_group', 'ts_group')
filter_horizontal = ('ts_group',)
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'auth_group':
kwargs['queryset'] = Group.objects.exclude(name__in=ReservedGroupName.objects.values_list('name', flat=True))
return super().formfield_for_foreignkey(db_field, request, **kwargs)
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == 'ts_group':
kwargs['queryset'] = TSgroup.objects.exclude(ts_group_name__in=ReservedGroupName.objects.values_list('name', flat=True))
return super().formfield_for_manytomany(db_field, request, **kwargs)
def _ts_group(self, obj):
return [x for x in obj.ts_group.all().order_by('ts_group_id')]

View File

@ -4,6 +4,7 @@ from django.conf import settings
from .util.ts3 import TS3Server, TeamspeakError
from .models import TSgroup
from allianceauth.groupmanagement.models import ReservedGroupName
logger = logging.getLogger(__name__)
@ -156,32 +157,25 @@ class Teamspeak3Manager:
logger.info(f"Removed user id {uid} from group id {groupid} on TS3 server.")
def _sync_ts_group_db(self):
logger.debug("_sync_ts_group_db function called.")
try:
remote_groups = self._group_list()
local_groups = TSgroup.objects.all()
logger.debug("Comparing remote groups to TSgroup objects: %s" % local_groups)
for key in remote_groups:
logger.debug(f"Typecasting remote_group value at position {key} to int: {remote_groups[key]}")
remote_groups[key] = int(remote_groups[key])
managed_groups = {g:int(remote_groups[g]) for g in remote_groups if g in set(remote_groups.keys()) - set(ReservedGroupName.objects.values_list('name', flat=True))}
remove = TSgroup.objects.exclude(ts_group_id__in=managed_groups.values())
if remove:
logger.debug(f"Deleting {remove.count()} TSgroup models: not found on server, or reserved name.")
remove.delete()
add = {g:managed_groups[g] for g in managed_groups if managed_groups[g] in set(managed_groups.values()) - set(TSgroup.objects.values_list("ts_group_id", flat=True))}
if add:
logger.debug(f"Adding {len(add)} new TSgroup models.")
models = [TSgroup(ts_group_name=name, ts_group_id=add[name]) for name in add]
TSgroup.objects.bulk_create(models)
for group in local_groups:
logger.debug("Checking local group %s" % group)
if group.ts_group_id not in remote_groups.values():
logger.debug(
f"Local group id {group.ts_group_id} not found on server. Deleting model {group}")
TSgroup.objects.filter(ts_group_id=group.ts_group_id).delete()
for key in remote_groups:
g = TSgroup(ts_group_id=remote_groups[key], ts_group_name=key)
q = TSgroup.objects.filter(ts_group_id=g.ts_group_id)
if not q:
logger.debug("Local group does not exist for TS group {}. Creating TSgroup model {}".format(
remote_groups[key], g))
g.save()
except TeamspeakError as e:
logger.error("Error occured while syncing TS group db: %s" % str(e))
except:
logger.exception("An unhandled exception has occured while syncing TS groups.")
logger.error(f"Error occurred while syncing TS group db: {str(e)}")
except Exception:
logger.exception(f"An unhandled exception has occurred while syncing TS groups.")
def add_user(self, user, fmt_name):
username_clean = self.__santatize_username(fmt_name[:30])
@ -240,7 +234,7 @@ class Teamspeak3Manager:
logger.exception(f"Failed to delete user id {uid} from TS3 - received response {ret}")
return False
else:
logger.warn("User with id %s not found on TS3 server. Assuming succesful deletion." % uid)
logger.warning("User with id %s not found on TS3 server. Assuming succesful deletion." % uid)
return True
def check_user_exists(self, uid):
@ -270,7 +264,8 @@ class Teamspeak3Manager:
addgroups.append(ts_groups[ts_group_key])
for user_ts_group_key in user_ts_groups:
if user_ts_groups[user_ts_group_key] not in ts_groups.values():
remgroups.append(user_ts_groups[user_ts_group_key])
if not ReservedGroupName.objects.filter(name=user_ts_group_key).exists():
remgroups.append(user_ts_groups[user_ts_group_key])
for g in addgroups:
logger.info(f"Adding Teamspeak user {userid} into group {g}")

View File

@ -5,16 +5,18 @@ from django import urls
from django.contrib.auth.models import User, Group, Permission
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import signals
from django.contrib.admin import AdminSite
from allianceauth.tests.auth_utils import AuthUtils
from .auth_hooks import Teamspeak3Service
from .models import Teamspeak3User, AuthTS, TSgroup, StateGroup
from .tasks import Teamspeak3Tasks
from .signals import m2m_changed_authts_group, post_save_authts, post_delete_authts
from .admin import AuthTSgroupAdmin
from .manager import Teamspeak3Manager
from .util.ts3 import TeamspeakError
from allianceauth.authentication.models import State
from allianceauth.groupmanagement.models import ReservedGroupName
MODULE_PATH = 'allianceauth.services.modules.teamspeak3'
DEFAULT_AUTH_GROUP = 'Member'
@ -315,6 +317,9 @@ class Teamspeak3SignalsTestCase(TestCase):
class Teamspeak3ManagerTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.reserved = ReservedGroupName.objects.create(name='reserved', reason='tests', created_by='Bob, praise be!')
@staticmethod
def my_side_effect(*args, **kwargs):
@ -334,8 +339,135 @@ class Teamspeak3ManagerTestCase(TestCase):
manager._server = server
# create test data
user = User.objects.create_user("dummy")
user.profile.state = State.objects.filter(name="Member").first()
user = AuthUtils.create_user("dummy")
AuthUtils.assign_state(user, AuthUtils.get_member_state())
# perform test
manager.add_user(user, "Dummy User")
@mock.patch.object(Teamspeak3Manager, '_get_userid')
@mock.patch.object(Teamspeak3Manager, '_user_group_list')
@mock.patch.object(Teamspeak3Manager, '_add_user_to_group')
@mock.patch.object(Teamspeak3Manager, '_remove_user_from_group')
def test_update_groups_add(self, remove, add, groups, userid):
"""Add to one group"""
userid.return_value = 1
groups.return_value = {'test': 1}
Teamspeak3Manager().update_groups(1, {'test': 1, 'dummy': 2})
self.assertEqual(add.call_count, 1)
self.assertEqual(remove.call_count, 0)
self.assertEqual(add.call_args[0][1], 2)
@mock.patch.object(Teamspeak3Manager, '_get_userid')
@mock.patch.object(Teamspeak3Manager, '_user_group_list')
@mock.patch.object(Teamspeak3Manager, '_add_user_to_group')
@mock.patch.object(Teamspeak3Manager, '_remove_user_from_group')
def test_update_groups_remove(self, remove, add, groups, userid):
"""Remove from one group"""
userid.return_value = 1
groups.return_value = {'test': '1', 'dummy': '2'}
Teamspeak3Manager().update_groups(1, {'test': 1})
self.assertEqual(add.call_count, 0)
self.assertEqual(remove.call_count, 1)
self.assertEqual(remove.call_args[0][1], 2)
@mock.patch.object(Teamspeak3Manager, '_get_userid')
@mock.patch.object(Teamspeak3Manager, '_user_group_list')
@mock.patch.object(Teamspeak3Manager, '_add_user_to_group')
@mock.patch.object(Teamspeak3Manager, '_remove_user_from_group')
def test_update_groups_remove_reserved(self, remove, add, groups, userid):
"""Remove from one group, but do not touch reserved group"""
userid.return_value = 1
groups.return_value = {'test': 1, 'dummy': 2, self.reserved.name: 3}
Teamspeak3Manager().update_groups(1, {'test': 1})
self.assertEqual(add.call_count, 0)
self.assertEqual(remove.call_count, 1)
self.assertEqual(remove.call_args[0][1], 2)
@mock.patch.object(Teamspeak3Manager, '_group_list')
def test_sync_group_db_create(self, group_list):
"""Populate the list of all TSgroups"""
group_list.return_value = {'allowed':'1', 'also allowed':'2'}
Teamspeak3Manager()._sync_ts_group_db()
self.assertEqual(TSgroup.objects.all().count(), 2)
@mock.patch.object(Teamspeak3Manager, '_group_list')
def test_sync_group_db_delete(self, group_list):
"""Populate the list of all TSgroups, and delete one which no longer exists"""
TSgroup.objects.create(ts_group_name='deleted', ts_group_id=3)
group_list.return_value = {'allowed': '1'}
Teamspeak3Manager()._sync_ts_group_db()
self.assertEqual(TSgroup.objects.all().count(), 1)
self.assertFalse(TSgroup.objects.filter(ts_group_name='deleted').exists())
@mock.patch.object(Teamspeak3Manager, '_group_list')
def test_sync_group_db_dont_create_reserved(self, group_list):
"""Populate the list of all TSgroups, ignoring a reserved group name"""
group_list.return_value = {'allowed': '1', 'reserved': '4'}
Teamspeak3Manager()._sync_ts_group_db()
self.assertEqual(TSgroup.objects.all().count(), 1)
self.assertFalse(TSgroup.objects.filter(ts_group_name='reserved').exists())
@mock.patch.object(Teamspeak3Manager, '_group_list')
def test_sync_group_db_delete_reserved(self, group_list):
"""Populate the list of all TSgroups, deleting the TSgroup model for one which has become reserved"""
TSgroup.objects.create(ts_group_name='reserved', ts_group_id=4)
group_list.return_value = {'allowed': '1', 'reserved': '4'}
Teamspeak3Manager()._sync_ts_group_db()
self.assertEqual(TSgroup.objects.all().count(), 1)
self.assertFalse(TSgroup.objects.filter(ts_group_name='reserved').exists())
@mock.patch.object(Teamspeak3Manager, '_group_list')
def test_sync_group_db_partial_addition(self, group_list):
"""Some TSgroups already exist in database, add new ones"""
TSgroup.objects.create(ts_group_name='allowed', ts_group_id=1)
group_list.return_value = {'allowed': '1', 'also allowed': '2'}
Teamspeak3Manager()._sync_ts_group_db()
self.assertEqual(TSgroup.objects.all().count(), 2)
@mock.patch.object(Teamspeak3Manager, '_group_list')
def test_sync_group_db_partial_removal(self, group_list):
"""One TSgroup has been deleted on server, so remove its model"""
TSgroup.objects.create(ts_group_name='allowed', ts_group_id=1)
TSgroup.objects.create(ts_group_name='also allowed', ts_group_id=2)
group_list.return_value = {'allowed': '1'}
Teamspeak3Manager()._sync_ts_group_db()
self.assertEqual(TSgroup.objects.all().count(), 1)
class MockRequest:
pass
class MockSuperUser:
def has_perm(self, perm, obj=None):
return True
request = MockRequest()
request.user = MockSuperUser()
class Teamspeak3AdminTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.site = AdminSite()
cls.admin = AuthTSgroupAdmin(AuthTS, cls.site)
cls.group = Group.objects.create(name='test')
cls.ts_group = TSgroup.objects.create(ts_group_name='test')
def test_field_queryset_no_reserved_names(self):
"""Ensure all groups are listed when no reserved names"""
form = self.admin.get_form(request)
self.assertQuerysetEqual(form.base_fields['auth_group']._get_queryset(), Group.objects.all())
self.assertQuerysetEqual(form.base_fields['ts_group']._get_queryset(), TSgroup.objects.all())
def test_field_queryset_reserved_names(self):
"""Ensure reserved group names are filtered out"""
ReservedGroupName.objects.bulk_create([ReservedGroupName(name='test', reason='tests', created_by='Bob')])
form = self.admin.get_form(request)
self.assertQuerysetEqual(form.base_fields['auth_group']._get_queryset(), Group.objects.none())
self.assertQuerysetEqual(form.base_fields['ts_group']._get_queryset(), TSgroup.objects.none())

View File

@ -48,7 +48,7 @@ When using Alliance Auth to manage external services like Discord, Auth will aut
```eval_rst
.. note::
While this feature can help to avoid naming conflicts with groups on external services, the respective service component in Alliance Auth also needs to be build in such a way that it knows how to prevent these conflicts. Currently only the Discord service has this ability.
While this feature can help to avoid naming conflicts with groups on external services, the respective service component in Alliance Auth also needs to be build in such a way that it knows how to prevent these conflicts. Currently only the Discord and Teamspeak3 services have this ability.
```
## Managing groups

View File

@ -160,7 +160,7 @@ This error generally means teamspeak returned an error message that went unhandl
This most commonly happens when your teamspeak server is externally hosted. You need to add the auth server IP to the teamspeak serverquery whitelist. This varies by provider.
If you have SSH access to the server hosting it, you need to locate the teamspeak server folder and add the auth server IP on a new line in `server_query_whitelist.txt`
If you have SSH access to the server hosting it, you need to locate the teamspeak server folder and add the auth server IP on a new line in `query_ip_allowlist.txt` (named `query_ip_whitelist.txt` on older teamspeak versions).
### `520 invalid loginname or password`