From 1066e6ac9895ea2df3ea3c4db433172b697e6b2b Mon Sep 17 00:00:00 2001 From: Basraah Date: Wed, 25 Jan 2017 12:50:16 +1000 Subject: [PATCH] The Great Services Refactor (#594) * Hooks registration, discovery and retrieval module Will discover @hooks.register decorated functions inside the auth_hooks module in any installed django app. * Class to register modular service apps * Register service modules URLs * Example service module * Refactor services into modules Each service type has been split out into its own django app/module. A hook mechanism is provided to register a subclass of the ServiceHook class. The modules then overload functions defined in ServiceHook as required to provide interoperability with alliance auth. Service modules provide their own urls and views for user registration and account management and a partial template to display on the services page. Where possible, new modules should provide their own models for local data storage. * Added menu items hooks and template tags * Added menu item hook for broadcasts * Added str method to ServicesHook * Added exception handling to hook iterators * Refactor mumble migration and table name Upgrading will require `migrate mumble --fake-initial` to be run first and then `migrate mumble` to rename the table. * Refactor teamspeak3 migration and rename table Upgrading will require `migrate teamspeak3 --fake-initial` * Added module models and migrations for refactoring AuthServicesInfo * Migrate AuthServiceInfo fields to service modules models * Added helper for getting a users main character * Added new style celery instance * Changed Discord from AuthServicesInfo to DiscordUser model * Switch celery tasks to staticmethods * Changed Discourse from AuthServicesInfo to DiscourseUser model * Changed IPBoard from AuthServicesInfo to IpboardUser model * Changed Ips4 from AuthServicesInfo to Ips4User model Also added disable service task. This service still needs some love though. Was always missing a deactivate services hook (before refactoring) for reasons I'm unsure of so I'm reluctant to add it without knowing why. * Changed Market from AuthServicesInfo to MarketUser model * Changed Mumble from AuthServicesInfo to MumbleUser model Switched user foreign key to one to one relationship. Removed implicit password change on user exists. Combined regular and blue user creation. * Changed Openfire from AuthServicesInfo to OpenfireUser model * Changed SMF from AuthServicesInfo to SmfUser model Added disable task * Changed Phpbb3 from AuthServicesInfo to Phpbb3User model * Changed XenForo from AuthServicesInfo to XenforoUser model * Changed Teamspeak3 from AuthServicesInfo to Teamspeak3User model * Remove obsolete manager functions * Standardise URL format This will break some callback URLs Discord changes from /discord_callback/ to /discord/callback/ * Removed unnecessary imports * Mirror upstream decorator change * Setup for unit testing * Unit tests for discord service * Added add main character helper * Added Discourse unit tests * Added Ipboard unit tests * Added Ips4 unit tests * Fix naming of market manager, switch to use class methods * Remove unused hook functions * Added market service unit tests * Added corp ticker to add main character helper * Added mumble unit tests * Fix url name and remove namespace * Fix missing return and add missing URL * Added openfire unit tests * Added missing return * Added phpbb3 unit tests * Fix SmfManager naming inconsistency and switch to classmethods * Added smf unit tests * Remove unused functions, Added missing return * Added xenforo unit tests * Added missing return * Fixed reference to old model * Fixed error preventing groups from syncing on reset request * Added teamspeak3 unit tests * Added nose as test runner and some test settings * Added package requirements for running tests * Added unit tests for services signals and tasks * Remove unused tests file * Fix teamspeak3 service signals * Added unit tests for teamspeak3 signals Changed other unit tests setUp to inert signals * Fix password gen and hashing python3 compatibility Fixes #630 Adds unit tests to check the password functions run on both platforms. * Fix unit test to not rely on checking url params * Add Travis CI settings file * Remove default blank values from services models * Added dynamic user model admin actions for syncing service groups * Remove unused search fields * Add hook function for syncing nicknames * Added discord hook for sync nickname * Added user admin model menu actions for sync nickname hook * Remove obsolete code * Rename celery config app to avoid package name clash * Added new style celerybeat schedule configuration periodic_task decorator is depreciated * Added string representations * Added admin pages for services user models * Removed legacy code * Move link discord button to correct template * Remove blank default fields from example model * Disallow empty django setting * Fix typos * Added coverage configuration file * Add coverage and coveralls to travis config Should probably use nose's built in coverage, but this works for now. * Replace AuthServicesInfo get_or_create instances with get Reflects upstream changes to AuthServicesInfo behaviour. * Update mumble user table name * Split out mumble authenticator requirements zeroc-ice seems to cause long build times on travis-ci and isn't required for the core projects functionality or testing. --- .coveragerc | 31 + .travis.yml | 14 + alliance_auth/__init__.py | 6 +- alliance_auth/celeryapp.py | 17 + alliance_auth/hooks.py | 127 ++ alliance_auth/settings.py.example | 23 + alliance_auth/tests/__init__.py | 0 alliance_auth/tests/auth_utils.py | 81 ++ alliance_auth/tests/test_settings.py | 604 +++++++++ alliance_auth/urls.py | 95 +- authentication/admin.py | 188 +-- authentication/managers.py | 112 -- .../migrations/0013_service_modules.py | 275 ++++ authentication/models.py | 13 - docs/installation/services/mumble.md | 4 + eveonline/managers.py | 11 + .../fleetup_manager.py => fleetup/managers.py | 0 fleetup/views.py | 2 +- requirements.txt | 1 - runtests.py | 24 + services/admin.py | 12 - services/forms.py | 22 +- services/hooks.py | 134 ++ services/managers/mumble_manager.py | 121 -- services/migrations/0001_initial.py | 45 - services/models.py | 46 - services/modules/__init__.py | 0 services/modules/discord/__init__.py | 0 services/modules/discord/admin.py | 10 + services/modules/discord/apps.py | 7 + services/modules/discord/auth_hooks.py | 59 + .../discord/manager.py} | 0 .../discord/migrations/0001_initial.py | 26 + .../modules/discord/migrations/__init__.py | 0 services/modules/discord/models.py | 15 + services/modules/discord/tasks.py | 131 ++ .../registered/discord_service_ctrl.html | 27 + services/modules/discord/tests.py | 193 +++ services/modules/discord/urls.py | 17 + services/modules/discord/views.py | 71 + services/modules/discourse/__init__.py | 0 services/modules/discourse/admin.py | 10 + services/modules/discourse/apps.py | 7 + services/modules/discourse/auth_hooks.py | 57 + .../discourse/manager.py} | 7 +- .../discourse/migrations/0001_initial.py | 26 + .../modules/discourse/migrations/__init__.py | 0 services/modules/discourse/models.py | 15 + services/modules/discourse/tasks.py | 62 + .../registered/discourse_service_ctrl.html | 8 + services/modules/discourse/tests.py | 135 ++ services/modules/discourse/urls.py | 9 + services/modules/discourse/views.py | 119 ++ services/modules/example/__init__.py | 0 services/modules/example/apps.py | 7 + services/modules/example/auth_hooks.py | 44 + services/modules/example/models.py | 16 + services/modules/example/urls.py | 10 + services/modules/example/views.py | 1 + services/modules/ipboard/__init__.py | 0 services/modules/ipboard/admin.py | 10 + services/modules/ipboard/apps.py | 7 + services/modules/ipboard/auth_hooks.py | 68 + .../ipboard/manager.py} | 21 +- .../ipboard/migrations/0001_initial.py | 26 + .../modules/ipboard/migrations/__init__.py | 0 services/modules/ipboard/models.py | 16 + services/modules/ipboard/tasks.py | 73 ++ services/modules/ipboard/tests.py | 207 +++ services/modules/ipboard/urls.py | 16 + services/modules/ipboard/views.py | 110 ++ services/modules/ips4/__init__.py | 0 services/modules/ips4/admin.py | 10 + services/modules/ips4/apps.py | 7 + services/modules/ips4/auth_hooks.py | 53 + .../ips4/manager.py} | 62 +- .../modules/ips4/migrations/0001_initial.py | 27 + services/modules/ips4/migrations/__init__.py | 0 services/modules/ips4/models.py | 17 + services/modules/ips4/tasks.py | 43 + services/modules/ips4/tests.py | 164 +++ services/modules/ips4/urls.py | 16 + services/modules/ips4/views.py | 107 ++ services/modules/market/__init__.py | 0 services/modules/market/admin.py | 10 + services/modules/market/apps.py | 7 + services/modules/market/auth_hooks.py | 59 + .../market/manager.py} | 108 +- .../modules/market/migrations/0001_initial.py | 26 + .../modules/market/migrations/__init__.py | 0 services/modules/market/models.py | 16 + services/modules/market/tasks.py | 44 + services/modules/market/tests.py | 176 +++ services/modules/market/urls.py | 16 + services/modules/market/views.py | 107 ++ services/modules/mumble/__init__.py | 0 services/modules/mumble/admin.py | 11 + services/modules/mumble/apps.py | 7 + services/modules/mumble/auth_hooks.py | 68 + services/modules/mumble/manager.py | 105 ++ .../modules/mumble/migrations/0001_initial.py | 28 + .../migrations/0002_auto_20161212_0100.py | 19 + .../mumble/migrations/0003_mumbleuser_user.py | 23 + .../migrations/0004_auto_20161214_1024.py | 22 + .../modules/mumble/migrations/__init__.py | 0 services/modules/mumble/models.py | 14 + services/modules/mumble/tasks.py | 63 + services/modules/mumble/tests.py | 201 +++ services/modules/mumble/urls.py | 17 + services/modules/mumble/views.py | 120 ++ services/modules/openfire/__init__.py | 0 services/modules/openfire/admin.py | 10 + services/modules/openfire/apps.py | 7 + services/modules/openfire/auth_hooks.py | 93 ++ services/modules/openfire/forms.py | 9 + .../openfire/manager.py} | 5 +- .../openfire/migrations/0001_initial.py | 26 + .../modules/openfire/migrations/__init__.py | 0 services/modules/openfire/models.py | 15 + services/modules/openfire/tasks.py | 75 ++ services/modules/openfire/tests.py | 207 +++ services/modules/openfire/urls.py | 28 + services/modules/openfire/views.py | 160 +++ services/modules/phpbb3/__init__.py | 0 services/modules/phpbb3/admin.py | 10 + services/modules/phpbb3/apps.py | 7 + services/modules/phpbb3/auth_hooks.py | 68 + .../phpbb3/manager.py} | 5 +- .../modules/phpbb3/migrations/0001_initial.py | 26 + .../modules/phpbb3/migrations/__init__.py | 0 services/modules/phpbb3/models.py | 15 + services/modules/phpbb3/tasks.py | 73 ++ services/modules/phpbb3/tests.py | 212 +++ services/modules/phpbb3/urls.py | 16 + services/modules/phpbb3/views.py | 112 ++ services/modules/smf/__init__.py | 0 services/modules/smf/admin.py | 10 + services/modules/smf/apps.py | 7 + services/modules/smf/auth_hooks.py | 68 + .../smf_manager.py => modules/smf/manager.py} | 151 +-- .../modules/smf/migrations/0001_initial.py | 26 + services/modules/smf/migrations/__init__.py | 0 services/modules/smf/models.py | 15 + services/modules/smf/tasks.py | 75 ++ services/modules/smf/tests.py | 212 +++ services/modules/smf/urls.py | 16 + services/modules/smf/views.py | 111 ++ services/modules/teamspeak3/__init__.py | 0 services/modules/teamspeak3/admin.py | 17 + services/modules/teamspeak3/apps.py | 10 + services/modules/teamspeak3/auth_hooks.py | 61 + services/modules/teamspeak3/forms.py | 15 + .../teamspeak3/manager.py} | 8 +- .../teamspeak3/migrations/0001_initial.py | 59 + .../migrations/0002_auto_20161212_0133.py | 27 + .../migrations/0003_teamspeak3user.py | 26 + .../modules/teamspeak3/migrations/__init__.py | 0 services/modules/teamspeak3/models.py | 52 + services/modules/teamspeak3/signals.py | 38 + services/modules/teamspeak3/tasks.py | 96 ++ .../registered/teamspeak3_service_ctrl.html | 34 + services/modules/teamspeak3/tests.py | 328 +++++ services/modules/teamspeak3/urls.py | 21 + .../teamspeak3}/util/__init__.py | 0 .../teamspeak3}/util/ts3.py | 0 services/modules/teamspeak3/views.py | 121 ++ services/modules/xenforo/__init__.py | 0 services/modules/xenforo/admin.py | 10 + services/modules/xenforo/apps.py | 7 + services/modules/xenforo/auth_hooks.py | 58 + .../xenforo/manager.py} | 5 +- .../xenforo/migrations/0001_initial.py | 26 + .../modules/xenforo/migrations/__init__.py | 0 services/modules/xenforo/models.py | 15 + services/modules/xenforo/tasks.py | 46 + services/modules/xenforo/tests.py | 192 +++ services/modules/xenforo/urls.py | 16 + services/modules/xenforo/views.py | 101 ++ services/signals.py | 75 +- services/tasks.py | 472 +------ services/templates/public/menublock.html | 3 + services/templates/public/menuitem.html | 8 + services/templatetags/__init__.py | 0 services/templatetags/menu_items.py | 43 + services/tests.py | 1 - services/tests/__init__.py | 0 services/tests/test_signals.py | 91 ++ services/tests/test_tasks.py | 81 ++ services/views.py | 1166 +---------------- stock/templates/public/base.html | 16 +- stock/templates/registered/services.html | 268 +--- stock/templates/registered/services_ctrl.html | 32 + testing-requirements.txt | 9 + thirdparty/Mumble/authenticator.py | 8 +- thirdparty/Mumble/requirements.txt | 3 + 195 files changed, 8260 insertions(+), 2699 deletions(-) create mode 100644 .coveragerc create mode 100644 .travis.yml create mode 100644 alliance_auth/celeryapp.py create mode 100644 alliance_auth/hooks.py create mode 100644 alliance_auth/tests/__init__.py create mode 100644 alliance_auth/tests/auth_utils.py create mode 100644 alliance_auth/tests/test_settings.py create mode 100644 authentication/migrations/0013_service_modules.py rename services/managers/fleetup_manager.py => fleetup/managers.py (100%) create mode 100644 runtests.py create mode 100644 services/hooks.py delete mode 100755 services/managers/mumble_manager.py create mode 100644 services/modules/__init__.py create mode 100644 services/modules/discord/__init__.py create mode 100644 services/modules/discord/admin.py create mode 100644 services/modules/discord/apps.py create mode 100644 services/modules/discord/auth_hooks.py rename services/{managers/discord_manager.py => modules/discord/manager.py} (100%) create mode 100644 services/modules/discord/migrations/0001_initial.py create mode 100644 services/modules/discord/migrations/__init__.py create mode 100644 services/modules/discord/models.py create mode 100644 services/modules/discord/tasks.py create mode 100644 services/modules/discord/templates/registered/discord_service_ctrl.html create mode 100644 services/modules/discord/tests.py create mode 100644 services/modules/discord/urls.py create mode 100644 services/modules/discord/views.py create mode 100644 services/modules/discourse/__init__.py create mode 100644 services/modules/discourse/admin.py create mode 100644 services/modules/discourse/apps.py create mode 100644 services/modules/discourse/auth_hooks.py rename services/{managers/discourse_manager.py => modules/discourse/manager.py} (99%) create mode 100644 services/modules/discourse/migrations/0001_initial.py create mode 100644 services/modules/discourse/migrations/__init__.py create mode 100644 services/modules/discourse/models.py create mode 100644 services/modules/discourse/tasks.py create mode 100644 services/modules/discourse/templates/registered/discourse_service_ctrl.html create mode 100644 services/modules/discourse/tests.py create mode 100644 services/modules/discourse/urls.py create mode 100644 services/modules/discourse/views.py create mode 100644 services/modules/example/__init__.py create mode 100644 services/modules/example/apps.py create mode 100644 services/modules/example/auth_hooks.py create mode 100644 services/modules/example/models.py create mode 100644 services/modules/example/urls.py create mode 100644 services/modules/example/views.py create mode 100644 services/modules/ipboard/__init__.py create mode 100644 services/modules/ipboard/admin.py create mode 100644 services/modules/ipboard/apps.py create mode 100644 services/modules/ipboard/auth_hooks.py rename services/{managers/ipboard_manager.py => modules/ipboard/manager.py} (92%) create mode 100644 services/modules/ipboard/migrations/0001_initial.py create mode 100644 services/modules/ipboard/migrations/__init__.py create mode 100644 services/modules/ipboard/models.py create mode 100644 services/modules/ipboard/tasks.py create mode 100644 services/modules/ipboard/tests.py create mode 100644 services/modules/ipboard/urls.py create mode 100644 services/modules/ipboard/views.py create mode 100644 services/modules/ips4/__init__.py create mode 100644 services/modules/ips4/admin.py create mode 100644 services/modules/ips4/apps.py create mode 100644 services/modules/ips4/auth_hooks.py rename services/{managers/ips4_manager.py => modules/ips4/manager.py} (67%) create mode 100644 services/modules/ips4/migrations/0001_initial.py create mode 100644 services/modules/ips4/migrations/__init__.py create mode 100644 services/modules/ips4/models.py create mode 100644 services/modules/ips4/tasks.py create mode 100644 services/modules/ips4/tests.py create mode 100644 services/modules/ips4/urls.py create mode 100644 services/modules/ips4/views.py create mode 100644 services/modules/market/__init__.py create mode 100644 services/modules/market/admin.py create mode 100644 services/modules/market/apps.py create mode 100644 services/modules/market/auth_hooks.py rename services/{managers/market_manager.py => modules/market/manager.py} (58%) create mode 100644 services/modules/market/migrations/0001_initial.py create mode 100644 services/modules/market/migrations/__init__.py create mode 100644 services/modules/market/models.py create mode 100644 services/modules/market/tasks.py create mode 100644 services/modules/market/tests.py create mode 100644 services/modules/market/urls.py create mode 100644 services/modules/market/views.py create mode 100644 services/modules/mumble/__init__.py create mode 100644 services/modules/mumble/admin.py create mode 100644 services/modules/mumble/apps.py create mode 100644 services/modules/mumble/auth_hooks.py create mode 100755 services/modules/mumble/manager.py create mode 100644 services/modules/mumble/migrations/0001_initial.py create mode 100644 services/modules/mumble/migrations/0002_auto_20161212_0100.py create mode 100644 services/modules/mumble/migrations/0003_mumbleuser_user.py create mode 100644 services/modules/mumble/migrations/0004_auto_20161214_1024.py create mode 100644 services/modules/mumble/migrations/__init__.py create mode 100644 services/modules/mumble/models.py create mode 100644 services/modules/mumble/tasks.py create mode 100644 services/modules/mumble/tests.py create mode 100644 services/modules/mumble/urls.py create mode 100644 services/modules/mumble/views.py create mode 100644 services/modules/openfire/__init__.py create mode 100644 services/modules/openfire/admin.py create mode 100644 services/modules/openfire/apps.py create mode 100644 services/modules/openfire/auth_hooks.py create mode 100644 services/modules/openfire/forms.py rename services/{managers/openfire_manager.py => modules/openfire/manager.py} (98%) create mode 100644 services/modules/openfire/migrations/0001_initial.py create mode 100644 services/modules/openfire/migrations/__init__.py create mode 100644 services/modules/openfire/models.py create mode 100644 services/modules/openfire/tasks.py create mode 100644 services/modules/openfire/tests.py create mode 100644 services/modules/openfire/urls.py create mode 100644 services/modules/openfire/views.py create mode 100644 services/modules/phpbb3/__init__.py create mode 100644 services/modules/phpbb3/admin.py create mode 100644 services/modules/phpbb3/apps.py create mode 100644 services/modules/phpbb3/auth_hooks.py rename services/{managers/phpbb3_manager.py => modules/phpbb3/manager.py} (99%) create mode 100644 services/modules/phpbb3/migrations/0001_initial.py create mode 100644 services/modules/phpbb3/migrations/__init__.py create mode 100644 services/modules/phpbb3/models.py create mode 100644 services/modules/phpbb3/tasks.py create mode 100644 services/modules/phpbb3/tests.py create mode 100644 services/modules/phpbb3/urls.py create mode 100644 services/modules/phpbb3/views.py create mode 100644 services/modules/smf/__init__.py create mode 100644 services/modules/smf/admin.py create mode 100644 services/modules/smf/apps.py create mode 100644 services/modules/smf/auth_hooks.py rename services/{managers/smf_manager.py => modules/smf/manager.py} (66%) create mode 100644 services/modules/smf/migrations/0001_initial.py create mode 100644 services/modules/smf/migrations/__init__.py create mode 100644 services/modules/smf/models.py create mode 100644 services/modules/smf/tasks.py create mode 100644 services/modules/smf/tests.py create mode 100644 services/modules/smf/urls.py create mode 100644 services/modules/smf/views.py create mode 100644 services/modules/teamspeak3/__init__.py create mode 100644 services/modules/teamspeak3/admin.py create mode 100644 services/modules/teamspeak3/apps.py create mode 100644 services/modules/teamspeak3/auth_hooks.py create mode 100644 services/modules/teamspeak3/forms.py rename services/{managers/teamspeak3_manager.py => modules/teamspeak3/manager.py} (99%) create mode 100644 services/modules/teamspeak3/migrations/0001_initial.py create mode 100644 services/modules/teamspeak3/migrations/0002_auto_20161212_0133.py create mode 100644 services/modules/teamspeak3/migrations/0003_teamspeak3user.py create mode 100644 services/modules/teamspeak3/migrations/__init__.py create mode 100644 services/modules/teamspeak3/models.py create mode 100644 services/modules/teamspeak3/signals.py create mode 100644 services/modules/teamspeak3/tasks.py create mode 100644 services/modules/teamspeak3/templates/registered/teamspeak3_service_ctrl.html create mode 100644 services/modules/teamspeak3/tests.py create mode 100644 services/modules/teamspeak3/urls.py rename services/{managers => modules/teamspeak3}/util/__init__.py (100%) rename services/{managers => modules/teamspeak3}/util/ts3.py (100%) create mode 100644 services/modules/teamspeak3/views.py create mode 100644 services/modules/xenforo/__init__.py create mode 100644 services/modules/xenforo/admin.py create mode 100644 services/modules/xenforo/apps.py create mode 100644 services/modules/xenforo/auth_hooks.py rename services/{managers/xenforo_manager.py => modules/xenforo/manager.py} (96%) create mode 100644 services/modules/xenforo/migrations/0001_initial.py create mode 100644 services/modules/xenforo/migrations/__init__.py create mode 100644 services/modules/xenforo/models.py create mode 100644 services/modules/xenforo/tasks.py create mode 100644 services/modules/xenforo/tests.py create mode 100644 services/modules/xenforo/urls.py create mode 100644 services/modules/xenforo/views.py create mode 100644 services/templates/public/menublock.html create mode 100644 services/templates/public/menuitem.html create mode 100644 services/templatetags/__init__.py create mode 100644 services/templatetags/menu_items.py delete mode 100644 services/tests.py create mode 100644 services/tests/__init__.py create mode 100644 services/tests/test_signals.py create mode 100644 services/tests/test_tasks.py create mode 100644 stock/templates/registered/services_ctrl.html create mode 100644 testing-requirements.txt create mode 100644 thirdparty/Mumble/requirements.txt diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..43d4b1a8 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,31 @@ +[run] +branch = True +source = + alliance_auth + authentication + corputils + eveonline + fleetactivitytracking + fleetup + groupmanagement + hrapplications + notifications + optimer + services + srp + timerboard + +omit = + */migrations/* + */example/* + +[report] +exclude_lines = + if self.debug: + pragma: no cover + raise NotImplementedError + if __name__ == .__main__.: + def __repr__ + raise AssertionError + +ignore_errors = True diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..6ca8c709 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: python +python: + - "2.7" + - "3.5" +# command to install dependencies +install: + - pip install requests + - pip install -r requirements.txt + - pip install -r testing-requirements.txt +# command to run tests +script: coverage run runtests.py +cache: pip +after_success: + coveralls diff --git a/alliance_auth/__init__.py b/alliance_auth/__init__.py index 97dcd825..3eac6fde 100644 --- a/alliance_auth/__init__.py +++ b/alliance_auth/__init__.py @@ -1,4 +1,8 @@ -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals + +# This will make sure the app is always imported when +# Django starts so that shared_task will use this app. +from .celeryapp import app as celery_app # noqa __version__ = '1.14.2' NAME = 'Alliance Auth v%s' % __version__ diff --git a/alliance_auth/celeryapp.py b/alliance_auth/celeryapp.py new file mode 100644 index 00000000..26a34e0d --- /dev/null +++ b/alliance_auth/celeryapp.py @@ -0,0 +1,17 @@ +from __future__ import absolute_import, unicode_literals +import os +from celery import Celery + +# set the default Django settings module for the 'celery' program. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'alliance_auth.settings') + +from django.conf import settings # noqa + +app = Celery('alliance_auth') + +# Using a string here means the worker don't have to serialize +# the configuration object to child processes. +app.config_from_object('django.conf:settings') + +# Load task modules from all registered Django app configs. +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) diff --git a/alliance_auth/hooks.py b/alliance_auth/hooks.py new file mode 100644 index 00000000..b10afaa9 --- /dev/null +++ b/alliance_auth/hooks.py @@ -0,0 +1,127 @@ +""" +Copyright (c) 2014 Torchbox Ltd and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of Torchbox nor the names of its contributors may be used + to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Based on https://github.com/torchbox/wagtail/blob/master/wagtail/wagtailcore/hooks.py +""" + +from __future__ import unicode_literals + +from importlib import import_module + +from django.apps import apps +from django.utils.module_loading import module_has_submodule + +import logging + +logger = logging.getLogger(__name__) + +_hooks = {} # Dict of Name: Fn's of registered hooks + +_all_hooks_registered = False # If all hooks have been searched for and registered yet + + +def register(name, fn=None): + """ + Decorator to register a function as a hook + + Register hook for ``hook_name``. Can be used as a decorator:: + @register('hook_name') + def my_hook(...): + pass + + or as a function call:: + def my_hook(...): + pass + register('hook_name', my_hook) + + :param name: str Name of the hook/callback to register it as + :param fn: function to register in the hook/callback + :return: function Decorator if applied as a decorator + """ + def _hook_add(func): + if name not in _hooks: + logger.debug("Creating new hook %s" % name) + _hooks[name] = [] + + logger.debug('Registering hook %s for function %s' % (name, fn)) + _hooks[name].append(func) + + if fn is None: + # Behave like a decorator + def decorator(func): + _hook_add(func) + return func + return decorator + else: + # Behave like a function, just register hook + _hook_add(fn) + + +def get_app_modules(): + """ + Get all the modules of the django app + :return: name, module tuple + """ + for app in apps.get_app_configs(): + yield app.name, app.module + + +def get_app_submodules(module_name): + """ + Get a specific sub module of the app + :param module_name: module name to get + :return: name, module tuple + """ + for name, module in get_app_modules(): + if module_has_submodule(module, module_name): + yield name, import_module('{0}.{1}'.format(name, module_name)) + + +def register_all_hooks(): + """ + Register all hooks found in 'auth_hooks' sub modules + :return: + """ + global _all_hooks_registered + if not _all_hooks_registered: + logger.debug("Searching for hooks") + hooks = list(get_app_submodules('auth_hooks')) + logger.debug("Got %s hooks" % len(hooks)) + _all_hooks_registered = True + + +def get_hooks(name): + """ + Get all the hook functions for the given hook name + :param name: str name of the hook to get the functions for + :return: list of hook functions + """ + register_all_hooks() + return _hooks.get(name, []) + diff --git a/alliance_auth/settings.py.example b/alliance_auth/settings.py.example index 54dc4b15..e4fe0d42 100644 --- a/alliance_auth/settings.py.example +++ b/alliance_auth/settings.py.example @@ -15,12 +15,22 @@ import os import djcelery from django.contrib import messages +from celery.schedules import crontab djcelery.setup_loader() # Celery configuration BROKER_URL = 'redis://localhost:6379/0' CELERYBEAT_SCHEDULER = "djcelery.schedulers.DatabaseScheduler" +CELERYBEAT_SCHEDULE = { + """ + Uncomment this if you are using the Teamspeak3 service + 'run_ts3_group_update': { + 'task': 'services.modules.teamspeak3.tasks.Teamspeak3Tasks.run_ts3_group_update', + 'schedule': crontab(minute='*/30'), + }, + """ +} # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -64,6 +74,19 @@ INSTALLED_APPS = [ 'esi', 'geelweb.django.navhelper', 'bootstrap_pagination', + + # Services + 'services.modules.mumble', + 'services.modules.discord', + 'services.modules.discourse', + 'services.modules.ipboard', + 'services.modules.ips4', + 'services.modules.market', + 'services.modules.openfire', + 'services.modules.smf', + 'services.modules.phpbb3', + 'services.modules.xenforo', + 'services.modules.teamspeak3', ] MIDDLEWARE = [ diff --git a/alliance_auth/tests/__init__.py b/alliance_auth/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/alliance_auth/tests/auth_utils.py b/alliance_auth/tests/auth_utils.py new file mode 100644 index 00000000..c3ca78a3 --- /dev/null +++ b/alliance_auth/tests/auth_utils.py @@ -0,0 +1,81 @@ +from __future__ import unicode_literals + +from django.db.models.signals import m2m_changed, pre_save +from django.contrib.auth.models import User + +from services.signals import m2m_changed_user_groups, pre_save_user +from authentication.signals import pre_save_auth_state + +from authentication.tasks import make_member, make_blue +from authentication.models import AuthServicesInfo +from authentication.states import MEMBER_STATE, BLUE_STATE, NONE_STATE + +from eveonline.models import EveCharacter + + +class AuthUtils: + def __init__(self): + pass + + @staticmethod + def _create_user(username): + return User.objects.create(username=username) + + @classmethod + def create_user(cls, username, disconnect_signals=False): + if disconnect_signals: + cls.disconnect_signals() + user = cls._create_user(username) + user.authservicesinfo.state = NONE_STATE + user.authservicesinfo.save() + if disconnect_signals: + cls.connect_signals() + return user + + @classmethod + def create_member(cls, username): + cls.disconnect_signals() + user = cls._create_user(username) + user.authservicesinfo.state = MEMBER_STATE + user.authservicesinfo.save() + make_member(user.authservicesinfo) + cls.connect_signals() + return user + + @classmethod + def create_blue(cls, username): + cls.disconnect_signals() + user = cls._create_user(username) + user.authservicesinfo.state = BLUE_STATE + user.authservicesinfo.save() + make_blue(user.authservicesinfo) + cls.connect_signals() + return user + + @classmethod + def disconnect_signals(cls): + m2m_changed.disconnect(m2m_changed_user_groups, sender=User.groups.through) + pre_save.disconnect(pre_save_user, sender=User) + pre_save.disconnect(pre_save_auth_state, sender=AuthServicesInfo) + + @classmethod + def connect_signals(cls): + m2m_changed.connect(m2m_changed_user_groups, sender=User.groups.through) + pre_save.connect(pre_save_user, sender=User) + pre_save.connect(pre_save_auth_state, sender=AuthServicesInfo) + + @classmethod + def add_main_character(cls, user, name, character_id, corp_id='', corp_name='', corp_ticker='', alliance_id='', + alliance_name=''): + EveCharacter.objects.create( + character_id=character_id, + character_name=name, + corporation_id=corp_id, + corporation_name=corp_name, + corporation_ticker=corp_ticker, + alliance_id=alliance_id, + alliance_name=alliance_name, + api_id='1234', + user=user + ) + AuthServicesInfo.objects.update_or_create(user=user, defaults={'main_char_id': character_id}) diff --git a/alliance_auth/tests/test_settings.py b/alliance_auth/tests/test_settings.py new file mode 100644 index 00000000..f150cfd4 --- /dev/null +++ b/alliance_auth/tests/test_settings.py @@ -0,0 +1,604 @@ +""" +Alliance Auth Test Suite Django settings. +""" + +import os + +import djcelery + +from django.contrib import messages + +import alliance_auth + +djcelery.setup_loader() + +# Use nose to run all tests +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' + +NOSE_ARGS = [ + #'--with-coverage', + #'--cover-package=', +] + +# Celery configuration +CELERY_ALWAYS_EAGER = True # Forces celery to run locally for testing + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(alliance_auth.__file__))) + +SECRET_KEY = 'testing only' + +DEBUG = True + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.humanize', + 'djcelery', + 'bootstrapform', + 'authentication', + 'services', + 'eveonline', + 'groupmanagement', + 'hrapplications', + 'timerboard', + 'srp', + 'optimer', + 'corputils', + 'fleetactivitytracking', + 'notifications', + 'esi', + 'geelweb.django.navhelper', + 'bootstrap_pagination', + 'services.modules.mumble', + 'services.modules.discord', + 'services.modules.discourse', + 'services.modules.ipboard', + 'services.modules.ips4', + 'services.modules.market', + 'services.modules.openfire', + 'services.modules.smf', + 'services.modules.phpbb3', + 'services.modules.xenforo', + 'services.modules.teamspeak3', + 'django_nose', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.locale.LocaleMiddleware', +] + +ROOT_URLCONF = 'alliance_auth.urls' + +LOCALE_PATHS = ( + os.path.join(BASE_DIR, 'locale/'), +) + +ugettext = lambda s: s +LANGUAGES = ( + ('en', ugettext('English')), + ('de', ugettext('German')), +) + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + os.path.join(BASE_DIR, 'customization/templates'), + os.path.join(BASE_DIR, 'stock/templates'), + ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'django.template.context_processors.i18n', + 'django.template.context_processors.media', + 'django.template.context_processors.static', + 'django.template.context_processors.tz', + 'services.context_processors.auth_settings', + 'notifications.context_processors.user_notification_count', + 'authentication.context_processors.states', + 'authentication.context_processors.membership_state', + 'groupmanagement.context_processors.can_manage_groups', + ], + }, + }, +] + +# Database +# https://docs.djangoproject.com/en/1.10/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'alliance_auth', + 'USER': os.environ.get('AA_DB_DEFAULT_USER', None), + 'PASSWORD': os.environ.get('AA_DB_DEFAULT_PASSWORD', None), + 'HOST': os.environ.get('AA_DB_DEFAULT_HOST', None) + }, +} + +LOGIN_URL = 'auth_login_user' + +SUPERUSER_STATE_BYPASS = 'True' == os.environ.get('AA_SUPERUSER_STATE_BYPASS', 'True') + +# Internationalization +# https://docs.djangoproject.com/en/1.10/topics/i18n/ + +LANGUAGE_CODE = os.environ.get('AA_LANGUAGE_CODE', 'en-us') + +TIME_ZONE = os.environ.get('AA_TIME_ZONE', 'UTC') + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) + +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, "static") +STATICFILES_DIRS = ( + os.path.join(BASE_DIR, "customization/static"), + os.path.join(BASE_DIR, "stock/static"), +) + +# Bootstrap messaging css workaround +MESSAGE_TAGS = { + messages.ERROR: 'danger' +} + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + } +} + +##################################################### +## +## Auth configuration starts here +## +##################################################### + +########################### +# ALLIANCE / CORP TOGGLE +########################### +# Specifies to run membership checks against corp or alliance +# Set to FALSE for alliance +# Set to TRUE for corp +########################### +IS_CORP = 'True' == os.environ.get('AA_IS_CORP', 'True') + +################# +# EMAIL SETTINGS +################# +# DOMAIN - The alliance auth domain_url +# EMAIL_HOST - SMTP Server URL +# EMAIL_PORT - SMTP Server PORT +# EMAIL_HOST_USER - Email Username (for gmail, the entire address) +# EMAIL_HOST_PASSWORD - Email Password +# EMAIL_USE_TLS - Set to use TLS encryption +################# +DOMAIN = os.environ.get('AA_DOMAIN', 'https://example.com') +EMAIL_HOST = os.environ.get('AA_EMAIL_HOST', 'smtp.example.com') +EMAIL_PORT = int(os.environ.get('AA_EMAIL_PORT', '587')) +EMAIL_HOST_USER = os.environ.get('AA_EMAIL_HOST_USER', '') +EMAIL_HOST_PASSWORD = os.environ.get('AA_EMAIL_HOST_PASSWORD', '') +EMAIL_USE_TLS = 'True' == os.environ.get('AA_EMAIL_USE_TLS', 'True') + +#################### +# Front Page Links +#################### +# KILLBOARD_URL - URL for your killboard. Blank to hide link +# MEDIA_URL - URL for your media page (youtube etc). Blank to hide link +# FORUM_URL - URL for your forums. Blank to hide link +# SITE_NAME - Name of the auth site. +#################### +KILLBOARD_URL = os.environ.get('AA_KILLBOARD_URL', '') +EXTERNAL_MEDIA_URL = os.environ.get('AA_EXTERNAL_MEDIA_URL', '') +FORUM_URL = os.environ.get('AA_FORUM_URL', '') +SITE_NAME = os.environ.get('AA_SITE_NAME', 'Test Alliance Auth') + +################### +# SSO Settings +################### +# Optional SSO. +# Get client ID and client secret from registering an app at +# https://developers.eveonline.com/ +# Callback URL should be http://mydomain.com/sso/callback +# Leave callback blank to hide SSO button on login page +################### +ESI_SSO_CLIENT_ID = os.environ.get('AA_ESI_SSO_CLIENT_ID', '') +ESI_SSO_CLIENT_SECRET = os.environ.get('AA_ESI_SSO_CLIENT_SECRET', '') +ESI_SSO_CALLBACK_URL = os.environ.get('AA_ESI_SSO_CALLBACK_URL', '') + +######################### +# Default Group Settings +######################### +# DEFAULT_AUTH_GROUP - Default group members are put in +# DEFAULT_BLUE_GROUP - Default group for blue members +# MEMBER_CORP_GROUPS - Assign members to a group representing their main corp +# BLUE_CORP_GROUPS - Assign blues to a group representing their main corp +######################### +DEFAULT_AUTH_GROUP = os.environ.get('AA_DEFAULT_ALLIANCE_GROUP', 'Member') +DEFAULT_BLUE_GROUP = os.environ.get('AA_DEFAULT_BLUE_GROUP', 'Blue') +MEMBER_CORP_GROUPS = 'True' == os.environ.get('AA_MEMBER_CORP_GROUPS', 'True') +MEMBER_ALLIANCE_GROUPS = 'True' == os.environ.get('AA_MEMBER_ALLIANCE_GROUPS', 'False') +BLUE_CORP_GROUPS = 'True' == os.environ.get('AA_BLUE_CORP_GROUPS', 'False') +BLUE_ALLIANCE_GROUPS = 'True' == os.environ.get('AA_BLUE_ALLIANCE_GROUPS', 'False') + +######################### +# Alliance Service Setup +######################### +# ENABLE_AUTH_FORUM - Enable forum support in the auth for auth'd members +# ENABLE_AUTH_JABBER - Enable jabber support in the auth for auth'd members +# ENABLE_AUTH_MUMBLE - Enable mumble support in the auth for auth'd members +# ENABLE_AUTH_IPBOARD - Enable IPBoard forum support in the auth for auth'd members +# ENABLE_AUTH_DISCORD - Enable Discord support in the auth for auth'd members +# ENABLE_AUTH_DISCOURSE - Enable Discourse support in the auth for auth'd members +# ENABLE_AUTH_IPS4 - Enable IPS4 support in the auth for auth'd members +# ENABLE_AUTH_SMF - Enable SMF forum support in the auth for auth'd members +# ENABLE_AUTH_MARKET = Enable Alliance Market support in auth for auth'd members +# ENABLE_AUTH_PATHFINDER = Enable Alliance Pathfinder suppor in auth for auth'd members +# ENABLE_AUTH_XENFORO = Enable XenForo forums support in the auth for auth'd members +######################### +ENABLE_AUTH_FORUM = 'True' == os.environ.get('AA_ENABLE_AUTH_FORUM', 'True') +ENABLE_AUTH_JABBER = 'True' == os.environ.get('AA_ENABLE_AUTH_JABBER', 'True') +ENABLE_AUTH_MUMBLE = 'True' == os.environ.get('AA_ENABLE_AUTH_MUMBLE', 'True') +ENABLE_AUTH_IPBOARD = 'True' == os.environ.get('AA_ENABLE_AUTH_IPBOARD', 'True') +ENABLE_AUTH_TEAMSPEAK3 = 'True' == os.environ.get('AA_ENABLE_AUTH_TEAMSPEAK3', 'True') +ENABLE_AUTH_DISCORD = 'True' == os.environ.get('AA_ENABLE_AUTH_DISCORD', 'True') +ENABLE_AUTH_DISCOURSE = 'True' == os.environ.get('AA_ENABLE_AUTH_DISCOURSE', 'True') +ENABLE_AUTH_IPS4 = 'True' == os.environ.get('AA_ENABLE_AUTH_IPS4', 'True') +ENABLE_AUTH_SMF = 'True' == os.environ.get('AA_ENABLE_AUTH_SMF', 'True') +ENABLE_AUTH_MARKET = 'True' == os.environ.get('AA_ENABLE_AUTH_MARKET', 'True') +ENABLE_AUTH_XENFORO = 'True' == os.environ.get('AA_ENABLE_AUTH_XENFORO', 'True') + +##################### +# Blue service Setup +##################### +# BLUE_STANDING - The default lowest standings setting to consider blue +# ENABLE_BLUE_FORUM - Enable forum support in the auth for blues +# ENABLE_BLUE_JABBER - Enable jabber support in the auth for blues +# ENABLE_BLUE_MUMBLE - Enable mumble support in the auth for blues +# ENABLE_BLUE_IPBOARD - Enable IPBoard forum support in the auth for blues +# ENABLE_BLUE_DISCORD - Enable Discord support in the auth for blues +# ENABLE_BLUE_DISCOURSE - Enable Discord support in the auth for blues +# ENABLE_BLUE_IPS4 - Enable IPS4 forum support in the auth for blues +# ENABLE_BLUE_SMF - Enable SMF forum support in the auth for blues +# ENABLE_BLUE_MARKET - Enable Alliance Market in the auth for blues +# ENABLE_BLUE_PATHFINDER = Enable Pathfinder support in the auth for blues +# ENABLE_BLUE_XENFORO = Enable XenForo forum support in the auth for blue +##################### +BLUE_STANDING = float(os.environ.get('AA_BLUE_STANDING', '5.0')) +ENABLE_BLUE_FORUM = 'True' == os.environ.get('AA_ENABLE_BLUE_FORUM', 'True') +ENABLE_BLUE_JABBER = 'True' == os.environ.get('AA_ENABLE_BLUE_JABBER', 'True') +ENABLE_BLUE_MUMBLE = 'True' == os.environ.get('AA_ENABLE_BLUE_MUMBLE', 'True') +ENABLE_BLUE_IPBOARD = 'True' == os.environ.get('AA_ENABLE_BLUE_IPBOARD', 'True') +ENABLE_BLUE_TEAMSPEAK3 = 'True' == os.environ.get('AA_ENABLE_BLUE_TEAMSPEAK3', 'True') +ENABLE_BLUE_DISCORD = 'True' == os.environ.get('AA_ENABLE_BLUE_DISCORD', 'True') +ENABLE_BLUE_DISCOURSE = 'True' == os.environ.get('AA_ENABLE_BLUE_DISCOURSE', 'True') +ENABLE_BLUE_IPS4 = 'True' == os.environ.get('AA_ENABLE_BLUE_IPS4', 'True') +ENABLE_BLUE_SMF = 'True' == os.environ.get('AA_ENABLE_BLUE_SMF', 'True') +ENABLE_BLUE_MARKET = 'True' == os.environ.get('AA_ENABLE_BLUE_MARKET', 'True') +ENABLE_BLUE_XENFORO = 'True' == os.environ.get('AA_ENABLE_BLUE_XENFORO', 'True') + +######################### +# Corp Configuration +######################### +# If running in alliance mode, the following should be for the executor corp# +# CORP_ID - Set this to your corp ID (get this from https://zkillboard.com/corporation/#######) +# CORP_NAME - Set this to your Corporation Name +# CORP_API_ID - Set this to the api id for the corp API key +# CORP_API_VCODE - Set this to the api vcode for the corp API key +######################## +CORP_ID = os.environ.get('AA_CORP_ID', '1234') +CORP_NAME = os.environ.get('AA_CORP_NAME', 'Alliance Auth Test Corp') +CORP_API_ID = os.environ.get('AA_CORP_API_ID', '') +CORP_API_VCODE = os.environ.get('AA_CORP_API_VCODE', '') + +######################### +# Alliance Configuration +######################### +# ALLIANCE_ID - Set this to your Alliance ID (get this from https://zkillboard.com/alliance/#######) +# ALLIANCE_NAME - Set this to your Alliance Name +######################## +ALLIANCE_ID = os.environ.get('AA_ALLIANCE_ID', '12345') +ALLIANCE_NAME = os.environ.get('AA_ALLIANCE_NAME', 'Alliance Auth Test Alliance') + +######################## +# API Configuration +######################## +# MEMBER_API_MASK - Numeric value of minimum API mask required for members +# MEMBER_API_ACCOUNT - Require API to be for Account and not character restricted +# BLUE_API_MASK - Numeric value of minimum API mask required for blues +# BLUE_API_ACCOUNT - Require API to be for Account and not character restricted +# REJECT_OLD_APIS - Require each submitted API be newer than the latest submitted API +# REJECT_OLD_APIS_MARGIN - Margin from latest submitted API ID within which a newly submitted API is still accepted +# API_SSO_VALIDATION - Require users to prove ownership of newly entered API keys via SSO +# Requires SSO to be configured. +####################### +MEMBER_API_MASK = os.environ.get('AA_MEMBER_API_MASK', 268435455) +MEMBER_API_ACCOUNT = 'True' == os.environ.get('AA_MEMBER_API_ACCOUNT', 'True') +BLUE_API_MASK = os.environ.get('AA_BLUE_API_MASK', 8388608) +BLUE_API_ACCOUNT = 'True' == os.environ.get('AA_BLUE_API_ACCOUNT', 'True') +REJECT_OLD_APIS = 'True' == os.environ.get('AA_REJECT_OLD_APIS', 'False') +REJECT_OLD_APIS_MARGIN = os.environ.get('AA_REJECT_OLD_APIS_MARGIN', 50) +API_SSO_VALIDATION = 'True' == os.environ.get('AA_API_SSO_VALIDATION', 'False') + +####################### +# EVE Provider Settings +####################### +# EVEONLINE_CHARACTER_PROVIDER - Name of default data source for getting eve character data +# EVEONLINE_CORP_PROVIDER - Name of default data source for getting eve corporation data +# EVEONLINE_ALLIANCE_PROVIDER - Name of default data source for getting eve alliance data +# EVEONLINE_ITEMTYPE_PROVIDER - Name of default data source for getting eve item type data +# +# Available sources are 'esi' and 'xml'. Leaving blank results in the default 'esi' being used. +####################### +EVEONLINE_CHARACTER_PROVIDER = os.environ.get('AA_EVEONLINE_CHARACTER_PROVIDER', 'xml') +EVEONLINE_CORP_PROVIDER = os.environ.get('AA_EVEONLINE_CORP_PROVIDER', 'xml') +EVEONLINE_ALLIANCE_PROVIDER = os.environ.get('AA_EVEONLINE_ALLIANCE_PROVIDER', 'xml') +EVEONLINE_ITEMTYPE_PROVIDER = os.environ.get('AA_EVEONLINE_ITEMTYPE_PROVIDER', 'xml') + +##################### +# Alliance Market +##################### +MARKET_URL = os.environ.get('AA_MARKET_URL', 'http://yourdomain.com/market') + +##################### +# HR Configuration +##################### +# JACK_KNIFE_URL - Url for the audit page of API Jack knife +# Should seriously replace with your own. +##################### +JACK_KNIFE_URL = os.environ.get('AA_JACK_KNIFE_URL', 'http://example.com/eveapi/audit.php') + +##################### +# Forum Configuration +##################### +# IPBOARD_ENDPOINT - Api endpoint if using ipboard +# IPBOARD_APIKEY - Api key to interact with ipboard +# IPBOARD_APIMODULE - Module for alliance auth *leave alone* +##################### +IPBOARD_ENDPOINT = os.environ.get('AA_IPBOARD_ENDPOINT', 'example.com/interface/board/index.php') +IPBOARD_APIKEY = os.environ.get('AA_IPBOARD_APIKEY', 'somekeyhere') +IPBOARD_APIMODULE = 'aa' + +######################## +# XenForo Configuration +######################## +XENFORO_ENDPOINT = os.environ.get('AA_XENFORO_ENDPOINT', 'example.com/api.php') +XENFORO_DEFAULT_GROUP = os.environ.get('AA_XENFORO_DEFAULT_GROUP', 0) +XENFORO_APIKEY = os.environ.get('AA_XENFORO_APIKEY', 'yourapikey') +##################### + +###################### +# Jabber Configuration +###################### +# JABBER_URL - Jabber address url +# JABBER_PORT - Jabber service portal +# JABBER_SERVER - Jabber server url +# OPENFIRE_ADDRESS - Address of the openfire admin console including port +# Please use http with 9090 or https with 9091 +# OPENFIRE_SECRET_KEY - Openfire REST API secret key +# BROADCAST_USER - Broadcast user JID +# BROADCAST_USER_PASSWORD - Broadcast user password +###################### +JABBER_URL = os.environ.get('AA_JABBER_URL', "example.com") +JABBER_PORT = int(os.environ.get('AA_JABBER_PORT', '5223')) +JABBER_SERVER = os.environ.get('AA_JABBER_SERVER', "example.com") +OPENFIRE_ADDRESS = os.environ.get('AA_OPENFIRE_ADDRESS', "http://example.com:9090") +OPENFIRE_SECRET_KEY = os.environ.get('AA_OPENFIRE_SECRET_KEY', "somekey") +BROADCAST_USER = os.environ.get('AA_BROADCAST_USER', "broadcast@") + JABBER_URL +BROADCAST_USER_PASSWORD = os.environ.get('AA_BROADCAST_USER_PASSWORD', "somepassword") +BROADCAST_SERVICE_NAME = os.environ.get('AA_BROADCAST_SERVICE_NAME', "broadcast") + +###################################### +# Mumble Configuration +###################################### +# MUMBLE_URL - Mumble server url +# MUMBLE_SERVER_ID - Mumble server id +###################################### +MUMBLE_URL = os.environ.get('AA_MUMBLE_URL', "example.com") +MUMBLE_SERVER_ID = int(os.environ.get('AA_MUMBLE_SERVER_ID', '1')) + +###################################### +# PHPBB3 Configuration +###################################### + +###################################### +# Teamspeak3 Configuration +###################################### +# TEAMSPEAK3_SERVER_IP - Teamspeak3 server ip +# TEAMSPEAK3_SERVER_PORT - Teamspeak3 server port +# TEAMSPEAK3_SERVERQUERY_USER - Teamspeak3 serverquery username +# TEAMSPEAK3_SERVERQUERY_PASSWORD - Teamspeak3 serverquery password +# TEAMSPEAK3_VIRTUAL_SERVER - Virtual server id +# TEAMSPEAK3_AUTHED_GROUP_ID - Default authed group id +# TEAMSPEAK3_PUBLIC_URL - teamspeak3 public url used for link creation +###################################### +TEAMSPEAK3_SERVER_IP = os.environ.get('AA_TEAMSPEAK3_SERVER_IP', '127.0.0.1') +TEAMSPEAK3_SERVER_PORT = int(os.environ.get('AA_TEAMSPEAK3_SERVER_PORT', '10011')) +TEAMSPEAK3_SERVERQUERY_USER = os.environ.get('AA_TEAMSPEAK3_SERVERQUERY_USER', 'serveradmin') +TEAMSPEAK3_SERVERQUERY_PASSWORD = os.environ.get('AA_TEAMSPEAK3_SERVERQUERY_PASSWORD', 'passwordhere') +TEAMSPEAK3_VIRTUAL_SERVER = int(os.environ.get('AA_TEAMSPEAK3_VIRTUAL_SERVER', '1')) +TEAMSPEAK3_PUBLIC_URL = os.environ.get('AA_TEAMSPEAK3_PUBLIC_URL', 'example.com') + +###################################### +# Discord Configuration +###################################### +# DISCORD_GUILD_ID - ID of the guild to manage +# DISCORD_BOT_TOKEN - oauth token of the app bot user +# DISCORD_INVITE_CODE - invite code to the server +# DISCORD_APP_ID - oauth app client ID +# DISCORD_APP_SECRET - oauth app secret +# DISCORD_CALLBACK_URL - oauth callback url +# DISCORD_SYNC_NAMES - enable to force discord nicknames to be set to eve char name (bot needs Manage Nicknames permission) +###################################### +DISCORD_GUILD_ID = os.environ.get('AA_DISCORD_GUILD_ID', '') +DISCORD_BOT_TOKEN = os.environ.get('AA_DISCORD_BOT_TOKEN', '') +DISCORD_INVITE_CODE = os.environ.get('AA_DISCORD_INVITE_CODE', '') +DISCORD_APP_ID = os.environ.get('AA_DISCORD_APP_ID', '') +DISCORD_APP_SECRET = os.environ.get('AA_DISCORD_APP_SECRET', '') +DISCORD_CALLBACK_URL = os.environ.get('AA_DISCORD_CALLBACK_URL', 'http://example.com/discord_callback') +DISCORD_SYNC_NAMES = 'True' == os.environ.get('AA_DISCORD_SYNC_NAMES', 'False') + +###################################### +# Discourse Configuration +###################################### +# DISCOURSE_URL - Web address of the forums (no trailing slash) +# DISCOURSE_API_USERNAME - API account username +# DISCOURSE_API_KEY - API Key +# DISCOURSE_SSO_SECRET - SSO secret key +###################################### +DISCOURSE_URL = os.environ.get('AA_DISCOURSE_URL', 'https://example.com') +DISCOURSE_API_USERNAME = os.environ.get('AA_DISCOURSE_API_USERNAME', '') +DISCOURSE_API_KEY = os.environ.get('AA_DISCOURSE_API_KEY', '') +DISCOURSE_SSO_SECRET = 'd836444a9e4084d5b224a60c208dce14' +# Example secret from https://meta.discourse.org/t/official-single-sign-on-for-discourse/13045 + +##################################### +# IPS4 Configuration +##################################### +# IPS4_URL - base url of the IPS4 install (no trailing slash) +# IPS4_API_KEY - API key provided by IPS4 +##################################### +IPS4_URL = os.environ.get('AA_IPS4_URL', 'http://example.com/ips4') +IPS4_API_KEY = os.environ.get('AA_IPS4_API_KEY', '') + + +###################################### +# SMF Configuration +###################################### +SMF_URL = os.environ.get('AA_SMF_URL', '') + +###################################### +# Fleet-Up Configuration +###################################### +# FLEETUP_APP_KEY - The app key from http://fleet-up.com/Api/MyApps +# FLEETUP_USER_ID - The user id from http://fleet-up.com/Api/MyKeys +# FLEETUP_API_ID - The API id from http://fleet-up.com/Api/MyKeys +# FLEETUP_GROUP_ID - The id of the group you want to pull data from, see http://fleet-up.com/Api/Endpoints#groups_mygroupmemberships +###################################### +FLEETUP_APP_KEY = os.environ.get('AA_FLEETUP_APP_KEY', '') +FLEETUP_USER_ID = os.environ.get('AA_FLEETUP_USER_ID', '') +FLEETUP_API_ID = os.environ.get('AA_FLEETUP_API_ID', '') +FLEETUP_GROUP_ID = os.environ.get('AA_FLEETUP_GROUP_ID', '') + +PASSWORD_HASHERS = [ + 'django.contrib.auth.hashers.MD5PasswordHasher', +] + + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format' : "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", + 'datefmt' : "%d/%b/%Y %H:%M:%S" + }, + 'simple': { + 'format': '%(levelname)s %(message)s' + }, + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', # edit this line to change logging level to console + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + 'notifications': { # creates notifications for users with logging_notifications permission + 'level': 'ERROR', # edit this line to change logging level to notifications + 'class': 'notifications.handlers.NotificationHandler', + 'formatter': 'verbose', + }, + }, + 'loggers': { + 'authentication': { + 'handlers': ['console', 'notifications'], + 'level': 'DEBUG', + }, + 'celerytask': { + 'handlers': ['console', 'notifications'], + 'level': 'DEBUG', + }, + 'eveonline': { + 'handlers': ['console', 'notifications'], + 'level': 'DEBUG', + }, + 'groupmanagement': { + 'handlers': ['console', 'notifications'], + 'level': 'DEBUG', + }, + 'hrapplications': { + 'handlers': ['console', 'notifications'], + 'level': 'DEBUG', + }, + 'portal': { + 'handlers': ['console', 'notifications'], + 'level': 'DEBUG', + }, + 'registration': { + 'handlers': ['console', 'notifications'], + 'level': 'DEBUG', + }, + 'services': { + 'handlers': ['console', 'notifications'], + 'level': 'DEBUG', + }, + 'srp': { + 'handlers': ['console', 'notifications'], + 'level': 'DEBUG', + }, + 'timerboard': { + 'handlers': ['console', 'notifications'], + 'level': 'DEBUG', + }, + 'sigtracker': { + 'handlers': ['console', 'notifications'], + 'level': 'DEBUG', + }, + 'optimer': { + 'handlers': ['console', 'notifications'], + 'level': 'DEBUG', + }, + 'corputils': { + 'handlers': ['console', 'notifications'], + 'level': 'DEBUG', + }, + 'fleetactivitytracking': { + 'handlers': ['console', 'notifications'], + 'level': 'ERROR', + }, + 'util': { + 'handlers': ['console', 'notifications'], + 'level': 'DEBUG', + }, + 'django': { + 'handlers': ['console', 'notifications'], + 'level': 'ERROR', + }, + } +} + +LOGGING = None # Comment out to enable logging for debugging diff --git a/alliance_auth/urls.py b/alliance_auth/urls.py index 48f137c4..b857d402 100755 --- a/alliance_auth/urls.py +++ b/alliance_auth/urls.py @@ -21,6 +21,8 @@ from alliance_auth import NAME admin.site.site_header = NAME +from alliance_auth.hooks import get_hooks + # Functional/Untranslated URL's urlpatterns = [ # Locale @@ -47,85 +49,6 @@ urlpatterns = [ name='auth_main_character_change'), url(r'^api_verify_owner/(\w+)/$', eveonline.views.api_sso_validate, name='auth_api_sso'), - # Forum Service Control - url(r'^activate_forum/$', services.views.activate_forum, name='auth_activate_forum'), - url(r'^deactivate_forum/$', services.views.deactivate_forum, name='auth_deactivate_forum'), - url(r'^reset_forum_password/$', services.views.reset_forum_password, - name='auth_reset_forum_password'), - url(r'^set_forum_password/$', services.views.set_forum_password, name='auth_set_forum_password'), - - # Jabber Service Control - url(r'^activate_jabber/$', services.views.activate_jabber, name='auth_activate_jabber'), - url(r'^deactivate_jabber/$', services.views.deactivate_jabber, name='auth_deactivate_jabber'), - url(r'^reset_jabber_password/$', services.views.reset_jabber_password, - name='auth_reset_jabber_password'), - - # Mumble service control - url(r'^activate_mumble/$', services.views.activate_mumble, name='auth_activate_mumble'), - url(r'^deactivate_mumble/$', services.views.deactivate_mumble, name='auth_deactivate_mumble'), - url(r'^reset_mumble_password/$', services.views.reset_mumble_password, - name='auth_reset_mumble_password'), - url(r'^set_mumble_password/$', services.views.set_mumble_password, name='auth_set_mumble_password'), - - # Ipboard service control - url(r'^activate_ipboard/$', services.views.activate_ipboard_forum, - name='auth_activate_ipboard'), - url(r'^deactivate_ipboard/$', services.views.deactivate_ipboard_forum, - name='auth_deactivate_ipboard'), - url(r'^reset_ipboard_password/$', services.views.reset_ipboard_password, - name='auth_reset_ipboard_password'), - url(r'^set_ipboard_password/$', services.views.set_ipboard_password, name='auth_set_ipboard_password'), - - # XenForo service control - url(r'^activate_xenforo/$', services.views.activate_xenforo_forum, - name='auth_activate_xenforo'), - url(r'^deactivate_xenforo/$', services.views.deactivate_xenforo_forum, - name='auth_deactivate_xenforo'), - url(r'^reset_xenforo_password/$', services.views.reset_xenforo_password, - name='auth_reset_xenforo_password'), - url(r'^set_xenforo_password/$', services.views.set_xenforo_password, name='auth_set_xenforo_password'), - - # Teamspeak3 service control - url(r'^activate_teamspeak3/$', services.views.activate_teamspeak3, - name='auth_activate_teamspeak3'), - url(r'^deactivate_teamspeak3/$', services.views.deactivate_teamspeak3, - name='auth_deactivate_teamspeak3'), - url(r'reset_teamspeak3_perm/$', services.views.reset_teamspeak3_perm, - name='auth_reset_teamspeak3_perm'), - - # Discord Service Control - url(r'^activate_discord/$', services.views.activate_discord, name='auth_activate_discord'), - url(r'^deactivate_discord/$', services.views.deactivate_discord, name='auth_deactivate_discord'), - url(r'^reset_discord/$', services.views.reset_discord, name='auth_reset_discord'), - url(r'^discord_callback/$', services.views.discord_callback, name='auth_discord_callback'), - url(r'^discord_add_bot/$', services.views.discord_add_bot, name='auth_discord_add_bot'), - - # Discourse Service Control - url(r'^discourse_sso$', services.views.discourse_sso, name='auth_discourse_sso'), - - # IPS4 Service Control - url(r'^activate_ips4/$', services.views.activate_ips4, - name='auth_activate_ips4'), - url(r'^deactivate_ips4/$', services.views.deactivate_ips4, - name='auth_deactivate_ips4'), - url(r'^reset_ips4_password/$', services.views.reset_ips4_password, - name='auth_reset_ips4_password'), - url(r'^set_ips4_password/$', services.views.set_ips4_password, name='auth_set_ips4_password'), - - # SMF Service Control - url(r'^activate_smf/$', services.views.activate_smf, name='auth_activate_smf'), - url(r'^deactivate_smf/$', services.views.deactivate_smf, name='auth_deactivate_smf'), - url(r'^reset_smf_password/$', services.views.reset_smf_password, - name='auth_reset_smf_password'), - url(r'^set_smf_password/$', services.views.set_smf_password, name='auth_set_smf_password'), - - # Alliance Market Control - url(r'^activate_market/$', services.views.activate_market, name='auth_activate_market'), - url(r'^deactivate_market/$', services.views.deactivate_market, name='auth_deactivate_market'), - url(r'^reset_market_password/$', services.views.reset_market_password, - name='auth_reset_market_password'), - url(r'^set_market_password/$', services.views.set_market_password, name='auth_set_market_password'), - # SRP URLS url(r'^srp_fleet_remove/(\w+)$', srp.views.srp_fleet_remove, name='auth_srp_fleet_remove'), url(r'^srp_fleet_disable/(\w+)$', srp.views.srp_fleet_disable, name='auth_srp_fleet_disable'), @@ -241,11 +164,6 @@ urlpatterns += i18n_patterns( # Service Urls url(_(r'^services/$'), services.views.services_view, name='auth_services'), - url(_(r'^services/jabber_broadcast/$'), services.views.jabber_broadcast_view, - name='auth_jabber_broadcast_view'), - - # Teamspeak Urls - url(r'verify_teamspeak3/$', services.views.verify_teamspeak3, name='auth_verify_teamspeak3'), # Timer URLS url(_(r'^timers/$'), timerboard.views.timer_view, name='auth_timer_view'), @@ -271,9 +189,6 @@ urlpatterns += i18n_patterns( url(_(r'^notifications/$'), notifications.views.notification_list, name='auth_notification_list'), url(_(r'^notifications/(\w+)/$'), notifications.views.notification_view, name='auth_notification_view'), - # Jabber - url(_(r'^set_jabber_password/$'), services.views.set_jabber_password, name='auth_set_jabber_password'), - # FleetActivityTracking (FAT) url(r'^fat/$', fleetactivitytracking.views.fatlink_view, name='auth_fatlink_view'), url(r'^fat/statistics/$', fleetactivitytracking.views.fatlink_statistics_view, name='auth_fatlink_view_statistics'), @@ -297,3 +212,9 @@ urlpatterns += i18n_patterns( url(r'^fat/link/(?P[a-zA-Z0-9]+)/(?P[a-z0-9_-]+)/$', fleetactivitytracking.views.click_fatlink_view), ) + +# Append hooked service urls +services = get_hooks('services_hook') +for svc in services: + urlpatterns += svc().urlpatterns + diff --git a/authentication/admin.py b/authentication/admin.py index 22860d17..06b255ff 100644 --- a/authentication/admin.py +++ b/authentication/admin.py @@ -1,22 +1,19 @@ from __future__ import unicode_literals from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.auth.models import User +from django.utils.text import slugify from authentication.models import AuthServicesInfo from eveonline.models import EveCharacter -from services.tasks import update_jabber_groups -from services.tasks import update_mumble_groups -from services.tasks import update_forum_groups -from services.tasks import update_ipboard_groups -from services.tasks import update_smf_groups -from services.tasks import update_teamspeak3_groups -from services.tasks import update_discord_groups -from services.tasks import update_discord_nickname -from services.tasks import update_discourse_groups +from alliance_auth.hooks import get_hooks +from services.hooks import ServicesHook @admin.register(AuthServicesInfo) class AuthServicesInfoManager(admin.ModelAdmin): + @staticmethod def main_character(obj): if obj.main_char_id: @@ -34,120 +31,69 @@ class AuthServicesInfoManager(admin.ModelAdmin): def has_add_permission(request, obj=None): return False - def sync_jabber(self, request, queryset): - count = 0 - for a in queryset: # queryset filtering doesn't work here? - if a.jabber_username != "": - update_jabber_groups.delay(a.user.pk) - count += 1 - self.message_user(request, "%s jabber accounts queued for group sync." % count) - - sync_jabber.short_description = "Sync groups for selected jabber accounts" - - def sync_mumble(self, request, queryset): - count = 0 - for a in queryset: - if a.mumble_username != "": - update_mumble_groups.delay(a.user.pk) - count += 1 - self.message_user(request, "%s mumble accounts queued for group sync." % count) - - sync_mumble.short_description = "Sync groups for selected mumble accounts" - - def sync_forum(self, request, queryset): - count = 0 - for a in queryset: - if a.forum_username != "": - update_forum_groups.delay(a.user.pk) - count += 1 - self.message_user(request, "%s forum accounts queued for group sync." % count) - - sync_forum.short_description = "Sync groups for selected forum accounts" - - def sync_ipboard(self, request, queryset): - count = 0 - for a in queryset: - if a.ipboard_username != "": - update_ipboard_groups.delay(a.user.pk) - count += 1 - self.message_user(request, "%s ipboard accounts queued for group sync." % count) - - sync_ipboard.short_description = "Sync groups for selected ipboard accounts" - - def sync_smf(self, request, queryset): - count = 0 - for a in queryset: - if a.smf_username != "": - update_smf_groups.delay(a.user.pk) - count += 1 - self.message_user(request, "%s smf accounts queued for group sync." % count) - - sync_smf.short_description = "Sync groups for selected smf accounts" - - def sync_teamspeak(self, request, queryset): - count = 0 - for a in queryset: - if a.teamspeak3_uid != "": - update_teamspeak3_groups.delay(a.user.pk) - count += 1 - self.message_user(request, "%s teamspeak accounts queued for group sync." % count) - - sync_teamspeak.short_description = "Sync groups for selected teamspeak accounts" - - def sync_discord(self, request, queryset): - count = 0 - for a in queryset: - if a.discord_uid != "": - update_discord_groups.delay(a.user.pk) - count += 1 - self.message_user(request, "%s discord accounts queued for group sync." % count) - - sync_discord.short_description = "Sync groups for selected discord accounts" - - def sync_discourse(self, request, queryset): - count = 0 - for a in queryset: - if a.discourse_enabled: - update_discourse_groups.delay(a.user.pk) - count += 1 - self.message_user(request, "%s discourse accounts queued for group sync." % count) - - sync_discourse.short_description = "Sync groups for selected discourse accounts" - - def sync_nicknames(self, request, queryset): - count = 0 - for a in queryset: - if a.discord_uid != "": - update_discord_nickname(a.user.pk) - count += 1 - self.message_user(request, "%s discord accounts queued for nickname sync." % count) - - sync_nicknames.short_description = "Sync nicknames for selected discord accounts" - - actions = [ - 'sync_jabber', - 'sync_mumble', - 'sync_forum', - 'sync_ipboard', - 'sync_smf', - 'sync_teamspeak', - 'sync_discord', - 'sync_discourse', - 'sync_nicknames', - ] - search_fields = [ 'user__username', - 'ipboard_username', - 'xenforo_username', - 'forum_username', - 'jabber_username', - 'mumble_username', - 'teamspeak3_uid', - 'discord_uid', - 'ips4_username', - 'smf_username', - 'market_username', 'main_char_id', ] list_display = ('user', 'main_character') + + +def make_service_hooks_update_groups_action(service): + """ + Make a admin action for the given service + :param service: services.hooks.ServicesHook + :return: fn to update services groups for the selected users + """ + def update_service_groups(modeladmin, request, queryset): + for user in queryset: # queryset filtering doesn't work here? + service.update_groups(user) + + update_service_groups.__name__ = str('update_{}_groups'.format(slugify(service.name))) + update_service_groups.short_description = "Sync groups for selected {} accounts".format(service.title) + return update_service_groups + + +def make_service_hooks_sync_nickname_action(service): + """ + Make a sync_nickname admin action for the given service + :param service: services.hooks.ServicesHook + :return: fn to sync nickname for the selected users + """ + def sync_nickname(modeladmin, request, queryset): + for user in queryset: # queryset filtering doesn't work here? + service.sync_nickname(user) + + sync_nickname.__name__ = str('sync_{}_nickname'.format(slugify(service.name))) + sync_nickname.short_description = "Sync nicknames for selected {} accounts".format(service.title) + return sync_nickname + + +class UserAdmin(BaseUserAdmin): + """ + Extending Django's UserAdmin model + """ + def get_actions(self, request): + actions = super(BaseUserAdmin, self).get_actions(request) + + for hook in get_hooks('services_hook'): + svc = hook() + # Check update_groups is redefined/overloaded + if svc.update_groups.__module__ != ServicesHook.update_groups.__module__: + action = make_service_hooks_update_groups_action(svc) + actions[action.__name__] = (action, + action.__name__, + action.short_description) + # Create sync nickname action if service implements it + if svc.sync_nickname.__module__ != ServicesHook.sync_nickname.__module__: + action = make_service_hooks_sync_nickname_action(svc) + actions[action.__name__] = (action, + action.__name__, + action.short_description) + + return actions + +# Re-register UserAdmin +try: + admin.site.unregister(User) +finally: + admin.site.register(User, UserAdmin) diff --git a/authentication/managers.py b/authentication/managers.py index f9b2ef7b..4bfa0a39 100755 --- a/authentication/managers.py +++ b/authentication/managers.py @@ -24,73 +24,6 @@ class AuthServicesInfoManager: else: logger.error("Failed to update user %s main character id to %s: user does not exist." % (user, char_id)) - @staticmethod - def update_user_forum_info(username, user): - if User.objects.filter(username=user.username).exists(): - logger.debug("Updating user %s forum info: username %s" % (user, username)) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - authserviceinfo.forum_username = username - authserviceinfo.save(update_fields=['forum_username']) - logger.info("Updated user %s forum info in authservicesinfo model." % user) - else: - logger.error("Failed to update user %s forum info: user does not exist." % user) - - @staticmethod - def update_user_jabber_info(username, user): - if User.objects.filter(username=user.username).exists(): - logger.debug("Updating user %s jabber info: username %s" % (user, username)) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - authserviceinfo.jabber_username = username - authserviceinfo.save(update_fields=['jabber_username']) - logger.info("Updated user %s jabber info in authservicesinfo model." % user) - else: - logger.error("Failed to update user %s jabber info: user does not exist." % user) - - @staticmethod - def update_user_mumble_info(username, user): - if User.objects.filter(username=user.username).exists(): - logger.debug("Updating user %s mumble info: username %s" % (user, username)) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - authserviceinfo.mumble_username = username - authserviceinfo.save(update_fields=['mumble_username']) - logger.info("Updated user %s mumble info in authservicesinfo model." % user) - else: - logger.error("Failed to update user %s mumble info: user does not exist." % user) - - @staticmethod - def update_user_ipboard_info(username, user): - if User.objects.filter(username=user.username).exists(): - logger.debug("Updating user %s ipboard info: uername %s" % (user, username)) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - authserviceinfo.ipboard_username = username - authserviceinfo.save(update_fields=['ipboard_username']) - logger.info("Updated user %s ipboard info in authservicesinfo model." % user) - else: - logger.error("Failed to update user %s ipboard info: user does not exist." % user) - - @staticmethod - def update_user_xenforo_info(username, user): - if User.objects.filter(username=user.username).exists(): - logger.debug("Updating user %s xenforo info: uername %s" % (user, username)) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - authserviceinfo.xenforo_username = username - authserviceinfo.save(update_fields=['xenforo_username']) - logger.info("Updated user %s xenforo info in authservicesinfo model." % user) - else: - logger.error("Failed to update user %s xenforo info: user does not exist." % user) - - @staticmethod - def update_user_teamspeak3_info(uid, perm_key, user): - if User.objects.filter(username=user.username).exists(): - logger.debug("Updating user %s teamspeak3 info: uid %s" % (user, uid)) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - authserviceinfo.teamspeak3_uid = uid - authserviceinfo.teamspeak3_perm_key = perm_key - authserviceinfo.save(update_fields=['teamspeak3_uid', 'teamspeak3_perm_key']) - logger.info("Updated user %s teamspeak3 info in authservicesinfo model." % user) - else: - logger.error("Failed to update user %s teamspeak3 info: user does not exist." % user) - @staticmethod def update_is_blue(is_blue, user): if User.objects.filter(username=user.username).exists(): @@ -100,51 +33,6 @@ class AuthServicesInfoManager: authserviceinfo.save(update_fields=['is_blue']) logger.info("Updated user %s blue status to %s in authservicesinfo model." % (user, is_blue)) - @staticmethod - def update_user_discord_info(user_id, user): - if User.objects.filter(username=user.username).exists(): - logger.debug("Updating user %s discord info: user_id %s" % (user, user_id)) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - authserviceinfo.discord_uid = user_id - authserviceinfo.save(update_fields=['discord_uid']) - logger.info("Updated user %s discord info in authservicesinfo model." % user) - else: - logger.error("Failed to update user %s discord info: user does not exist." % user) - - @staticmethod - def update_user_ips4_info(username, id, user): - if User.objects.filter(username=user.username).exists(): - logger.debug("Updating user %s IPS4 info: username %s" % (user, username)) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - authserviceinfo.ips4_username = username - authserviceinfo.ips4_id = id - authserviceinfo.save(update_fields=['ips4_username', 'ips4_id']) - logger.info("Updated user %s IPS4 info in authservicesinfo model." % user) - else: - logger.error("Failed to update user %s IPS4 info: user does not exist." % user) - - @staticmethod - def update_user_smf_info(username, user): - if User.objects.filter(username=user.username).exists(): - logger.debug("Updating user %s forum info: username %s" % (user, username)) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - authserviceinfo.smf_username = username - authserviceinfo.save(update_fields=['smf_username']) - logger.info("Updated user %s smf info in authservicesinfo model." % user) - else: - logger.error("Failed to update user %s smf info: user does not exist." % user) - - @staticmethod - def update_user_market_info(username, user): - if User.objects.filter(username=user.username).exists(): - logger.debug("Updating user %s market info: username %s" % (user, username)) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - authserviceinfo.market_username = username - authserviceinfo.save(update_fields=['market_username']) - logger.info("Updated user %s market info in authservicesinfo model." % user) - else: - logger.error("Failed to update user %s market info: user does not exist." % user) - class UserState: def __init__(self): diff --git a/authentication/migrations/0013_service_modules.py b/authentication/migrations/0013_service_modules.py new file mode 100644 index 00000000..11f2c613 --- /dev/null +++ b/authentication/migrations/0013_service_modules.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-12-11 23:14 +from __future__ import unicode_literals + +from django.db import migrations +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist + +import logging + +logger = logging.getLogger(__name__) + + +def optional_dependencies(): + """ + Only require these migrations if the given app + is installed. If the app isn't installed then + the relevant AuthServicesInfo field will be LOST + when the data migration is run. + """ + installed_apps = settings.INSTALLED_APPS + + dependencies = [] + + if 'services.modules.xenforo' in installed_apps: + dependencies.append(('xenforo', '0001_initial')) + if 'services.modules.discord' in installed_apps: + dependencies.append(('discord', '0001_initial')) + if 'services.modules.discourse' in installed_apps: + dependencies.append(('discourse', '0001_initial')) + if 'services.modules.ipboard' in installed_apps: + dependencies.append(('ipboard', '0001_initial')) + if 'services.modules.ips4' in installed_apps: + dependencies.append(('ips4', '0001_initial')) + if 'services.modules.market' in installed_apps: + dependencies.append(('market', '0001_initial')) + if 'services.modules.openfire' in installed_apps: + dependencies.append(('openfire', '0001_initial')) + if 'services.modules.smf' in installed_apps: + dependencies.append(('smf', '0001_initial')) + if 'services.modules.teamspeak3' in installed_apps: + dependencies.append(('teamspeak3', '0003_teamspeak3user')) + if 'services.modules.mumble' in installed_apps: + dependencies.append(('mumble', '0003_mumbleuser_user')) + if 'services.modules.phpbb3' in installed_apps: + dependencies.append(('phpbb3', '0001_initial')) + + return dependencies + + +def forward(apps, schema_editor): + installed_apps = settings.INSTALLED_APPS + AuthServicesInfo = apps.get_model("authentication", "AuthServicesInfo") + + XenforoUser = apps.get_model('xenforo', 'XenforoUser') if 'services.modules.xenforo' in installed_apps else None + DiscordUser = apps.get_model('discord', 'DiscordUser') if 'services.modules.discord' in installed_apps else None + DiscourseUser = apps.get_model('discourse', 'DiscourseUser') if 'services.modules.discourse' in installed_apps else None + IpboardUser = apps.get_model('ipboard', 'IpboardUser') if 'services.modules.ipboard' in installed_apps else None + Ips4User = apps.get_model('ips4', 'Ips4User') if 'services.modules.ips4' in installed_apps else None + MarketUser = apps.get_model('market', 'MarketUser') if 'services.modules.market' in installed_apps else None + OpenfireUser = apps.get_model('openfire', 'OpenfireUser') if 'services.modules.openfire' in installed_apps else None + SmfUser = apps.get_model('smf', 'SmfUser') if 'services.modules.smf' in installed_apps else None + Teamspeak3User = apps.get_model('teamspeak3', 'Teamspeak3User') if 'services.modules.teamspeak3' in installed_apps else None + MumbleUser = apps.get_model('mumble', 'MumbleUser') if 'services.modules.mumble' in installed_apps else None + Phpbb3User = apps.get_model('phpbb3', 'Phpbb3User') if 'services.modules.phpbb3' in installed_apps else None + + for authinfo in AuthServicesInfo.objects.all(): + user = authinfo.user + + if XenforoUser is not None and authinfo.xenforo_username: + logging.debug('Updating Xenforo info for %s' % user.username) + xfu = XenforoUser() + xfu.user = user + xfu.username = authinfo.xenforo_username + xfu.save() + + if DiscordUser is not None and authinfo.discord_uid: + logging.debug('Updating Discord info for %s' % user.username) + du = DiscordUser() + du.user = user + du.uid = authinfo.discord_uid + du.save() + + if DiscourseUser is not None and authinfo.discourse_enabled: + logging.debug('Updating Discourse info for %s' % user.username) + du = DiscourseUser() + du.user = user + du.enabled = authinfo.discourse_enabled + du.save() + + if IpboardUser is not None and authinfo.ipboard_username: + logging.debug('Updating IPBoard info for %s' % user.username) + ipb = IpboardUser() + ipb.user = user + ipb.username = authinfo.ipboard_username + ipb.save() + + if Ips4User is not None and authinfo.ips4_id: + logging.debug('Updating Ips4 info for %s' % user.username) + ips = Ips4User() + ips.user = user + ips.id = authinfo.ips4_id + ips.username = authinfo.ips4_username + ips.save() + + if MarketUser is not None and authinfo.market_username: + logging.debug('Updating Market info for %s' % user.username) + mkt = MarketUser() + mkt.user = user + mkt.username = authinfo.market_username + mkt.save() + + if OpenfireUser is not None and authinfo.jabber_username: + logging.debug('Updating Openfire (jabber) info for %s' % user.username) + ofu = OpenfireUser() + ofu.user = user + ofu.username = authinfo.jabber_username + ofu.save() + + if SmfUser is not None and authinfo.smf_username: + logging.debug('Updating SMF info for %s' % user.username) + smf = SmfUser() + smf.user = user + smf.username = authinfo.smf_username + smf.save() + + if Teamspeak3User is not None and authinfo.teamspeak3_uid: + logging.debug('Updating Teamspeak3 info for %s' % user.username) + ts3 = Teamspeak3User() + ts3.user = user + ts3.uid = authinfo.teamspeak3_uid + ts3.perm_key = authinfo.teamspeak3_perm_key + ts3.save() + + if MumbleUser is not None and authinfo.mumble_username: + logging.debug('Updating mumble info for %s' % user.username) + try: + mbl = MumbleUser.objects.get(username=authinfo.mumble_username) + mbl.user = user + mbl.save() + except ObjectDoesNotExist: + logger.warn('AuthServiceInfo mumble_username for {} but no ' + 'corresponding record in MumbleUser, dropping'.format(user.username)) + + if Phpbb3User is not None and authinfo.forum_username: + logging.debug('Updating phpbb3 info for %s' % user.username) + phb = Phpbb3User() + phb.user = user + phb.username = authinfo.forum_username + phb.save() + + +def reverse(apps, schema_editor): + User = apps.get_model('auth', 'User') + AuthServicesInfo = apps.get_model("authentication", "AuthServicesInfo") + + for user in User.objects.all(): + authinfo, c = AuthServicesInfo.objects.get_or_create(user=user) + + if hasattr(user, 'xenforo'): + logging.debug('Reversing xenforo for %s' % user.username) + authinfo.xenforo_username = user.xenforo.username + + if hasattr(user, 'discord'): + logging.debug('Reversing discord for %s' % user.username) + authinfo.discord_uid = user.discord.uid + + if hasattr(user, 'discourse'): + logging.debug('Reversing discourse for %s' % user.username) + authinfo.discourse_enabled = user.discourse.enabled + + if hasattr(user, 'ipboard'): + logging.debug('Reversing ipboard for %s' % user.username) + authinfo.ipboard_username = user.ipboard.username + + if hasattr(user, 'ips4'): + logging.debug('Reversing ips4 for %s' % user.username) + authinfo.ips4_id = user.ips4.id + authinfo.ips4_username = user.ips4.username + + if hasattr(user, 'market'): + logging.debug('Reversing market for %s' % user.username) + authinfo.market_username = user.market.username + + if hasattr(user, 'openfire'): + logging.debug('Reversing openfire (jabber) for %s' % user.username) + authinfo.jabber_username = user.openfire.username + + if hasattr(user, 'smf'): + logging.debug('Reversing smf for %s' % user.username) + authinfo.smf_username = user.smf.username + + if hasattr(user, 'teamspeak3'): + logging.debug('Reversing teamspeak3 for %s' % user.username) + authinfo.teamspeak3_uid = user.teamspeak3.uid + authinfo.teamspeak3_perm_key = user.teamspeak3.perm_key + + if hasattr(user, 'mumble'): + logging.debug('Reversing mumble for %s' % user.username) + try: + authinfo.mumble_username = user.mumble.all()[:1].get().username + except ObjectDoesNotExist: + logging.debug('Failed to reverse mumble for %s' % user.username) + + if hasattr(user, 'phpbb3'): + logging.debug('Reversing phpbb3 for %s' % user.username) + authinfo.forum_username = user.phpbb3.username + + logging.debug('Saving AuthServicesInfo for %s ' % user.username) + authinfo.save() + + +class Migration(migrations.Migration): + + dependencies = optional_dependencies() + [ + ('authentication', '0012_remove_add_delete_authservicesinfo_permissions'), + ] + + operations = [ + # Migrate data + migrations.RunPython(forward, reverse), + # Remove fields + migrations.RemoveField( + model_name='authservicesinfo', + name='discord_uid', + ), + migrations.RemoveField( + model_name='authservicesinfo', + name='discourse_enabled', + ), + migrations.RemoveField( + model_name='authservicesinfo', + name='forum_username', + ), + migrations.RemoveField( + model_name='authservicesinfo', + name='ipboard_username', + ), + migrations.RemoveField( + model_name='authservicesinfo', + name='ips4_id', + ), + migrations.RemoveField( + model_name='authservicesinfo', + name='ips4_username', + ), + migrations.RemoveField( + model_name='authservicesinfo', + name='jabber_username', + ), + migrations.RemoveField( + model_name='authservicesinfo', + name='market_username', + ), + migrations.RemoveField( + model_name='authservicesinfo', + name='mumble_username', + ), + migrations.RemoveField( + model_name='authservicesinfo', + name='smf_username', + ), + migrations.RemoveField( + model_name='authservicesinfo', + name='teamspeak3_perm_key', + ), + migrations.RemoveField( + model_name='authservicesinfo', + name='teamspeak3_uid', + ), + migrations.RemoveField( + model_name='authservicesinfo', + name='xenforo_username', + ), + ] diff --git a/authentication/models.py b/authentication/models.py index 024c037c..98c398de 100755 --- a/authentication/models.py +++ b/authentication/models.py @@ -16,19 +16,6 @@ class AuthServicesInfo(models.Model): (MEMBER_STATE, 'Member'), ) - ipboard_username = models.CharField(max_length=254, blank=True, default="") - xenforo_username = models.CharField(max_length=254, blank=True, default="") - forum_username = models.CharField(max_length=254, blank=True, default="") - jabber_username = models.CharField(max_length=254, blank=True, default="") - mumble_username = models.CharField(max_length=254, blank=True, default="") - teamspeak3_uid = models.CharField(max_length=254, blank=True, default="") - teamspeak3_perm_key = models.CharField(max_length=254, blank=True, default="") - discord_uid = models.CharField(max_length=254, blank=True, default="") - discourse_enabled = models.BooleanField(default=False, blank=True) - ips4_username = models.CharField(max_length=254, blank=True, default="") - ips4_id = models.CharField(max_length=254, blank=True, default="") - smf_username = models.CharField(max_length=254, blank=True, default="") - market_username = models.CharField(max_length=254, blank=True, default="") main_char_id = models.CharField(max_length=64, blank=True, default="") user = models.OneToOneField(User) state = models.CharField(blank=True, null=True, choices=STATE_CHOICES, default=NONE_STATE, max_length=10) diff --git a/docs/installation/services/mumble.md b/docs/installation/services/mumble.md index 7cc494c0..4e96c217 100644 --- a/docs/installation/services/mumble.md +++ b/docs/installation/services/mumble.md @@ -11,6 +11,10 @@ The mumble server package can be retrieved from a repository we need to add, mum Now two packages need to be installed: sudo apt-get install python-software-properties mumble-server + +You will also need to install the python dependencies for the authenticator script: + + pip install -r thirdparty/Mumble/requirements.txt ## Configuring Mumble Mumble ships with a configuration file that needs customization. By default it’s located at /etc/mumble-server.ini. Open it with your favourite text editor: diff --git a/eveonline/managers.py b/eveonline/managers.py index 013fecca..32af3312 100644 --- a/eveonline/managers.py +++ b/eveonline/managers.py @@ -3,6 +3,7 @@ from eveonline.models import EveCharacter from eveonline.models import EveApiKeyPair from eveonline.models import EveAllianceInfo from eveonline.models import EveCorporationInfo +from authentication.models import AuthServicesInfo from eveonline.providers import eve_adapter_factory, EveXmlProvider from services.managers.eve_api_manager import EveApiManager import logging @@ -241,6 +242,16 @@ class EveManager(object): return None + @staticmethod + def get_main_character(user): + """ + Get a characters main + :param user: django.contrib.auth.models.User + :return: EveCharacter + """ + authserviceinfo = AuthServicesInfo.objects.get(user=user) + return EveManager.get_character_by_id(authserviceinfo.main_char_id) + @staticmethod def get_characters_by_api_id(api_id): return EveCharacter.objects.filter(api_id=api_id) diff --git a/services/managers/fleetup_manager.py b/fleetup/managers.py similarity index 100% rename from services/managers/fleetup_manager.py rename to fleetup/managers.py diff --git a/fleetup/views.py b/fleetup/views.py index b92fe451..fbd832d6 100755 --- a/fleetup/views.py +++ b/fleetup/views.py @@ -4,7 +4,7 @@ from django.shortcuts import render from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import permission_required from django.template.defaulttags import register -from services.managers.fleetup_manager import FleetUpManager +from fleetup.managers import FleetUpManager from authentication.decorators import members_and_blues import logging diff --git a/requirements.txt b/requirements.txt index 1aaf75f3..5ccdd5a0 100755 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,6 @@ dnspython passlib requests>=2.9.1 bcrypt -zeroc-ice slugify requests-oauthlib sleekxmpp diff --git a/runtests.py b/runtests.py new file mode 100644 index 00000000..4088735c --- /dev/null +++ b/runtests.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +from __future__ import unicode_literals +import os +import sys + +if __name__ == "__main__": + os.environ['DJANGO_SETTINGS_MODULE'] = 'alliance_auth.tests.test_settings' + + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + execute_from_command_line(sys.argv.insert(1, 'test')) diff --git a/services/admin.py b/services/admin.py index cfb11651..5da65126 100644 --- a/services/admin.py +++ b/services/admin.py @@ -1,17 +1,5 @@ from __future__ import unicode_literals from django.contrib import admin -from services.models import AuthTS -from services.models import MumbleUser from services.models import GroupCache - -class AuthTSgroupAdmin(admin.ModelAdmin): - fields = ['auth_group', 'ts_group'] - filter_horizontal = ('ts_group',) - - -admin.site.register(AuthTS, AuthTSgroupAdmin) - -admin.site.register(MumbleUser) - admin.site.register(GroupCache) diff --git a/services/forms.py b/services/forms.py index 4d97c738..c892563c 100644 --- a/services/forms.py +++ b/services/forms.py @@ -1,14 +1,9 @@ from __future__ import unicode_literals + from django import forms -from services.managers.teamspeak3_manager import Teamspeak3Manager from django.utils.translation import ugettext_lazy as _ -class JabberBroadcastForm(forms.Form): - group = forms.ChoiceField(label=_('Group'), widget=forms.Select) - message = forms.CharField(label=_('Message'), widget=forms.Textarea) - - class FleetFormatterForm(forms.Form): fleet_name = forms.CharField(label=_('Name of Fleet:'), required=True) fleet_commander = forms.CharField(label=_('Fleet Commander:'), required=True) @@ -26,12 +21,6 @@ class FleetFormatterForm(forms.Form): comments = forms.CharField(label=_('Comments'), widget=forms.Textarea, required=False) -class DiscordForm(forms.Form): - email = forms.CharField(label=_("Email Address"), required=True) - password = forms.CharField(label=_("Password"), required=True, widget=forms.PasswordInput) - update_avatar = forms.BooleanField(label=_("Update Avatar"), required=False, initial=True) - - class ServicePasswordForm(forms.Form): password = forms.CharField(label=_("Password"), required=True) @@ -40,12 +29,3 @@ class ServicePasswordForm(forms.Form): if not len(password) >= 8: raise forms.ValidationError(_("Password must be at least 8 characters long.")) return password - - -class TeamspeakJoinForm(forms.Form): - username = forms.CharField(widget=forms.HiddenInput()) - - def clean(self): - if Teamspeak3Manager._get_userid(self.cleaned_data['username']): - return self.cleaned_data - raise forms.ValidationError(_("Unable to locate user %s on server") % self.cleaned_data['username']) diff --git a/services/hooks.py b/services/hooks.py new file mode 100644 index 00000000..921cb3ab --- /dev/null +++ b/services/hooks.py @@ -0,0 +1,134 @@ +from __future__ import unicode_literals + +from django.template.loader import render_to_string +from django.utils.safestring import mark_safe + +from authentication.states import MEMBER_STATE, BLUE_STATE +from authentication.models import AuthServicesInfo + + +class ServicesHook: + """ + Abstract base class for creating a compatible services + hook. Decorate with @register('services_hook') to have the + services module registered for callbacks. Must be in + auth_hook(.py) sub module + """ + def __init__(self): + self.name = 'Undefined' + self.urlpatterns = [] + self.service_ctrl_template = 'registered/services_ctrl.html' + + @property + def title(self): + """ + A nicely formatted title of the service, for client facing + display. + :return: str + """ + return self.name.title() + + def delete_user(self, user, notify_user=False): + """ + Delete the users service account, optionally notify them + that the service has been disabled + :param user: Django.contrib.auth.models.User + :param notify_user: Whether the service should sent a + notification to the user about the disabling of their + service account. + :return: True if the service account has been disabled, + or False if it doesnt exist. + """ + pass + + def validate_user(self, user): + pass + + def sync_nickname(self, user): + """ + Sync the users nickname + :param user: Django.contrib.auth.models.User + :return: None + """ + pass + + def update_groups(self, user): + """ + Update the users group membership + :param user: Django.contrib.auth.models.User + :return: None + """ + pass + + def update_all_groups(self): + """ + Iterate through and update all users groups + :return: None + """ + pass + + def service_enabled_members(self): + """ + Return setting config for service enabled for members + :return: bool True if enabled + """ + return False + + def service_enabled_blues(self): + """ + Return setting config for service enabled for Blues + :return: bool True if enabled + """ + return False + + def service_active_for_user(self, user): + state = AuthServicesInfo.objects.get(user=user).state + return ( + (state == MEMBER_STATE and self.service_enabled_members()) or + (state == BLUE_STATE and self.service_enabled_blues()) + ) + + def show_service_ctrl(self, user, state): + """ + Whether the service control should be displayed to the given user + who has the given service state. Usually this function wont + require overloading. + :param user: django.contrib.auth.models.User + :param state: auth user state + :return: bool True if the service should be shown + """ + return (self.service_enabled_members() and ( + state == MEMBER_STATE or user.is_superuser)) or ( + self.service_enabled_blues() and (state == BLUE_STATE or user.is_superuser)) + + def render_services_ctrl(self, request): + """ + Render the services control template row + :param request: + :return: + """ + return '' + + def __str__(self): + return self.name or 'Unknown Service Module' + + class Urls: + def __init__(self): + self.auth_activate = '' + self.auth_set_password = '' + self.auth_reset_password = '' + self.auth_deactivate = '' + + +class MenuItemHook: + def __init__(self, text, classes, url_name, order=None): + self.text = text + self.classes = classes + self.url_name = url_name + self.template = 'public/menuitem.html' + self.order = order if order is not None else 9999 + + def render(self, request): + return render_to_string(self.template, + {'item': self}, + request=request) diff --git a/services/managers/mumble_manager.py b/services/managers/mumble_manager.py deleted file mode 100755 index 8c56d44d..00000000 --- a/services/managers/mumble_manager.py +++ /dev/null @@ -1,121 +0,0 @@ -from __future__ import unicode_literals -import os -import hashlib - -from services.models import MumbleUser - -import logging - -logger = logging.getLogger(__name__) - - -class MumbleManager: - def __init__(self): - pass - - @staticmethod - def __santatize_username(username): - sanatized = username.replace(" ", "_") - return sanatized - - @staticmethod - def __generate_random_pass(): - return os.urandom(8).encode('hex') - - @staticmethod - def __generate_username(username, corp_ticker): - return "[" + corp_ticker + "]" + username - - @staticmethod - def __generate_username_blue(username, corp_ticker): - return "[BLUE][" + corp_ticker + "]" + username - - @staticmethod - def _gen_pwhash(password): - return hashlib.sha1(password).hexdigest() - - @staticmethod - def create_user(corp_ticker, username): - logger.debug("Creating mumble user with username %s and ticker %s" % (username, corp_ticker)) - username_clean = MumbleManager.__santatize_username(MumbleManager.__generate_username(username, corp_ticker)) - password = MumbleManager.__generate_random_pass() - pwhash = MumbleManager._gen_pwhash(password) - logger.debug("Proceeding with mumble user creation: clean username %s, pwhash starts with %s" % ( - username_clean, pwhash[0:5])) - if MumbleUser.objects.filter(username=username_clean).exists() is False: - logger.info("Creating mumble user %s" % username_clean) - MumbleUser.objects.create(username=username_clean, pwhash=pwhash) - return username_clean, password - else: - logger.warn("Mumble user %s already exists. Updating password") - model = MumbleUser.objects.get(username=username_clean) - model.pwhash = pwhash - model.save() - logger.info("Updated mumble user %s" % username_clean) - return username_clean, password - - @staticmethod - def create_blue_user(corp_ticker, username): - logger.debug("Creating mumble blue user with username %s and ticker %s" % (username, corp_ticker)) - username_clean = MumbleManager.__santatize_username( - MumbleManager.__generate_username_blue(username, corp_ticker)) - password = MumbleManager.__generate_random_pass() - pwhash = MumbleManager._gen_pwhash(password) - logger.debug("Proceeding with mumble user creation: clean username %s, pwhash starts with %s" % ( - username_clean, pwhash[0:5])) - if MumbleUser.objects.filter(username=username_clean).exists() is False: - logger.info("Creating mumble user %s" % username_clean) - MumbleUser.objects.create(username=username_clean, pwhash=pwhash) - return username_clean, password - else: - logger.warn("Mumble user %s already exists. Updating password") - model = MumbleUser.objects.get(username=username_clean) - model.pwhash = pwhash - model.save() - logger.info("Updated mumble user %s" % username_clean) - return username_clean, password - - @staticmethod - def delete_user(username): - logger.debug("Deleting user %s from mumble." % username) - if MumbleUser.objects.filter(username=username).exists(): - MumbleUser.objects.filter(username=username).delete() - logger.info("Deleted user %s from mumble" % username) - return True - logger.error("Unable to delete user %s from mumble: MumbleUser model not found" % username) - return False - - @staticmethod - def update_user_password(username, password=None): - logger.debug("Updating mumble user %s password." % username) - if not password: - password = MumbleManager.__generate_random_pass() - pwhash = MumbleManager._gen_pwhash(password) - logger.debug("Proceeding with mumble user %s password update - pwhash starts with %s" % (username, pwhash[0:5])) - if MumbleUser.objects.filter(username=username).exists(): - model = MumbleUser.objects.get(username=username) - model.pwhash = pwhash - model.save() - return password - logger.error("User %s not found on mumble. Unable to update password." % username) - return "" - - @staticmethod - def update_groups(username, groups): - logger.debug("Updating mumble user %s groups %s" % (username, groups)) - safe_groups = list(set([g.replace(' ', '-') for g in groups])) - groups = '' - for g in safe_groups: - groups = groups + g + ',' - groups = groups.strip(',') - if MumbleUser.objects.filter(username=username).exists(): - logger.info("Updating mumble user %s groups to %s" % (username, safe_groups)) - model = MumbleUser.objects.get(username=username) - model.groups = groups - model.save() - else: - logger.error("User %s not found on mumble. Unable to update groups." % username) - - @staticmethod - def user_exists(username): - return MumbleUser.objects.filter(username=username).exists() diff --git a/services/migrations/0001_initial.py b/services/migrations/0001_initial.py index 5787cfb8..a482d393 100644 --- a/services/migrations/0001_initial.py +++ b/services/migrations/0001_initial.py @@ -17,16 +17,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name='AuthTS', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('auth_group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')), - ], - options={ - 'verbose_name': 'Auth / TS Group', - }, - ), migrations.CreateModel( name='DiscordAuthToken', fields=[ @@ -45,39 +35,4 @@ class Migration(migrations.Migration): ('service', models.CharField(choices=[(b'discourse', b'discourse'), (b'discord', b'discord')], max_length=254, unique=True)), ], ), - migrations.CreateModel( - name='MumbleUser', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('username', models.CharField(max_length=254, unique=True)), - ('pwhash', models.CharField(max_length=40)), - ('groups', models.TextField(blank=True, null=True)), - ], - ), - migrations.CreateModel( - name='TSgroup', - fields=[ - ('ts_group_id', models.IntegerField(primary_key=True, serialize=False)), - ('ts_group_name', models.CharField(max_length=30)), - ], - options={ - 'verbose_name': 'TS Group', - }, - ), - migrations.CreateModel( - name='UserTSgroup', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('ts_group', models.ManyToManyField(to='services.TSgroup')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'User TS Group', - }, - ), - migrations.AddField( - model_name='authts', - name='ts_group', - field=models.ManyToManyField(to='services.TSgroup'), - ), ] diff --git a/services/models.py b/services/models.py index 80b62c08..45bf9f0b 100644 --- a/services/models.py +++ b/services/models.py @@ -3,52 +3,6 @@ from django.utils.encoding import python_2_unicode_compatible from django.db import models -@python_2_unicode_compatible -class TSgroup(models.Model): - ts_group_id = models.IntegerField(primary_key=True) - ts_group_name = models.CharField(max_length=30) - - class Meta: - verbose_name = 'TS Group' - - def __str__(self): - return self.ts_group_name - - -@python_2_unicode_compatible -class AuthTS(models.Model): - auth_group = models.ForeignKey('auth.Group') - ts_group = models.ManyToManyField(TSgroup) - - class Meta: - verbose_name = 'Auth / TS Group' - - def __str__(self): - return self.auth_group.name - - -@python_2_unicode_compatible -class UserTSgroup(models.Model): - user = models.ForeignKey('auth.User') - ts_group = models.ManyToManyField(TSgroup) - - class Meta: - verbose_name = 'User TS Group' - - def __str__(self): - return self.user.name - - -@python_2_unicode_compatible -class MumbleUser(models.Model): - username = models.CharField(max_length=254, unique=True) - pwhash = models.CharField(max_length=40) - groups = models.TextField(blank=True, null=True) - - def __str__(self): - return self.username - - @python_2_unicode_compatible class GroupCache(models.Model): SERVICE_CHOICES = ( diff --git a/services/modules/__init__.py b/services/modules/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/discord/__init__.py b/services/modules/discord/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/discord/admin.py b/services/modules/discord/admin.py new file mode 100644 index 00000000..51d0c0cf --- /dev/null +++ b/services/modules/discord/admin.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from django.contrib import admin +from .models import DiscordUser + + +class DiscordUserAdmin(admin.ModelAdmin): + list_display = ('user', 'uid') + search_fields = ('user__username', 'uid') + +admin.site.register(DiscordUser, DiscordUserAdmin) diff --git a/services/modules/discord/apps.py b/services/modules/discord/apps.py new file mode 100644 index 00000000..2d0e2667 --- /dev/null +++ b/services/modules/discord/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class DiscordServiceConfig(AppConfig): + name = 'discord' diff --git a/services/modules/discord/auth_hooks.py b/services/modules/discord/auth_hooks.py new file mode 100644 index 00000000..b1738073 --- /dev/null +++ b/services/modules/discord/auth_hooks.py @@ -0,0 +1,59 @@ +from __future__ import unicode_literals + +import logging + +from django.conf import settings +from django.template.loader import render_to_string + +from alliance_auth import hooks +from services.hooks import ServicesHook +from .tasks import DiscordTasks +from .urls import urlpatterns + +logger = logging.getLogger(__name__) + + +class DiscordService(ServicesHook): + def __init__(self): + ServicesHook.__init__(self) + self.urlpatterns = urlpatterns + self.name = 'discord' + self.service_ctrl_template = 'registered/discord_service_ctrl.html' + + def delete_user(self, user, notify_user=False): + logger.debug('Deleting user %s %s account' % (user, self.name)) + return DiscordTasks.delete_user(user, notify_user=notify_user) + + def update_groups(self, user): + logger.debug('Processing %s groups for %s' % (self.name, user)) + if DiscordTasks.has_account(user): + DiscordTasks.update_groups.delay(user.pk) + + def validate_user(self, user): + logger.debug('Validating user %s %s account' % (user, self.name)) + if DiscordTasks.has_account(user) and not self.service_active_for_user(user): + self.delete_user(user, notify_user=True) + + def sync_nickname(self, user): + logger.debug('Syncing %s nickname for user %s' % (self.name, user)) + DiscordTasks.update_nickname.delay(user.pk) + + def update_all_groups(self): + logger.debug('Update all %s groups called' % self.name) + DiscordTasks.update_all_groups.delay() + + def service_enabled_members(self): + return settings.ENABLE_AUTH_DISCORD or False + + def service_enabled_blues(self): + return settings.ENABLE_BLUE_DISCORD or False + + def render_services_ctrl(self, request): + return render_to_string(self.service_ctrl_template, { + 'discord_uid': request.user.discord.uid if DiscordTasks.has_account(request.user) else None, + }, request=request) + + +@hooks.register('services_hook') +def register_service(): + return DiscordService() diff --git a/services/managers/discord_manager.py b/services/modules/discord/manager.py similarity index 100% rename from services/managers/discord_manager.py rename to services/modules/discord/manager.py diff --git a/services/modules/discord/migrations/0001_initial.py b/services/modules/discord/migrations/0001_initial.py new file mode 100644 index 00000000..b9865117 --- /dev/null +++ b/services/modules/discord/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-12-12 03:14 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='DiscordUser', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='discord', serialize=False, to=settings.AUTH_USER_MODEL)), + ('uid', models.CharField(max_length=254)), + ], + ), + ] diff --git a/services/modules/discord/migrations/__init__.py b/services/modules/discord/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/discord/models.py b/services/modules/discord/models.py new file mode 100644 index 00000000..cfe38358 --- /dev/null +++ b/services/modules/discord/models.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals +from django.utils.encoding import python_2_unicode_compatible +from django.contrib.auth.models import User +from django.db import models + + +class DiscordUser(models.Model): + user = models.OneToOneField(User, + primary_key=True, + on_delete=models.CASCADE, + related_name='discord') + uid = models.CharField(max_length=254) + + def __str__(self): + return "{} - {}".format(self.user.username, self.uid) diff --git a/services/modules/discord/tasks.py b/services/modules/discord/tasks.py new file mode 100644 index 00000000..a616a546 --- /dev/null +++ b/services/modules/discord/tasks.py @@ -0,0 +1,131 @@ +from __future__ import unicode_literals + +import logging + +from alliance_auth.celeryapp import app +from django.conf import settings +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist + +from eveonline.managers import EveManager +from notifications import notify +from services.modules.discord.manager import DiscordOAuthManager +from services.tasks import only_one +from .models import DiscordUser + +logger = logging.getLogger(__name__) + + +class DiscordTasks: + def __init__(self): + pass + + @classmethod + def add_user(cls, user, code): + user_id = DiscordOAuthManager.add_user(code) + if user_id: + discord_user = DiscordUser() + discord_user.user = user + discord_user.uid = user_id + discord_user.save() + if settings.DISCORD_SYNC_NAMES: + cls.update_nickname.delay(user.pk) + cls.update_groups.delay(user.pk) + return True + return False + + @classmethod + def delete_user(cls, user, notify_user=False): + if cls.has_account(user): + logger.debug("User %s has discord account %s. Deleting." % (user, user.discord.uid)) + if DiscordOAuthManager.delete_user(user.discord.uid): + user.discord.delete() + if notify_user: + notify(user, 'Discord Account Disabled', level='danger') + return True + return False + + @classmethod + def has_account(cls, user): + """ + Check if the user has an account (has a DiscordUser record) + :param user: django.contrib.auth.models.User + :return: bool + """ + try: + user.discord + except ObjectDoesNotExist: + return False + else: + return True + + @staticmethod + @app.task(bind=True) + def update_groups(task_self, pk): + user = User.objects.get(pk=pk) + logger.debug("Updating discord groups for user %s" % user) + if DiscordTasks.has_account(user): + groups = [] + for group in user.groups.all(): + groups.append(str(group.name)) + if len(groups) == 0: + logger.debug("No syncgroups found for user. Adding empty group.") + groups.append('empty') + logger.debug("Updating user %s discord groups to %s" % (user, groups)) + try: + DiscordOAuthManager.update_groups(user.discord.uid, groups) + except Exception as e: + if task_self: + logger.exception("Discord group sync failed for %s, retrying in 10 mins" % user) + raise task_self.retry(countdown=60 * 10) + else: + # Rethrow + raise e + logger.debug("Updated user %s discord groups." % user) + else: + logger.debug("User does not have a discord account, skipping") + + @staticmethod + @app.task + def update_all_groups(): + logger.debug("Updating ALL discord groups") + for discord_user in DiscordUser.objects.exclude(uid__exact=''): + DiscordTasks.update_groups.delay(discord_user.user.pk) + + @staticmethod + @app.task(bind=True) + def update_nickname(self, pk): + user = User.objects.get(pk=pk) + logger.debug("Updating discord nickname for user %s" % user) + if DiscordTasks.has_account(user): + character = EveManager.get_main_character(user) + logger.debug("Updating user %s discord nickname to %s" % (user, character.character_name)) + try: + DiscordOAuthManager.update_nickname(user.discord.uid, character.character_name) + except Exception as e: + if self: + logger.exception("Discord nickname sync failed for %s, retrying in 10 mins" % user) + raise self.retry(countdown=60 * 10) + else: + # Rethrow + raise e + logger.debug("Updated user %s discord nickname." % user) + else: + logger.debug("User %s does not have a discord account" % user) + + @staticmethod + @app.task + def update_all_nicknames(): + logger.debug("Updating ALL discord nicknames") + for discord_user in DiscordUser.objects.exclude(uid__exact=''): + DiscordTasks.update_nickname.delay(discord_user.user.user_id) + + @classmethod + def disable(cls): + if settings.ENABLE_AUTH_DISCORD: + logger.warn( + "ENABLE_AUTH_DISCORD still True, after disabling users will still be able to link Discord accounts") + if settings.ENABLE_BLUE_DISCORD: + logger.warn( + "ENABLE_BLUE_DISCORD still True, after disabling blues will still be able to link Discord accounts") + DiscordUser.objects.all().delete() diff --git a/services/modules/discord/templates/registered/discord_service_ctrl.html b/services/modules/discord/templates/registered/discord_service_ctrl.html new file mode 100644 index 00000000..f9315c9a --- /dev/null +++ b/services/modules/discord/templates/registered/discord_service_ctrl.html @@ -0,0 +1,27 @@ +{% load i18n %} + + + + Discord + + https://discordapp.com + + {% if not discord_uid %} + + + + {% else %} + + + + + + + {% endif %} + {% if request.user.is_superuser %} + + {% endif %} + + diff --git a/services/modules/discord/tests.py b/services/modules/discord/tests.py new file mode 100644 index 00000000..3ac9b27e --- /dev/null +++ b/services/modules/discord/tests.py @@ -0,0 +1,193 @@ +from __future__ import unicode_literals + +try: + # Py3 + from unittest import mock +except ImportError: + # Py2 + import mock + +from django.test import TestCase, RequestFactory +from django.conf import settings +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist + +from alliance_auth.tests.auth_utils import AuthUtils + +from .auth_hooks import DiscordService +from .models import DiscordUser +from .tasks import DiscordTasks + +MODULE_PATH = 'services.modules.discord' + + +class DiscordHooksTestCase(TestCase): + def setUp(self): + self.member = 'member_user' + member = AuthUtils.create_member(self.member) + DiscordUser.objects.create(user=member, uid='12345') + self.blue = 'blue_user' + blue = AuthUtils.create_blue(self.blue) + DiscordUser.objects.create(user=blue, uid='67891') + self.none_user = 'none_user' + none_user = AuthUtils.create_user(self.none_user) + self.service = DiscordService + + def test_has_account(self): + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(DiscordTasks.has_account(member)) + self.assertTrue(DiscordTasks.has_account(blue)) + self.assertFalse(DiscordTasks.has_account(none_user)) + + def test_service_enabled(self): + service = self.service() + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(service.service_enabled_members()) + self.assertTrue(service.service_enabled_blues()) + + self.assertTrue(service.service_active_for_user(member)) + self.assertTrue(service.service_active_for_user(blue)) + self.assertFalse(service.service_active_for_user(none_user)) + + @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager') + def test_update_all_groups(self, manager): + service = self.service() + service.update_all_groups() + # Check member and blue user have groups updated + self.assertTrue(manager.update_groups.called) + self.assertEqual(manager.update_groups.call_count, 2) + + def test_update_groups(self): + # Check member has Member group updated + with mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager') as manager: + service = self.service() + member = User.objects.get(username=self.member) + service.update_groups(member) + self.assertTrue(manager.update_groups.called) + args, kwargs = manager.update_groups.call_args + user_id, groups = args + self.assertIn(settings.DEFAULT_AUTH_GROUP, groups) + self.assertEqual(user_id, member.discord.uid) + + # Check none user does not have groups updated + with mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager') as manager: + service = self.service() + none_user = User.objects.get(username=self.none_user) + service.update_groups(none_user) + self.assertFalse(manager.update_groups.called) + + @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager') + def test_validate_user(self, manager): + service = self.service() + # Test member is not deleted + member = User.objects.get(username=self.member) + service.validate_user(member) + self.assertTrue(member.discord) + + # Test none user is deleted + none_user = User.objects.get(username=self.none_user) + DiscordUser.objects.create(user=none_user, uid='abc123') + service.validate_user(none_user) + self.assertTrue(manager.delete_user.called) + with self.assertRaises(ObjectDoesNotExist): + none_discord = User.objects.get(username=self.none_user).discord + + @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager') + def test_sync_nickname(self, manager): + service = self.service() + member = User.objects.get(username=self.member) + AuthUtils.add_main_character(member, 'test user', '12345', corp_ticker='AAUTH') + + service.sync_nickname(member) + + self.assertTrue(manager.update_nickname.called) + args, kwargs = manager.update_nickname.call_args + self.assertEqual(args[0], member.discord.uid) + self.assertEqual(args[1], 'test user') + + @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager') + def test_delete_user(self, manager): + member = User.objects.get(username=self.member) + + service = self.service() + result = service.delete_user(member) + + self.assertTrue(result) + self.assertTrue(manager.delete_user.called) + with self.assertRaises(ObjectDoesNotExist): + discord_user = User.objects.get(username=self.member).discord + + def test_render_services_ctrl(self): + service = self.service() + member = User.objects.get(username=self.member) + request = RequestFactory().get('/en/services/') + request.user = member + + response = service.render_services_ctrl(request) + self.assertTemplateUsed(service.service_ctrl_template) + self.assertIn('/discord/reset/', response) + self.assertIn('/discord/deactivate/', response) + + # Test register becomes available + member.discord.delete() + member = User.objects.get(username=self.member) + request.user = member + response = service.render_services_ctrl(request) + self.assertIn('/discord/activate/', response) + + # TODO: Test update nicknames + + +class DiscordViewsTestCase(TestCase): + def setUp(self): + self.member = AuthUtils.create_member('auth_member') + self.member.set_password('password') + self.member.save() + + def login(self): + self.client.login(username=self.member.username, password='password') + + @mock.patch(MODULE_PATH + '.views.DiscordOAuthManager') + def test_activate(self, manager): + self.login() + manager.generate_oauth_redirect_url.return_value = '/example.com/oauth/' + response = self.client.get('/discord/activate/', follow=False) + self.assertRedirects(response, expected_url='/example.com/oauth/', target_status_code=404) + + @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager') + def test_callback(self, manager): + self.login() + manager.add_user.return_value = '1234' + response = self.client.get('/discord/callback/', data={'code': '1234'}) + + self.assertTrue(manager.add_user.called) + self.assertEqual(manager.update_nickname.called, settings.DISCORD_SYNC_NAMES) + self.assertEqual(self.member.discord.uid, '1234') + self.assertRedirects(response, expected_url='/en/services/', target_status_code=200) + + @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager') + def test_reset(self, manager): + self.login() + DiscordUser.objects.create(user=self.member, uid='12345') + manager.delete_user.return_value = True + + response = self.client.get('/discord/reset/') + + self.assertRedirects(response, expected_url='/discord/activate/', target_status_code=302) + + @mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager') + def test_deactivate(self, manager): + self.login() + DiscordUser.objects.create(user=self.member, uid='12345') + manager.delete_user.return_value = True + + response = self.client.get('/discord/deactivate/') + + self.assertTrue(manager.delete_user.called) + self.assertRedirects(response, expected_url='/en/services/', target_status_code=200) + with self.assertRaises(ObjectDoesNotExist): + discord_user = User.objects.get(pk=self.member.pk).discord diff --git a/services/modules/discord/urls.py b/services/modules/discord/urls.py new file mode 100644 index 00000000..17862c89 --- /dev/null +++ b/services/modules/discord/urls.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals +from django.conf.urls import url, include + +from . import views + +module_urls = [ + # Discord Service Control + url(r'^activate/$', views.activate_discord, name='auth_activate_discord'), + url(r'^deactivate/$', views.deactivate_discord, name='auth_deactivate_discord'), + url(r'^reset/$', views.reset_discord, name='auth_reset_discord'), + url(r'^callback/$', views.discord_callback, name='auth_discord_callback'), + url(r'^add_bot/$', views.discord_add_bot, name='auth_discord_add_bot'), +] + +urlpatterns = [ + url(r'^discord/', include(module_urls)) +] diff --git a/services/modules/discord/views.py b/services/modules/discord/views.py new file mode 100644 index 00000000..3223c5b9 --- /dev/null +++ b/services/modules/discord/views.py @@ -0,0 +1,71 @@ +from __future__ import unicode_literals + +import logging + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import user_passes_test +from django.shortcuts import redirect + +from authentication.decorators import members_and_blues +from .manager import DiscordOAuthManager +from .tasks import DiscordTasks +from services.views import superuser_test + + +logger = logging.getLogger(__name__) + + +@login_required +@members_and_blues() +def deactivate_discord(request): + logger.debug("deactivate_discord called by user %s" % request.user) + if DiscordTasks.delete_user(request.user): + logger.info("Successfully deactivated discord for user %s" % request.user) + messages.success(request, 'Deactivated Discord account.') + else: + logger.error("Unsuccessful attempt to deactivate discord for user %s" % request.user) + messages.error(request, 'An error occurred while processing your Discord account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def reset_discord(request): + logger.debug("reset_discord called by user %s" % request.user) + if DiscordTasks.delete_user(request.user): + logger.info("Successfully deleted discord user for user %s - forwarding to discord activation." % request.user) + return redirect("auth_activate_discord") + logger.error("Unsuccessful attempt to reset discord for user %s" % request.user) + messages.error(request, 'An error occurred while processing your Discord account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def activate_discord(request): + logger.debug("activate_discord called by user %s" % request.user) + return redirect(DiscordOAuthManager.generate_oauth_redirect_url()) + + +@login_required +@members_and_blues() +def discord_callback(request): + logger.debug("Received Discord callback for activation of user %s" % request.user) + code = request.GET.get('code', None) + if not code: + logger.warn("Did not receive OAuth code from callback of user %s" % request.user) + return redirect("auth_services") + if DiscordTasks.add_user(request.user, code): + logger.info("Successfully activated Discord for user %s" % request.user) + messages.success(request, 'Activated Discord account.') + else: + logger.error("Failed to activate Discord for user %s" % request.user) + messages.error(request, 'An error occurred while processing your Discord account.') + return redirect("auth_services") + + +@login_required +@user_passes_test(superuser_test) +def discord_add_bot(request): + return redirect(DiscordOAuthManager.generate_bot_add_url()) diff --git a/services/modules/discourse/__init__.py b/services/modules/discourse/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/discourse/admin.py b/services/modules/discourse/admin.py new file mode 100644 index 00000000..81240d4a --- /dev/null +++ b/services/modules/discourse/admin.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from django.contrib import admin +from .models import DiscourseUser + + +class DiscourseUserAdmin(admin.ModelAdmin): + list_display = ('user',) + search_fields = ('user__username',) + +admin.site.register(DiscourseUser, DiscourseUserAdmin) diff --git a/services/modules/discourse/apps.py b/services/modules/discourse/apps.py new file mode 100644 index 00000000..b5fc3ab7 --- /dev/null +++ b/services/modules/discourse/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class DiscourseServiceConfig(AppConfig): + name = 'discourse' diff --git a/services/modules/discourse/auth_hooks.py b/services/modules/discourse/auth_hooks.py new file mode 100644 index 00000000..3af0276f --- /dev/null +++ b/services/modules/discourse/auth_hooks.py @@ -0,0 +1,57 @@ +from __future__ import unicode_literals + +from django.conf import settings +from django.template.loader import render_to_string + +from services.hooks import ServicesHook +from alliance_auth import hooks +from eveonline.managers import EveManager + +from .urls import urlpatterns +from .tasks import DiscourseTasks + +import logging + +logger = logging.getLogger(__name__) + + +class DiscourseService(ServicesHook): + def __init__(self): + ServicesHook.__init__(self) + self.urlpatterns = urlpatterns + self.name = 'discourse' + self.service_ctrl_template = 'registered/discourse_service_ctrl.html' + + def delete_user(self, user, notify_user=False): + logger.debug('Deleting user %s %s account' % (user, self.name)) + return DiscourseTasks.delete_user(user, notify_user=notify_user) + + def update_groups(self, user): + logger.debug('Processing %s groups for %s' % (self.name, user)) + if DiscourseTasks.has_account(user): + DiscourseTasks.update_groups.delay(user.pk) + + def validate_user(self, user): + logger.debug('Validating user %s %s account' % (user, self.name)) + if DiscourseTasks.has_account(user) and not self.service_active_for_user(user): + self.delete_user(user, notify_user=True) + + def update_all_groups(self): + logger.debug('Update all %s groups called' % self.name) + DiscourseTasks.update_all_groups.delay() + + def service_enabled_members(self): + return settings.ENABLE_AUTH_DISCOURSE or False + + def service_enabled_blues(self): + return settings.ENABLE_BLUE_DISCOURSE or False + + def render_services_ctrl(self, request): + return render_to_string(self.service_ctrl_template, { + 'char': EveManager.get_main_character(request.user) + }, request=request) + + +@hooks.register('services_hook') +def register_service(): + return DiscourseService() diff --git a/services/managers/discourse_manager.py b/services/modules/discourse/manager.py similarity index 99% rename from services/managers/discourse_manager.py rename to services/modules/discourse/manager.py index 1953bf57..9d1413b6 100644 --- a/services/managers/discourse_manager.py +++ b/services/modules/discourse/manager.py @@ -1,7 +1,8 @@ from __future__ import unicode_literals import logging import requests -import os +import random +import string import datetime import json import re @@ -11,10 +12,12 @@ from services.models import GroupCache logger = logging.getLogger(__name__) + class DiscourseError(Exception): def __init__(self, endpoint, errors): self.endpoint = endpoint self.errors = errors + def __str__(self): return "API execution failed.\nErrors: %s\nEndpoint: %s" % (self.errors, self.endpoint) @@ -188,7 +191,7 @@ class DiscourseManager: @staticmethod def __generate_random_pass(): - return os.urandom(8).encode('hex') + return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(16)]) @staticmethod def __get_groups(): diff --git a/services/modules/discourse/migrations/0001_initial.py b/services/modules/discourse/migrations/0001_initial.py new file mode 100644 index 00000000..ef80496c --- /dev/null +++ b/services/modules/discourse/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-12-12 03:15 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='DiscourseUser', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='discourse', serialize=False, to=settings.AUTH_USER_MODEL)), + ('enabled', models.BooleanField()), + ], + ), + ] diff --git a/services/modules/discourse/migrations/__init__.py b/services/modules/discourse/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/discourse/models.py b/services/modules/discourse/models.py new file mode 100644 index 00000000..3b2c6a8c --- /dev/null +++ b/services/modules/discourse/models.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals +from django.utils.encoding import python_2_unicode_compatible +from django.contrib.auth.models import User +from django.db import models + + +class DiscourseUser(models.Model): + user = models.OneToOneField(User, + primary_key=True, + on_delete=models.CASCADE, + related_name='discourse') + enabled = models.BooleanField() + + def __str__(self): + return self.user.username diff --git a/services/modules/discourse/tasks.py b/services/modules/discourse/tasks.py new file mode 100644 index 00000000..0c59ee8c --- /dev/null +++ b/services/modules/discourse/tasks.py @@ -0,0 +1,62 @@ +from __future__ import unicode_literals + +from alliance_auth.celeryapp import app +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist +from notifications import notify + +from services.tasks import only_one + +from .manager import DiscourseManager +from .models import DiscourseUser + +import logging + +logger = logging.getLogger(__name__) + + +class DiscourseTasks: + def __init__(self): + pass + + @classmethod + def delete_user(cls, user, notify_user=False): + if cls.has_account(user) and user.discourse.enabled: + logger.debug("User %s has a Discourse account. Disabling login." % user) + if DiscourseManager.disable_user(user): + user.discourse.delete() + if notify_user: + notify(user, 'Discourse Account Disabled', level='danger') + return True + return False + + @staticmethod + def has_account(user): + """ + Check if the user has a discourse account + :param user: django.contrib.auth.models.User + :return: bool + """ + try: + return user.discourse.enabled + except ObjectDoesNotExist: + return False + + @staticmethod + @app.task(bind=True) + def update_groups(self, pk): + user = User.objects.get(pk=pk) + logger.debug("Updating discourse groups for user %s" % user) + try: + DiscourseManager.update_groups(user) + except: + logger.warn("Discourse group sync failed for %s, retrying in 10 mins" % user) + raise self.retry(countdown=60 * 10) + logger.debug("Updated user %s discourse groups." % user) + + @staticmethod + @app.task + def update_all_groups(): + logger.debug("Updating ALL discourse groups") + for discourse_user in DiscourseUser.objects.filter(enabled=True): + DiscourseTasks.update_groups.delay(discourse_user.user.pk) diff --git a/services/modules/discourse/templates/registered/discourse_service_ctrl.html b/services/modules/discourse/templates/registered/discourse_service_ctrl.html new file mode 100644 index 00000000..7cae0cd2 --- /dev/null +++ b/services/modules/discourse/templates/registered/discourse_service_ctrl.html @@ -0,0 +1,8 @@ +{% load i18n %} + +Discourse +{{ char.character_name }} +{{ DISCOURSE_URL }} + + + diff --git a/services/modules/discourse/tests.py b/services/modules/discourse/tests.py new file mode 100644 index 00000000..ade9f3e6 --- /dev/null +++ b/services/modules/discourse/tests.py @@ -0,0 +1,135 @@ +from __future__ import unicode_literals + +try: + # Py3 + from unittest import mock +except ImportError: + # Py2 + import mock + +from django.test import TestCase, RequestFactory +from django.conf import settings +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist + +from alliance_auth.tests.auth_utils import AuthUtils + +from .auth_hooks import DiscourseService +from .models import DiscourseUser +from .tasks import DiscourseTasks + +MODULE_PATH = 'services.modules.discourse' + + +class DiscourseHooksTestCase(TestCase): + def setUp(self): + self.member = 'member_user' + member = AuthUtils.create_member(self.member) + DiscourseUser.objects.create(user=member, enabled=True) + self.blue = 'blue_user' + blue = AuthUtils.create_blue(self.blue) + DiscourseUser.objects.create(user=blue, enabled=True) + self.none_user = 'none_user' + none_user = AuthUtils.create_user(self.none_user) + self.service = DiscourseService + + def test_has_account(self): + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(DiscourseTasks.has_account(member)) + self.assertTrue(DiscourseTasks.has_account(blue)) + self.assertFalse(DiscourseTasks.has_account(none_user)) + + def test_service_enabled(self): + service = self.service() + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(service.service_enabled_members()) + self.assertTrue(service.service_enabled_blues()) + + self.assertEqual(service.service_active_for_user(member), settings.ENABLE_AUTH_DISCOURSE) + self.assertEqual(service.service_active_for_user(blue), settings.ENABLE_BLUE_DISCOURSE) + self.assertFalse(service.service_active_for_user(none_user)) + + @mock.patch(MODULE_PATH + '.tasks.DiscourseManager') + def test_update_all_groups(self, manager): + service = self.service() + service.update_all_groups() + # Check member and blue user have groups updated + self.assertTrue(manager.update_groups.called) + self.assertEqual(manager.update_groups.call_count, 2) + + def test_update_groups(self): + # Check member has Member group updated + with mock.patch(MODULE_PATH + '.tasks.DiscourseManager') as manager: + service = self.service() + member = User.objects.get(username=self.member) + service.update_groups(member) + self.assertTrue(manager.update_groups.called) + args, kwargs = manager.update_groups.call_args + user, = args + self.assertEqual(user, member) + + # Check none user does not have groups updated + with mock.patch(MODULE_PATH + '.tasks.DiscourseManager') as manager: + service = self.service() + none_user = User.objects.get(username=self.none_user) + service.update_groups(none_user) + self.assertFalse(manager.update_groups.called) + + @mock.patch(MODULE_PATH + '.tasks.DiscourseManager') + def test_validate_user(self, manager): + service = self.service() + # Test member is not deleted + member = User.objects.get(username=self.member) + service.validate_user(member) + self.assertTrue(member.discourse) + + # Test none user is deleted + none_user = User.objects.get(username=self.none_user) + DiscourseUser.objects.create(user=none_user, enabled=True) + service.validate_user(none_user) + self.assertTrue(manager.disable_user.called) + with self.assertRaises(ObjectDoesNotExist): + none_discourse = User.objects.get(username=self.none_user).discourse + + @mock.patch(MODULE_PATH + '.tasks.DiscourseManager') + def test_delete_user(self, manager): + member = User.objects.get(username=self.member) + + service = self.service() + result = service.delete_user(member) + + self.assertTrue(result) + self.assertTrue(manager.disable_user.called) + with self.assertRaises(ObjectDoesNotExist): + discourse_user = User.objects.get(username=self.member).discourse + + def test_render_services_ctrl(self): + service = self.service() + member = User.objects.get(username=self.member) + request = RequestFactory().get('/en/services/') + request.user = member + + response = service.render_services_ctrl(request) + self.assertTemplateUsed(service.service_ctrl_template) + self.assertIn('href="%s"' % settings.DISCOURSE_URL, response) + + +class DiscourseViewsTestCase(TestCase): + def setUp(self): + self.member = AuthUtils.create_member('auth_member') + self.member.set_password('password') + self.member.save() + AuthUtils.add_main_character(self.member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation') + + @mock.patch(MODULE_PATH + '.tasks.DiscourseManager') + def test_sso_member(self, manager): + self.client.login(username=self.member.username, password='password') + data = {'sso': 'bm9uY2U9Y2I2ODI1MWVlZmI1MjExZTU4YzAwZmYxMzk1ZjBjMGI%3D%0A', + 'sig': '2828aa29899722b35a2f191d34ef9b3ce695e0e6eeec47deb46d588d70c7cb56'} + response = self.client.get('/discourse/sso', data=data, follow=False) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url[:37], 'https://example.com/session/sso_login') diff --git a/services/modules/discourse/urls.py b/services/modules/discourse/urls.py new file mode 100644 index 00000000..cf4154cb --- /dev/null +++ b/services/modules/discourse/urls.py @@ -0,0 +1,9 @@ +from __future__ import unicode_literals +from django.conf.urls import url + +from . import views + +urlpatterns = [ + # Discourse Service Control + url(r'^discourse/sso$', views.discourse_sso, name='auth_discourse_sso'), +] diff --git a/services/modules/discourse/views.py b/services/modules/discourse/views.py new file mode 100644 index 00000000..670c4154 --- /dev/null +++ b/services/modules/discourse/views.py @@ -0,0 +1,119 @@ +from __future__ import unicode_literals + +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.shortcuts import render, redirect +from authentication.states import MEMBER_STATE, BLUE_STATE, NONE_STATE +from eveonline.models import EveCharacter +from eveonline.managers import EveManager +from authentication.models import AuthServicesInfo + +from .manager import DiscourseManager +from .tasks import DiscourseTasks +from .models import DiscourseUser + +import base64 +import hmac +import hashlib + +try: + from urllib import unquote, urlencode +except ImportError: #py3 + from urllib.parse import unquote, urlencode +try: + from urlparse import parse_qs +except ImportError: #py3 + from urllib.parse import parse_qs + +import logging + +logger = logging.getLogger(__name__) + + +@login_required +def discourse_sso(request): + + ## Check if user has access + + auth = AuthServicesInfo.objects.get(user=request.user) + if not request.user.is_superuser: + if not settings.ENABLE_AUTH_DISCOURSE and auth.state == MEMBER_STATE: + messages.error(request, 'Members are not authorized to access Discourse.') + return redirect('auth_dashboard') + elif not settings.ENABLE_BLUE_DISCOURSE and auth.state == BLUE_STATE: + messages.error(request, 'Blues are not authorized to access Discourse.') + return redirect('auth_dashboard') + elif auth.state == NONE_STATE: + messages.error(request, 'You are not authorized to access Discourse.') + return redirect('auth_dashboard') + + if not auth.main_char_id: + messages.error(request, "You must have a main character set to access Discourse.") + return redirect('auth_characters') + + main_char = EveManager.get_main_character(request.user) + if main_char is None: + messages.error(request, "Your main character is missing a database model. Please select a new one.") + return redirect('auth_characters') + + payload = request.GET.get('sso') + signature = request.GET.get('sig') + + if None in [payload, signature]: + messages.error(request, 'No SSO payload or signature. Please contact support if this problem persists.') + return redirect('auth_dashboard') + + ## Validate the payload + + try: + payload = unquote(payload).encode('utf-8') + decoded = base64.decodestring(payload).decode('utf-8') + assert 'nonce' in decoded + assert len(payload) > 0 + except AssertionError: + messages.error(request, 'Invalid payload. Please contact support if this problem persists.') + return redirect('auth_dashboard') + + key = str(settings.DISCOURSE_SSO_SECRET).encode('utf-8') + h = hmac.new(key, payload, digestmod=hashlib.sha256) + this_signature = h.hexdigest() + + if this_signature != signature: + messages.error(request, 'Invalid payload. Please contact support if this problem persists.') + return redirect('auth_dashboard') + + ## Build the return payload + + username = DiscourseManager._sanitize_username(main_char.character_name) + + qs = parse_qs(decoded) + params = { + 'nonce': qs['nonce'][0], + 'email': request.user.email, + 'external_id': request.user.pk, + 'username': username, + 'name': username, + } + + if auth.main_char_id: + params['avatar_url'] = 'https://image.eveonline.com/Character/%s_256.jpg' % auth.main_char_id + + return_payload = base64.encodestring(urlencode(params).encode('utf-8')) + h = hmac.new(key, return_payload, digestmod=hashlib.sha256) + query_string = urlencode({'sso': return_payload, 'sig': h.hexdigest()}) + + ## Record activation and queue group sync + + if not DiscourseTasks.has_account(request.user): + discourse_user = DiscourseUser() + discourse_user.user = request.user + discourse_user.enabled = True + discourse_user.save() + DiscourseTasks.update_groups.apply_async(args=[request.user.pk], countdown=30) # wait 30s for new user creation on Discourse + + ## Redirect back to Discourse + + url = '%s/session/sso_login' % settings.DISCOURSE_URL + return redirect('%s?%s' % (url, query_string)) + diff --git a/services/modules/example/__init__.py b/services/modules/example/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/example/apps.py b/services/modules/example/apps.py new file mode 100644 index 00000000..8d49787c --- /dev/null +++ b/services/modules/example/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class ExampleServiceConfig(AppConfig): + name = 'example_service' diff --git a/services/modules/example/auth_hooks.py b/services/modules/example/auth_hooks.py new file mode 100644 index 00000000..fecdecf7 --- /dev/null +++ b/services/modules/example/auth_hooks.py @@ -0,0 +1,44 @@ +from __future__ import unicode_literals + +from django.template.loader import render_to_string + +from services.hooks import ServicesHook +from alliance_auth import hooks + +from .urls import urlpatterns + + +class ExampleService(ServicesHook): + def __init__(self): + ServicesHook.__init__(self) + self.urlpatterns = urlpatterns + self.service_url = 'http://exampleservice.example.com' + + """ + Overload base methods here to implement functionality + """ + + def render_services_ctrl(self, request): + """ + Example for rendering the service control panel row + You can override the default template and create a + custom one if you wish. + :param request: + :return: + """ + urls = self.Urls() + urls.auth_activate = 'auth_example_activate' + urls.auth_deactivate = 'auth_example_deactivate' + urls.auth_reset_password = 'auth_example_reset_password' + urls.auth_set_password = 'auth_example_set_password' + return render_to_string(self.service_ctrl_template, { + 'service_name': self.title, + 'urls': urls, + 'service_url': self.service_url, + 'username': 'example username' + }, request=request) + + +@hooks.register('services_hook') +def register_service(): + return ExampleService() diff --git a/services/modules/example/models.py b/services/modules/example/models.py new file mode 100644 index 00000000..692f3152 --- /dev/null +++ b/services/modules/example/models.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals +from django.utils.encoding import python_2_unicode_compatible +from django.contrib.auth.models import User +from django.db import models + + +@python_2_unicode_compatible +class ExampleUser(models.Model): + user = models.OneToOneField(User, + primary_key=True, + on_delete=models.CASCADE, + related_name='example') + username = models.CharField(max_length=254) + + def __str__(self): + return self.username diff --git a/services/modules/example/urls.py b/services/modules/example/urls.py new file mode 100644 index 00000000..5c895021 --- /dev/null +++ b/services/modules/example/urls.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from django.conf.urls import url, include + +module_urls = [ + # Add your module URLs here +] + +urlpatterns = [ + url(r'^example/', include(module_urls)), +] diff --git a/services/modules/example/views.py b/services/modules/example/views.py new file mode 100644 index 00000000..36e3c085 --- /dev/null +++ b/services/modules/example/views.py @@ -0,0 +1 @@ +# Add your Views here diff --git a/services/modules/ipboard/__init__.py b/services/modules/ipboard/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/ipboard/admin.py b/services/modules/ipboard/admin.py new file mode 100644 index 00000000..9351245c --- /dev/null +++ b/services/modules/ipboard/admin.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from django.contrib import admin +from .models import IpboardUser + + +class IpboardUserAdmin(admin.ModelAdmin): + list_display = ('user', 'username') + search_fields = ('user__username', 'username') + +admin.site.register(IpboardUser, IpboardUserAdmin) diff --git a/services/modules/ipboard/apps.py b/services/modules/ipboard/apps.py new file mode 100644 index 00000000..6bad20bb --- /dev/null +++ b/services/modules/ipboard/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class IpboardServiceConfig(AppConfig): + name = 'ipboard' diff --git a/services/modules/ipboard/auth_hooks.py b/services/modules/ipboard/auth_hooks.py new file mode 100644 index 00000000..7bbf095c --- /dev/null +++ b/services/modules/ipboard/auth_hooks.py @@ -0,0 +1,68 @@ +from __future__ import unicode_literals + +from django.conf import settings +from django.template.loader import render_to_string + +from services.hooks import ServicesHook +from alliance_auth import hooks + +from .urls import urlpatterns +from .tasks import IpboardTasks + +import logging + +logger = logging.getLogger(__name__) + + +class IpboardService(ServicesHook): + def __init__(self): + ServicesHook.__init__(self) + self.name = 'ipboard' + self.service_url = settings.IPBOARD_ENDPOINT + self.urlpatterns = urlpatterns + + @property + def title(self): + return 'IPBoard Forums' + + def delete_user(self, user, notify_user=False): + logger.debug('Deleting user %s %s account' % (user, self.name)) + return IpboardTasks.delete_user(user, notify_user=notify_user) + + def update_groups(self, user): + logger.debug("Updating %s groups for %s" % (self.name, user)) + if IpboardTasks.has_account(user): + IpboardTasks.update_groups.delay(user.pk) + + def validate_user(self, user): + logger.debug('Validating user %s %s account' % (user, self.name)) + if IpboardTasks.has_account(user) and not self.service_active_for_user(user): + self.delete_user(user, notify_user=True) + + def update_all_groups(self): + logger.debug('Update all %s groups called' % self.name) + IpboardTasks.update_all_groups.delay() + + def service_enabled_members(self): + return settings.ENABLE_AUTH_IPBOARD or False + + def service_enabled_blues(self): + return settings.ENABLE_BLUE_IPBOARD or False + + def render_services_ctrl(self, request): + urls = self.Urls() + urls.auth_activate = 'auth_activate_ipboard' + urls.auth_deactivate = 'auth_deactivate_ipboard' + urls.auth_reset_password = 'auth_reset_ipboard_password' + urls.auth_set_password = 'auth_set_ipboard_password' + return render_to_string(self.service_ctrl_template, { + 'service_name': self.title, + 'urls': urls, + 'service_url': self.service_url, + 'username': request.user.ipboard.username if IpboardTasks.has_account(request.user) else '', + }, request=request) + + +@hooks.register('services_hook') +def register_service(): + return IpboardService() diff --git a/services/managers/ipboard_manager.py b/services/modules/ipboard/manager.py similarity index 92% rename from services/managers/ipboard_manager.py rename to services/modules/ipboard/manager.py index 4f6e5124..ecc59fdd 100755 --- a/services/managers/ipboard_manager.py +++ b/services/modules/ipboard/manager.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals -import os +import random +import string import re from hashlib import md5 try: @@ -27,7 +28,11 @@ class IPBoardManager: @staticmethod def __generate_random_pass(): - return os.urandom(8).encode('hex') + return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(16)]) + + @staticmethod + def _gen_pwhash(password): + return md5(password.encode('utf-8')).hexdigest() @staticmethod def _sanitize_groupname(name): @@ -49,13 +54,13 @@ class IPBoardManager: except: return {} - @staticmethod - def add_user(username, email): + @classmethod + def add_user(cls, username, email): """ Add user to service """ sanatized = str(IPBoardManager.__santatize_username(username)) logger.debug("Adding user to IPBoard with username %s" % sanatized) plain_password = IPBoardManager.__generate_random_pass() - password = md5(plain_password).hexdigest() + password = cls._gen_pwhash(plain_password) IPBoardManager.exec_xmlrpc('createUser', username=sanatized, email=str(email), display_name=sanatized, md5_passwordHash=password) logger.info("Added IPBoard user with username %s" % sanatized) @@ -75,10 +80,10 @@ class IPBoardManager: logger.info("Disabled IPBoard user with username %s" % username) return username - @staticmethod - def update_user(username, email, password): + @classmethod + def update_user(cls, username, email, password): """ Add user to service """ - password = md5(password).hexdigest() + password = cls._gen_pwhash(password) logger.debug("Updating IPBoard username %s with email %s and password hash starting with %s" % ( username, email, password[0:5])) IPBoardManager.exec_xmlrpc('updateUser', username=username, email=email, md5_passwordHash=password) diff --git a/services/modules/ipboard/migrations/0001_initial.py b/services/modules/ipboard/migrations/0001_initial.py new file mode 100644 index 00000000..448c1064 --- /dev/null +++ b/services/modules/ipboard/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-12-12 03:27 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='IpboardUser', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='ipboard', serialize=False, to=settings.AUTH_USER_MODEL)), + ('username', models.CharField(max_length=254)), + ], + ), + ] diff --git a/services/modules/ipboard/migrations/__init__.py b/services/modules/ipboard/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/ipboard/models.py b/services/modules/ipboard/models.py new file mode 100644 index 00000000..8cb0cb4c --- /dev/null +++ b/services/modules/ipboard/models.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals +from django.utils.encoding import python_2_unicode_compatible +from django.contrib.auth.models import User +from django.db import models + + +@python_2_unicode_compatible +class IpboardUser(models.Model): + user = models.OneToOneField(User, + primary_key=True, + on_delete=models.CASCADE, + related_name='ipboard') + username = models.CharField(max_length=254) + + def __str__(self): + return self.username diff --git a/services/modules/ipboard/tasks.py b/services/modules/ipboard/tasks.py new file mode 100644 index 00000000..0e1a4ace --- /dev/null +++ b/services/modules/ipboard/tasks.py @@ -0,0 +1,73 @@ +from __future__ import unicode_literals + +from alliance_auth.celeryapp import app +from django.conf import settings +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist +from notifications import notify + +from .manager import IPBoardManager +from .models import IpboardUser + +import logging + +logger = logging.getLogger(__name__) + + +class IpboardTasks: + def __init__(self): + pass + + @classmethod + def delete_user(cls, user, notify_user=False): + if cls.has_account(user): + if IPBoardManager.disable_user(user.ipboard.username): + user.ipboard.delete() + if notify_user: + notify(user, 'IPBoard Account Disabled', level='danger') + return True + return False + + @staticmethod + def has_account(user): + try: + return user.ipboard.username != '' + except ObjectDoesNotExist: + return False + + @staticmethod + @app.task(bind=True) + def update_groups(self, pk): + user = User.objects.get(pk=pk) + logger.debug("Updating user %s ipboard groups." % user) + groups = [] + for group in user.groups.all(): + groups.append(str(group.name)) + if len(groups) == 0: + groups.append('empty') + logger.debug("Updating user %s ipboard groups to %s" % (user, groups)) + try: + IPBoardManager.update_groups(user.ipboard.username, groups) + except: + logger.exception("IPBoard group sync failed for %s, retrying in 10 mins" % user) + raise self.retry(countdown=60 * 10) + logger.debug("Updated user %s ipboard groups." % user) + + @staticmethod + @app.task + def update_all_groups(): + logger.debug("Updating ALL ipboard groups") + for ipboard_user in IpboardUser.objects.exclude(username__exact=''): + IpboardTasks.update_groups.delay(ipboard_user.user.pk) + + @staticmethod + @app.task + def disable(): + if settings.ENABLE_AUTH_IPBOARD: + logger.warn( + "ENABLE_AUTH_IPBOARD still True, after disabling users will still be able to create IPBoard accounts") + if settings.ENABLE_BLUE_IPBOARD: + logger.warn( + "ENABLE_BLUE_IPBOARD still True, after disabling blues will still be able to create IPBoard accounts") + logger.debug("Deleting all Ipboard Users") + IpboardUser.objects.all().delete() diff --git a/services/modules/ipboard/tests.py b/services/modules/ipboard/tests.py new file mode 100644 index 00000000..cb3b127d --- /dev/null +++ b/services/modules/ipboard/tests.py @@ -0,0 +1,207 @@ +from __future__ import unicode_literals + +try: + # Py3 + from unittest import mock +except ImportError: + # Py2 + import mock + +from django.test import TestCase, RequestFactory +from django.conf import settings +from django.contrib.auth.models import User +from django import urls +from django.core.exceptions import ObjectDoesNotExist + +from alliance_auth.tests.auth_utils import AuthUtils + +from .auth_hooks import IpboardService +from .models import IpboardUser +from .tasks import IpboardTasks +from .manager import IPBoardManager + +MODULE_PATH = 'services.modules.ipboard' + + +class IpboardHooksTestCase(TestCase): + def setUp(self): + self.member = 'member_user' + member = AuthUtils.create_member(self.member) + IpboardUser.objects.create(user=member, username=self.member) + self.blue = 'blue_user' + blue = AuthUtils.create_blue(self.blue) + IpboardUser.objects.create(user=blue, username=self.blue) + self.none_user = 'none_user' + none_user = AuthUtils.create_user(self.none_user) + self.service = IpboardService + + def test_has_account(self): + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(IpboardTasks.has_account(member)) + self.assertTrue(IpboardTasks.has_account(blue)) + self.assertFalse(IpboardTasks.has_account(none_user)) + + def test_service_enabled(self): + service = self.service() + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(service.service_enabled_members()) + self.assertTrue(service.service_enabled_blues()) + + self.assertEqual(service.service_active_for_user(member), settings.ENABLE_AUTH_IPBOARD) + self.assertEqual(service.service_active_for_user(blue), settings.ENABLE_BLUE_IPBOARD) + self.assertFalse(service.service_active_for_user(none_user)) + + @mock.patch(MODULE_PATH + '.tasks.IPBoardManager') + def test_update_all_groups(self, manager): + service = self.service() + service.update_all_groups() + # Check member and blue user have groups updated + self.assertTrue(manager.update_groups.called) + self.assertEqual(manager.update_groups.call_count, 2) + + def test_update_groups(self): + # Check member has Member group updated + with mock.patch(MODULE_PATH + '.tasks.IPBoardManager') as manager: + service = self.service() + member = User.objects.get(username=self.member) + service.update_groups(member) + self.assertTrue(manager.update_groups.called) + + # Check none user does not have groups updated + with mock.patch(MODULE_PATH + '.tasks.IPBoardManager') as manager: + service = self.service() + none_user = User.objects.get(username=self.none_user) + service.update_groups(none_user) + self.assertFalse(manager.update_groups.called) + + @mock.patch(MODULE_PATH + '.tasks.IPBoardManager') + def test_validate_user(self, manager): + service = self.service() + # Test member is not deleted + member = User.objects.get(username=self.member) + service.validate_user(member) + self.assertTrue(User.objects.get(username=self.member).ipboard) + + # Test none user is deleted + none_user = User.objects.get(username=self.none_user) + IpboardUser.objects.create(user=none_user, username='none_user') + service.validate_user(none_user) + self.assertTrue(manager.disable_user.called) + with self.assertRaises(ObjectDoesNotExist): + none_ipboard = User.objects.get(username=self.none_user).ipboard + + @mock.patch(MODULE_PATH + '.tasks.IPBoardManager') + def test_delete_user(self, manager): + member = User.objects.get(username=self.member) + + service = self.service() + result = service.delete_user(member) + + self.assertTrue(result) + self.assertTrue(manager.disable_user.called) + with self.assertRaises(ObjectDoesNotExist): + Ipboard_user = User.objects.get(username=self.member).ipboard + + def test_render_services_ctrl(self): + service = self.service() + member = User.objects.get(username=self.member) + request = RequestFactory().get('/en/services/') + request.user = member + + response = service.render_services_ctrl(request) + self.assertTemplateUsed(service.service_ctrl_template) + self.assertIn('/ipboard/set_password/', response) + self.assertIn('/ipboard/reset_password/', response) + self.assertIn('/ipboard/deactivate/', response) + + # Test register becomes available + member.ipboard.delete() + member = User.objects.get(username=self.member) + request.user = member + response = service.render_services_ctrl(request) + self.assertIn('/ipboard/activate/', response) + + +class IpboardViewsTestCase(TestCase): + def setUp(self): + self.member = AuthUtils.create_member('auth_member') + self.member.set_password('password') + self.member.email = 'auth_member@example.com' + self.member.save() + AuthUtils.add_main_character(self.member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation') + + def login(self): + self.client.login(username=self.member.username, password='password') + + @mock.patch(MODULE_PATH + '.views.IPBoardManager') + def test_activate(self, manager): + self.login() + expected_username = 'auth_member' + expected_password = 'abc123' + manager.add_user.return_value = (expected_username, expected_password) + response = self.client.get(urls.reverse('auth_activate_ipboard'), follow=False) + self.assertEqual(response.status_code, 200) + self.assertContains(response, expected_username) + self.assertContains(response, expected_password) + self.assertTrue(manager.add_user.called) + args, kwargs = manager.add_user.call_args + self.assertEqual(args[0], 'auth_member') # Character name + self.assertEqual(args[1], self.member.email) + self.assertEqual(self.member.ipboard.username, expected_username) + + @mock.patch(MODULE_PATH + '.tasks.IPBoardManager') + def test_deactivate(self, manager): + self.login() + IpboardUser.objects.create(user=self.member, username='12345') + manager.disable_user.return_value = True + + response = self.client.get(urls.reverse('auth_deactivate_ipboard')) + + self.assertTrue(manager.disable_user.called) + self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) + with self.assertRaises(ObjectDoesNotExist): + ipboard_user = User.objects.get(pk=self.member.pk).ipboard + + @mock.patch(MODULE_PATH + '.views.IPBoardManager') + def test_set_password(self, manager): + self.login() + IpboardUser.objects.create(user=self.member, username='12345') + expected_password = 'password' + manager.update_user_password.return_value = expected_password + + response = self.client.post(urls.reverse('auth_set_ipboard_password'), data={'password': expected_password}) + + self.assertTrue(manager.update_user_password.called) + args, kwargs = manager.update_user_password.call_args + self.assertEqual(kwargs['plain_password'], expected_password) + self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) + + @mock.patch(MODULE_PATH + '.views.IPBoardManager') + def test_reset_password(self, manager): + self.login() + IpboardUser.objects.create(user=self.member, username='12345') + + response = self.client.get(urls.reverse('auth_reset_ipboard_password')) + + self.assertTrue(manager.update_user_password.called) + self.assertTemplateUsed(response, 'registered/service_credentials.html') + + +class IpboardManagerTestCase(TestCase): + def setUp(self): + self.manager = IPBoardManager + + def test_generate_random_password(self): + password = self.manager._IPBoardManager__generate_random_pass() + + self.assertEqual(len(password), 16) + self.assertIsInstance(password, type('')) + + def test_gen_pwhash(self): + pwhash = self.manager._gen_pwhash('test') + + self.assertEqual(pwhash, '098f6bcd4621d373cade4e832627b4f6') diff --git a/services/modules/ipboard/urls.py b/services/modules/ipboard/urls.py new file mode 100644 index 00000000..78f1d70a --- /dev/null +++ b/services/modules/ipboard/urls.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals +from django.conf.urls import url, include + +from . import views + +module_urls = [ + # Ipboard service control + url(r'^activate/$', views.activate_ipboard_forum, name='auth_activate_ipboard'), + url(r'^deactivate/$', views.deactivate_ipboard_forum, name='auth_deactivate_ipboard'), + url(r'^reset_password/$', views.reset_ipboard_password, name='auth_reset_ipboard_password'), + url(r'^set_password/$', views.set_ipboard_password, name='auth_set_ipboard_password'), +] + +urlpatterns = [ + url(r'^ipboard/', include(module_urls)) +] diff --git a/services/modules/ipboard/views.py b/services/modules/ipboard/views.py new file mode 100644 index 00000000..6033ebf8 --- /dev/null +++ b/services/modules/ipboard/views.py @@ -0,0 +1,110 @@ +from __future__ import unicode_literals + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.shortcuts import render, redirect + +from authentication.decorators import members_and_blues +from services.forms import ServicePasswordForm +from eveonline.managers import EveManager + +from .manager import IPBoardManager +from .tasks import IpboardTasks +from .models import IpboardUser + +import logging + +logger = logging.getLogger(__name__) + + +@login_required +@members_and_blues() +def activate_ipboard_forum(request): + logger.debug("activate_ipboard_forum called by user %s" % request.user) + character = EveManager.get_main_character(request.user) + logger.debug("Adding ipboard user for user %s with main character %s" % (request.user, character)) + result = IPBoardManager.add_user(character.character_name, request.user.email) + if result[0] != "": + ipboard_user = IpboardUser() + ipboard_user.user = request.user + ipboard_user.username = result[0] + ipboard_user.save() + logger.debug("Updated authserviceinfo for user %s with ipboard credentials. Updating groups." % request.user) + IpboardTasks.update_groups.delay(request.user.pk) + logger.info("Successfully activated ipboard for user %s" % request.user) + messages.success(request, 'Activated IPBoard account.') + credentials = { + 'username': result[0], + 'password': result[1], + } + return render(request, 'registered/service_credentials.html', + context={'credentials': credentials, 'service': 'IPBoard'}) + else: + logger.error("Unsuccessful attempt to activate ipboard for user %s" % request.user) + messages.error(request, 'An error occurred while processing your IPBoard account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def deactivate_ipboard_forum(request): + logger.debug("deactivate_ipboard_forum called by user %s" % request.user) + # false we failed + if IpboardTasks.delete_user(request.user): + logger.info("Successfully deactivated ipboard for user %s" % request.user) + messages.success(request, 'Deactivated IPBoard account.') + else: + logger.error("Unsuccessful attempt to deactviate ipboard for user %s" % request.user) + messages.error(request, 'An error occurred while processing your IPBoard account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def set_ipboard_password(request): + logger.debug("set_ipboard_password called by user %s" % request.user) + error = None + if request.method == 'POST': + logger.debug("Received POST request with form.") + form = ServicePasswordForm(request.POST) + logger.debug("Form is valid: %s" % form.is_valid()) + if form.is_valid() and IpboardTasks.has_account(request.user): + password = form.cleaned_data['password'] + logger.debug("Form contains password of length %s" % len(password)) + result = IPBoardManager.update_user_password(request.user.ipboard.username, request.user.email, + plain_password=password) + if result != "": + logger.info("Successfully set IPBoard password for user %s" % request.user) + messages.success(request, 'Set IPBoard password.') + else: + logger.error("Failed to install custom ipboard password for user %s" % request.user) + messages.error(request, 'An error occurred while processing your IPBoard account.') + return redirect("auth_services") + else: + logger.debug("Request is not type POST - providing empty form.") + form = ServicePasswordForm() + + logger.debug("Rendering form for user %s" % request.user) + context = {'form': form, 'service': 'IPBoard', 'error': error} + return render(request, 'registered/service_password.html', context=context) + + +@login_required +@members_and_blues() +def reset_ipboard_password(request): + logger.debug("reset_ipboard_password called by user %s" % request.user) + if IpboardTasks.has_account(request.user): + result = IPBoardManager.update_user_password(request.user.ipboard.username, request.user.email) + if result != "": + logger.info("Successfully reset ipboard password for user %s" % request.user) + messages.success(request, 'Reset IPBoard password.') + credentials = { + 'username': request.user.ipboard.username, + 'password': result, + } + return render(request, 'registered/service_credentials.html', + context={'credentials': credentials, 'service': 'IPBoard'}) + + logger.error("Unsuccessful attempt to reset ipboard password for user %s" % request.user) + messages.error(request, 'An error occurred while processing your IPBoard account.') + return redirect("auth_services") diff --git a/services/modules/ips4/__init__.py b/services/modules/ips4/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/ips4/admin.py b/services/modules/ips4/admin.py new file mode 100644 index 00000000..bb25bea8 --- /dev/null +++ b/services/modules/ips4/admin.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from django.contrib import admin +from .models import Ips4User + + +class Ips4UserAdmin(admin.ModelAdmin): + list_display = ('user', 'username', 'id') + search_fields = ('user__username', 'username', 'id') + +admin.site.register(Ips4User, Ips4UserAdmin) diff --git a/services/modules/ips4/apps.py b/services/modules/ips4/apps.py new file mode 100644 index 00000000..40047d31 --- /dev/null +++ b/services/modules/ips4/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class Ips4ServiceConfig(AppConfig): + name = 'ips4' diff --git a/services/modules/ips4/auth_hooks.py b/services/modules/ips4/auth_hooks.py new file mode 100644 index 00000000..a29b4c55 --- /dev/null +++ b/services/modules/ips4/auth_hooks.py @@ -0,0 +1,53 @@ +from __future__ import unicode_literals + +from django.conf import settings +from django.template.loader import render_to_string + +from services.hooks import ServicesHook +from alliance_auth import hooks + +from .urls import urlpatterns +from .tasks import Ips4Tasks + + +class Ips4Service(ServicesHook): + def __init__(self): + ServicesHook.__init__(self) + self.name = 'ips4' + self.urlpatterns = urlpatterns + self.service_url = settings.IPS4_URL + + @property + def title(self): + return 'IPS4' + + def service_enabled_members(self): + return settings.ENABLE_AUTH_IPS4 or False + + def service_enabled_blues(self): + return settings.ENABLE_BLUE_IPS4 or False + + def render_services_ctrl(self, request): + """ + Example for rendering the service control panel row + You can override the default template and create a + custom one if you wish. + :param request: + :return: + """ + urls = self.Urls() + urls.auth_activate = 'auth_activate_ips4' + urls.auth_deactivate = 'auth_deactivate_ips4' + urls.auth_reset_password = 'auth_reset_ips4_password' + urls.auth_set_password = 'auth_set_ips4_password' + return render_to_string(self.service_ctrl_template, { + 'service_name': self.title, + 'urls': urls, + 'service_url': self.service_url, + 'username': request.user.ips4.username if Ips4Tasks.has_account(request.user) else '' + }, request=request) + + +@hooks.register('services_hook') +def register_service(): + return Ips4Service() diff --git a/services/managers/ips4_manager.py b/services/modules/ips4/manager.py similarity index 67% rename from services/managers/ips4_manager.py rename to services/modules/ips4/manager.py index 1fd59b5c..2e7468c6 100644 --- a/services/managers/ips4_manager.py +++ b/services/modules/ips4/manager.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals import logging -import os +import random +import string +import re from django.db import connections from passlib.hash import bcrypt @@ -16,18 +18,16 @@ class Ips4Manager: MEMBER_GROUP_ID = 3 - @staticmethod - def add_user(username, email): + @classmethod + def add_user(cls, username, email): logger.debug("Adding new IPS4 user %s" % username) - plain_password = Ips4Manager.__generate_random_pass() - hash = bcrypt.encrypt(plain_password, rounds=13) - hash_result = hash - rounds_striped = hash_result.strip('$2a$13$') - salt = rounds_striped[:22] - group = Ips4Manager.MEMBER_GROUP_ID + plain_password = cls.__generate_random_pass() + hash = cls._gen_pwhash(plain_password) + salt = cls._get_salt(hash) + group = cls.MEMBER_GROUP_ID cursor = connections['ips4'].cursor() - cursor.execute(Ips4Manager.SQL_ADD_USER, [username, email, hash, salt, group]) - member_id = Ips4Manager.get_user_id(username) + cursor.execute(cls.SQL_ADD_USER, [username, email, hash, salt, group]) + member_id = cls.get_user_id(username) return username, plain_password, member_id @staticmethod @@ -44,7 +44,17 @@ class Ips4Manager: @staticmethod def __generate_random_pass(): - return os.urandom(8).encode('hex') + return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(16)]) + + @staticmethod + def _gen_pwhash(password): + return bcrypt.encrypt(password.encode('utf-8'), rounds=13) + + @staticmethod + def _get_salt(pw_hash): + search = re.compile(r"^\$2[a-z]?\$([0-9]+)\$(.{22})(.{31})$") + match = re.match(search, pw_hash) + return match.group(2) @staticmethod def delete_user(id): @@ -58,17 +68,15 @@ class Ips4Manager: logger.exception("Failed to delete IPS4 user id %s" % id) return False - @staticmethod - def update_user_password(username): + @classmethod + def update_user_password(cls, username): logger.debug("Updating IPS4 user id %s password" % id) - if Ips4Manager.check_user(username): + if cls.check_user(username): plain_password = Ips4Manager.__generate_random_pass() - hash = bcrypt.encrypt(plain_password, rounds=13) - hash_result = hash - rounds_striped = hash_result.strip('$2a$13$') - salt = rounds_striped[:22] + hash = cls._gen_pwhash(plain_password) + salt = cls._get_salt(hash) cursor = connections['ips4'].cursor() - cursor.execute(Ips4Manager.SQL_UPDATE_PASSWORD, [hash, salt, username]) + cursor.execute(cls.SQL_UPDATE_PASSWORD, [hash, salt, username]) return plain_password else: logger.error("Unable to update ips4 user %s password" % username) @@ -86,16 +94,14 @@ class Ips4Manager: logger.debug("User %s not found on IPS4" % username) return False - @staticmethod - def update_custom_password(username, plain_password): + @classmethod + def update_custom_password(cls, username, plain_password): logger.debug("Updating IPS4 user id %s password" % id) - if Ips4Manager.check_user(username): - hash = bcrypt.encrypt(plain_password, rounds=13) - hash_result = hash - rounds_striped = hash_result.strip('$2a$13$') - salt = rounds_striped[:22] + if cls.check_user(username): + hash = cls._gen_pwhash(plain_password) + salt = cls._get_salt(hash) cursor = connections['ips4'].cursor() - cursor.execute(Ips4Manager.SQL_UPDATE_PASSWORD, [hash, salt, username]) + cursor.execute(cls.SQL_UPDATE_PASSWORD, [hash, salt, username]) return plain_password else: logger.error("Unable to update ips4 user %s password" % username) diff --git a/services/modules/ips4/migrations/0001_initial.py b/services/modules/ips4/migrations/0001_initial.py new file mode 100644 index 00000000..1d3826f8 --- /dev/null +++ b/services/modules/ips4/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-12-12 03:27 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='Ips4User', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='ips4', serialize=False, to=settings.AUTH_USER_MODEL)), + ('username', models.CharField(max_length=254)), + ('id', models.CharField(max_length=254)), + ], + ), + ] diff --git a/services/modules/ips4/migrations/__init__.py b/services/modules/ips4/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/ips4/models.py b/services/modules/ips4/models.py new file mode 100644 index 00000000..445ec19a --- /dev/null +++ b/services/modules/ips4/models.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals +from django.utils.encoding import python_2_unicode_compatible +from django.contrib.auth.models import User +from django.db import models + + +@python_2_unicode_compatible +class Ips4User(models.Model): + user = models.OneToOneField(User, + primary_key=True, + on_delete=models.CASCADE, + related_name='ips4') + username = models.CharField(max_length=254) + id = models.CharField(max_length=254) + + def __str__(self): + return self.username diff --git a/services/modules/ips4/tasks.py b/services/modules/ips4/tasks.py new file mode 100644 index 00000000..73fb6aa8 --- /dev/null +++ b/services/modules/ips4/tasks.py @@ -0,0 +1,43 @@ +from __future__ import unicode_literals, absolute_import + +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist + +from .manager import Ips4Manager +from .models import Ips4User + +import logging + +logger = logging.getLogger(__name__) + + +class Ips4Tasks: + def __init__(self): + pass + + @classmethod + def delete_user(cls, user): + logging.debug("Attempting to delete IPS4 account for %s" % user) + if cls.has_account(user) and Ips4Manager.delete_user(user.ips4.id): + user.ips4.delete() + logger.info("Successfully deactivated IPS4 for user %s" % user) + return True + return False + + @staticmethod + def has_account(user): + try: + return user.ips4.id != '' + except ObjectDoesNotExist: + return False + + @staticmethod + def disable(): + if settings.ENABLE_AUTH_IPS4: + logger.warn( + "ENABLE_AUTH_IPS4 still True, after disabling users will still be able to create IPS4 accounts") + if settings.ENABLE_BLUE_IPS4: + logger.warn( + "ENABLE_BLUE_IPS4 still True, after disabling blues will still be able to create IPS4 accounts") + logging.debug("Deleting all IPS4 users") + Ips4User.objects.all().delete() diff --git a/services/modules/ips4/tests.py b/services/modules/ips4/tests.py new file mode 100644 index 00000000..aafe1781 --- /dev/null +++ b/services/modules/ips4/tests.py @@ -0,0 +1,164 @@ +from __future__ import unicode_literals + +try: + # Py3 + from unittest import mock +except ImportError: + # Py2 + import mock + +from django.test import TestCase, RequestFactory +from django.conf import settings +from django import urls +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist + +from alliance_auth.tests.auth_utils import AuthUtils + +from .auth_hooks import Ips4Service +from .models import Ips4User +from .tasks import Ips4Tasks + +MODULE_PATH = 'services.modules.ips4' + + +class Ips4HooksTestCase(TestCase): + def setUp(self): + self.member = 'member_user' + member = AuthUtils.create_member(self.member) + Ips4User.objects.create(user=member, id='12345', username=self.member) + self.blue = 'blue_user' + blue = AuthUtils.create_blue(self.blue) + Ips4User.objects.create(user=blue, id='67891', username=self.blue) + self.none_user = 'none_user' + none_user = AuthUtils.create_user(self.none_user) + self.service = Ips4Service + + def test_has_account(self): + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(Ips4Tasks.has_account(member)) + self.assertTrue(Ips4Tasks.has_account(blue)) + self.assertFalse(Ips4Tasks.has_account(none_user)) + + def test_service_enabled(self): + service = self.service() + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(service.service_enabled_members()) + self.assertTrue(service.service_enabled_blues()) + + self.assertEqual(service.service_active_for_user(member), settings.ENABLE_AUTH_IPS4) + self.assertEqual(service.service_active_for_user(blue), settings.ENABLE_BLUE_IPS4) + self.assertFalse(service.service_active_for_user(none_user)) + + def test_render_services_ctrl(self): + service = self.service() + member = User.objects.get(username=self.member) + request = RequestFactory().get('/en/services/') + request.user = member + + response = service.render_services_ctrl(request) + self.assertTemplateUsed(service.service_ctrl_template) + self.assertIn(urls.reverse('auth_set_ips4_password'), response) + self.assertIn(urls.reverse('auth_reset_ips4_password'), response) + self.assertIn(urls.reverse('auth_deactivate_ips4'), response) + + # Test register becomes available + member.ips4.delete() + member = User.objects.get(username=self.member) + request.user = member + response = service.render_services_ctrl(request) + self.assertIn(urls.reverse('auth_activate_ips4'), response) + + +class Ips4ViewsTestCase(TestCase): + def setUp(self): + self.member = AuthUtils.create_member('auth_member') + self.member.set_password('password') + self.member.email = 'auth_member@example.com' + self.member.save() + AuthUtils.add_main_character(self.member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation') + + def login(self): + self.client.login(username=self.member.username, password='password') + + @mock.patch(MODULE_PATH + '.views.Ips4Manager') + def test_activate(self, manager): + self.login() + expected_username = 'auth_member' + expected_password = 'password' + expected_id = '1234' + + manager.add_user.return_value = (expected_username, expected_password, expected_id) + + response = self.client.get(urls.reverse('auth_activate_ips4'), follow=False) + + self.assertTrue(manager.add_user.called) + args, kwargs = manager.add_user.call_args + self.assertEqual(args[0], expected_username) + self.assertEqual(args[1], self.member.email) + + self.assertTemplateUsed(response, 'registered/service_credentials.html') + self.assertContains(response, expected_username) + self.assertContains(response, expected_password) + + @mock.patch(MODULE_PATH + '.tasks.Ips4Manager') + def test_deactivate(self, manager): + self.login() + Ips4User.objects.create(user=self.member, username='12345', id='1234') + manager.delete_user.return_value = True + + response = self.client.get(urls.reverse('auth_deactivate_ips4')) + + self.assertTrue(manager.delete_user.called) + self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) + with self.assertRaises(ObjectDoesNotExist): + ips4_user = User.objects.get(pk=self.member.pk).ips4 + + @mock.patch(MODULE_PATH + '.views.Ips4Manager') + def test_set_password(self, manager): + self.login() + Ips4User.objects.create(user=self.member, username='12345', id='1234') + expected_password = 'password' + manager.update_user_password.return_value = expected_password + + response = self.client.post(urls.reverse('auth_set_ips4_password'), data={'password': expected_password}) + + self.assertTrue(manager.update_custom_password.called) + args, kwargs = manager.update_custom_password.call_args + self.assertEqual(kwargs['plain_password'], expected_password) + self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) + + @mock.patch(MODULE_PATH + '.views.Ips4Manager') + def test_reset_password(self, manager): + self.login() + Ips4User.objects.create(user=self.member, username='12345', id='1234') + + response = self.client.get(urls.reverse('auth_reset_ips4_password')) + + self.assertTrue(manager.update_user_password.called) + self.assertTemplateUsed(response, 'registered/service_credentials.html') + + +class Ips4ManagerTestCase(TestCase): + def setUp(self): + from .manager import Ips4Manager + self.manager = Ips4Manager + + def test_generate_random_password(self): + password = self.manager._Ips4Manager__generate_random_pass() + + self.assertEqual(len(password), 16) + self.assertIsInstance(password, type('')) + + def test_gen_pwhash(self): + pwhash = self.manager._gen_pwhash('test') + salt = self.manager._get_salt(pwhash) + + self.assertIsInstance(pwhash, str) + self.assertGreaterEqual(len(pwhash), 59) + self.assertIsInstance(salt, str) + self.assertEqual(len(salt), 22) diff --git a/services/modules/ips4/urls.py b/services/modules/ips4/urls.py new file mode 100644 index 00000000..ac1a86a9 --- /dev/null +++ b/services/modules/ips4/urls.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals +from django.conf.urls import url, include + +from . import views + +module_urls = [ + # IPS4 Service Control + url(r'^activate/$', views.activate_ips4, name='auth_activate_ips4'), + url(r'^deactivate/$', views.deactivate_ips4, name='auth_deactivate_ips4'), + url(r'^reset_password/$', views.reset_ips4_password, name='auth_reset_ips4_password'), + url(r'^set_password/$', views.set_ips4_password, name='auth_set_ips4_password'), +] + +urlpatterns = [ + url(r'^ips4/', include(module_urls)) +] diff --git a/services/modules/ips4/views.py b/services/modules/ips4/views.py new file mode 100644 index 00000000..a1d862d8 --- /dev/null +++ b/services/modules/ips4/views.py @@ -0,0 +1,107 @@ +from __future__ import unicode_literals + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.shortcuts import render, redirect + +from authentication.decorators import members_and_blues +from eveonline.managers import EveManager +from services.forms import ServicePasswordForm + +from .manager import Ips4Manager +from .models import Ips4User +from .tasks import Ips4Tasks + +import logging + +logger = logging.getLogger(__name__) + + +@login_required +@members_and_blues() +def activate_ips4(request): + logger.debug("activate_ips4 called by user %s" % request.user) + character = EveManager.get_main_character(request.user) + logger.debug("Adding IPS4 user for user %s with main character %s" % (request.user, character)) + result = Ips4Manager.add_user(character.character_name, request.user.email) + # if empty we failed + if result[0] != "" and not Ips4Tasks.has_account(request.user): + ips_user = Ips4User.objects.create(user=request.user, id=result[2], username=result[0]) + logger.debug("Updated authserviceinfo for user %s with IPS4 credentials." % request.user) + # update_ips4_groups.delay(request.user.pk) + logger.info("Successfully activated IPS4 for user %s" % request.user) + messages.success(request, 'Activated IPSuite4 account.') + credentials = { + 'username': result[0], + 'password': result[1], + } + return render(request, 'registered/service_credentials.html', + context={'credentials': credentials, 'service': 'IPSuite4'}) + else: + logger.error("Unsuccessful attempt to activate IPS4 for user %s" % request.user) + messages.error(request, 'An error occurred while processing your IPSuite4 account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def reset_ips4_password(request): + logger.debug("reset_ips4_password called by user %s" % request.user) + if Ips4Tasks.has_account(request.user): + result = Ips4Manager.update_user_password(request.user.ips4.username) + # false we failed + if result != "": + logger.info("Successfully reset IPS4 password for user %s" % request.user) + messages.success(request, 'Reset IPSuite4 password.') + credentials = { + 'username': request.user.ips4.username, + 'password': result, + } + return render(request, 'registered/service_credentials.html', + context={'credentials': credentials, 'service': 'IPSuite4'}) + + logger.error("Unsuccessful attempt to reset IPS4 password for user %s" % request.user) + messages.error(request, 'An error occurred while processing your IPSuite4 account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def set_ips4_password(request): + logger.debug("set_ips4_password called by user %s" % request.user) + if request.method == 'POST': + logger.debug("Received POST request with form.") + form = ServicePasswordForm(request.POST) + logger.debug("Form is valid: %s" % form.is_valid()) + if form.is_valid() and Ips4Tasks.has_account(request.user): + password = form.cleaned_data['password'] + logger.debug("Form contains password of length %s" % len(password)) + result = Ips4Manager.update_custom_password(request.user.ips4.username, plain_password=password) + if result != "": + logger.info("Successfully set IPS4 password for user %s" % request.user) + messages.success(request, 'Set IPSuite4 password.') + else: + logger.error("Failed to install custom IPS4 password for user %s" % request.user) + messages.error(request, 'An error occurred while processing your IPSuite4 account.') + return redirect('auth_services') + else: + logger.debug("Request is not type POST - providing empty form.") + form = ServicePasswordForm() + + logger.debug("Rendering form for user %s" % request.user) + context = {'form': form, 'service': 'IPS4'} + return render(request, 'registered/service_password.html', context=context) + + +@login_required +@members_and_blues() +def deactivate_ips4(request): + logger.debug("deactivate_ips4 called by user %s" % request.user) + if Ips4Tasks.delete_user(request.user): + logger.info("Successfully deactivated IPS4 for user %s" % request.user) + messages.success(request, 'Deactivated IPSuite4 account.') + else: + logger.error("Unsuccessful attempt to deactivate IPS4 for user %s" % request.user) + messages.error(request, 'An error occurred while processing your IPSuite4 account.') + return redirect("auth_services") + diff --git a/services/modules/market/__init__.py b/services/modules/market/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/market/admin.py b/services/modules/market/admin.py new file mode 100644 index 00000000..6b7ca8d2 --- /dev/null +++ b/services/modules/market/admin.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from django.contrib import admin +from .models import MarketUser + + +class MarketUserAdmin(admin.ModelAdmin): + list_display = ('user', 'username') + search_fields = ('user__username', 'username') + +admin.site.register(MarketUser, MarketUserAdmin) diff --git a/services/modules/market/apps.py b/services/modules/market/apps.py new file mode 100644 index 00000000..ff750cb7 --- /dev/null +++ b/services/modules/market/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class MarketServiceConfig(AppConfig): + name = 'market' diff --git a/services/modules/market/auth_hooks.py b/services/modules/market/auth_hooks.py new file mode 100644 index 00000000..8a680e2b --- /dev/null +++ b/services/modules/market/auth_hooks.py @@ -0,0 +1,59 @@ +from __future__ import unicode_literals + +from django.conf import settings +from django.template.loader import render_to_string + +from services.hooks import ServicesHook +from alliance_auth import hooks + +from .urls import urlpatterns +from .tasks import MarketTasks + +import logging + +logger = logging.getLogger(__name__) + + +class MarketService(ServicesHook): + def __init__(self): + ServicesHook.__init__(self) + self.name = 'market' + self.urlpatterns = urlpatterns + self.service_url = settings.MARKET_URL + + @property + def title(self): + return "Alliance Market" + + def delete_user(self, user, notify_user=False): + logger.debug('Deleting user %s %s account' % (user, self.name)) + return MarketTasks.delete_user(user, notify_user=notify_user) + + def validate_user(self, user): + logger.debug('Validating user %s %s account' % (user, self.name)) + if MarketTasks.has_account(user) and self.service_active_for_user(user): + self.delete_user(user) + + def service_enabled_members(self): + return settings.ENABLE_AUTH_MARKET or False + + def service_enabled_blues(self): + return settings.ENABLE_BLUE_MARKET or False + + def render_services_ctrl(self, request): + urls = self.Urls() + urls.auth_activate = 'auth_activate_market' + urls.auth_deactivate = 'auth_deactivate_market' + urls.auth_reset_password = 'auth_reset_market_password' + urls.auth_set_password = 'auth_set_market_password' + return render_to_string(self.service_ctrl_template, { + 'service_name': self.title, + 'urls': urls, + 'service_url': self.service_url, + 'username': request.user.market.username if MarketTasks.has_account(request.user) else '' + }, request=request) + + +@hooks.register('services_hook') +def register_service(): + return MarketService() diff --git a/services/managers/market_manager.py b/services/modules/market/manager.py similarity index 58% rename from services/managers/market_manager.py rename to services/modules/market/manager.py index c1958fbb..5938add2 100644 --- a/services/managers/market_manager.py +++ b/services/modules/market/manager.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals import logging -import os +import random +import string +import re from django.db import connections from passlib.hash import bcrypt @@ -10,7 +12,7 @@ from passlib.hash import bcrypt logger = logging.getLogger(__name__) -class marketManager: +class MarketManager: def __init__(self): pass @@ -32,13 +34,23 @@ class marketManager: @staticmethod def __generate_random_pass(): - return os.urandom(8).encode('hex') + return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(16)]) @staticmethod - def check_username(username): + def _gen_pwhash(password): + return bcrypt.encrypt(password.encode('utf-8'), rounds=13) + + @staticmethod + def _get_salt(pw_hash): + search = re.compile(r"^\$2[a-z]?\$([0-9]+)\$(.{22})(.{31})$") + match = re.match(search, pw_hash) + return match.group(2) + + @classmethod + def check_username(cls, username): logger.debug("Checking alliance market username %s" % username) cursor = connections['market'].cursor() - cursor.execute(marketManager.SQL_CHECK_USERNAME, [marketManager.__santatize_username(username)]) + cursor.execute(cls.SQL_CHECK_USERNAME, [cls.__santatize_username(username)]) row = cursor.fetchone() if row: logger.debug("Found user %s on alliance market" % username) @@ -46,11 +58,11 @@ class marketManager: logger.debug("User %s not found on alliance market" % username) return False - @staticmethod - def check_user_email(username, email): + @classmethod + def check_user_email(cls, username, email): logger.debug("Checking if alliance market email exists for user %s" % username) cursor = connections['market'].cursor() - cursor.execute(marketManager.SQL_CHECK_EMAIL, [email]) + cursor.execute(cls.SQL_CHECK_EMAIL, [email]) row = cursor.fetchone() if row: logger.debug("Found user %s email address on alliance market" % username) @@ -58,21 +70,19 @@ class marketManager: logger.debug("User %s email address not found on alliance market" % username) return False - @staticmethod - def add_user(username, email, characterid, charactername): + @classmethod + def add_user(cls, username, email, characterid, charactername): logger.debug("Adding new market user %s" % username) - plain_password = marketManager.__generate_random_pass() - hash = bcrypt.encrypt(plain_password, rounds=13) - hash_result = hash - rounds_striped = hash_result.strip('$2a$13$') - salt = rounds_striped[:22] - username_clean = marketManager.__santatize_username(username) - if not marketManager.check_username(username): - if not marketManager.check_user_email(username, email): + plain_password = cls.__generate_random_pass() + hash = cls._gen_pwhash(plain_password) + salt = cls._get_salt(hash) + username_clean = cls.__santatize_username(username) + if not cls.check_username(username): + if not cls.check_user_email(username, email): try: logger.debug("Adding user %s to alliance market" % username) cursor = connections['market'].cursor() - cursor.execute(marketManager.SQL_ADD_USER, [username_clean, username_clean, email, email, salt, + cursor.execute(cls.SQL_ADD_USER, [username_clean, username_clean, email, email, salt, hash, characterid, charactername]) return username_clean, plain_password except: @@ -80,65 +90,59 @@ class marketManager: return "", "" else: logger.debug("Alliance market email %s already exists Updating instead" % email) - username_clean, password = marketManager.update_user_info(username) + username_clean, password = cls.update_user_info(username) return username_clean, password else: logger.debug("Alliance market username %s already exists Updating instead" % username) - username_clean, password = marketManager.update_user_info(username) + username_clean, password = cls.update_user_info(username) return username_clean, password - @staticmethod - def disable_user(username): + @classmethod + def disable_user(cls, username): logger.debug("Disabling alliance market user %s " % username) cursor = connections['market'].cursor() - cursor.execute(marketManager.SQL_DISABLE_USER, [username]) + cursor.execute(cls.SQL_DISABLE_USER, [username]) return True - @staticmethod - def update_custom_password(username, plain_password): + @classmethod + def update_custom_password(cls, username, plain_password): logger.debug("Updating alliance market user %s password" % username) - if marketManager.check_username(username): - username_clean = marketManager.__santatize_username(username) - hash = bcrypt.encrypt(plain_password, rounds=13) - hash_result = hash - rounds_striped = hash_result.strip('$2a$13$') - salt = rounds_striped[:22] + if cls.check_username(username): + username_clean = cls.__santatize_username(username) + hash = cls._gen_pwhash(plain_password) + salt = cls._get_salt(hash) cursor = connections['market'].cursor() - cursor.execute(marketManager.SQL_UPDATE_PASSWORD, [hash, salt, username_clean]) + cursor.execute(cls.SQL_UPDATE_PASSWORD, [hash, salt, username_clean]) return plain_password else: logger.error("Unable to update alliance market user %s password" % username) return "" - @staticmethod - def update_user_password(username): + @classmethod + def update_user_password(cls, username): logger.debug("Updating alliance market user %s password" % username) - if marketManager.check_username(username): - username_clean = marketManager.__santatize_username(username) - plain_password = marketManager.__generate_random_pass() - hash = bcrypt.encrypt(plain_password, rounds=13) - hash_result = hash - rounds_striped = hash_result.strip('$2a$13$') - salt = rounds_striped[:22] + if cls.check_username(username): + username_clean = cls.__santatize_username(username) + plain_password = cls.__generate_random_pass() + hash = cls._gen_pwhash(plain_password) + salt = cls._get_salt(hash) cursor = connections['market'].cursor() - cursor.execute(marketManager.SQL_UPDATE_PASSWORD, [hash, salt, username_clean]) + cursor.execute(cls.SQL_UPDATE_PASSWORD, [hash, salt, username_clean]) return plain_password else: logger.error("Unable to update alliance market user %s password" % username) return "" - @staticmethod - def update_user_info(username): + @classmethod + def update_user_info(cls, username): logger.debug("Updating alliance market user %s" % username) try: - username_clean = marketManager.__santatize_username(username) - plain_password = marketManager.__generate_random_pass() - hash = bcrypt.encrypt(plain_password, rounds=13) - hash_result = hash - rounds_striped = hash_result.strip('$2a$13$') - salt = rounds_striped[:22] + username_clean = cls.__santatize_username(username) + plain_password = cls.__generate_random_pass() + hash = cls._gen_pwhash(plain_password) + salt = cls._get_salt(hash) cursor = connections['market'].cursor() - cursor.execute(marketManager.SQL_UPDATE_USER, [hash, salt, username_clean]) + cursor.execute(cls.SQL_UPDATE_USER, [hash, salt, username_clean]) return username_clean, plain_password except: logger.debug("Alliance market update user failed for %s" % username) diff --git a/services/modules/market/migrations/0001_initial.py b/services/modules/market/migrations/0001_initial.py new file mode 100644 index 00000000..1e306c9f --- /dev/null +++ b/services/modules/market/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-12-12 03:27 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='MarketUser', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='market', serialize=False, to=settings.AUTH_USER_MODEL)), + ('username', models.CharField(max_length=254)), + ], + ), + ] diff --git a/services/modules/market/migrations/__init__.py b/services/modules/market/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/market/models.py b/services/modules/market/models.py new file mode 100644 index 00000000..1edba722 --- /dev/null +++ b/services/modules/market/models.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals +from django.utils.encoding import python_2_unicode_compatible +from django.contrib.auth.models import User +from django.db import models + + +@python_2_unicode_compatible +class MarketUser(models.Model): + user = models.OneToOneField(User, + primary_key=True, + on_delete=models.CASCADE, + related_name='market') + username = models.CharField(max_length=254) + + def __str__(self): + return self.username diff --git a/services/modules/market/tasks.py b/services/modules/market/tasks.py new file mode 100644 index 00000000..adb1160b --- /dev/null +++ b/services/modules/market/tasks.py @@ -0,0 +1,44 @@ +from __future__ import unicode_literals + +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from notifications import notify + +from .models import MarketUser +from .manager import MarketManager + + +import logging + +logger = logging.getLogger(__name__) + + +class MarketTasks: + def __init__(self): + pass + + @classmethod + def delete_user(cls, user, notify_user=False): + if cls.has_account(user): + logger.debug("User %s has a Market account %s. Deleting." % (user, user.market.username)) + if MarketManager.disable_user(user.market.username): + user.market.delete() + if notify_user: + notify(user, 'Alliance Market Account Disabled', level='danger') + return True + return False + + @staticmethod + def has_account(user): + try: + return user.market.username != '' + except ObjectDoesNotExist: + return False + + @staticmethod + def disable(): + if settings.ENABLE_AUTH_MARKET: + logger.warn("ENABLE_AUTH_MARKET still True, after disabling users will still be able to activate Market accounts") + if settings.ENABLE_BLUE_MARKET: + logger.warn("ENABLE_BLUE_MARKET still True, after disabling blues will still be able to activate Market accounts") + MarketUser.objects.all().delete() diff --git a/services/modules/market/tests.py b/services/modules/market/tests.py new file mode 100644 index 00000000..70c75d10 --- /dev/null +++ b/services/modules/market/tests.py @@ -0,0 +1,176 @@ +from __future__ import unicode_literals + +try: + # Py3 + from unittest import mock +except ImportError: + # Py2 + import mock + +from django.test import TestCase, RequestFactory +from django.conf import settings +from django import urls +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist + +from alliance_auth.tests.auth_utils import AuthUtils + +from .auth_hooks import MarketService +from .models import MarketUser +from .tasks import MarketTasks + +MODULE_PATH = 'services.modules.market' + + +class MarketHooksTestCase(TestCase): + def setUp(self): + self.member = 'member_user' + member = AuthUtils.create_member(self.member) + MarketUser.objects.create(user=member, username=self.member) + self.blue = 'blue_user' + blue = AuthUtils.create_blue(self.blue) + MarketUser.objects.create(user=blue, username=self.blue) + self.none_user = 'none_user' + none_user = AuthUtils.create_user(self.none_user) + self.service = MarketService + + def test_has_account(self): + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(MarketTasks.has_account(member)) + self.assertTrue(MarketTasks.has_account(blue)) + self.assertFalse(MarketTasks.has_account(none_user)) + + def test_service_enabled(self): + service = self.service() + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(service.service_enabled_members()) + self.assertTrue(service.service_enabled_blues()) + + self.assertEqual(service.service_active_for_user(member), settings.ENABLE_AUTH_MARKET) + self.assertEqual(service.service_active_for_user(blue), settings.ENABLE_BLUE_MARKET) + self.assertFalse(service.service_active_for_user(none_user)) + + @mock.patch(MODULE_PATH + '.tasks.MarketManager') + def test_delete_user(self, manager): + member = User.objects.get(username=self.member) + + service = self.service() + result = service.delete_user(member) + + self.assertTrue(result) + self.assertTrue(manager.disable_user.called) + with self.assertRaises(ObjectDoesNotExist): + market_user = User.objects.get(username=self.member).market + + def test_render_services_ctrl(self): + service = self.service() + member = User.objects.get(username=self.member) + request = RequestFactory().get('/en/services/') + request.user = member + + response = service.render_services_ctrl(request) + self.assertTemplateUsed(service.service_ctrl_template) + self.assertIn(urls.reverse('auth_set_market_password'), response) + self.assertIn(urls.reverse('auth_reset_market_password'), response) + self.assertIn(urls.reverse('auth_deactivate_market'), response) + + # Test register becomes available + member.market.delete() + member = User.objects.get(username=self.member) + request.user = member + response = service.render_services_ctrl(request) + self.assertIn(urls.reverse('auth_activate_market'), response) + + +class MarketViewsTestCase(TestCase): + def setUp(self): + self.member = AuthUtils.create_member('auth_member') + self.member.set_password('password') + self.member.email = 'auth_member@example.com' + self.member.save() + AuthUtils.add_main_character(self.member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation') + + def login(self): + self.client.login(username=self.member.username, password='password') + + @mock.patch(MODULE_PATH + '.views.MarketManager') + def test_activate(self, manager): + self.login() + expected_username = 'auth_member' + expected_password = 'password' + expected_id = '1234' + + manager.add_user.return_value = (expected_username, expected_password, expected_id) + + response = self.client.get(urls.reverse('auth_activate_market'), follow=False) + + self.assertTrue(manager.add_user.called) + args, kwargs = manager.add_user.call_args + self.assertEqual(args[0], expected_username) + self.assertEqual(args[1], self.member.email) + + self.assertTemplateUsed(response, 'registered/service_credentials.html') + self.assertContains(response, expected_username) + self.assertContains(response, expected_password) + + @mock.patch(MODULE_PATH + '.tasks.MarketManager') + def test_deactivate(self, manager): + self.login() + MarketUser.objects.create(user=self.member, username='12345') + manager.disable_user.return_value = True + + response = self.client.get(urls.reverse('auth_deactivate_market')) + + self.assertTrue(manager.disable_user.called) + self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) + with self.assertRaises(ObjectDoesNotExist): + market_user = User.objects.get(pk=self.member.pk).market + + @mock.patch(MODULE_PATH + '.views.MarketManager') + def test_set_password(self, manager): + self.login() + MarketUser.objects.create(user=self.member, username='12345') + expected_password = 'password' + manager.update_user_password.return_value = expected_password + + response = self.client.post(urls.reverse('auth_set_market_password'), data={'password': expected_password}) + + self.assertTrue(manager.update_custom_password.called) + args, kwargs = manager.update_custom_password.call_args + self.assertEqual(args[1], expected_password) + self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) + + @mock.patch(MODULE_PATH + '.views.MarketManager') + def test_reset_password(self, manager): + self.login() + MarketUser.objects.create(user=self.member, username='12345') + + response = self.client.get(urls.reverse('auth_reset_market_password')) + + self.assertTrue(manager.update_user_password.called) + self.assertTemplateUsed(response, 'registered/service_credentials.html') + + +class MarketManagerTestCase(TestCase): + def setUp(self): + from .manager import MarketManager + self.manager = MarketManager + + def test_generate_random_password(self): + password = self.manager._MarketManager__generate_random_pass() + + self.assertEqual(len(password), 16) + self.assertIsInstance(password, type('')) + + def test_gen_pwhash(self): + pwhash = self.manager._gen_pwhash('test') + salt = self.manager._get_salt(pwhash) + + self.assertIsInstance(pwhash, str) + self.assertGreaterEqual(len(pwhash), 59) + self.assertIsInstance(salt, str) + self.assertEqual(len(salt), 22) diff --git a/services/modules/market/urls.py b/services/modules/market/urls.py new file mode 100644 index 00000000..b0ebdea2 --- /dev/null +++ b/services/modules/market/urls.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals +from django.conf.urls import url, include + +from . import views + +module_urls = [ + # Alliance Market Control + url(r'^activate/$', views.activate_market, name='auth_activate_market'), + url(r'^deactivate/$', views.deactivate_market, name='auth_deactivate_market'), + url(r'^reset_password/$', views.reset_market_password, name='auth_reset_market_password'), + url(r'^set_password/$', views.set_market_password, name='auth_set_market_password'), +] + +urlpatterns = [ + url(r'^market/', include(module_urls)) +] diff --git a/services/modules/market/views.py b/services/modules/market/views.py new file mode 100644 index 00000000..706bf5c0 --- /dev/null +++ b/services/modules/market/views.py @@ -0,0 +1,107 @@ +from __future__ import unicode_literals + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.shortcuts import render, redirect + +from authentication.decorators import members_and_blues +from services.forms import ServicePasswordForm +from eveonline.managers import EveManager + +from .manager import MarketManager +from .models import MarketUser +from .tasks import MarketTasks + +import logging + +logger = logging.getLogger(__name__) + + +@login_required +@members_and_blues() +def activate_market(request): + logger.debug("activate_market called by user %s" % request.user) + character = EveManager.get_main_character(request.user) + if character is not None: + logger.debug("Adding market user for user %s with main character %s" % (request.user, character)) + result = MarketManager.add_user(character.character_name, request.user.email, character.character_id, + character.character_name) + # if empty we failed + if result[0] != "": + MarketUser.objects.create(user=request.user, username=result[0]) + logger.debug("Updated authserviceinfo for user %s with market credentials." % request.user) + logger.info("Successfully activated market for user %s" % request.user) + messages.success(request, 'Activated Alliance Market account.') + credentials = { + 'username': result[0], + 'password': result[1], + } + return render(request, 'registered/service_credentials.html', + context={'credentials': credentials, 'service': 'Alliance Market'}) + logger.error("Unsuccessful attempt to activate market for user %s" % request.user) + messages.error(request, 'An error occurred while processing your Alliance Market account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def deactivate_market(request): + logger.debug("deactivate_market called by user %s" % request.user) + # false we failed + if MarketTasks.delete_user(request.user): + logger.info("Successfully deactivated market for user %s" % request.user) + messages.success(request, 'Deactivated Alliance Market account.') + else: + logger.error("Unsuccessful attempt to activate market for user %s" % request.user) + messages.error(request, 'An error occurred while processing your Alliance Market account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def reset_market_password(request): + logger.debug("reset_market_password called by user %s" % request.user) + if MarketTasks.has_account(request.user): + result = MarketManager.update_user_password(request.user.market.username) + # false we failed + if result != "": + logger.info("Successfully reset market password for user %s" % request.user) + messages.success(request, 'Reset Alliance Market password.') + credentials = { + 'username': request.user.market.username, + 'password': result, + } + return render(request, 'registered/service_credentials.html', + context={'credentials': credentials, 'service': 'Alliance Market'}) + + logger.error("Unsuccessful attempt to reset market password for user %s" % request.user) + messages.error(request, 'An error occurred while processing your Alliance Market account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def set_market_password(request): + logger.debug("set_market_password called by user %s" % request.user) + if request.method == 'POST': + logger.debug("Received POST request with form.") + form = ServicePasswordForm(request.POST) + logger.debug("Form is valid: %s" % form.is_valid()) + if form.is_valid() and MarketTasks.has_account(request.user): + password = form.cleaned_data['password'] + logger.debug("Form contains password of length %s" % len(password)) + result = MarketManager.update_custom_password(request.user.market.username, password) + if result != "": + logger.info("Successfully reset market password for user %s" % request.user) + messages.success(request, 'Set Alliance Market password.') + else: + logger.error("Failed to install custom market password for user %s" % request.user) + messages.error(request, 'An error occurred while processing your Alliance Market account.') + return redirect("auth_services") + else: + logger.debug("Request is not type POST - providing empty form.") + form = ServicePasswordForm() + + logger.debug("Rendering form for user %s" % request.user) + context = {'form': form, 'service': 'Market'} + return render(request, 'registered/service_password.html', context=context) diff --git a/services/modules/mumble/__init__.py b/services/modules/mumble/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/mumble/admin.py b/services/modules/mumble/admin.py new file mode 100644 index 00000000..b475ef4b --- /dev/null +++ b/services/modules/mumble/admin.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals +from django.contrib import admin +from .models import MumbleUser + + +class MumbleUserAdmin(admin.ModelAdmin): + fields = ('user', 'username', 'groups') # pwhash is hidden from admin panel + list_display = ('user', 'username', 'groups') + search_fields = ('user__username', 'username', 'groups') + +admin.site.register(MumbleUser, MumbleUserAdmin) diff --git a/services/modules/mumble/apps.py b/services/modules/mumble/apps.py new file mode 100644 index 00000000..b104c642 --- /dev/null +++ b/services/modules/mumble/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class MumbleServiceConfig(AppConfig): + name = 'mumble' diff --git a/services/modules/mumble/auth_hooks.py b/services/modules/mumble/auth_hooks.py new file mode 100644 index 00000000..c191b01a --- /dev/null +++ b/services/modules/mumble/auth_hooks.py @@ -0,0 +1,68 @@ +from __future__ import unicode_literals +from django.template.loader import render_to_string +from django.conf import settings +from notifications import notify + +from alliance_auth import hooks +from services.hooks import ServicesHook +from .tasks import MumbleTasks +from .manager import MumbleManager +from .urls import urlpatterns + +import logging + +logger = logging.getLogger(__name__) + + +class MumbleService(ServicesHook): + def __init__(self): + ServicesHook.__init__(self) + self.name = 'mumble' + self.urlpatterns = urlpatterns + self.service_url = settings.MUMBLE_URL + + def delete_user(self, user, notify_user=False): + logging.debug("Deleting user %s %s account" % (user, self.name)) + if MumbleManager.delete_user(user): + if notify_user: + notify(user, 'Mumble Account Disabled', level='danger') + return True + return False + + def update_groups(self, user): + logger.debug("Updating %s groups for %s" % (self.name, user)) + if MumbleTasks.has_account(user): + MumbleTasks.update_groups.delay(user.pk) + + def validate_user(self, user): + if MumbleTasks.has_account(user) and not self.service_active_for_user(user): + self.delete_user(user, notify_user=True) + + def update_all_groups(self): + logger.debug("Updating all %s groups" % self.name) + MumbleTasks.update_all_groups.delay() + + def service_enabled_members(self): + return settings.ENABLE_AUTH_MUMBLE or False + + def service_enabled_blues(self): + return settings.ENABLE_BLUE_MUMBLE or False + + def render_services_ctrl(self, request): + urls = self.Urls() + urls.auth_activate = 'auth_activate_mumble' + urls.auth_deactivate = 'auth_deactivate_mumble' + urls.auth_reset_password = 'auth_reset_mumble_password' + urls.auth_set_password = 'auth_set_mumble_password' + + return render_to_string(self.service_ctrl_template, { + 'service_name': self.title, + 'urls': urls, + 'service_url': self.service_url, + 'username': request.user.mumble.username if MumbleTasks.has_account(request.user) else '', + }, request=request) + + +@hooks.register('services_hook') +def register_mumble_service(): + return MumbleService() diff --git a/services/modules/mumble/manager.py b/services/modules/mumble/manager.py new file mode 100755 index 00000000..71612ee5 --- /dev/null +++ b/services/modules/mumble/manager.py @@ -0,0 +1,105 @@ +from __future__ import unicode_literals +import random +import string +import hashlib + +from django.core.exceptions import ObjectDoesNotExist + +from .models import MumbleUser + +import logging + +logger = logging.getLogger(__name__) + + +class MumbleManager: + def __init__(self): + pass + + @staticmethod + def __santatize_username(username): + sanatized = username.replace(" ", "_") + return sanatized + + @staticmethod + def __generate_random_pass(): + return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(16)]) + + @staticmethod + def __generate_username(username, corp_ticker): + return "[" + corp_ticker + "]" + username + + @staticmethod + def __generate_username_blue(username, corp_ticker): + return "[BLUE][" + corp_ticker + "]" + username + + @staticmethod + def _gen_pwhash(password): + return hashlib.sha1(password.encode('utf-8')).hexdigest() + + @staticmethod + def create_user(user, corp_ticker, username, blue=False): + logger.debug("Creating%s mumble user with username %s and ticker %s" % (' blue' if blue else '', + username, corp_ticker)) + username_clean = MumbleManager.__santatize_username( + MumbleManager.__generate_username_blue(username, corp_ticker) if blue else + MumbleManager.__generate_username(username, corp_ticker)) + password = MumbleManager.__generate_random_pass() + pwhash = MumbleManager._gen_pwhash(password) + logger.debug("Proceeding with mumble user creation: clean username %s, pwhash starts with %s" % ( + username_clean, pwhash[0:5])) + if not MumbleUser.objects.filter(username=username_clean).exists(): + logger.info("Creating mumble user %s" % username_clean) + MumbleUser.objects.create(user=user, username=username_clean, pwhash=pwhash) + return username_clean, password + else: + logger.warn("Mumble user %s already exists.") + return False + + @staticmethod + def delete_user(user): + logger.debug("Deleting user %s from mumble." % user) + if MumbleUser.objects.filter(user=user).exists(): + MumbleUser.objects.filter(user=user).delete() + logger.info("Deleted user %s from mumble" % user) + return True + logger.error("Unable to delete user %s from mumble: MumbleUser model not found" % user) + return False + + @staticmethod + def update_user_password(user, password=None): + logger.debug("Updating mumble user %s password." % user) + if not password: + password = MumbleManager.__generate_random_pass() + pwhash = MumbleManager._gen_pwhash(password) + logger.debug("Proceeding with mumble user %s password update - pwhash starts with %s" % (user, pwhash[0:5])) + try: + model = MumbleUser.objects.get(user=user) + model.pwhash = pwhash + model.save() + return password + except ObjectDoesNotExist: + logger.error("User %s not found on mumble. Unable to update password." % user) + return False + + @staticmethod + def update_groups(user, groups): + logger.debug("Updating mumble user %s groups %s" % (user, groups)) + safe_groups = list(set([g.replace(' ', '-') for g in groups])) + groups = '' + for g in safe_groups: + groups = groups + g + ',' + groups = groups.strip(',') + if MumbleUser.objects.filter(user=user).exists(): + logger.info("Updating mumble user %s groups to %s" % (user, safe_groups)) + model = MumbleUser.objects.get(user=user) + model.groups = groups + model.save() + return True + else: + logger.error("User %s not found on mumble. Unable to update groups." % user) + return False + + @staticmethod + def user_exists(username): + return MumbleUser.objects.filter(username=username).exists() diff --git a/services/modules/mumble/migrations/0001_initial.py b/services/modules/mumble/migrations/0001_initial.py new file mode 100644 index 00000000..a2cdb69c --- /dev/null +++ b/services/modules/mumble/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-12-12 00:58 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='MumbleUser', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('username', models.CharField(max_length=254, unique=True)), + ('pwhash', models.CharField(max_length=40)), + ('groups', models.TextField(blank=True, null=True)), + ], + options={ + 'db_table': 'services_mumbleuser', + }, + ), + ] diff --git a/services/modules/mumble/migrations/0002_auto_20161212_0100.py b/services/modules/mumble/migrations/0002_auto_20161212_0100.py new file mode 100644 index 00000000..784d4276 --- /dev/null +++ b/services/modules/mumble/migrations/0002_auto_20161212_0100.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-12-12 01:00 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mumble', '0001_initial'), + ] + + operations = [ + migrations.AlterModelTable( + name='mumbleuser', + table=None, + ), + ] diff --git a/services/modules/mumble/migrations/0003_mumbleuser_user.py b/services/modules/mumble/migrations/0003_mumbleuser_user.py new file mode 100644 index 00000000..7d8b63c1 --- /dev/null +++ b/services/modules/mumble/migrations/0003_mumbleuser_user.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-12-12 03:31 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('mumble', '0002_auto_20161212_0100'), + ] + + operations = [ + migrations.AddField( + model_name='mumbleuser', + name='user', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='mumble', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/services/modules/mumble/migrations/0004_auto_20161214_1024.py b/services/modules/mumble/migrations/0004_auto_20161214_1024.py new file mode 100644 index 00000000..35daf08d --- /dev/null +++ b/services/modules/mumble/migrations/0004_auto_20161214_1024.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2016-12-14 10:24 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('mumble', '0003_mumbleuser_user'), + ] + + operations = [ + migrations.AlterField( + model_name='mumbleuser', + name='user', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='mumble', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/services/modules/mumble/migrations/__init__.py b/services/modules/mumble/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/mumble/models.py b/services/modules/mumble/models.py new file mode 100644 index 00000000..28e303b9 --- /dev/null +++ b/services/modules/mumble/models.py @@ -0,0 +1,14 @@ +from __future__ import unicode_literals +from django.utils.encoding import python_2_unicode_compatible +from django.db import models + + +@python_2_unicode_compatible +class MumbleUser(models.Model): + user = models.OneToOneField('auth.User', related_name='mumble', null=True) + username = models.CharField(max_length=254, unique=True) + pwhash = models.CharField(max_length=40) + groups = models.TextField(blank=True, null=True) + + def __str__(self): + return self.username diff --git a/services/modules/mumble/tasks.py b/services/modules/mumble/tasks.py new file mode 100644 index 00000000..d9986c0e --- /dev/null +++ b/services/modules/mumble/tasks.py @@ -0,0 +1,63 @@ +from __future__ import unicode_literals + +from alliance_auth.celeryapp import app +from django.conf import settings +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist + +from .models import MumbleUser +from .manager import MumbleManager + +import logging + +logger = logging.getLogger(__name__) + + +class MumbleTasks: + def __init__(self): + pass + + @staticmethod + def has_account(user): + try: + return user.mumble.username != '' + except ObjectDoesNotExist: + return False + + @staticmethod + def disable_mumble(): + if settings.ENABLE_AUTH_MUMBLE: + logger.warn("ENABLE_AUTH_MUMBLE still True, after disabling users will still be able to create mumble accounts") + if settings.ENABLE_BLUE_MUMBLE: + logger.warn("ENABLE_BLUE_MUMBLE still True, after disabling blues will still be able to create mumble accounts") + logger.info("Deleting all MumbleUser models") + MumbleUser.objects.all().delete() + + @staticmethod + @app.task(bind=True) + def update_groups(self, pk): + user = User.objects.get(pk=pk) + logger.debug("Updating mumble groups for user %s" % user) + if MumbleTasks.has_account(user): + groups = [] + for group in user.groups.all(): + groups.append(str(group.name)) + if len(groups) == 0: + groups.append('empty') + logger.debug("Updating user %s mumble groups to %s" % (user, groups)) + try: + if not MumbleManager.update_groups(user, groups): + raise Exception("Group sync failed") + except: + logger.exception("Mumble group sync failed for %s, retrying in 10 mins" % user) + raise self.retry(countdown=60 * 10) + logger.debug("Updated user %s mumble groups." % user) + else: + logger.debug("User %s does not have a mumble account, skipping" % user) + + @staticmethod + @app.task + def update_all_groups(): + logger.debug("Updating ALL mumble groups") + for mumble_user in MumbleUser.objects.exclude(username__exact=''): + MumbleTasks.update_groups.delay(mumble_user.user.pk) diff --git a/services/modules/mumble/tests.py b/services/modules/mumble/tests.py new file mode 100644 index 00000000..6b6f0339 --- /dev/null +++ b/services/modules/mumble/tests.py @@ -0,0 +1,201 @@ +from __future__ import unicode_literals + +try: + # Py3 + from unittest import mock +except ImportError: + # Py2 + import mock + +from django.test import TestCase, RequestFactory +from django.conf import settings +from django import urls +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist + +from alliance_auth.tests.auth_utils import AuthUtils + +from .auth_hooks import MumbleService +from .models import MumbleUser +from .tasks import MumbleTasks + +import hashlib + +MODULE_PATH = 'services.modules.mumble' + + +def gen_pwhash(password): + return hashlib.sha1(password).hexdigest() + + +class MumbleHooksTestCase(TestCase): + def setUp(self): + self.member = 'member_user' + member = AuthUtils.create_member(self.member) + MumbleUser.objects.create(user=member, username=self.member, pwhash='password', groups='Member') + self.blue = 'blue_user' + blue = AuthUtils.create_blue(self.blue) + MumbleUser.objects.create(user=blue, username=self.blue, pwhash='password', groups='Blue') + self.none_user = 'none_user' + none_user = AuthUtils.create_user(self.none_user) + self.service = MumbleService + + def test_has_account(self): + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(MumbleTasks.has_account(member)) + self.assertTrue(MumbleTasks.has_account(blue)) + self.assertFalse(MumbleTasks.has_account(none_user)) + + def test_service_enabled(self): + service = self.service() + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(service.service_enabled_members()) + self.assertTrue(service.service_enabled_blues()) + + self.assertEqual(service.service_active_for_user(member), settings.ENABLE_AUTH_MUMBLE) + self.assertEqual(service.service_active_for_user(blue), settings.ENABLE_BLUE_MUMBLE) + self.assertFalse(service.service_active_for_user(none_user)) + + @mock.patch(MODULE_PATH + '.tasks.MumbleManager') + def test_update_all_groups(self, manager): + service = self.service() + service.update_all_groups() + # Check member and blue user have groups updated + self.assertTrue(manager.update_groups.called) + self.assertEqual(manager.update_groups.call_count, 2) + + def test_update_groups(self): + # Check member has Member group updated + service = self.service() + member = User.objects.get(username=self.member) + member.mumble.groups = '' # Remove the group set in setUp + member.mumble.save() + + service.update_groups(member) + + mumble_user = MumbleUser.objects.get(user=member) + self.assertIn(settings.DEFAULT_AUTH_GROUP, mumble_user.groups) + + # Check none user does not have groups updated + with mock.patch(MODULE_PATH + '.tasks.MumbleManager') as manager: + service = self.service() + none_user = User.objects.get(username=self.none_user) + service.update_groups(none_user) + self.assertFalse(manager.update_groups.called) + + def test_validate_user(self): + service = self.service() + # Test member is not deleted + member = User.objects.get(username=self.member) + service.validate_user(member) + self.assertTrue(member.mumble) + + # Test none user is deleted + none_user = User.objects.get(username=self.none_user) + MumbleUser.objects.create(user=none_user, username='mr no-name', pwhash='password', groups='Blue,Orange') + service.validate_user(none_user) + with self.assertRaises(ObjectDoesNotExist): + none_mumble = User.objects.get(username=self.none_user).mumble + + def test_delete_user(self): + member = User.objects.get(username=self.member) + + service = self.service() + result = service.delete_user(member) + + self.assertTrue(result) + with self.assertRaises(ObjectDoesNotExist): + mumble_user = User.objects.get(username=self.member).mumble + + def test_render_services_ctrl(self): + service = self.service() + member = User.objects.get(username=self.member) + request = RequestFactory().get('/en/services/') + request.user = member + + response = service.render_services_ctrl(request) + self.assertTemplateUsed(service.service_ctrl_template) + self.assertIn(urls.reverse('auth_deactivate_mumble'), response) + self.assertIn(urls.reverse('auth_reset_mumble_password'), response) + self.assertIn(urls.reverse('auth_set_mumble_password'), response) + + # Test register becomes available + member.mumble.delete() + member = User.objects.get(username=self.member) + request.user = member + response = service.render_services_ctrl(request) + self.assertIn(urls.reverse('auth_activate_mumble'), response) + + +class MumbleViewsTestCase(TestCase): + def setUp(self): + self.member = AuthUtils.create_member('auth_member') + self.member.set_password('password') + self.member.email = 'auth_member@example.com' + self.member.save() + AuthUtils.add_main_character(self.member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation', + corp_ticker='TESTR') + + def login(self): + self.client.login(username=self.member.username, password='password') + + def test_activate(self): + self.login() + expected_username = '[TESTR]auth_member' + response = self.client.get(urls.reverse('auth_activate_mumble'), follow=False) + self.assertEqual(response.status_code, 200) + self.assertContains(response, expected_username) + mumble_user = MumbleUser.objects.get(user=self.member) + self.assertEqual(mumble_user.username, expected_username) + self.assertTrue(mumble_user.pwhash) + self.assertEqual(self.member.mumble.username, expected_username) + + def test_deactivate(self): + self.login() + MumbleUser.objects.create(user=self.member, username='some member') + + response = self.client.get(urls.reverse('auth_deactivate_mumble')) + + self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) + with self.assertRaises(ObjectDoesNotExist): + mumble_user = User.objects.get(pk=self.member.pk).mumble + + def test_set_password(self): + self.login() + MumbleUser.objects.create(user=self.member, username='some member', pwhash='old') + + response = self.client.post(urls.reverse('auth_set_mumble_password'), data={'password': '1234asdf'}) + + self.assertNotEqual(MumbleUser.objects.get(user=self.member).pwhash, 'old') + self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) + + def test_reset_password(self): + self.login() + MumbleUser.objects.create(user=self.member, username='some member', pwhash='old') + + response = self.client.get(urls.reverse('auth_reset_mumble_password')) + + self.assertNotEqual(MumbleUser.objects.get(user=self.member).pwhash, 'old') + self.assertTemplateUsed(response, 'registered/service_credentials.html') + self.assertContains(response, 'some member') + + +class MumbleManagerTestCase(TestCase): + def setUp(self): + from .manager import MumbleManager + self.manager = MumbleManager + + def test_generate_random_password(self): + password = self.manager._MumbleManager__generate_random_pass() + + self.assertEqual(len(password), 16) + self.assertIsInstance(password, type('')) + + def test_gen_pwhash(self): + pwhash = self.manager._gen_pwhash('test') + + self.assertEqual(pwhash, 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3') diff --git a/services/modules/mumble/urls.py b/services/modules/mumble/urls.py new file mode 100644 index 00000000..ec22746f --- /dev/null +++ b/services/modules/mumble/urls.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals +from django.conf.urls import url, include + +from . import views + +module_urls = [ + # Mumble service control + url(r'^activate/$', views.activate_mumble, name='auth_activate_mumble'), + url(r'^deactivate/$', views.deactivate_mumble, name='auth_deactivate_mumble'), + url(r'^reset_password/$', views.reset_mumble_password, + name='auth_reset_mumble_password'), + url(r'^set_password/$', views.set_mumble_password, name='auth_set_mumble_password'), +] + +urlpatterns = [ + url(r'^mumble/', include(module_urls)) +] diff --git a/services/modules/mumble/views.py b/services/modules/mumble/views.py new file mode 100644 index 00000000..497f0796 --- /dev/null +++ b/services/modules/mumble/views.py @@ -0,0 +1,120 @@ +from __future__ import unicode_literals +from django.contrib.auth.decorators import login_required +from django.shortcuts import render, redirect +from django.contrib import messages + +from authentication.decorators import members_and_blues +from eveonline.managers import EveManager +from eveonline.models import EveAllianceInfo +from authentication.states import MEMBER_STATE, BLUE_STATE, NONE_STATE +from authentication.models import AuthServicesInfo + +from services.forms import ServicePasswordForm + +from .manager import MumbleManager +from .tasks import MumbleTasks +from .models import MumbleUser + +import logging + +logger = logging.getLogger(__name__) + + +@login_required +@members_and_blues() +def activate_mumble(request): + logger.debug("activate_mumble called by user %s" % request.user) + authinfo = AuthServicesInfo.objects.get(user=request.user) + character = EveManager.get_main_character(request.user) + ticker = character.corporation_ticker + + if authinfo.state == BLUE_STATE: + logger.debug("Adding mumble user for blue user %s with main character %s" % (request.user, character)) + # Blue members should have alliance ticker (if in alliance) + if EveAllianceInfo.objects.filter(alliance_id=character.alliance_id).exists(): + alliance = EveAllianceInfo.objects.filter(alliance_id=character.alliance_id)[0] + ticker = alliance.alliance_ticker + result = MumbleManager.create_user(request.user, ticker, character.character_name, blue=True) + else: + logger.debug("Adding mumble user for user %s with main character %s" % (request.user, character)) + result = MumbleManager.create_user(request.user, ticker, character.character_name) + + if result: + logger.debug("Updated authserviceinfo for user %s with mumble credentials. Updating groups." % request.user) + MumbleTasks.update_groups.apply(request.user.pk) # Run synchronously to prevent timing issues + logger.info("Successfully activated mumble for user %s" % request.user) + messages.success(request, 'Activated Mumble account.') + credentials = { + 'username': result[0], + 'password': result[1], + } + return render(request, 'registered/service_credentials.html', + context={'credentials': credentials, 'service': 'Mumble'}) + else: + logger.error("Unsuccessful attempt to activate mumble for user %s" % request.user) + messages.error(request, 'An error occurred while processing your Mumble account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def deactivate_mumble(request): + logger.debug("deactivate_mumble called by user %s" % request.user) + # if we successfully remove the user or the user is already removed + if MumbleManager.delete_user(request.user): + logger.info("Successfully deactivated mumble for user %s" % request.user) + messages.success(request, 'Deactivated Mumble account.') + else: + logger.error("Unsuccessful attempt to deactivate mumble for user %s" % request.user) + messages.error(request, 'An error occurred while processing your Mumble account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def reset_mumble_password(request): + logger.debug("reset_mumble_password called by user %s" % request.user) + result = MumbleManager.update_user_password(request.user) + + # if blank we failed + if result != "": + logger.info("Successfully reset mumble password for user %s" % request.user) + messages.success(request, 'Reset Mumble password.') + credentials = { + 'username': request.user.mumble.username, + 'password': result, + } + return render(request, 'registered/service_credentials.html', + context={'credentials': credentials, 'service': 'Mumble'}) + else: + logger.error("Unsuccessful attempt to reset mumble password for user %s" % request.user) + messages.error(request, 'An error occurred while processing your Mumble account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def set_mumble_password(request): + logger.debug("set_mumble_password called by user %s" % request.user) + if request.method == 'POST': + logger.debug("Received POST request with form.") + form = ServicePasswordForm(request.POST) + logger.debug("Form is valid: %s" % form.is_valid()) + if form.is_valid() and MumbleTasks.has_account(request.user): + password = form.cleaned_data['password'] + logger.debug("Form contains password of length %s" % len(password)) + result = MumbleManager.update_user_password(request.user, password=password) + if result != "": + logger.info("Successfully reset forum password for user %s" % request.user) + messages.success(request, 'Set Mumble password.') + else: + logger.error("Failed to install custom mumble password for user %s" % request.user) + messages.error(request, 'An error occurred while processing your Mumble account.') + return redirect("auth_services") + else: + logger.debug("Request is not type POST - providing empty form.") + form = ServicePasswordForm() + + logger.debug("Rendering form for user %s" % request.user) + context = {'form': form, 'service': 'Mumble'} + return render(request, 'registered/service_password.html', context=context) diff --git a/services/modules/openfire/__init__.py b/services/modules/openfire/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/openfire/admin.py b/services/modules/openfire/admin.py new file mode 100644 index 00000000..44d88289 --- /dev/null +++ b/services/modules/openfire/admin.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from django.contrib import admin +from .models import OpenfireUser + + +class OpenfireUserAdmin(admin.ModelAdmin): + list_display = ('user', 'username') + search_fields = ('user__username', 'username') + +admin.site.register(OpenfireUser, OpenfireUserAdmin) diff --git a/services/modules/openfire/apps.py b/services/modules/openfire/apps.py new file mode 100644 index 00000000..f47161c0 --- /dev/null +++ b/services/modules/openfire/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class OpenfireServiceConfig(AppConfig): + name = 'openfire' diff --git a/services/modules/openfire/auth_hooks.py b/services/modules/openfire/auth_hooks.py new file mode 100644 index 00000000..ab0b8b2a --- /dev/null +++ b/services/modules/openfire/auth_hooks.py @@ -0,0 +1,93 @@ +from __future__ import unicode_literals + +from django.conf import settings +from django.template.loader import render_to_string + +from services.hooks import ServicesHook, MenuItemHook +from alliance_auth import hooks + +from .urls import urlpatterns +from .tasks import OpenfireTasks + +import logging + +logger = logging.getLogger(__name__) + + +class OpenfireService(ServicesHook): + def __init__(self): + ServicesHook.__init__(self) + self.name = 'openfire' + self.urlpatterns = urlpatterns + self.service_url = settings.JABBER_URL + + @property + def title(self): + return "Jabber" + + def delete_user(self, user, notify_user=False): + logger.debug('Deleting user %s %s account' % (user, self.name)) + return OpenfireTasks.delete_user(user, notify_user=notify_user) + + def validate_user(self, user): + logger.debug('Validating user %s %s account' % (user, self.name)) + if OpenfireTasks.has_account(user) and not self.service_active_for_user(user): + self.delete_user(user, notify_user=True) + + def update_groups(self, user): + logger.debug('Updating %s groups for %s' % (self.name, user)) + if OpenfireTasks.has_account(user): + OpenfireTasks.update_groups.delay(user.pk) + + def update_all_groups(self): + logger.debug('Update all %s groups called' % self.name) + OpenfireTasks.update_all_groups.delay() + + def service_enabled_members(self): + return settings.ENABLE_AUTH_JABBER or False # TODO: Rename this setting + + def service_enabled_blues(self): + return settings.ENABLE_BLUE_JABBER or False # TODO: Rename this setting + + def render_services_ctrl(self, request): + """ + Example for rendering the service control panel row + You can override the default template and create a + custom one if you wish. + :param request: + :return: + """ + urls = self.Urls() + urls.auth_activate = 'auth_activate_openfire' + urls.auth_deactivate = 'auth_deactivate_openfire' + urls.auth_set_password = 'auth_set_openfire_password' + urls.auth_reset_password = 'auth_reset_openfire_password' + return render_to_string(self.service_ctrl_template, { + 'service_name': self.title, + 'urls': urls, + 'service_url': self.service_url, + 'username': request.user.openfire.username if OpenfireTasks.has_account(request.user) else '' + }, request=request) + + +@hooks.register('services_hook') +def register_service(): + return OpenfireService() + + +class JabberBroadcast(MenuItemHook): + def __init__(self): + MenuItemHook.__init__(self, + 'Jabber Broadcast', + 'fa fa-lock fa-fw fa-bullhorn grayiconecolor', + 'auth_jabber_broadcast_view') + + def render(self, request): + if request.user.has_perm('auth.jabber_broadcast'): + return MenuItemHook.render(self, request) + return '' + + +@hooks.register('menu_util_hook') +def register_menu(): + return JabberBroadcast() diff --git a/services/modules/openfire/forms.py b/services/modules/openfire/forms.py new file mode 100644 index 00000000..593c710c --- /dev/null +++ b/services/modules/openfire/forms.py @@ -0,0 +1,9 @@ +from __future__ import unicode_literals + +from django import forms +from django.utils.translation import ugettext_lazy as _ + + +class JabberBroadcastForm(forms.Form): + group = forms.ChoiceField(label=_('Group'), widget=forms.Select) + message = forms.CharField(label=_('Message'), widget=forms.Textarea) diff --git a/services/managers/openfire_manager.py b/services/modules/openfire/manager.py similarity index 98% rename from services/managers/openfire_manager.py rename to services/modules/openfire/manager.py index 4cdb054a..257907b7 100755 --- a/services/managers/openfire_manager.py +++ b/services/modules/openfire/manager.py @@ -1,7 +1,8 @@ from __future__ import unicode_literals from django.utils import six import re -import os +import random +import string try: from urlparse import urlparse except ImportError: @@ -42,7 +43,7 @@ class OpenfireManager: @staticmethod def __generate_random_pass(): - return os.urandom(8).encode('hex') + return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(16)]) @staticmethod def _sanitize_groupname(name): diff --git a/services/modules/openfire/migrations/0001_initial.py b/services/modules/openfire/migrations/0001_initial.py new file mode 100644 index 00000000..d6f89303 --- /dev/null +++ b/services/modules/openfire/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-12-12 03:27 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='OpenfireUser', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='openfire', serialize=False, to=settings.AUTH_USER_MODEL)), + ('username', models.CharField(max_length=254)), + ], + ), + ] diff --git a/services/modules/openfire/migrations/__init__.py b/services/modules/openfire/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/openfire/models.py b/services/modules/openfire/models.py new file mode 100644 index 00000000..dca95993 --- /dev/null +++ b/services/modules/openfire/models.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals +from django.utils.encoding import python_2_unicode_compatible +from django.db import models + + +@python_2_unicode_compatible +class OpenfireUser(models.Model): + user = models.OneToOneField('auth.User', + primary_key=True, + on_delete=models.CASCADE, + related_name='openfire') + username = models.CharField(max_length=254) + + def __str__(self): + return self.username diff --git a/services/modules/openfire/tasks.py b/services/modules/openfire/tasks.py new file mode 100644 index 00000000..c463a9f1 --- /dev/null +++ b/services/modules/openfire/tasks.py @@ -0,0 +1,75 @@ +from __future__ import unicode_literals + +import logging + +from alliance_auth.celeryapp import app +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from django.contrib.auth.models import User +from notifications import notify + +from services.modules.openfire.manager import OpenfireManager + +from .models import OpenfireUser + +logger = logging.getLogger(__name__) + + +class OpenfireTasks: + def __init__(self): + pass + + @classmethod + def delete_user(cls, user, notify_user=False): + if cls.has_account(user): + logger.debug("User %s has jabber account %s. Deleting." % (user, user.openfire.username)) + OpenfireManager.delete_user(user.openfire.username) + user.openfire.delete() + if notify_user: + notify(user, 'Jabber Account Disabled', level='danger') + return True + return False + + @staticmethod + def has_account(user): + try: + return user.openfire.username != '' + except ObjectDoesNotExist: + return False + + @staticmethod + def disable_jabber(): + if settings.ENABLE_AUTH_JABBER: + logger.warn("ENABLE_AUTH_JABBER still True, after disabling users will still be able to create jabber accounts") + if settings.ENABLE_BLUE_JABBER: + logger.warn("ENABLE_BLUE_JABBER still True, after disabling blues will still be able to create jabber accounts") + logging.debug("Deleting all Openfire users") + OpenfireUser.objects.all().delete() + + @staticmethod + @app.task(bind=True) + def update_groups(self, pk): + user = User.objects.get(pk=pk) + logger.debug("Updating jabber groups for user %s" % user) + if OpenfireTasks.has_account(user): + groups = [] + for group in user.groups.all(): + groups.append(str(group.name)) + if len(groups) == 0: + groups.append('empty') + logger.debug("Updating user %s jabber groups to %s" % (user, groups)) + try: + OpenfireManager.update_user_groups(user.openfire.username, groups) + except: + logger.exception("Jabber group sync failed for %s, retrying in 10 mins" % user) + raise self.retry(countdown=60 * 10) + logger.debug("Updated user %s jabber groups." % user) + else: + logger.debug("User does not have an openfire account") + + @staticmethod + @app.task + def update_all_groups(): + logger.debug("Updating ALL jabber groups") + for openfire_user in OpenfireUser.objects.exclude(username__exact=''): + OpenfireTasks.update_groups.delay(openfire_user.user.pk) diff --git a/services/modules/openfire/tests.py b/services/modules/openfire/tests.py new file mode 100644 index 00000000..5257f7cc --- /dev/null +++ b/services/modules/openfire/tests.py @@ -0,0 +1,207 @@ +from __future__ import unicode_literals + +try: + # Py3 + from unittest import mock +except ImportError: + # Py2 + import mock + +from django.test import TestCase, RequestFactory +from django.conf import settings +from django import urls +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist + +from alliance_auth.tests.auth_utils import AuthUtils + +from .auth_hooks import OpenfireService +from .models import OpenfireUser +from .tasks import OpenfireTasks + +MODULE_PATH = 'services.modules.openfire' + + +class OpenfireHooksTestCase(TestCase): + def setUp(self): + self.member = 'member_user' + member = AuthUtils.create_member(self.member) + OpenfireUser.objects.create(user=member, username=self.member) + self.blue = 'blue_user' + blue = AuthUtils.create_blue(self.blue) + OpenfireUser.objects.create(user=blue, username=self.blue) + self.none_user = 'none_user' + none_user = AuthUtils.create_user(self.none_user) + self.service = OpenfireService + + def test_has_account(self): + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(OpenfireTasks.has_account(member)) + self.assertTrue(OpenfireTasks.has_account(blue)) + self.assertFalse(OpenfireTasks.has_account(none_user)) + + def test_service_enabled(self): + service = self.service() + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(service.service_enabled_members()) + self.assertTrue(service.service_enabled_blues()) + + self.assertEqual(service.service_active_for_user(member), settings.ENABLE_AUTH_JABBER) + self.assertEqual(service.service_active_for_user(blue), settings.ENABLE_BLUE_JABBER) + self.assertFalse(service.service_active_for_user(none_user)) + + @mock.patch(MODULE_PATH + '.tasks.OpenfireManager') + def test_update_all_groups(self, manager): + service = self.service() + service.update_all_groups() + # Check member and blue user have groups updated + self.assertTrue(manager.update_user_groups.called) + self.assertEqual(manager.update_user_groups.call_count, 2) + + def test_update_groups(self): + # Check member has Member group updated + with mock.patch(MODULE_PATH + '.tasks.OpenfireManager') as manager: + service = self.service() + member = User.objects.get(username=self.member) + service.update_groups(member) + self.assertTrue(manager.update_user_groups.called) + args, kwargs = manager.update_user_groups.call_args + user_id, groups = args + self.assertIn(settings.DEFAULT_AUTH_GROUP, groups) + self.assertEqual(user_id, member.openfire.username) + + # Check none user does not have groups updated + with mock.patch(MODULE_PATH + '.tasks.OpenfireManager') as manager: + service = self.service() + none_user = User.objects.get(username=self.none_user) + service.update_groups(none_user) + self.assertFalse(manager.update_user_groups.called) + + @mock.patch(MODULE_PATH + '.tasks.OpenfireManager') + def test_validate_user(self, manager): + service = self.service() + # Test member is not deleted + member = User.objects.get(username=self.member) + service.validate_user(member) + self.assertTrue(member.openfire) + + # Test none user is deleted + none_user = User.objects.get(username=self.none_user) + OpenfireUser.objects.create(user=none_user, username='abc123') + service.validate_user(none_user) + self.assertTrue(manager.delete_user.called) + with self.assertRaises(ObjectDoesNotExist): + none_openfire = User.objects.get(username=self.none_user).openfire + + @mock.patch(MODULE_PATH + '.tasks.OpenfireManager') + def test_delete_user(self, manager): + member = User.objects.get(username=self.member) + + service = self.service() + result = service.delete_user(member) + + self.assertTrue(result) + self.assertTrue(manager.delete_user.called) + with self.assertRaises(ObjectDoesNotExist): + openfire_user = User.objects.get(username=self.member).openfire + + def test_render_services_ctrl(self): + service = self.service() + member = User.objects.get(username=self.member) + request = RequestFactory().get('/en/services/') + request.user = member + + response = service.render_services_ctrl(request) + self.assertTemplateUsed(service.service_ctrl_template) + self.assertIn(urls.reverse('auth_deactivate_openfire'), response) + self.assertIn(urls.reverse('auth_reset_openfire_password'), response) + self.assertIn(urls.reverse('auth_set_openfire_password'), response) + + # Test register becomes available + member.openfire.delete() + member = User.objects.get(username=self.member) + request.user = member + response = service.render_services_ctrl(request) + self.assertIn(urls.reverse('auth_activate_openfire'), response) + + +class OpenfireViewsTestCase(TestCase): + def setUp(self): + self.member = AuthUtils.create_member('auth_member') + self.member.set_password('password') + self.member.email = 'auth_member@example.com' + self.member.save() + AuthUtils.add_main_character(self.member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation') + + def login(self): + self.client.login(username=self.member.username, password='password') + + @mock.patch(MODULE_PATH + '.tasks.OpenfireManager') + @mock.patch(MODULE_PATH + '.views.OpenfireManager') + def test_activate(self, manager, tasks_manager): + self.login() + expected_username = 'auth_member' + manager.add_user.return_value = (expected_username, 'abc123') + + response = self.client.get(urls.reverse('auth_activate_openfire')) + + self.assertTrue(manager.add_user.called) + self.assertTrue(tasks_manager.update_user_groups.called) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed('registered/service_credentials.html') + self.assertContains(response, expected_username) + openfire_user = OpenfireUser.objects.get(user=self.member) + self.assertEqual(openfire_user.username, expected_username) + + @mock.patch(MODULE_PATH + '.tasks.OpenfireManager') + def test_deactivate(self, manager): + self.login() + OpenfireUser.objects.create(user=self.member, username='some member') + + response = self.client.get(urls.reverse('auth_deactivate_openfire')) + + self.assertTrue(manager.delete_user.called) + self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) + with self.assertRaises(ObjectDoesNotExist): + openfire_user = User.objects.get(pk=self.member.pk).openfire + + @mock.patch(MODULE_PATH + '.views.OpenfireManager') + def test_set_password(self, manager): + self.login() + OpenfireUser.objects.create(user=self.member, username='some member') + + response = self.client.post(urls.reverse('auth_set_openfire_password'), data={'password': '1234asdf'}) + + self.assertTrue(manager.update_user_pass.called) + args, kwargs = manager.update_user_pass.call_args + self.assertEqual(kwargs['password'], '1234asdf') + self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) + + @mock.patch(MODULE_PATH + '.views.OpenfireManager') + def test_reset_password(self, manager): + self.login() + OpenfireUser.objects.create(user=self.member, username='some member') + + manager.update_user_pass.return_value = 'hunter2' + + response = self.client.get(urls.reverse('auth_reset_openfire_password')) + + self.assertTemplateUsed(response, 'registered/service_credentials.html') + self.assertContains(response, 'some member') + self.assertContains(response, 'hunter2') + + +class OpenfireManagerTestCase(TestCase): + def setUp(self): + from .manager import OpenfireManager + self.manager = OpenfireManager + + def test_generate_random_password(self): + password = self.manager._OpenfireManager__generate_random_pass() + + self.assertEqual(len(password), 16) + self.assertIsInstance(password, type('')) diff --git a/services/modules/openfire/urls.py b/services/modules/openfire/urls.py new file mode 100644 index 00000000..bb63cbd6 --- /dev/null +++ b/services/modules/openfire/urls.py @@ -0,0 +1,28 @@ +from __future__ import unicode_literals +from django.conf.urls import url, include +from django.conf.urls.i18n import i18n_patterns +from django.utils.translation import ugettext_lazy as _ + +from . import views + +module_urls = [ + # Jabber Service Control + url(r'^activate/$', views.activate_jabber, name='auth_activate_openfire'), + url(r'^deactivate/$', views.deactivate_jabber, name='auth_deactivate_openfire'), + url(r'^reset_password/$', views.reset_jabber_password, name='auth_reset_openfire_password'), +] + +module_i18n_urls = [ + url(_(r'^set_password/$'), views.set_jabber_password, name='auth_set_openfire_password'), +] + +urlpatterns = [ + url(r'^openfire/', include(module_urls)) +] + +urlpatterns += i18n_patterns( + # Jabber Broadcast + url(_(r'^services/jabber_broadcast/$'), views.jabber_broadcast_view, name='auth_jabber_broadcast_view'), + # Jabber + url(r'openfire/', include(module_i18n_urls)) +) diff --git a/services/modules/openfire/views.py b/services/modules/openfire/views.py new file mode 100644 index 00000000..90ff272f --- /dev/null +++ b/services/modules/openfire/views.py @@ -0,0 +1,160 @@ +from __future__ import unicode_literals + +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.models import Group +from django.shortcuts import render, redirect + +from authentication.decorators import members_and_blues +from eveonline.managers import EveManager +from eveonline.models import EveCharacter +from services.forms import ServicePasswordForm + +from .manager import OpenfireManager +from .tasks import OpenfireTasks +from .forms import JabberBroadcastForm +from .models import OpenfireUser + +import datetime + +import logging + +logger = logging.getLogger(__name__) + + +@login_required +@members_and_blues() +def activate_jabber(request): + logger.debug("activate_jabber called by user %s" % request.user) + character = EveManager.get_main_character(request.user) + logger.debug("Adding jabber user for user %s with main character %s" % (request.user, character)) + info = OpenfireManager.add_user(character.character_name) + # If our username is blank means we already had a user + if info[0] is not "": + OpenfireUser.objects.update_or_create(user=request.user, defaults={'username': info[0]}) + logger.debug("Updated authserviceinfo for user %s with jabber credentials. Updating groups." % request.user) + OpenfireTasks.update_groups.delay(request.user.pk) + logger.info("Successfully activated jabber for user %s" % request.user) + messages.success(request, 'Activated jabber account.') + credentials = { + 'username': info[0], + 'password': info[1], + } + return render(request, 'registered/service_credentials.html', + context={'credentials': credentials, 'service': 'Jabber'}) + else: + logger.error("Unsuccessful attempt to activate jabber for user %s" % request.user) + messages.error(request, 'An error occurred while processing your jabber account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def deactivate_jabber(request): + logger.debug("deactivate_jabber called by user %s" % request.user) + if OpenfireTasks.has_account(request.user) and OpenfireTasks.delete_user(request.user): + logger.info("Successfully deactivated jabber for user %s" % request.user) + messages.success(request, 'Deactivated jabber account.') + else: + logger.error("Unsuccessful attempt to deactivate jabber for user %s" % request.user) + messages.error(request, 'An error occurred while processing your jabber account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def reset_jabber_password(request): + logger.debug("reset_jabber_password called by user %s" % request.user) + if OpenfireTasks.has_account(request.user): + result = OpenfireManager.update_user_pass(request.user.openfire.username) + # If our username is blank means we failed + if result != "": + logger.info("Successfully reset jabber password for user %s" % request.user) + messages.success(request, 'Reset jabber password.') + credentials = { + 'username': request.user.openfire.username, + 'password': result, + } + return render(request, 'registered/service_credentials.html', + context={'credentials': credentials, 'service': 'Jabber'}) + logger.error("Unsuccessful attempt to reset jabber for user %s" % request.user) + messages.error(request, 'An error occurred while processing your jabber account.') + return redirect("auth_services") + + +@login_required +@permission_required('auth.jabber_broadcast') +def jabber_broadcast_view(request): + logger.debug("jabber_broadcast_view called by user %s" % request.user) + allchoices = [] + if request.user.has_perm('auth.jabber_broadcast_all'): + allchoices.append(('all', 'all')) + for g in Group.objects.all(): + allchoices.append((str(g.name), str(g.name))) + else: + for g in request.user.groups.all(): + allchoices.append((str(g.name), str(g.name))) + if request.method == 'POST': + form = JabberBroadcastForm(request.POST) + form.fields['group'].choices = allchoices + logger.debug("Received POST request containing form, valid: %s" % form.is_valid()) + if form.is_valid(): + main_char = EveManager.get_main_character(request.user) + logger.debug("Processing jabber broadcast for user %s with main character %s" % (request.user, main_char)) + if main_char is not None: + message_to_send = form.cleaned_data[ + 'message'] + "\n##### SENT BY: " + "[" + main_char.corporation_ticker + "]" + \ + main_char.character_name + " TO: " + \ + form.cleaned_data['group'] + " WHEN: " + datetime.datetime.utcnow().strftime( + "%Y-%m-%d %H:%M:%S") + " #####\n##### Replies are NOT monitored #####\n" + group_to_send = form.cleaned_data['group'] + + OpenfireManager.send_broadcast_threaded(group_to_send, message_to_send, ) + + else: + message_to_send = form.cleaned_data[ + 'message'] + "\n##### SENT BY: " + "No character but can send pings?" + " TO: " + \ + form.cleaned_data['group'] + " WHEN: " + datetime.datetime.utcnow().strftime( + "%Y-%m-%d %H:%M:%S") + " #####\n##### Replies are NOT monitored #####\n" + group_to_send = form.cleaned_data['group'] + + OpenfireManager.send_broadcast_threaded(group_to_send, message_to_send, ) + + messages.success(request, 'Sent jabber broadcast to %s' % group_to_send) + logger.info("Sent jabber broadcast on behalf of user %s" % request.user) + else: + form = JabberBroadcastForm() + form.fields['group'].choices = allchoices + logger.debug("Generated broadcast form for user %s containing %s groups" % ( + request.user, len(form.fields['group'].choices))) + + context = {'form': form} + return render(request, 'registered/jabberbroadcast.html', context=context) + + +@login_required +@members_and_blues() +def set_jabber_password(request): + logger.debug("set_jabber_password called by user %s" % request.user) + if request.method == 'POST': + logger.debug("Received POST request with form.") + form = ServicePasswordForm(request.POST) + logger.debug("Form is valid: %s" % form.is_valid()) + if form.is_valid() and OpenfireTasks.has_account(request.user): + password = form.cleaned_data['password'] + logger.debug("Form contains password of length %s" % len(password)) + result = OpenfireManager.update_user_pass(request.user.openfire.username, password=password) + if result != "": + logger.info("Successfully set jabber password for user %s" % request.user) + messages.success(request, 'Set jabber password.') + else: + logger.error("Failed to install custom jabber password for user %s" % request.user) + messages.error(request, 'An error occurred while processing your jabber account.') + return redirect("auth_services") + else: + logger.debug("Request is not type POST - providing empty form.") + form = ServicePasswordForm() + + logger.debug("Rendering form for user %s" % request.user) + context = {'form': form, 'service': 'Jabber'} + return render(request, 'registered/service_password.html', context=context) diff --git a/services/modules/phpbb3/__init__.py b/services/modules/phpbb3/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/phpbb3/admin.py b/services/modules/phpbb3/admin.py new file mode 100644 index 00000000..97376ea5 --- /dev/null +++ b/services/modules/phpbb3/admin.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from django.contrib import admin +from .models import Phpbb3User + + +class Phpbb3UserAdmin(admin.ModelAdmin): + list_display = ('user', 'username') + search_fields = ('user__username', 'username') + +admin.site.register(Phpbb3User, Phpbb3UserAdmin) diff --git a/services/modules/phpbb3/apps.py b/services/modules/phpbb3/apps.py new file mode 100644 index 00000000..ed8a7bb7 --- /dev/null +++ b/services/modules/phpbb3/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class Phpbb3ServiceConfig(AppConfig): + name = 'phpbb3' diff --git a/services/modules/phpbb3/auth_hooks.py b/services/modules/phpbb3/auth_hooks.py new file mode 100644 index 00000000..df698103 --- /dev/null +++ b/services/modules/phpbb3/auth_hooks.py @@ -0,0 +1,68 @@ +from __future__ import unicode_literals + +from django.conf import settings +from django.template.loader import render_to_string + +from services.hooks import ServicesHook +from alliance_auth import hooks + +from .urls import urlpatterns +from .tasks import Phpbb3Tasks + +import logging + +logger = logging.getLogger(__name__) + + +class Phpbb3Service(ServicesHook): + def __init__(self): + ServicesHook.__init__(self) + self.name = 'phpbb3' + self.urlpatterns = urlpatterns + self.service_url = settings.FORUM_URL # TODO: This needs to be renamed at some point... + + @property + def title(self): + return 'phpBB3 Forum' + + def delete_user(self, user, notify_user=False): + logger.debug('Deleting user %s %s account' % (user, self.name)) + return Phpbb3Tasks.delete_user(user, notify_user=notify_user) + + def validate_user(self, user): + logger.debug('Validating user %s %s account' % (user, self.name)) + if Phpbb3Tasks.has_account(user) and not self.service_active_for_user(user): + self.delete_user(user, notify_user=True) + + def update_groups(self, user): + logger.debug('Updating %s groups for %s' % (self.name, user)) + if Phpbb3Tasks.has_account(user): + Phpbb3Tasks.update_groups.delay(user.pk) + + def update_all_groups(self): + logger.debug('Update all %s groups called' % self.name) + Phpbb3Tasks.update_all_groups.delay() + + def service_enabled_members(self): + return settings.ENABLE_AUTH_FORUM or False # TODO: Rename this setting + + def service_enabled_blues(self): + return settings.ENABLE_BLUE_FORUM or False # TODO: Rename this setting + + def render_services_ctrl(self, request): + urls = self.Urls() + urls.auth_activate = 'auth_activate_phpbb3' + urls.auth_deactivate = 'auth_deactivate_phpbb3' + urls.auth_reset_password = 'auth_reset_phpbb3_password' + urls.auth_set_password = 'auth_set_phpbb3_password' + return render_to_string(self.service_ctrl_template, { + 'service_name': self.title, + 'urls': urls, + 'service_url': self.service_url, + 'username': request.user.phpbb3.username if Phpbb3Tasks.has_account(request.user) else '' + }, request=request) + + +@hooks.register('services_hook') +def register_service(): + return Phpbb3Service() diff --git a/services/managers/phpbb3_manager.py b/services/modules/phpbb3/manager.py similarity index 99% rename from services/managers/phpbb3_manager.py rename to services/modules/phpbb3/manager.py index 55f2e883..f3d60caf 100755 --- a/services/managers/phpbb3_manager.py +++ b/services/modules/phpbb3/manager.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals -import os +import random +import string import calendar import re from datetime import datetime @@ -62,7 +63,7 @@ class Phpbb3Manager: @staticmethod def __generate_random_pass(): - return os.urandom(8).encode('hex') + return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(16)]) @staticmethod def __gen_hash(password): diff --git a/services/modules/phpbb3/migrations/0001_initial.py b/services/modules/phpbb3/migrations/0001_initial.py new file mode 100644 index 00000000..50da2682 --- /dev/null +++ b/services/modules/phpbb3/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-12-12 03:28 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='Phpbb3User', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='phpbb3', serialize=False, to=settings.AUTH_USER_MODEL)), + ('username', models.CharField(max_length=254)), + ], + ), + ] diff --git a/services/modules/phpbb3/migrations/__init__.py b/services/modules/phpbb3/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/phpbb3/models.py b/services/modules/phpbb3/models.py new file mode 100644 index 00000000..05cfebf5 --- /dev/null +++ b/services/modules/phpbb3/models.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals +from django.utils.encoding import python_2_unicode_compatible +from django.db import models + + +@python_2_unicode_compatible +class Phpbb3User(models.Model): + user = models.OneToOneField('auth.User', + primary_key=True, + on_delete=models.CASCADE, + related_name='phpbb3') + username = models.CharField(max_length=254) + + def __str__(self): + return self.username diff --git a/services/modules/phpbb3/tasks.py b/services/modules/phpbb3/tasks.py new file mode 100644 index 00000000..a47719b1 --- /dev/null +++ b/services/modules/phpbb3/tasks.py @@ -0,0 +1,73 @@ +from __future__ import unicode_literals + +from alliance_auth.celeryapp import app +from django.conf import settings +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist +from notifications import notify + +from .manager import Phpbb3Manager +from .models import Phpbb3User + +import logging + +logger = logging.getLogger(__name__) + + +class Phpbb3Tasks: + def __init__(self): + pass + + @classmethod + def delete_user(cls, user, notify_user=False): + if cls.has_account(user): + logger.debug("User %s has forum account %s. Deleting." % (user, user.phpbb3.username)) + if Phpbb3Manager.disable_user(user.phpbb3.username): + user.phpbb3.delete() + if notify_user: + notify(user, 'Forum Account Disabled', level='danger') + return True + return False + + @staticmethod + def has_account(user): + try: + return user.phpbb3.username != '' + except ObjectDoesNotExist: + return False + + @staticmethod + @app.task(bind=True) + def update_groups(self, pk): + user = User.objects.get(pk=pk) + logger.debug("Updating phpbb3 groups for user %s" % user) + if Phpbb3Tasks.has_account(user): + groups = [] + for group in user.groups.all(): + groups.append(str(group.name)) + if len(groups) == 0: + groups.append('empty') + logger.debug("Updating user %s phpbb3 groups to %s" % (user, groups)) + try: + Phpbb3Manager.update_groups(user.phpbb3.username, groups) + except: + logger.exception("Phpbb group sync failed for %s, retrying in 10 mins" % user) + raise self.retry(countdown=60 * 10) + logger.debug("Updated user %s phpbb3 groups." % user) + else: + logger.debug("User does not have a Phpbb3 account") + + @staticmethod + @app.task + def update_all_groups(): + logger.debug("Updating ALL phpbb3 groups") + for user in Phpbb3User.objects.exclude(username__exact=''): + Phpbb3Tasks.update_groups.delay(user.user_id) + + @staticmethod + def disable(): + if settings.ENABLE_AUTH_FORUM: + logger.warn("ENABLE_AUTH_FORUM still True, after disabling users will still be able to create forum accounts") + if settings.ENABLE_BLUE_FORUM: + logger.warn("ENABLE_BLUE_FORUM still True, after disabling blues will still be able to create forum accounts") + Phpbb3User.objects.all().delete() diff --git a/services/modules/phpbb3/tests.py b/services/modules/phpbb3/tests.py new file mode 100644 index 00000000..4ee24c60 --- /dev/null +++ b/services/modules/phpbb3/tests.py @@ -0,0 +1,212 @@ +from __future__ import unicode_literals + +try: + # Py3 + from unittest import mock +except ImportError: + # Py2 + import mock + +from django.test import TestCase, RequestFactory +from django.conf import settings +from django import urls +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist + +from alliance_auth.tests.auth_utils import AuthUtils + +from .auth_hooks import Phpbb3Service +from .models import Phpbb3User +from .tasks import Phpbb3Tasks + +MODULE_PATH = 'services.modules.phpbb3' + + +class Phpbb3HooksTestCase(TestCase): + def setUp(self): + self.member = 'member_user' + member = AuthUtils.create_member(self.member) + Phpbb3User.objects.create(user=member, username=self.member) + self.blue = 'blue_user' + blue = AuthUtils.create_blue(self.blue) + Phpbb3User.objects.create(user=blue, username=self.blue) + self.none_user = 'none_user' + none_user = AuthUtils.create_user(self.none_user) + self.service = Phpbb3Service + + def test_has_account(self): + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(Phpbb3Tasks.has_account(member)) + self.assertTrue(Phpbb3Tasks.has_account(blue)) + self.assertFalse(Phpbb3Tasks.has_account(none_user)) + + def test_service_enabled(self): + service = self.service() + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(service.service_enabled_members()) + self.assertTrue(service.service_enabled_blues()) + + self.assertEqual(service.service_active_for_user(member), settings.ENABLE_AUTH_FORUM) + self.assertEqual(service.service_active_for_user(blue), settings.ENABLE_BLUE_FORUM) + self.assertFalse(service.service_active_for_user(none_user)) + + @mock.patch(MODULE_PATH + '.tasks.Phpbb3Manager') + def test_update_all_groups(self, manager): + service = self.service() + service.update_all_groups() + # Check member and blue user have groups updated + self.assertTrue(manager.update_groups.called) + self.assertEqual(manager.update_groups.call_count, 2) + + def test_update_groups(self): + # Check member has Member group updated + with mock.patch(MODULE_PATH + '.tasks.Phpbb3Manager') as manager: + service = self.service() + member = User.objects.get(username=self.member) + service.update_groups(member) + self.assertTrue(manager.update_groups.called) + args, kwargs = manager.update_groups.call_args + user_id, groups = args + self.assertIn(settings.DEFAULT_AUTH_GROUP, groups) + self.assertEqual(user_id, member.phpbb3.username) + + # Check none user does not have groups updated + with mock.patch(MODULE_PATH + '.tasks.Phpbb3Manager') as manager: + service = self.service() + none_user = User.objects.get(username=self.none_user) + service.update_groups(none_user) + self.assertFalse(manager.update_groups.called) + + @mock.patch(MODULE_PATH + '.tasks.Phpbb3Manager') + def test_validate_user(self, manager): + service = self.service() + # Test member is not deleted + member = User.objects.get(username=self.member) + service.validate_user(member) + self.assertTrue(member.phpbb3) + + # Test none user is deleted + none_user = User.objects.get(username=self.none_user) + Phpbb3User.objects.create(user=none_user, username='abc123') + service.validate_user(none_user) + self.assertTrue(manager.disable_user.called) + with self.assertRaises(ObjectDoesNotExist): + none_phpbb3 = User.objects.get(username=self.none_user).phpbb3 + + @mock.patch(MODULE_PATH + '.tasks.Phpbb3Manager') + def test_delete_user(self, manager): + member = User.objects.get(username=self.member) + + service = self.service() + result = service.delete_user(member) + + self.assertTrue(result) + self.assertTrue(manager.disable_user.called) + with self.assertRaises(ObjectDoesNotExist): + phpbb3_user = User.objects.get(username=self.member).phpbb3 + + def test_render_services_ctrl(self): + service = self.service() + member = User.objects.get(username=self.member) + request = RequestFactory().get('/en/services/') + request.user = member + + response = service.render_services_ctrl(request) + self.assertTemplateUsed(service.service_ctrl_template) + self.assertIn(urls.reverse('auth_deactivate_phpbb3'), response) + self.assertIn(urls.reverse('auth_reset_phpbb3_password'), response) + self.assertIn(urls.reverse('auth_set_phpbb3_password'), response) + + # Test register becomes available + member.phpbb3.delete() + member = User.objects.get(username=self.member) + request.user = member + response = service.render_services_ctrl(request) + self.assertIn(urls.reverse('auth_activate_phpbb3'), response) + + +class Phpbb3ViewsTestCase(TestCase): + def setUp(self): + self.member = AuthUtils.create_member('auth_member') + self.member.set_password('password') + self.member.email = 'auth_member@example.com' + self.member.save() + AuthUtils.add_main_character(self.member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation') + + def login(self): + self.client.login(username=self.member.username, password='password') + + @mock.patch(MODULE_PATH + '.tasks.Phpbb3Manager') + @mock.patch(MODULE_PATH + '.views.Phpbb3Manager') + def test_activate(self, manager, tasks_manager): + self.login() + expected_username = 'auth_member' + manager.add_user.return_value = (expected_username, 'abc123') + + response = self.client.get(urls.reverse('auth_activate_phpbb3')) + + self.assertTrue(manager.add_user.called) + self.assertTrue(tasks_manager.update_groups.called) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed('registered/service_credentials.html') + self.assertContains(response, expected_username) + phpbb3_user = Phpbb3User.objects.get(user=self.member) + self.assertEqual(phpbb3_user.username, expected_username) + + @mock.patch(MODULE_PATH + '.tasks.Phpbb3Manager') + def test_deactivate(self, manager): + self.login() + Phpbb3User.objects.create(user=self.member, username='some member') + + response = self.client.get(urls.reverse('auth_deactivate_phpbb3')) + + self.assertTrue(manager.disable_user.called) + self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) + with self.assertRaises(ObjectDoesNotExist): + phpbb3_user = User.objects.get(pk=self.member.pk).phpbb3 + + @mock.patch(MODULE_PATH + '.views.Phpbb3Manager') + def test_set_password(self, manager): + self.login() + Phpbb3User.objects.create(user=self.member, username='some member') + + response = self.client.post(urls.reverse('auth_set_phpbb3_password'), data={'password': '1234asdf'}) + + self.assertTrue(manager.update_user_password.called) + args, kwargs = manager.update_user_password.call_args + self.assertEqual(kwargs['password'], '1234asdf') + self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) + + @mock.patch(MODULE_PATH + '.views.Phpbb3Manager') + def test_reset_password(self, manager): + self.login() + Phpbb3User.objects.create(user=self.member, username='some member') + + manager.update_user_password.return_value = 'hunter2' + + response = self.client.get(urls.reverse('auth_reset_phpbb3_password')) + + self.assertTemplateUsed(response, 'registered/service_credentials.html') + self.assertContains(response, 'some member') + self.assertContains(response, 'hunter2') + + +class Phpbb3ManagerTestCase(TestCase): + def setUp(self): + from .manager import Phpbb3Manager + self.manager = Phpbb3Manager + + def test_generate_random_password(self): + password = self.manager._Phpbb3Manager__generate_random_pass() + + self.assertEqual(len(password), 16) + self.assertIsInstance(password, type('')) + + def test_gen_pwhash(self): + pwhash = self.manager._Phpbb3Manager__gen_hash('test') + + self.assertIsInstance(pwhash, str) diff --git a/services/modules/phpbb3/urls.py b/services/modules/phpbb3/urls.py new file mode 100644 index 00000000..a64f4cc1 --- /dev/null +++ b/services/modules/phpbb3/urls.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals +from django.conf.urls import url, include + +from . import views + +module_urls = [ + # Forum Service Control + url(r'^activate/$', views.activate_forum, name='auth_activate_phpbb3'), + url(r'^deactivate/$', views.deactivate_forum, name='auth_deactivate_phpbb3'), + url(r'^reset_password/$', views.reset_forum_password, name='auth_reset_phpbb3_password'), + url(r'^set_password/$', views.set_forum_password, name='auth_set_phpbb3_password'), +] + +urlpatterns = [ + url(r'^phpbb3/', include(module_urls)) +] diff --git a/services/modules/phpbb3/views.py b/services/modules/phpbb3/views.py new file mode 100644 index 00000000..2c1b21db --- /dev/null +++ b/services/modules/phpbb3/views.py @@ -0,0 +1,112 @@ +from __future__ import unicode_literals + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.shortcuts import render, redirect + +from authentication.decorators import members_and_blues +from eveonline.managers import EveManager +from services.forms import ServicePasswordForm + +from .manager import Phpbb3Manager +from .tasks import Phpbb3Tasks +from .models import Phpbb3User + +import logging + +logger = logging.getLogger(__name__) + + +@login_required +@members_and_blues() +def activate_forum(request): + logger.debug("activate_forum called by user %s" % request.user) + # Valid now we get the main characters + character = EveManager.get_main_character(request.user) + logger.debug("Adding phpbb user for user %s with main character %s" % (request.user, character)) + result = Phpbb3Manager.add_user(character.character_name, request.user.email, ['REGISTERED'], + character.character_id) + # if empty we failed + if result[0] != "": + Phpbb3User.objects.update_or_create(user=request.user, defaults={'username': result[0]}) + logger.debug("Updated authserviceinfo for user %s with forum credentials. Updating groups." % request.user) + Phpbb3Tasks.update_groups.delay(request.user.pk) + logger.info("Successfully activated forum for user %s" % request.user) + messages.success(request, 'Activated forum account.') + credentials = { + 'username': result[0], + 'password': result[1], + } + return render(request, 'registered/service_credentials.html', + context={'credentials': credentials, 'service': 'Forum'}) + else: + logger.error("Unsuccessful attempt to activate forum for user %s" % request.user) + messages.error(request, 'An error occurred while processing your forum account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def deactivate_forum(request): + logger.debug("deactivate_forum called by user %s" % request.user) + # false we failed + if Phpbb3Tasks.delete_user(request.user): + logger.info("Successfully deactivated forum for user %s" % request.user) + messages.success(request, 'Deactivated forum account.') + else: + logger.error("Unsuccessful attempt to activate forum for user %s" % request.user) + messages.error(request, 'An error occurred while processing your forum account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def reset_forum_password(request): + logger.debug("reset_forum_password called by user %s" % request.user) + if Phpbb3Tasks.has_account(request.user): + character = EveManager.get_main_character(request.user) + result = Phpbb3Manager.update_user_password(request.user.phpbb3.username, character.character_id) + # false we failed + if result != "": + logger.info("Successfully reset forum password for user %s" % request.user) + messages.success(request, 'Reset forum password.') + credentials = { + 'username': request.user.phpbb3.username, + 'password': result, + } + return render(request, 'registered/service_credentials.html', + context={'credentials': credentials, 'service': 'Forum'}) + + logger.error("Unsuccessful attempt to reset forum password for user %s" % request.user) + messages.error(request, 'An error occurred while processing your forum account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def set_forum_password(request): + logger.debug("set_forum_password called by user %s" % request.user) + if request.method == 'POST': + logger.debug("Received POST request with form.") + form = ServicePasswordForm(request.POST) + logger.debug("Form is valid: %s" % form.is_valid()) + if form.is_valid() and Phpbb3Tasks.has_account(request.user): + password = form.cleaned_data['password'] + logger.debug("Form contains password of length %s" % len(password)) + character = EveManager.get_main_character(request.user) + result = Phpbb3Manager.update_user_password(request.user.phpbb3.username, character.character_id, + password=password) + if result != "": + logger.info("Successfully set forum password for user %s" % request.user) + messages.success(request, 'Set forum password.') + else: + logger.error("Failed to install custom forum password for user %s" % request.user) + messages.error(request, 'An error occurred while processing your forum account.') + return redirect("auth_services") + else: + logger.debug("Request is not type POST - providing empty form.") + form = ServicePasswordForm() + + logger.debug("Rendering form for user %s" % request.user) + context = {'form': form, 'service': 'Forum'} + return render(request, 'registered/service_password.html', context=context) diff --git a/services/modules/smf/__init__.py b/services/modules/smf/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/smf/admin.py b/services/modules/smf/admin.py new file mode 100644 index 00000000..a4291319 --- /dev/null +++ b/services/modules/smf/admin.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from django.contrib import admin +from .models import SmfUser + + +class SmfUserAdmin(admin.ModelAdmin): + list_display = ('user', 'username') + search_fields = ('user__username', 'username') + +admin.site.register(SmfUser, SmfUserAdmin) diff --git a/services/modules/smf/apps.py b/services/modules/smf/apps.py new file mode 100644 index 00000000..66059295 --- /dev/null +++ b/services/modules/smf/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class SmfServiceConfig(AppConfig): + name = 'smf' diff --git a/services/modules/smf/auth_hooks.py b/services/modules/smf/auth_hooks.py new file mode 100644 index 00000000..b6e30652 --- /dev/null +++ b/services/modules/smf/auth_hooks.py @@ -0,0 +1,68 @@ +from __future__ import unicode_literals + +from django.conf import settings +from django.template.loader import render_to_string + +from services.hooks import ServicesHook +from alliance_auth import hooks + +from .urls import urlpatterns +from .tasks import SmfTasks + +import logging + +logger = logging.getLogger(__name__) + + +class SmfService(ServicesHook): + def __init__(self): + ServicesHook.__init__(self) + self.name = 'smf' + self.urlpatterns = urlpatterns + self.service_url = settings.SMF_URL + + @property + def title(self): + return 'SMF Forums' + + def delete_user(self, user, notify_user=False): + logger.debug('Deleting user %s %s account' % (user, self.name)) + return SmfTasks.delete_user(user, notify_user=notify_user) + + def validate_user(self, user): + logger.debug('Validating user %s %s account' % (user, self.name)) + if SmfTasks.has_account(user) and not self.service_active_for_user(user): + self.delete_user(user) + + def update_groups(self, user): + logger.debug('Updating %s groups for %s' % (self.name, user)) + if SmfTasks.has_account(user): + SmfTasks.update_groups.delay(user.pk) + + def update_all_groups(self): + logger.debug('Update all %s groups called' % self.name) + SmfTasks.update_all_groups.delay() + + def service_enabled_members(self): + return settings.ENABLE_AUTH_SMF or False + + def service_enabled_blues(self): + return settings.ENABLE_BLUE_SMF or False + + def render_services_ctrl(self, request): + urls = self.Urls() + urls.auth_activate = 'auth_activate_smf' + urls.auth_deactivate = 'auth_deactivate_smf' + urls.auth_reset_password = 'auth_reset_smf_password' + urls.auth_set_password = 'auth_set_smf_password' + return render_to_string(self.service_ctrl_template, { + 'service_name': self.title, + 'urls': urls, + 'service_url': self.service_url, + 'username': request.user.smf.username if SmfTasks.has_account(request.user) else '' + }, request=request) + + +@hooks.register('services_hook') +def register_service(): + return SmfService() diff --git a/services/managers/smf_manager.py b/services/modules/smf/manager.py similarity index 66% rename from services/managers/smf_manager.py rename to services/modules/smf/manager.py index a8352c7b..36679b46 100644 --- a/services/managers/smf_manager.py +++ b/services/modules/smf/manager.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals -import os +import random +import string import calendar from datetime import datetime import hashlib @@ -12,7 +13,7 @@ from django.conf import settings logger = logging.getLogger(__name__) -class smfManager: +class SmfManager: def __init__(self): pass @@ -49,11 +50,11 @@ class smfManager: @staticmethod def generate_random_pass(): - return os.urandom(8).encode('hex') + return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(16)]) @staticmethod def gen_hash(username_clean, passwd): - return hashlib.sha1(username_clean + passwd).hexdigest() + return hashlib.sha1((username_clean + passwd).encode('utf-8')).hexdigest() @staticmethod def santatize_username(username): @@ -67,28 +68,28 @@ class smfManager: unixtime = calendar.timegm(d.utctimetuple()) return unixtime - @staticmethod - def create_group(groupname): + @classmethod + def create_group(cls, groupname): logger.debug("Creating smf group %s" % groupname) cursor = connections['smf'].cursor() - cursor.execute(smfManager.SQL_ADD_GROUP, [groupname, groupname]) + cursor.execute(cls.SQL_ADD_GROUP, [groupname, groupname]) logger.info("Created smf group %s" % groupname) - return smfManager.get_group_id(groupname) + return cls.get_group_id(groupname) - @staticmethod - def get_group_id(groupname): + @classmethod + def get_group_id(cls, groupname): logger.debug("Getting smf group id for groupname %s" % groupname) cursor = connections['smf'].cursor() - cursor.execute(smfManager.SQL_GET_GROUP_ID, [groupname]) + cursor.execute(cls.SQL_GET_GROUP_ID, [groupname]) row = cursor.fetchone() logger.debug("Got smf group id %s for groupname %s" % (row[0], groupname)) return row[0] - @staticmethod - def check_user(username): + @classmethod + def check_user(cls, username): logger.debug("Checking smf username %s" % username) cursor = connections['smf'].cursor() - cursor.execute(smfManager.SQL_USER_ID_FROM_USERNAME, [smfManager.santatize_username(username)]) + cursor.execute(cls.SQL_USER_ID_FROM_USERNAME, [cls.santatize_username(username)]) row = cursor.fetchone() if row: logger.debug("Found user %s on smf" % username) @@ -96,19 +97,19 @@ class smfManager: logger.debug("User %s not found on smf" % username) return False - @staticmethod - def add_avatar(member_name, characterid): + @classmethod + def add_avatar(cls, member_name, characterid): logger.debug("Adding EVE character id %s portrait as smf avatar for user %s" % (characterid, member_name)) avatar_url = "https://image.eveonline.com/Character/" + characterid + "_64.jpg" cursor = connections['smf'].cursor() - id_member = smfManager.get_user_id(member_name) - cursor.execute(smfManager.SQL_ADD_USER_AVATAR, [avatar_url, id_member]) + id_member = cls.get_user_id(member_name) + cursor.execute(cls.SQL_ADD_USER_AVATAR, [avatar_url, id_member]) - @staticmethod - def get_user_id(username): + @classmethod + def get_user_id(cls, username): logger.debug("Getting smf user id for username %s" % username) cursor = connections['smf'].cursor() - cursor.execute(smfManager.SQL_USER_ID_FROM_USERNAME, [username]) + cursor.execute(cls.SQL_USER_ID_FROM_USERNAME, [username]) row = cursor.fetchone() if row is not None: logger.debug("Got smf user id %s for username %s" % (row[0], username)) @@ -117,11 +118,11 @@ class smfManager: logger.error("username %s not found on smf. Unable to determine user id ." % username) return None - @staticmethod - def get_all_groups(): + @classmethod + def get_all_groups(cls): logger.debug("Getting all smf groups.") cursor = connections['smf'].cursor() - cursor.execute(smfManager.SQL_GET_ALL_GROUPS) + cursor.execute(cls.SQL_GET_ALL_GROUPS) rows = cursor.fetchall() out = {} for row in rows: @@ -129,137 +130,137 @@ class smfManager: logger.debug("Got smf groups %s" % out) return out - @staticmethod - def get_user_groups(userid): + @classmethod + def get_user_groups(cls, userid): logger.debug("Getting smf user id %s groups" % userid) cursor = connections['smf'].cursor() - cursor.execute(smfManager.SQL_GET_USER_GROUPS, [userid]) + cursor.execute(cls.SQL_GET_USER_GROUPS, [userid]) out = [row[0] for row in cursor.fetchall()] logger.debug("Got user %s smf groups %s" % (userid, out)) return out - @staticmethod - def add_user(username, email_address, groups, characterid): + @classmethod + def add_user(cls, username, email_address, groups, characterid): logger.debug("Adding smf user with member_name %s, email_address %s, characterid %s" % ( username, email_address, characterid)) cursor = connections['smf'].cursor() - username_clean = smfManager.santatize_username(username) - passwd = smfManager.generate_random_pass() - pwhash = smfManager.gen_hash(username_clean, passwd) + username_clean = cls.santatize_username(username) + passwd = cls.generate_random_pass() + pwhash = cls.gen_hash(username_clean, passwd) logger.debug("Proceeding to add smf user %s and pwhash starting with %s" % (username, pwhash[0:5])) - register_date = smfManager.get_current_utc_date() + register_date = cls.get_current_utc_date() # check if the username was simply revoked - if smfManager.check_user(username) is True: + if cls.check_user(username) is True: logger.warn("Unable to add smf user with username %s - already exists. Updating user instead." % username) - smfManager.__update_user_info(username_clean, email_address, pwhash) + cls.__update_user_info(username_clean, email_address, pwhash) else: try: - cursor.execute(smfManager.SQL_ADD_USER, + cursor.execute(cls.SQL_ADD_USER, [username_clean, passwd, email_address, register_date, username_clean]) - smfManager.add_avatar(username_clean, characterid) + cls.add_avatar(username_clean, characterid) logger.info("Added smf member_name %s" % username_clean) - smfManager.update_groups(username_clean, groups) + cls.update_groups(username_clean, groups) except: logger.warn("Unable to add smf user %s" % username_clean) pass return username_clean, passwd - @staticmethod - def __update_user_info(username, email_address, passwd): + @classmethod + def __update_user_info(cls, username, email_address, passwd): logger.debug( "Updating smf user %s info: username %s password of length %s" % (username, email_address, len(passwd))) cursor = connections['smf'].cursor() try: - cursor.execute(smfManager.SQL_DIS_USER, [email_address, passwd, username]) + cursor.execute(cls.SQL_DIS_USER, [email_address, passwd, username]) logger.info("Updated smf user %s info" % username) except: logger.exception("Unable to update smf user %s info." % username) pass - @staticmethod - def delete_user(username): + @classmethod + def delete_user(cls, username): logger.debug("Deleting smf user %s" % username) cursor = connections['smf'].cursor() - if smfManager.check_user(username): - cursor.execute(smfManager.SQL_DEL_USER, [username]) + if cls.check_user(username): + cursor.execute(cls.SQL_DEL_USER, [username]) logger.info("Deleted smf user %s" % username) return True logger.error("Unable to delete smf user %s - user not found on smf." % username) return False - @staticmethod - def update_groups(username, groups): - userid = smfManager.get_user_id(username) + @classmethod + def update_groups(cls, username, groups): + userid = cls.get_user_id(username) logger.debug("Updating smf user %s with id %s groups %s" % (username, userid, groups)) if userid is not None: - forum_groups = smfManager.get_all_groups() - user_groups = set(smfManager.get_user_groups(userid)) - act_groups = set([smfManager._sanitize_groupname(g) for g in groups]) + forum_groups = cls.get_all_groups() + user_groups = set(cls.get_user_groups(userid)) + act_groups = set([cls._sanitize_groupname(g) for g in groups]) addgroups = act_groups - user_groups remgroups = user_groups - act_groups logger.info("Updating smf user %s groups - adding %s, removing %s" % (username, addgroups, remgroups)) act_group_id = set() for g in addgroups: if g not in forum_groups: - forum_groups[g] = smfManager.create_group(g) - act_group_id.add(str(smfManager.get_group_id(g))) + forum_groups[g] = cls.create_group(g) + act_group_id.add(str(cls.get_group_id(g))) string_groups = ','.join(act_group_id) - smfManager.add_user_to_group(userid, string_groups) + cls.add_user_to_group(userid, string_groups) - @staticmethod - def add_user_to_group(userid, groupid): + @classmethod + def add_user_to_group(cls, userid, groupid): logger.debug("Adding smf user id %s to group id %s" % (userid, groupid)) try: cursor = connections['smf'].cursor() - cursor.execute(smfManager.SQL_ADD_USER_GROUP, [groupid, userid]) + cursor.execute(cls.SQL_ADD_USER_GROUP, [groupid, userid]) logger.info("Added smf user id %s to group id %s" % (userid, groupid)) except: logger.exception("Unable to add smf user id %s to group id %s" % (userid, groupid)) pass - @staticmethod - def remove_user_from_group(userid, groupid): + @classmethod + def remove_user_from_group(cls, userid, groupid): logger.debug("Removing smf user id %s from group id %s" % (userid, groupid)) try: cursor = connections['smf'].cursor() - cursor.execute(smfManager.SQL_REMOVE_USER_GROUP, [groupid, userid]) + cursor.execute(cls.SQL_REMOVE_USER_GROUP, [groupid, userid]) logger.info("Removed smf user id %s from group id %s" % (userid, groupid)) except: logger.exception("Unable to remove smf user id %s from group id %s" % (userid, groupid)) pass - @staticmethod - def disable_user(username): + @classmethod + def disable_user(cls, username): logger.debug("Disabling smf user %s" % username) cursor = connections['smf'].cursor() - password = smfManager.generate_random_pass() + password = cls.generate_random_pass() revoke_email = "revoked@" + settings.DOMAIN try: - pwhash = smfManager.gen_hash(username, password) - cursor.execute(smfManager.SQL_DIS_USER, [revoke_email, pwhash, username]) - smfManager.get_user_id(username) - smfManager.update_groups(username, []) + pwhash = cls.gen_hash(username, password) + cursor.execute(cls.SQL_DIS_USER, [revoke_email, pwhash, username]) + cls.get_user_id(username) + cls.update_groups(username, []) logger.info("Disabled smf user %s" % username) return True except TypeError: logger.exception("TypeError occured while disabling user %s - failed to disable." % username) return False - @staticmethod - def update_user_password(username, characterid, password=None): + @classmethod + def update_user_password(cls, username, characterid, password=None): logger.debug("Updating smf user %s password" % username) cursor = connections['smf'].cursor() if not password: - password = smfManager.generate_random_pass() - if smfManager.check_user(username): - username_clean = smfManager.santatize_username(username) - pwhash = smfManager.gen_hash(username_clean, password) + password = cls.generate_random_pass() + if cls.check_user(username): + username_clean = cls.santatize_username(username) + pwhash = cls.gen_hash(username_clean, password) logger.debug( "Proceeding to update smf user %s password with pwhash starting with %s" % (username, pwhash[0:5])) - cursor.execute(smfManager.SQL_UPDATE_USER_PASSWORD, [pwhash, username]) - smfManager.add_avatar(username, characterid) + cursor.execute(cls.SQL_UPDATE_USER_PASSWORD, [pwhash, username]) + cls.add_avatar(username, characterid) logger.info("Updated smf user %s password." % username) return password logger.error("Unable to update smf user %s password - user not found on smf." % username) diff --git a/services/modules/smf/migrations/0001_initial.py b/services/modules/smf/migrations/0001_initial.py new file mode 100644 index 00000000..23d14ace --- /dev/null +++ b/services/modules/smf/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-12-12 03:28 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='SmfUser', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='smf', serialize=False, to=settings.AUTH_USER_MODEL)), + ('username', models.CharField(max_length=254)), + ], + ), + ] diff --git a/services/modules/smf/migrations/__init__.py b/services/modules/smf/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/smf/models.py b/services/modules/smf/models.py new file mode 100644 index 00000000..726476a5 --- /dev/null +++ b/services/modules/smf/models.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals +from django.utils.encoding import python_2_unicode_compatible +from django.db import models + + +@python_2_unicode_compatible +class SmfUser(models.Model): + user = models.OneToOneField('auth.User', + primary_key=True, + on_delete=models.CASCADE, + related_name='smf') + username = models.CharField(max_length=254) + + def __str__(self): + return self.username diff --git a/services/modules/smf/tasks.py b/services/modules/smf/tasks.py new file mode 100644 index 00000000..e0580c55 --- /dev/null +++ b/services/modules/smf/tasks.py @@ -0,0 +1,75 @@ +from __future__ import unicode_literals + +import logging + +from alliance_auth.celeryapp import app +from django.conf import settings +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist +from notifications import notify + +from .manager import SmfManager +from .models import SmfUser + +logger = logging.getLogger(__name__) + + +class SmfTasks: + def __init__(self): + pass + + @classmethod + def delete_user(cls, user, notify_user=False): + if cls.has_account(user): + logger.debug("User %s has a SMF account %s. Deleting." % (user, user.smf.username)) + SmfManager.disable_user(user.smf.username) + user.smf.delete() + if notify_user: + notify(user, "SMF Account Disabled", level='danger') + return True + return False + + @staticmethod + def has_account(user): + try: + return user.smf.username != '' + except ObjectDoesNotExist: + return False + + @classmethod + def disable(cls): + if settings.ENABLE_AUTH_SMF: + logger.warn( + "ENABLE_AUTH_SMF still True, after disabling users will still be able to link smf accounts") + if settings.ENABLE_BLUE_SMF: + logger.warn( + "ENABLE_BLUE_SMF still True, after disabling blues will still be able to link smf accounts") + SmfUser.objects.all().delete() + + @staticmethod + @app.task(bind=True) + def update_groups(self, pk): + user = User.objects.get(pk=pk) + logger.debug("Updating smf groups for user %s" % user) + if SmfTasks.has_account(user): + groups = [] + for group in user.groups.all(): + groups.append(str(group.name)) + if len(groups) == 0: + groups.append('empty') + logger.debug("Updating user %s smf groups to %s" % (user, groups)) + try: + SmfManager.update_groups(user.smf.username, groups) + except: + logger.exception("smf group sync failed for %s, retrying in 10 mins" % user) + raise self.retry(countdown=60 * 10) + logger.debug("Updated user %s smf groups." % user) + else: + logger.debug("User does not have an smf account") + + @staticmethod + @app.task + def update_all_groups(): + logger.debug("Updating ALL smf groups") + for user in SmfUser.objects.exclude(username__exact=''): + SmfTasks.update_groups.delay(user.user_id) diff --git a/services/modules/smf/tests.py b/services/modules/smf/tests.py new file mode 100644 index 00000000..bace9368 --- /dev/null +++ b/services/modules/smf/tests.py @@ -0,0 +1,212 @@ +from __future__ import unicode_literals + +try: + # Py3 + from unittest import mock +except ImportError: + # Py2 + import mock + +from django.test import TestCase, RequestFactory +from django.conf import settings +from django import urls +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist + +from alliance_auth.tests.auth_utils import AuthUtils + +from .auth_hooks import SmfService +from .models import SmfUser +from .tasks import SmfTasks + +MODULE_PATH = 'services.modules.smf' + + +class SmfHooksTestCase(TestCase): + def setUp(self): + self.member = 'member_user' + member = AuthUtils.create_member(self.member) + SmfUser.objects.create(user=member, username=self.member) + self.blue = 'blue_user' + blue = AuthUtils.create_blue(self.blue) + SmfUser.objects.create(user=blue, username=self.blue) + self.none_user = 'none_user' + none_user = AuthUtils.create_user(self.none_user) + self.service = SmfService + + def test_has_account(self): + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(SmfTasks.has_account(member)) + self.assertTrue(SmfTasks.has_account(blue)) + self.assertFalse(SmfTasks.has_account(none_user)) + + def test_service_enabled(self): + service = self.service() + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(service.service_enabled_members()) + self.assertTrue(service.service_enabled_blues()) + + self.assertEqual(service.service_active_for_user(member), settings.ENABLE_AUTH_SMF) + self.assertEqual(service.service_active_for_user(blue), settings.ENABLE_BLUE_SMF) + self.assertFalse(service.service_active_for_user(none_user)) + + @mock.patch(MODULE_PATH + '.tasks.SmfManager') + def test_update_all_groups(self, manager): + service = self.service() + service.update_all_groups() + # Check member and blue user have groups updated + self.assertTrue(manager.update_groups.called) + self.assertEqual(manager.update_groups.call_count, 2) + + def test_update_groups(self): + # Check member has Member group updated + with mock.patch(MODULE_PATH + '.tasks.SmfManager') as manager: + service = self.service() + member = User.objects.get(username=self.member) + service.update_groups(member) + self.assertTrue(manager.update_groups.called) + args, kwargs = manager.update_groups.call_args + user_id, groups = args + self.assertIn(settings.DEFAULT_AUTH_GROUP, groups) + self.assertEqual(user_id, member.smf.username) + + # Check none user does not have groups updated + with mock.patch(MODULE_PATH + '.tasks.SmfManager') as manager: + service = self.service() + none_user = User.objects.get(username=self.none_user) + service.update_groups(none_user) + self.assertFalse(manager.update_groups.called) + + @mock.patch(MODULE_PATH + '.tasks.SmfManager') + def test_validate_user(self, manager): + service = self.service() + # Test member is not deleted + member = User.objects.get(username=self.member) + service.validate_user(member) + self.assertTrue(member.smf) + + # Test none user is deleted + none_user = User.objects.get(username=self.none_user) + SmfUser.objects.create(user=none_user, username='abc123') + service.validate_user(none_user) + self.assertTrue(manager.disable_user.called) + with self.assertRaises(ObjectDoesNotExist): + none_smf = User.objects.get(username=self.none_user).smf + + @mock.patch(MODULE_PATH + '.tasks.SmfManager') + def test_delete_user(self, manager): + member = User.objects.get(username=self.member) + + service = self.service() + result = service.delete_user(member) + + self.assertTrue(result) + self.assertTrue(manager.disable_user.called) + with self.assertRaises(ObjectDoesNotExist): + smf_user = User.objects.get(username=self.member).smf + + def test_render_services_ctrl(self): + service = self.service() + member = User.objects.get(username=self.member) + request = RequestFactory().get('/en/services/') + request.user = member + + response = service.render_services_ctrl(request) + self.assertTemplateUsed(service.service_ctrl_template) + self.assertIn(urls.reverse('auth_deactivate_smf'), response) + self.assertIn(urls.reverse('auth_reset_smf_password'), response) + self.assertIn(urls.reverse('auth_set_smf_password'), response) + + # Test register becomes available + member.smf.delete() + member = User.objects.get(username=self.member) + request.user = member + response = service.render_services_ctrl(request) + self.assertIn(urls.reverse('auth_activate_smf'), response) + + +class SmfViewsTestCase(TestCase): + def setUp(self): + self.member = AuthUtils.create_member('auth_member') + self.member.set_password('password') + self.member.email = 'auth_member@example.com' + self.member.save() + AuthUtils.add_main_character(self.member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation') + + def login(self): + self.client.login(username=self.member.username, password='password') + + @mock.patch(MODULE_PATH + '.tasks.SmfManager') + @mock.patch(MODULE_PATH + '.views.SmfManager') + def test_activate(self, manager, tasks_manager): + self.login() + expected_username = 'auth_member' + manager.add_user.return_value = (expected_username, 'abc123') + + response = self.client.get(urls.reverse('auth_activate_smf')) + + self.assertTrue(manager.add_user.called) + self.assertTrue(tasks_manager.update_groups.called) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed('registered/service_credentials.html') + self.assertContains(response, expected_username) + smf_user = SmfUser.objects.get(user=self.member) + self.assertEqual(smf_user.username, expected_username) + + @mock.patch(MODULE_PATH + '.tasks.SmfManager') + def test_deactivate(self, manager): + self.login() + SmfUser.objects.create(user=self.member, username='some member') + + response = self.client.get(urls.reverse('auth_deactivate_smf')) + + self.assertTrue(manager.disable_user.called) + self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) + with self.assertRaises(ObjectDoesNotExist): + smf_user = User.objects.get(pk=self.member.pk).smf + + @mock.patch(MODULE_PATH + '.views.SmfManager') + def test_set_password(self, manager): + self.login() + SmfUser.objects.create(user=self.member, username='some member') + + response = self.client.post(urls.reverse('auth_set_smf_password'), data={'password': '1234asdf'}) + + self.assertTrue(manager.update_user_password.called) + args, kwargs = manager.update_user_password.call_args + self.assertEqual(kwargs['password'], '1234asdf') + self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) + + @mock.patch(MODULE_PATH + '.views.SmfManager') + def test_reset_password(self, manager): + self.login() + SmfUser.objects.create(user=self.member, username='some member') + + manager.update_user_password.return_value = 'hunter2' + + response = self.client.get(urls.reverse('auth_reset_smf_password')) + + self.assertTemplateUsed(response, 'registered/service_credentials.html') + self.assertContains(response, 'some member') + self.assertContains(response, 'hunter2') + + +class SmfManagerTestCase(TestCase): + def setUp(self): + from .manager import SmfManager + self.manager = SmfManager + + def test_generate_random_password(self): + password = self.manager.generate_random_pass() + + self.assertEqual(len(password), 16) + self.assertIsInstance(password, type('')) + + def test_gen_hash(self): + pwhash = self.manager.gen_hash('username', 'test') + + self.assertEqual(pwhash, 'b6d21d37de84db76746b1c45696a00f9ce4f86fd') diff --git a/services/modules/smf/urls.py b/services/modules/smf/urls.py new file mode 100644 index 00000000..6fb2310e --- /dev/null +++ b/services/modules/smf/urls.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals +from django.conf.urls import url, include + +from . import views + +module_urls = [ + # SMF Service Control + url(r'^activate/$', views.activate_smf, name='auth_activate_smf'), + url(r'^deactivate/$', views.deactivate_smf, name='auth_deactivate_smf'), + url(r'^reset_password/$', views.reset_smf_password, name='auth_reset_smf_password'), + url(r'^set_password/$', views.set_smf_password, name='auth_set_smf_password'), +] + +urlpatterns = [ + url(r'^smf/', include(module_urls)), +] diff --git a/services/modules/smf/views.py b/services/modules/smf/views.py new file mode 100644 index 00000000..fbe58c86 --- /dev/null +++ b/services/modules/smf/views.py @@ -0,0 +1,111 @@ +from __future__ import unicode_literals + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.shortcuts import render, redirect + +from authentication.decorators import members_and_blues +from eveonline.managers import EveManager +from services.forms import ServicePasswordForm + +from .manager import SmfManager +from .tasks import SmfTasks +from .models import SmfUser + +import logging + +logger = logging.getLogger(__name__) + + +@login_required +@members_and_blues() +def activate_smf(request): + logger.debug("activate_smf called by user %s" % request.user) + # Valid now we get the main characters + character = EveManager.get_main_character(request.user) + logger.debug("Adding smf user for user %s with main character %s" % (request.user, character)) + result = SmfManager.add_user(character.character_name, request.user.email, ['Member'], character.character_id) + # if empty we failed + if result[0] != "": + SmfUser.objects.update_or_create(user=request.user, defaults={'username': result[0]}) + logger.debug("Updated authserviceinfo for user %s with smf credentials. Updating groups." % request.user) + SmfTasks.update_groups.delay(request.user.pk) + logger.info("Successfully activated smf for user %s" % request.user) + messages.success(request, 'Activated SMF account.') + credentials = { + 'username': result[0], + 'password': result[1], + } + return render(request, 'registered/service_credentials.html', + context={'credentials': credentials, 'service': 'SMF'}) + else: + logger.error("Unsuccessful attempt to activate smf for user %s" % request.user) + messages.error(request, 'An error occurred while processing your SMF account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def deactivate_smf(request): + logger.debug("deactivate_smf called by user %s" % request.user) + result = SmfTasks.delete_user(request.user) + # false we failed + if result: + logger.info("Successfully deactivated smf for user %s" % request.user) + messages.success(request, 'Deactivated SMF account.') + else: + logger.error("Unsuccessful attempt to activate smf for user %s" % request.user) + messages.error(request, 'An error occurred while processing your SMF account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def reset_smf_password(request): + logger.debug("reset_smf_password called by user %s" % request.user) + character = EveManager.get_main_character(request.user) + if SmfTasks.has_account(request.user) and character is not None: + result = SmfManager.update_user_password(request.user.smf.username, character.character_id) + # false we failed + if result != "": + logger.info("Successfully reset smf password for user %s" % request.user) + messages.success(request, 'Reset SMF password.') + credentials = { + 'username': request.user.smf.username, + 'password': result, + } + return render(request, 'registered/service_credentials.html', + context={'credentials': credentials, 'service': 'SMF'}) + logger.error("Unsuccessful attempt to reset smf password for user %s" % request.user) + messages.error(request, 'An error occurred while processing your SMF account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def set_smf_password(request): + logger.debug("set_smf_password called by user %s" % request.user) + if request.method == 'POST': + logger.debug("Received POST request with form.") + form = ServicePasswordForm(request.POST) + logger.debug("Form is valid: %s" % form.is_valid()) + character = EveManager.get_main_character(request.user) + if form.is_valid() and SmfTasks.has_account(request.user) and character is not None: + password = form.cleaned_data['password'] + logger.debug("Form contains password of length %s" % len(password)) + result = SmfManager.update_user_password(request.user.smf.username, character.character_id, + password=password) + if result != "": + logger.info("Successfully set smf password for user %s" % request.user) + messages.success(request, 'Set SMF password.') + else: + logger.error("Failed to install custom smf password for user %s" % request.user) + messages.error(request, 'An error occurred while processing your SMF account.') + return redirect("auth_services") + else: + logger.debug("Request is not type POST - providing empty form.") + form = ServicePasswordForm() + + logger.debug("Rendering form for user %s" % request.user) + context = {'form': form, 'service': 'SMF'} + return render(request, 'registered/service_password.html', context=context) diff --git a/services/modules/teamspeak3/__init__.py b/services/modules/teamspeak3/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/teamspeak3/admin.py b/services/modules/teamspeak3/admin.py new file mode 100644 index 00000000..2ff77e24 --- /dev/null +++ b/services/modules/teamspeak3/admin.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals +from django.contrib import admin +from .models import AuthTS, Teamspeak3User + + +class Teamspeak3UserAdmin(admin.ModelAdmin): + list_display = ('user', 'uid', 'perm_key') + search_fields = ('user__username', 'uid', 'perm_key') + + +class AuthTSgroupAdmin(admin.ModelAdmin): + fields = ['auth_group', 'ts_group'] + filter_horizontal = ('ts_group',) + + +admin.site.register(AuthTS, AuthTSgroupAdmin) +admin.site.register(Teamspeak3User, Teamspeak3UserAdmin) diff --git a/services/modules/teamspeak3/apps.py b/services/modules/teamspeak3/apps.py new file mode 100644 index 00000000..1d542597 --- /dev/null +++ b/services/modules/teamspeak3/apps.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class Teamspeak3ServiceConfig(AppConfig): + name = 'teamspeak3' + + def ready(self): + from . import signals diff --git a/services/modules/teamspeak3/auth_hooks.py b/services/modules/teamspeak3/auth_hooks.py new file mode 100644 index 00000000..b740b90f --- /dev/null +++ b/services/modules/teamspeak3/auth_hooks.py @@ -0,0 +1,61 @@ +from __future__ import unicode_literals + +from django.conf import settings +from django.template.loader import render_to_string + +from services.hooks import ServicesHook +from alliance_auth import hooks + +from .urls import urlpatterns +from .tasks import Teamspeak3Tasks + +import logging + +logger = logging.getLogger(__name__) + + +class Teamspeak3Service(ServicesHook): + def __init__(self): + ServicesHook.__init__(self) + self.name = 'teamspeak3' + self.urlpatterns = urlpatterns + self.service_ctrl_template = 'registered/teamspeak3_service_ctrl.html' + + def delete_user(self, user, notify_user=False): + logger.debug('Deleting user %s %s account' % (user, self.name)) + return Teamspeak3Tasks.delete_user(user, notify_user=notify_user) + + def update_groups(self, user): + logger.debug('Updating %s groups for %s' % (self.name, user)) + Teamspeak3Tasks.update_groups.delay(user.pk) + + def validate_user(self, user): + logger.debug('Validating user %s %s account' % (user, self.name)) + if Teamspeak3Tasks.has_account(user) and not self.service_active_for_user(user): + self.delete_user(user, notify_user=True) + + def update_all_groups(self): + logger.debug('Update all %s groups called' % self.name) + Teamspeak3Tasks.update_all_groups.delay() + + def service_enabled_members(self): + return settings.ENABLE_AUTH_TEAMSPEAK3 or False + + def service_enabled_blues(self): + return settings.ENABLE_BLUE_TEAMSPEAK3 or False + + def render_services_ctrl(self, request): + authinfo = {'teamspeak3_uid': '', + 'teamspeak3_perm_key': '',} + if Teamspeak3Tasks.has_account(request.user): + authinfo['teamspeak3_uid'] = request.user.teamspeak3.uid + authinfo['teamspeak3_perm_key'] = request.user.teamspeak3.perm_key + + return render_to_string(self.service_ctrl_template, { + 'authinfo': authinfo, + }, request=request) + + +@hooks.register('services_hook') +def register_service(): + return Teamspeak3Service() diff --git a/services/modules/teamspeak3/forms.py b/services/modules/teamspeak3/forms.py new file mode 100644 index 00000000..4377ebcc --- /dev/null +++ b/services/modules/teamspeak3/forms.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from .manager import Teamspeak3Manager + + +class TeamspeakJoinForm(forms.Form): + username = forms.CharField(widget=forms.HiddenInput()) + + def clean(self): + if Teamspeak3Manager._get_userid(self.cleaned_data['username']): + return self.cleaned_data + raise forms.ValidationError(_("Unable to locate user %s on server") % self.cleaned_data['username']) diff --git a/services/managers/teamspeak3_manager.py b/services/modules/teamspeak3/manager.py similarity index 99% rename from services/managers/teamspeak3_manager.py rename to services/modules/teamspeak3/manager.py index a6dc8016..5661b62f 100755 --- a/services/managers/teamspeak3_manager.py +++ b/services/modules/teamspeak3/manager.py @@ -1,9 +1,11 @@ from __future__ import unicode_literals + +import logging + from django.conf import settings -from services.managers.util.ts3 import TS3Server, TeamspeakError -from services.models import TSgroup -import logging +from services.modules.teamspeak3.util.ts3 import TS3Server, TeamspeakError +from .models import TSgroup logger = logging.getLogger(__name__) diff --git a/services/modules/teamspeak3/migrations/0001_initial.py b/services/modules/teamspeak3/migrations/0001_initial.py new file mode 100644 index 00000000..0e5f552f --- /dev/null +++ b/services/modules/teamspeak3/migrations/0001_initial.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-12-12 01:11 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='AuthTS', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('auth_group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')), + ], + options={ + 'db_table': 'services_authts', + 'verbose_name': 'Auth / TS Group', + }, + ), + migrations.CreateModel( + name='TSgroup', + fields=[ + ('ts_group_id', models.IntegerField(primary_key=True, serialize=False)), + ('ts_group_name', models.CharField(max_length=30)), + ], + options={ + 'db_table': 'services_tsgroup', + 'verbose_name': 'TS Group', + }, + ), + migrations.CreateModel( + name='UserTSgroup', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ts_group', models.ManyToManyField(to='teamspeak3.TSgroup')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'services_usertsgroup', + 'verbose_name': 'User TS Group', + }, + ), + migrations.AddField( + model_name='authts', + name='ts_group', + field=models.ManyToManyField(to='teamspeak3.TSgroup'), + ), + ] diff --git a/services/modules/teamspeak3/migrations/0002_auto_20161212_0133.py b/services/modules/teamspeak3/migrations/0002_auto_20161212_0133.py new file mode 100644 index 00000000..317de302 --- /dev/null +++ b/services/modules/teamspeak3/migrations/0002_auto_20161212_0133.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-12-12 01:33 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('teamspeak3', '0001_initial'), + ] + + operations = [ + migrations.AlterModelTable( + name='authts', + table=None, + ), + migrations.AlterModelTable( + name='tsgroup', + table=None, + ), + migrations.AlterModelTable( + name='usertsgroup', + table=None, + ), + ] diff --git a/services/modules/teamspeak3/migrations/0003_teamspeak3user.py b/services/modules/teamspeak3/migrations/0003_teamspeak3user.py new file mode 100644 index 00000000..714c6fff --- /dev/null +++ b/services/modules/teamspeak3/migrations/0003_teamspeak3user.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-12-12 03:14 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + ('teamspeak3', '0002_auto_20161212_0133'), + ] + + operations = [ + migrations.CreateModel( + name='Teamspeak3User', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='teamspeak3', serialize=False, to=settings.AUTH_USER_MODEL)), + ('uid', models.CharField(max_length=254)), + ('perm_key', models.CharField(max_length=254)), + ], + ), + ] diff --git a/services/modules/teamspeak3/migrations/__init__.py b/services/modules/teamspeak3/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/teamspeak3/models.py b/services/modules/teamspeak3/models.py new file mode 100644 index 00000000..6e0aeaf3 --- /dev/null +++ b/services/modules/teamspeak3/models.py @@ -0,0 +1,52 @@ +from __future__ import unicode_literals +from django.utils.encoding import python_2_unicode_compatible +from django.db import models + + +@python_2_unicode_compatible +class Teamspeak3User(models.Model): + user = models.OneToOneField('auth.User', + primary_key=True, + on_delete=models.CASCADE, + related_name='teamspeak3') + uid = models.CharField(max_length=254) + perm_key = models.CharField(max_length=254) + + def __str__(self): + return self.uid + + +@python_2_unicode_compatible +class TSgroup(models.Model): + ts_group_id = models.IntegerField(primary_key=True) + ts_group_name = models.CharField(max_length=30) + + class Meta: + verbose_name = 'TS Group' + + def __str__(self): + return self.ts_group_name + + +@python_2_unicode_compatible +class AuthTS(models.Model): + auth_group = models.ForeignKey('auth.Group') + ts_group = models.ManyToManyField(TSgroup) + + class Meta: + verbose_name = 'Auth / TS Group' + + def __str__(self): + return self.auth_group.name + + +@python_2_unicode_compatible +class UserTSgroup(models.Model): + user = models.ForeignKey('auth.User') + ts_group = models.ManyToManyField(TSgroup) + + class Meta: + verbose_name = 'User TS Group' + + def __str__(self): + return self.user.name diff --git a/services/modules/teamspeak3/signals.py b/services/modules/teamspeak3/signals.py new file mode 100644 index 00000000..ad8b4185 --- /dev/null +++ b/services/modules/teamspeak3/signals.py @@ -0,0 +1,38 @@ +from __future__ import unicode_literals + +import logging + +from django.db import transaction +from django.db.models.signals import m2m_changed +from django.db.models.signals import post_delete +from django.db.models.signals import post_save +from django.dispatch import receiver + +from .tasks import Teamspeak3Tasks +from .models import AuthTS + +logger = logging.getLogger(__name__) + + +def trigger_all_ts_update(): + logger.debug("Triggering update_all_groups") + Teamspeak3Tasks.update_all_groups() + + +@receiver(m2m_changed, sender=AuthTS.ts_group.through) +def m2m_changed_authts_group(sender, instance, action, *args, **kwargs): + logger.debug("Received m2m_changed from %s ts_group with action %s" % (instance, action)) + if action == "post_add" or action == "post_remove": + transaction.on_commit(trigger_all_ts_update) + + +@receiver(post_save, sender=AuthTS) +def post_save_authts(sender, instance, *args, **kwargs): + logger.debug("Received post_save from %s" % instance) + transaction.on_commit(trigger_all_ts_update) + + +@receiver(post_delete, sender=AuthTS) +def post_delete_authts(sender, instance, *args, **kwargs): + logger.debug("Received post_delete signal from %s" % instance) + transaction.on_commit(trigger_all_ts_update) diff --git a/services/modules/teamspeak3/tasks.py b/services/modules/teamspeak3/tasks.py new file mode 100644 index 00000000..be8839c9 --- /dev/null +++ b/services/modules/teamspeak3/tasks.py @@ -0,0 +1,96 @@ +from __future__ import unicode_literals + +import logging + +from alliance_auth.celeryapp import app +from celery.schedules import crontab +from django.conf import settings +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist +from notifications import notify + +from authentication.models import AuthServicesInfo +from .util.ts3 import TeamspeakError +from .manager import Teamspeak3Manager +from .models import AuthTS, TSgroup, UserTSgroup, Teamspeak3User + +logger = logging.getLogger(__name__) + + +class Teamspeak3Tasks: + def __init__(self): + pass + + @classmethod + def delete_user(cls, user, notify_user=False): + if cls.has_account(user): + logger.debug("User %s has TS3 account %s. Deleting." % (user, user.teamspeak3.uid)) + if Teamspeak3Manager.delete_user(user.teamspeak3.uid): + user.teamspeak3.delete() + if notify_user: + notify(user, 'TeamSpeak3 Account Disabled', level='danger') + return True + return False + + @staticmethod + def has_account(user): + try: + return user.teamspeak3.uid != '' + except ObjectDoesNotExist: + return False + + @staticmethod + @app.task() + def run_ts3_group_update(): + if settings.ENABLE_AUTH_TEAMSPEAK3 or settings.ENABLE_BLUE_TEAMSPEAK3: + logger.debug("TS3 installed. Syncing local group objects.") + Teamspeak3Manager._sync_ts_group_db() + + @staticmethod + def disable(): + if settings.ENABLE_AUTH_TEAMSPEAK3: + logger.warn( + "ENABLE_AUTH_TEAMSPEAK3 still True, after disabling users will still be able to create teamspeak accounts") + if settings.ENABLE_BLUE_TEAMSPEAK3: + logger.warn( + "ENABLE_BLUE_TEAMSPEAK3 still True, after disabling blues will still be able to create teamspeak accounts") + logger.info("Deleting all Teamspeak3Users") + Teamspeak3User.objects.all().delete() + logger.info("Deleting all UserTSgroup models") + UserTSgroup.objects.all().delete() + logger.info("Deleting all AuthTS models") + AuthTS.objects.all().delete() + logger.info("Deleting all TSgroup models") + TSgroup.objects.all().delete() + logger.info("Teamspeak3 disabled") + + @staticmethod + @app.task(bind=True) + def update_groups(self, pk): + user = User.objects.get(pk=pk) + logger.debug("Updating user %s teamspeak3 groups" % user) + if Teamspeak3Tasks.has_account(user): + usergroups = user.groups.all() + groups = {} + for usergroup in usergroups: + filtered_groups = AuthTS.objects.filter(auth_group=usergroup) + if filtered_groups: + for filtered_group in filtered_groups: + for ts_group in filtered_group.ts_group.all(): + groups[ts_group.ts_group_name] = ts_group.ts_group_id + logger.debug("Updating user %s teamspeak3 groups to %s" % (user, groups)) + try: + Teamspeak3Manager.update_groups(user.teamspeak3.uid, groups) + logger.debug("Updated user %s teamspeak3 groups." % user) + except TeamspeakError as e: + logger.error("Error occured while syncing TS groups for %s: %s" % (user, str(e))) + raise self.retry(countdown=60*10) + else: + logger.debug("User does not have a teamspeak3 account") + + @staticmethod + @app.task + def update_all_groups(): + logger.debug("Updating ALL teamspeak3 groups") + for user in Teamspeak3User.objects.exclude(uid__exact=''): + Teamspeak3Tasks.update_groups.delay(user.user_id) diff --git a/services/modules/teamspeak3/templates/registered/teamspeak3_service_ctrl.html b/services/modules/teamspeak3/templates/registered/teamspeak3_service_ctrl.html new file mode 100644 index 00000000..7ac4840b --- /dev/null +++ b/services/modules/teamspeak3/templates/registered/teamspeak3_service_ctrl.html @@ -0,0 +1,34 @@ +{% load i18n %} + + + {% trans "Service" %} + {% trans "Unique ID" %} + PermissionKey + {% trans "Action" %} + + + Teamspeak 3 + {{ authinfo.teamspeak3_uid }} + {{ authinfo.teamspeak3_perm_key }} + + {% ifequal authinfo.teamspeak3_uid "" %} + + + + {% else %} + + + + + + + + + + + + + {% endifequal %} + + \ No newline at end of file diff --git a/services/modules/teamspeak3/tests.py b/services/modules/teamspeak3/tests.py new file mode 100644 index 00000000..ba8e166c --- /dev/null +++ b/services/modules/teamspeak3/tests.py @@ -0,0 +1,328 @@ +from __future__ import unicode_literals + +try: + # Py3 + from unittest import mock +except ImportError: + # Py2 + import mock + +from django.test import TestCase, RequestFactory +from django.conf import settings +from django import urls +from django.contrib.auth.models import User, Group +from django.core.exceptions import ObjectDoesNotExist +from django.db.models import signals + +from alliance_auth.tests.auth_utils import AuthUtils + +from .auth_hooks import Teamspeak3Service +from .models import Teamspeak3User, AuthTS, TSgroup +from .tasks import Teamspeak3Tasks +from .signals import m2m_changed_authts_group, post_save_authts, post_delete_authts + +MODULE_PATH = 'services.modules.teamspeak3' + + +class Teamspeak3HooksTestCase(TestCase): + def setUp(self): + # Inert signals before setup begins + with mock.patch(MODULE_PATH + '.signals.trigger_all_ts_update') as trigger_all_ts_update: + self.member = 'member_user' + member = AuthUtils.create_member(self.member) + Teamspeak3User.objects.create(user=member, uid=self.member, perm_key='123ABC') + self.blue = 'blue_user' + blue = AuthUtils.create_blue(self.blue) + Teamspeak3User.objects.create(user=blue, uid=self.blue, perm_key='456DEF') + self.none_user = 'none_user' + none_user = AuthUtils.create_user(self.none_user) + + ts_member_group = TSgroup.objects.create(ts_group_id=1, ts_group_name='Member') + ts_blue_group = TSgroup.objects.create(ts_group_id=2, ts_group_name='Blue') + m2m_member_group = AuthTS.objects.create(auth_group=member.groups.all()[0]) + m2m_member_group.ts_group.add(ts_member_group) + m2m_member_group.save() + m2m_blue_group = AuthTS.objects.create(auth_group=blue.groups.all()[0]) + m2m_blue_group.ts_group.add(ts_blue_group) + m2m_blue_group.save() + self.service = Teamspeak3Service + + def test_has_account(self): + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(Teamspeak3Tasks.has_account(member)) + self.assertTrue(Teamspeak3Tasks.has_account(blue)) + self.assertFalse(Teamspeak3Tasks.has_account(none_user)) + + def test_service_enabled(self): + service = self.service() + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(service.service_enabled_members()) + self.assertTrue(service.service_enabled_blues()) + + self.assertEqual(service.service_active_for_user(member), settings.ENABLE_AUTH_TEAMSPEAK3) + self.assertEqual(service.service_active_for_user(blue), settings.ENABLE_BLUE_TEAMSPEAK3) + self.assertFalse(service.service_active_for_user(none_user)) + + @mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') + def test_update_all_groups(self, manager): + service = self.service() + service.update_all_groups() + # Check member and blue user have groups updated + self.assertTrue(manager.update_groups.called) + self.assertEqual(manager.update_groups.call_count, 2) + + def test_update_groups(self): + # Check member has Member group updated + with mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') as manager: + service = self.service() + member = User.objects.get(username=self.member) + service.update_groups(member) + self.assertTrue(manager.update_groups.called) + args, kwargs = manager.update_groups.call_args + # update_groups(user.teamspeak3.uid, groups) + self.assertEqual({'Member': 1}, args[1]) # Check groups + self.assertEqual(self.member, args[0]) # Check uid + + # Check none user does not have groups updated + with mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') as manager: + service = self.service() + none_user = User.objects.get(username=self.none_user) + service.update_groups(none_user) + self.assertFalse(manager.update_user_groups.called) + + @mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') + def test_validate_user(self, manager): + service = self.service() + # Test member is not deleted + member = User.objects.get(username=self.member) + service.validate_user(member) + self.assertTrue(member.teamspeak3) + + # Test none user is deleted + none_user = User.objects.get(username=self.none_user) + Teamspeak3User.objects.create(user=none_user, uid='abc123', perm_key='132ACB') + service.validate_user(none_user) + self.assertTrue(manager.delete_user.called) + with self.assertRaises(ObjectDoesNotExist): + none_teamspeak3 = User.objects.get(username=self.none_user).teamspeak3 + + @mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') + def test_delete_user(self, manager): + member = User.objects.get(username=self.member) + + service = self.service() + result = service.delete_user(member) + + self.assertTrue(result) + self.assertTrue(manager.delete_user.called) + with self.assertRaises(ObjectDoesNotExist): + teamspeak3_user = User.objects.get(username=self.member).teamspeak3 + + def test_render_services_ctrl(self): + service = self.service() + member = User.objects.get(username=self.member) + request = RequestFactory().get('/en/services/') + request.user = member + + response = service.render_services_ctrl(request) + self.assertTemplateUsed(service.service_ctrl_template) + self.assertIn(urls.reverse('auth_deactivate_teamspeak3'), response) + self.assertIn(urls.reverse('auth_reset_teamspeak3_perm'), response) + + # Test register becomes available + member.teamspeak3.delete() + member = User.objects.get(username=self.member) + request.user = member + response = service.render_services_ctrl(request) + self.assertIn(urls.reverse('auth_activate_teamspeak3'), response) + + +class Teamspeak3ViewsTestCase(TestCase): + def setUp(self): + # Inert signals before setup begins + with mock.patch(MODULE_PATH + '.signals.trigger_all_ts_update') as trigger_all_ts_update: + self.member = AuthUtils.create_member('auth_member') + self.member.set_password('password') + self.member.email = 'auth_member@example.com' + self.member.save() + AuthUtils.add_main_character(self.member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation') + self.blue_user = AuthUtils.create_blue('auth_blue') + self.blue_user.set_password('password') + self.blue_user.email = 'auth_blue@example.com' + self.blue_user.save() + AuthUtils.add_main_character(self.blue_user, 'auth_blue', '92345', corp_id='111', corp_name='Test Corporation') + + ts_member_group = TSgroup.objects.create(ts_group_id=1, ts_group_name='Member') + ts_blue_group = TSgroup.objects.create(ts_group_id=2, ts_group_name='Blue') + m2m_member = AuthTS.objects.create(auth_group=Group.objects.get(name='Member')) + m2m_member.ts_group.add(ts_member_group) + m2m_member.save() + m2m_blue = AuthTS.objects.create(auth_group=Group.objects.get(name='Blue')) + m2m_blue.ts_group.add(ts_blue_group) + m2m_blue.save() + + def login(self, user=None, password=None): + if user is None: + user = self.member + self.client.login(username=user.username, password=password if password else 'password') + + @mock.patch(MODULE_PATH + '.forms.Teamspeak3Manager') + @mock.patch(MODULE_PATH + '.views.Teamspeak3Manager') + def test_activate(self, manager, forms_manager): + self.login() + expected_username = 'auth_member' + manager.add_user.return_value = (expected_username, 'abc123') + + response = self.client.get(urls.reverse('auth_activate_teamspeak3')) + + self.assertTrue(manager.add_user.called) + teamspeak3_user = Teamspeak3User.objects.get(user=self.member) + self.assertTrue(teamspeak3_user.uid) + self.assertTrue(teamspeak3_user.perm_key) + self.assertRedirects(response, urls.reverse('auth_verify_teamspeak3'), target_status_code=200) + + @mock.patch(MODULE_PATH + '.forms.Teamspeak3Manager') + @mock.patch(MODULE_PATH + '.views.Teamspeak3Manager') + def test_activate_blue(self, manager, forms_manager): + self.login(self.blue_user) + expected_username = 'auth_blue' + manager.add_blue_user.return_value = (expected_username, 'abc123') + + response = self.client.get(urls.reverse('auth_activate_teamspeak3')) + + self.assertTrue(manager.add_blue_user.called) + teamspeak3_user = Teamspeak3User.objects.get(user=self.blue_user) + self.assertTrue(teamspeak3_user.uid) + self.assertTrue(teamspeak3_user.perm_key) + self.assertRedirects(response, urls.reverse('auth_verify_teamspeak3'), target_status_code=200) + + @mock.patch(MODULE_PATH + '.forms.Teamspeak3Manager') + @mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') + def test_verify_submit(self, manager, forms_manager): + self.login() + expected_username = 'auth_member' + + forms_manager._get_userid.return_value = '1234' + + Teamspeak3User.objects.update_or_create(user=self.member, defaults={'uid': '1234', 'perm_key': '5678'}) + data = {'username': 'auth_member'} + + response = self.client.post(urls.reverse('auth_verify_teamspeak3'), data) + + self.assertTrue(manager.update_groups.called) + self.assertRedirects(response, urls.reverse('auth_services'), target_status_code=200) + + @mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') + def test_deactivate(self, manager): + self.login() + Teamspeak3User.objects.create(user=self.member, uid='some member') + + response = self.client.get(urls.reverse('auth_deactivate_teamspeak3')) + + self.assertTrue(manager.delete_user.called) + self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) + with self.assertRaises(ObjectDoesNotExist): + teamspeak3_user = User.objects.get(pk=self.member.pk).teamspeak3 + + @mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') + @mock.patch(MODULE_PATH + '.views.Teamspeak3Manager') + def test_reset_perm(self, manager, tasks_manager): + self.login() + Teamspeak3User.objects.create(user=self.member, uid='some member') + + manager.generate_new_permissionkey.return_value = "valid_member", "123abc" + + response = self.client.get(urls.reverse('auth_reset_teamspeak3_perm')) + + self.assertRedirects(response, urls.reverse('auth_services'), target_status_code=200) + ts3_user = Teamspeak3User.objects.get(uid='valid_member') + self.assertEqual(ts3_user.uid, 'valid_member') + self.assertEqual(ts3_user.perm_key, '123abc') + self.assertTrue(tasks_manager.update_groups.called) + + @mock.patch(MODULE_PATH + '.tasks.Teamspeak3Manager') + @mock.patch(MODULE_PATH + '.views.Teamspeak3Manager') + def test_reset_perm_blue(self, manager, tasks_manager): + self.login(self.blue_user) + Teamspeak3User.objects.create(user=self.blue_user, uid='some member') + + manager.generate_new_blue_permissionkey.return_value = "valid_blue", "123abc" + + response = self.client.get(urls.reverse('auth_reset_teamspeak3_perm')) + + self.assertRedirects(response, urls.reverse('auth_services'), target_status_code=200) + ts3_user = Teamspeak3User.objects.get(uid='valid_blue') + self.assertEqual(ts3_user.uid, 'valid_blue') + self.assertEqual(ts3_user.perm_key, '123abc') + self.assertTrue(tasks_manager.update_groups.called) + + +class Teamspeak3SignalsTestCase(TestCase): + def setUp(self): + self.member = AuthUtils.create_member('auth_member') + + # Suppress signals action while setting up + with mock.patch(MODULE_PATH + '.signals.trigger_all_ts_update') as trigger_all_ts_update: + ts_member_group = TSgroup.objects.create(ts_group_id=1, ts_group_name='Member') + self.m2m_member = AuthTS.objects.create(auth_group=Group.objects.get(name='Member')) + self.m2m_member.ts_group.add(ts_member_group) + self.m2m_member.save() + + def test_m2m_signal_registry(self): + """ + Test that the m2m signal has been registered + """ + registered_functions = [r[1]() for r in signals.m2m_changed.receivers] + self.assertIn(m2m_changed_authts_group, registered_functions) + + def test_post_save_signal_registry(self): + """ + Test that the post_save signal has been registered + """ + registered_functions = [r[1]() for r in signals.post_save.receivers] + self.assertIn(post_save_authts, registered_functions) + + def test_post_delete_signal_registry(self): + """ + Test that the post_delete signal has been registered + """ + registered_functions = [r[1]() for r in signals.post_delete.receivers] + self.assertIn(post_delete_authts, registered_functions) + + @mock.patch(MODULE_PATH + '.signals.transaction') + @mock.patch(MODULE_PATH + '.signals.trigger_all_ts_update') + def test_m2m_changed_authts_group(self, trigger_all_ts_update, transaction): + + # Overload transaction.on_commit so everything happens synchronously + transaction.on_commit = lambda fn: fn() + + new_group = TSgroup.objects.create(ts_group_id=99, ts_group_name='new TS group') + self.m2m_member.ts_group.add(new_group) + self.m2m_member.save() # Triggers signal + + self.assertTrue(trigger_all_ts_update.called) + + @mock.patch(MODULE_PATH + '.signals.transaction') + @mock.patch(MODULE_PATH + '.signals.trigger_all_ts_update') + def test_post_save_authts(self, trigger_all_ts_update, transaction): + + # Overload transaction.on_commit so everything happens synchronously + transaction.on_commit = lambda fn: fn() + + AuthTS.objects.create(auth_group=Group.objects.create(name='Test Group')) # Trigger signal (AuthTS creation) + + self.assertTrue(trigger_all_ts_update.called) + + @mock.patch(MODULE_PATH + '.signals.transaction') + @mock.patch(MODULE_PATH + '.signals.trigger_all_ts_update') + def test_post_delete_authts(self, trigger_all_ts_update, transaction): + # Overload transaction.on_commit so everything happens synchronously + transaction.on_commit = lambda fn: fn() + + self.m2m_member.delete() # Trigger delete signal + + self.assertTrue(trigger_all_ts_update.called) diff --git a/services/modules/teamspeak3/urls.py b/services/modules/teamspeak3/urls.py new file mode 100644 index 00000000..c262bdab --- /dev/null +++ b/services/modules/teamspeak3/urls.py @@ -0,0 +1,21 @@ +from __future__ import unicode_literals +from django.conf.urls import url, include + +from . import views + +module_urls = [ + # Teamspeak3 service control + url(r'^activate/$', views.activate_teamspeak3, + name='auth_activate_teamspeak3'), + url(r'^deactivate/$', views.deactivate_teamspeak3, + name='auth_deactivate_teamspeak3'), + url(r'reset_perm/$', views.reset_teamspeak3_perm, + name='auth_reset_teamspeak3_perm'), + + # Teamspeak Urls + url(r'verify/$', views.verify_teamspeak3, name='auth_verify_teamspeak3'), +] + +urlpatterns = [ + url(r'^teamspeak3/', include(module_urls)), +] diff --git a/services/managers/util/__init__.py b/services/modules/teamspeak3/util/__init__.py similarity index 100% rename from services/managers/util/__init__.py rename to services/modules/teamspeak3/util/__init__.py diff --git a/services/managers/util/ts3.py b/services/modules/teamspeak3/util/ts3.py similarity index 100% rename from services/managers/util/ts3.py rename to services/modules/teamspeak3/util/ts3.py diff --git a/services/modules/teamspeak3/views.py b/services/modules/teamspeak3/views.py new file mode 100644 index 00000000..b13e48cf --- /dev/null +++ b/services/modules/teamspeak3/views.py @@ -0,0 +1,121 @@ +import logging + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.shortcuts import render, redirect + +from authentication.decorators import members_and_blues +from authentication.states import BLUE_STATE +from authentication.models import AuthServicesInfo +from eveonline.managers import EveManager +from eveonline.models import EveAllianceInfo + +from services.modules.teamspeak3.manager import Teamspeak3Manager + +from .forms import TeamspeakJoinForm +from .tasks import Teamspeak3Tasks +from .models import Teamspeak3User + +logger = logging.getLogger(__name__) + + +@login_required +@members_and_blues() +def activate_teamspeak3(request): + logger.debug("activate_teamspeak3 called by user %s" % request.user) + + authinfo = AuthServicesInfo.objects.get(user=request.user) + character = EveManager.get_main_character(request.user) + ticker = character.corporation_ticker + + if authinfo.state == BLUE_STATE: + logger.debug("Adding TS3 user for blue user %s with main character %s" % (request.user, character)) + # Blue members should have alliance ticker (if in alliance) + if EveAllianceInfo.objects.filter(alliance_id=character.alliance_id).exists(): + alliance = EveAllianceInfo.objects.filter(alliance_id=character.alliance_id)[0] + ticker = alliance.alliance_ticker + result = Teamspeak3Manager.add_blue_user(character.character_name, ticker) + else: + logger.debug("Adding TS3 user for user %s with main character %s" % (request.user, character)) + result = Teamspeak3Manager.add_user(character.character_name, ticker) + + # if its empty we failed + if result[0] is not "": + Teamspeak3User.objects.update_or_create(user=request.user, defaults={'uid': result[0], 'perm_key': result[1]}) + logger.debug("Updated authserviceinfo for user %s with TS3 credentials. Updating groups." % request.user) + logger.info("Successfully activated TS3 for user %s" % request.user) + messages.success(request, 'Activated TeamSpeak3 account.') + return redirect("auth_verify_teamspeak3") + logger.error("Unsuccessful attempt to activate TS3 for user %s" % request.user) + messages.error(request, 'An error occurred while processing your TeamSpeak3 account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def verify_teamspeak3(request): + logger.debug("verify_teamspeak3 called by user %s" % request.user) + if not Teamspeak3Tasks.has_account(request.user): + logger.warn("Unable to validate user %s teamspeak: no teamspeak data" % request.user) + return redirect("auth_services") + if request.method == "POST": + form = TeamspeakJoinForm(request.POST) + if form.is_valid(): + Teamspeak3Tasks.update_groups.delay(request.user.pk) + logger.debug("Validated user %s joined TS server" % request.user) + return redirect("auth_services") + else: + form = TeamspeakJoinForm({'username': request.user.teamspeak3.uid}) + context = { + 'form': form, + 'authinfo': {'teamspeak3_uid': request.user.teamspeak3.uid, + 'teamspeak3_perm_key': request.user.teamspeak3.perm_key}, + } + return render(request, 'registered/teamspeakjoin.html', context=context) + + +@login_required +@members_and_blues() +def deactivate_teamspeak3(request): + logger.debug("deactivate_teamspeak3 called by user %s" % request.user) + if Teamspeak3Tasks.has_account(request.user) and Teamspeak3Tasks.delete_user(request.user): + logger.info("Successfully deactivated TS3 for user %s" % request.user) + messages.success(request, 'Deactivated TeamSpeak3 account.') + logger.error("Unsuccessful attempt to deactivate TS3 for user %s" % request.user) + messages.error(request, 'An error occurred while processing your TeamSpeak3 account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def reset_teamspeak3_perm(request): + logger.debug("reset_teamspeak3_perm called by user %s" % request.user) + if not Teamspeak3Tasks.has_account(request.user): + return redirect("auth_services") + authinfo = AuthServicesInfo.objects.get(user=request.user) + character = EveManager.get_main_character(request.user) + logger.debug("Deleting TS3 user for user %s" % request.user) + Teamspeak3Manager.delete_user(request.user.teamspeak3.uid) + + if authinfo.state == BLUE_STATE: + logger.debug( + "Generating new permission key for blue user %s with main character %s" % (request.user, character)) + result = Teamspeak3Manager.generate_new_blue_permissionkey(request.user.teamspeak3.uid, + character.character_name, + character.corporation_ticker) + else: + logger.debug("Generating new permission key for user %s with main character %s" % (request.user, character)) + result = Teamspeak3Manager.generate_new_permissionkey(request.user.teamspeak3.uid, character.character_name, + character.corporation_ticker) + + # if blank we failed + if result[0] != "": + Teamspeak3User.objects.update_or_create(user=request.user, defaults={'uid': result[0], 'perm_key': result[1]}) + logger.debug("Updated authserviceinfo for user %s with TS3 credentials. Updating groups." % request.user) + Teamspeak3Tasks.update_groups.delay(request.user.pk) + logger.info("Successfully reset TS3 permission key for user %s" % request.user) + messages.success(request, 'Reset TeamSpeak3 permission key.') + else: + logger.error("Unsuccessful attempt to reset TS3 permission key for user %s" % request.user) + messages.error(request, 'An error occurred while processing your TeamSpeak3 account.') + return redirect("auth_services") diff --git a/services/modules/xenforo/__init__.py b/services/modules/xenforo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/xenforo/admin.py b/services/modules/xenforo/admin.py new file mode 100644 index 00000000..e614fb9c --- /dev/null +++ b/services/modules/xenforo/admin.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from django.contrib import admin +from .models import XenforoUser + + +class XenforoUserAdmin(admin.ModelAdmin): + list_display = ('user', 'username') + search_fields = ('user__username', 'username') + +admin.site.register(XenforoUser, XenforoUserAdmin) diff --git a/services/modules/xenforo/apps.py b/services/modules/xenforo/apps.py new file mode 100644 index 00000000..cc5d2400 --- /dev/null +++ b/services/modules/xenforo/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class XenforoServiceConfig(AppConfig): + name = 'xenforo' diff --git a/services/modules/xenforo/auth_hooks.py b/services/modules/xenforo/auth_hooks.py new file mode 100644 index 00000000..6309d029 --- /dev/null +++ b/services/modules/xenforo/auth_hooks.py @@ -0,0 +1,58 @@ +from __future__ import unicode_literals + +from django.conf import settings +from django.template.loader import render_to_string + +from services.hooks import ServicesHook +from alliance_auth import hooks + +from .urls import urlpatterns +from .tasks import XenforoTasks + +import logging + +logger = logging.getLogger(__name__) + + +class XenforoService(ServicesHook): + def __init__(self): + ServicesHook.__init__(self) + self.name = 'xenforo' + self.urlpatterns = urlpatterns + + @property + def title(self): + return 'XenForo Forums' + + def delete_user(self, user, notify_user=False): + logger.debug('Deleting user %s %s account' % (user, self.name)) + return XenforoTasks.delete_user(user, notify_user=notify_user) + + def validate_user(self, user): + logger.debug('Validating user %s %s account' % (user, self.name)) + if XenforoTasks.has_account(user) and not self.service_active_for_user(user): + self.delete_user(user, notify_user=True) + + def service_enabled_members(self): + return settings.ENABLE_AUTH_XENFORO or False + + def service_enabled_blues(self): + return settings.ENABLE_BLUE_XENFORO or False + + def render_services_ctrl(self, request): + urls = self.Urls() + urls.auth_activate = 'auth_activate_xenforo' + urls.auth_deactivate = 'auth_deactivate_xenforo' + urls.auth_reset_password = 'auth_reset_xenforo_password' + urls.auth_set_password = 'auth_set_xenforo_password' + return render_to_string(self.service_ctrl_template, { + 'service_name': self.title, + 'urls': urls, + 'service_url': '', + 'username': request.user.xenforo.username if XenforoTasks.has_account(request.user) else '' + }, request=request) + + +@hooks.register('services_hook') +def register_service(): + return XenforoService() diff --git a/services/managers/xenforo_manager.py b/services/modules/xenforo/manager.py similarity index 96% rename from services/managers/xenforo_manager.py rename to services/modules/xenforo/manager.py index aaa4ca3f..65415b50 100644 --- a/services/managers/xenforo_manager.py +++ b/services/modules/xenforo/manager.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals -import os +import random +import string import requests import json @@ -25,7 +26,7 @@ class XenForoManager: @staticmethod def __generate_password(): - return os.urandom(8).encode('hex') + return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(16)]) @staticmethod def exec_http_request(http_params): diff --git a/services/modules/xenforo/migrations/0001_initial.py b/services/modules/xenforo/migrations/0001_initial.py new file mode 100644 index 00000000..c721a776 --- /dev/null +++ b/services/modules/xenforo/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-12-12 03:14 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='XenforoUser', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='xenforo', serialize=False, to=settings.AUTH_USER_MODEL)), + ('username', models.CharField(max_length=254)), + ], + ), + ] diff --git a/services/modules/xenforo/migrations/__init__.py b/services/modules/xenforo/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/modules/xenforo/models.py b/services/modules/xenforo/models.py new file mode 100644 index 00000000..de9e864c --- /dev/null +++ b/services/modules/xenforo/models.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals +from django.utils.encoding import python_2_unicode_compatible +from django.db import models + + +@python_2_unicode_compatible +class XenforoUser(models.Model): + user = models.OneToOneField('auth.User', + primary_key=True, + on_delete=models.CASCADE, + related_name='xenforo') + username = models.CharField(max_length=254) + + def __str__(self): + return self.username diff --git a/services/modules/xenforo/tasks.py b/services/modules/xenforo/tasks.py new file mode 100644 index 00000000..d3bf9642 --- /dev/null +++ b/services/modules/xenforo/tasks.py @@ -0,0 +1,46 @@ +from __future__ import unicode_literals + +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from notifications import notify + +from .manager import XenForoManager +from .models import XenforoUser + +import logging + +logger = logging.getLogger(__name__) + + +class XenforoTasks: + def __init__(self): + pass + + @classmethod + def delete_user(cls, user, notify_user=False): + if cls.has_account(user): + logger.debug("User %s has a XenForo account %s. Deleting." % (user, user.xenforo.username)) + if XenForoManager.disable_user(user.xenforo.username) == 200: + user.xenforo.delete() + if notify_user: + notify(user, 'XenForo Account Disabled', level='danger') + return True + return False + + @staticmethod + def has_account(user): + try: + return user.xenforo.username != '' + except ObjectDoesNotExist: + return False + + @classmethod + def disable(cls): + if settings.ENABLE_AUTH_XENFORO: + logger.warn( + "ENABLE_AUTH_XENFORO still True, after disabling users will still be able to link XenForo accounts") + if settings.ENABLE_BLUE_XENFORO: + logger.warn( + "ENABLE_BLUE_XENFORO still True, after disabling blues will still be able to link XenForo accounts") + logger.debug("Deleting ALL XenForo users") + XenforoUser.objects.all().delete() diff --git a/services/modules/xenforo/tests.py b/services/modules/xenforo/tests.py new file mode 100644 index 00000000..7b8c65f2 --- /dev/null +++ b/services/modules/xenforo/tests.py @@ -0,0 +1,192 @@ +from __future__ import unicode_literals + +try: + # Py3 + from unittest import mock +except ImportError: + # Py2 + import mock + +from django.test import TestCase, RequestFactory +from django.conf import settings +from django import urls +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist + +from alliance_auth.tests.auth_utils import AuthUtils + +from .auth_hooks import XenforoService +from .models import XenforoUser +from .tasks import XenforoTasks + +MODULE_PATH = 'services.modules.xenforo' + + +class XenforoHooksTestCase(TestCase): + def setUp(self): + self.member = 'member_user' + member = AuthUtils.create_member(self.member) + XenforoUser.objects.create(user=member, username=self.member) + self.blue = 'blue_user' + blue = AuthUtils.create_blue(self.blue) + XenforoUser.objects.create(user=blue, username=self.blue) + self.none_user = 'none_user' + none_user = AuthUtils.create_user(self.none_user) + self.service = XenforoService + + def test_has_account(self): + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(XenforoTasks.has_account(member)) + self.assertTrue(XenforoTasks.has_account(blue)) + self.assertFalse(XenforoTasks.has_account(none_user)) + + def test_service_enabled(self): + service = self.service() + member = User.objects.get(username=self.member) + blue = User.objects.get(username=self.blue) + none_user = User.objects.get(username=self.none_user) + self.assertTrue(service.service_enabled_members()) + self.assertTrue(service.service_enabled_blues()) + + self.assertEqual(service.service_active_for_user(member), settings.ENABLE_AUTH_XENFORO) + self.assertEqual(service.service_active_for_user(blue), settings.ENABLE_BLUE_XENFORO) + self.assertFalse(service.service_active_for_user(none_user)) + + @mock.patch(MODULE_PATH + '.tasks.XenForoManager') + def test_validate_user(self, manager): + service = self.service() + # Test member is not deleted + member = User.objects.get(username=self.member) + manager.disable_user.return_value = 200 + + service.validate_user(member) + self.assertTrue(member.xenforo) + + # Test none user is deleted + none_user = User.objects.get(username=self.none_user) + XenforoUser.objects.create(user=none_user, username='abc123') + service.validate_user(none_user) + self.assertTrue(manager.disable_user.called) + with self.assertRaises(ObjectDoesNotExist): + none_xenforo = User.objects.get(username=self.none_user).xenforo + + @mock.patch(MODULE_PATH + '.tasks.XenForoManager') + def test_delete_user(self, manager): + member = User.objects.get(username=self.member) + manager.disable_user.return_value = 200 + service = self.service() + + result = service.delete_user(member) + + self.assertTrue(result) + self.assertTrue(manager.disable_user.called) + with self.assertRaises(ObjectDoesNotExist): + xenforo_user = User.objects.get(username=self.member).xenforo + + def test_render_services_ctrl(self): + service = self.service() + member = User.objects.get(username=self.member) + request = RequestFactory().get('/en/services/') + request.user = member + + response = service.render_services_ctrl(request) + self.assertTemplateUsed(service.service_ctrl_template) + self.assertIn(urls.reverse('auth_deactivate_xenforo'), response) + self.assertIn(urls.reverse('auth_reset_xenforo_password'), response) + self.assertIn(urls.reverse('auth_set_xenforo_password'), response) + + # Test register becomes available + member.xenforo.delete() + member = User.objects.get(username=self.member) + request.user = member + response = service.render_services_ctrl(request) + self.assertIn(urls.reverse('auth_activate_xenforo'), response) + + +class XenforoViewsTestCase(TestCase): + def setUp(self): + self.member = AuthUtils.create_member('auth_member') + self.member.set_password('password') + self.member.email = 'auth_member@example.com' + self.member.save() + AuthUtils.add_main_character(self.member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation') + + def login(self): + self.client.login(username=self.member.username, password='password') + + @mock.patch(MODULE_PATH + '.tasks.XenForoManager') + @mock.patch(MODULE_PATH + '.views.XenForoManager') + def test_activate(self, manager, tasks_manager): + self.login() + expected_username = 'auth_member' + manager.add_user.return_value = { + 'response': {'status_code': 200}, + 'password': 'hunter2', + 'username': expected_username, + } + + response = self.client.get(urls.reverse('auth_activate_xenforo')) + + self.assertTrue(manager.add_user.called) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed('registered/service_credentials.html') + self.assertContains(response, expected_username) + xenforo_user = XenforoUser.objects.get(user=self.member) + self.assertEqual(xenforo_user.username, expected_username) + + @mock.patch(MODULE_PATH + '.tasks.XenForoManager') + def test_deactivate(self, manager): + self.login() + XenforoUser.objects.create(user=self.member, username='some member') + + manager.disable_user.return_value = 200 + + response = self.client.get(urls.reverse('auth_deactivate_xenforo')) + + self.assertTrue(manager.disable_user.called) + self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) + with self.assertRaises(ObjectDoesNotExist): + xenforo_user = User.objects.get(pk=self.member.pk).xenforo + + @mock.patch(MODULE_PATH + '.views.XenForoManager') + def test_set_password(self, manager): + self.login() + XenforoUser.objects.create(user=self.member, username='some member') + + response = self.client.post(urls.reverse('auth_set_xenforo_password'), data={'password': '1234asdf'}) + + self.assertTrue(manager.update_user_password.called) + args, kwargs = manager.update_user_password.call_args + self.assertEqual(args[1], '1234asdf') + self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) + + @mock.patch(MODULE_PATH + '.views.XenForoManager') + def test_reset_password(self, manager): + self.login() + XenforoUser.objects.create(user=self.member, username='some member') + + manager.reset_password.return_value = { + 'response': {'status_code': 200}, + 'password': 'hunter2', + } + + response = self.client.get(urls.reverse('auth_reset_xenforo_password')) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'registered/service_credentials.html') + self.assertContains(response, 'some member') + self.assertContains(response, 'hunter2') + + +class XenforoManagerTestCase(TestCase): + def setUp(self): + from .manager import XenForoManager + self.manager = XenForoManager + + def test_generate_password(self): + password = self.manager._XenForoManager__generate_password() + + self.assertEqual(len(password), 16) + self.assertIsInstance(password, type('')) diff --git a/services/modules/xenforo/urls.py b/services/modules/xenforo/urls.py new file mode 100644 index 00000000..6993a33e --- /dev/null +++ b/services/modules/xenforo/urls.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals +from django.conf.urls import url, include + +from . import views + +module_urls = [ + # XenForo service control + url(r'^activate/$', views.activate_xenforo_forum, name='auth_activate_xenforo'), + url(r'^deactivate/$', views.deactivate_xenforo_forum, name='auth_deactivate_xenforo'), + url(r'^reset_password/$', views.reset_xenforo_password, name='auth_reset_xenforo_password'), + url(r'^set_password/$', views.set_xenforo_password, name='auth_set_xenforo_password'), +] + +urlpatterns = [ + url(r'^xenforo/', include(module_urls)), +] diff --git a/services/modules/xenforo/views.py b/services/modules/xenforo/views.py new file mode 100644 index 00000000..6fe79362 --- /dev/null +++ b/services/modules/xenforo/views.py @@ -0,0 +1,101 @@ +from __future__ import unicode_literals + +import logging + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.shortcuts import render, redirect + +from authentication.decorators import members_and_blues +from eveonline.managers import EveManager +from services.forms import ServicePasswordForm +from .manager import XenForoManager +from .models import XenforoUser +from .tasks import XenforoTasks + +logger = logging.getLogger(__name__) + +@login_required +@members_and_blues() +def activate_xenforo_forum(request): + logger.debug("activate_xenforo_forum called by user %s" % request.user) + character = EveManager.get_main_character(request.user) + logger.debug("Adding XenForo user for user %s with main character %s" % (request.user, character)) + result = XenForoManager.add_user(character.character_name, request.user.email) + # Based on XenAPI's response codes + if result['response']['status_code'] == 200: + XenforoUser.objects.update_or_create(user=request.user, defaults={'username': result['username']}) + logger.info("Updated user %s with XenForo credentials. Updating groups." % request.user) + messages.success(request, 'Activated XenForo account.') + credentials = { + 'username': result['username'], + 'password': result['password'], + } + return render(request, 'registered/service_credentials.html', + context={'credentials': credentials, 'service': 'XenForo'}) + + else: + logger.error("Unsuccessful attempt to activate xenforo for user %s" % request.user) + messages.error(request, 'An error occurred while processing your XenForo account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def deactivate_xenforo_forum(request): + logger.debug("deactivate_xenforo_forum called by user %s" % request.user) + if XenforoTasks.delete_user(request.user): + logger.info("Successfully deactivated XenForo for user %s" % request.user) + messages.success(request, 'Deactivated XenForo account.') + else: + messages.error(request, 'An error occurred while processing your XenForo account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def reset_xenforo_password(request): + logger.debug("reset_xenforo_password called by user %s" % request.user) + if XenforoTasks.has_account(request.user): + result = XenForoManager.reset_password(request.user.xenforo.username) + # Based on XenAPI's response codes + if result['response']['status_code'] == 200: + logger.info("Successfully reset XenForo password for user %s" % request.user) + messages.success(request, 'Reset XenForo account password.') + credentials = { + 'username': request.user.xenforo.username, + 'password': result['password'], + } + return render(request, 'registered/service_credentials.html', + context={'credentials': credentials, 'service': 'XenForo'}) + logger.error("Unsuccessful attempt to reset XenForo password for user %s" % request.user) + messages.error(request, 'An error occurred while processing your XenForo account.') + return redirect("auth_services") + + +@login_required +@members_and_blues() +def set_xenforo_password(request): + logger.debug("set_xenforo_password called by user %s" % request.user) + if request.method == 'POST': + logger.debug("Received POST request with form.") + form = ServicePasswordForm(request.POST) + logger.debug("Form is valid: %s" % form.is_valid()) + if form.is_valid() and XenforoTasks.has_account(request.user): + password = form.cleaned_data['password'] + logger.debug("Form contains password of length %s" % len(password)) + result = XenForoManager.update_user_password(request.user.xenforo.username, password) + if result['response']['status_code'] == 200: + logger.info("Successfully reset XenForo password for user %s" % request.user) + messages.success(request, 'Changed XenForo password.') + else: + logger.error("Failed to install custom XenForo password for user %s" % request.user) + messages.error(request, 'An error occurred while processing your XenForo account.') + return redirect('auth_services') + else: + logger.debug("Request is not type POST - providing empty form.") + form = ServicePasswordForm() + + logger.debug("Rendering form for user %s" % request.user) + context = {'form': form, 'service': 'Forum'} + return render(request, 'registered/service_password.html', context=context) diff --git a/services/signals.py b/services/signals.py index ebbaa874..2bd19636 100644 --- a/services/signals.py +++ b/services/signals.py @@ -1,25 +1,17 @@ from __future__ import unicode_literals + +import logging + +from django.contrib.auth.models import User from django.db import transaction from django.db.models.signals import m2m_changed -from django.db.models.signals import post_save -from django.db.models.signals import pre_save -from django.db.models.signals import post_delete from django.db.models.signals import pre_delete +from django.db.models.signals import pre_save from django.dispatch import receiver -from django.contrib.auth.models import User -import logging -from services.tasks import update_jabber_groups -from services.tasks import update_mumble_groups -from services.tasks import update_forum_groups -from services.tasks import update_ipboard_groups -from services.tasks import update_discord_groups -from services.tasks import update_teamspeak3_groups -from services.tasks import update_discourse_groups -from services.tasks import update_smf_groups -from authentication.tasks import set_state + +from alliance_auth.hooks import get_hooks from authentication.tasks import disable_member -from authentication.models import AuthServicesInfo -from services.models import AuthTS +from authentication.tasks import set_state logger = logging.getLogger(__name__) @@ -30,55 +22,20 @@ def m2m_changed_user_groups(sender, instance, action, *args, **kwargs): def trigger_service_group_update(): logger.debug("Triggering service group update for %s" % instance) - auth = AuthServicesInfo.objects.get(user=instance) - if auth.jabber_username: - update_jabber_groups.delay(instance.pk) - if auth.teamspeak3_uid: - update_teamspeak3_groups.delay(instance.pk) - if auth.forum_username: - update_forum_groups.delay(instance.pk) - if auth.smf_username: - update_smf_groups.delay(instance.pk) - if auth.ipboard_username: - update_ipboard_groups.delay(instance.pk) - if auth.discord_uid: - update_discord_groups.delay(instance.pk) - if auth.mumble_username: - update_mumble_groups.delay(instance.pk) - if auth.discourse_enabled: - update_discourse_groups.delay(instance.pk) - if auth.smf_username: - update_smf_groups.delay(instance.pk) + # Iterate through Service hooks + services = get_hooks('services_hook') + for fn in services: + svc = fn() + try: + svc.update_groups(instance) + except: + logger.exception('Exception running update_groups for services module %s on user %s' % (svc, instance)) if instance.pk and (action == "post_add" or action == "post_remove" or action == "post_clear"): logger.debug("Waiting for commit to trigger service group update for %s" % instance) transaction.on_commit(trigger_service_group_update) -def trigger_all_ts_update(): - for auth in AuthServicesInfo.objects.filter(teamspeak3_uid__isnull=False): - update_teamspeak3_groups.delay(auth.user.pk) - - -@receiver(m2m_changed, sender=AuthTS.ts_group.through) -def m2m_changed_authts_group(sender, instance, action, *args, **kwargs): - logger.debug("Received m2m_changed from %s ts_group with action %s" % (instance, action)) - if action == "post_add" or action == "post_remove": - trigger_all_ts_update() - - -@receiver(post_save, sender=AuthTS) -def post_save_authts(sender, instance, *args, **kwargs): - logger.debug("Received post_save from %s" % instance) - trigger_all_ts_update() - - -@receiver(post_delete, sender=AuthTS) -def post_delete_authts(sender, instance, *args, **kwargs): - logger.debug("Received post_delete signal from %s" % instance) - trigger_all_ts_update() - - @receiver(pre_delete, sender=User) def pre_delete_user(sender, instance, *args, **kwargs): logger.debug("Received pre_delete from %s" % instance) diff --git a/services/tasks.py b/services/tasks.py index 5b36e6ab..dbd609a3 100644 --- a/services/tasks.py +++ b/services/tasks.py @@ -1,36 +1,19 @@ from __future__ import unicode_literals -from django.conf import settings + import logging -from django.contrib.auth.models import User + from celery import task -from services.models import UserTSgroup -from services.models import AuthTS -from services.models import TSgroup -from services.models import MumbleUser -from authentication.managers import AuthServicesInfoManager -from authentication.models import AuthServicesInfo -from services.managers.openfire_manager import OpenfireManager -from services.managers.phpbb3_manager import Phpbb3Manager -from services.managers.mumble_manager import MumbleManager -from services.managers.ipboard_manager import IPBoardManager -from services.managers.teamspeak3_manager import Teamspeak3Manager -from services.managers.discord_manager import DiscordOAuthManager -from services.managers.xenforo_manager import XenForoManager -from services.managers.market_manager import marketManager -from services.managers.discourse_manager import DiscourseManager -from services.managers.smf_manager import smfManager -from services.managers.util.ts3 import TeamspeakError + +from alliance_auth.hooks import get_hooks from authentication.states import MEMBER_STATE, BLUE_STATE from notifications import notify -from celery.task import periodic_task -from celery.task.schedules import crontab -from eveonline.managers import EveManager import redis REDIS_CLIENT = redis.Redis() logger = logging.getLogger(__name__) + # http://loose-bits.com/2010/10/distributed-task-locking-in-celery.html def only_one(function=None, key="", timeout=None): """Enforce only one celery task at a time.""" @@ -58,169 +41,17 @@ def only_one(function=None, key="", timeout=None): return _dec(function) if function is not None else _dec -@periodic_task(run_every=crontab(minute="*/30")) -def run_ts3_group_update(): - if settings.ENABLE_AUTH_TEAMSPEAK3 or settings.ENABLE_BLUE_TEAMSPEAK3: - logger.debug("TS3 installed. Syncing local group objects.") - Teamspeak3Manager._sync_ts_group_db() - - -def disable_teamspeak(): - if settings.ENABLE_AUTH_TEAMSPEAK3: - logger.warn( - "ENABLE_AUTH_TEAMSPEAK3 still True, after disabling users will still be able to create teamspeak accounts") - if settings.ENABLE_BLUE_TEAMSPEAK3: - logger.warn( - "ENABLE_BLUE_TEAMSPEAK3 still True, after disabling blues will still be able to create teamspeak accounts") - for auth in AuthServicesInfo.objects.all(): - if auth.teamspeak3_uid: - logger.info("Clearing %s Teamspeak3 UID" % auth.user) - auth.teamspeak3_uid = '' - auth.save() - if auth.teamspeak3_perm_key: - logger.info("Clearing %s Teamspeak3 permission key" % auth.user) - auth.teamspeak3_perm_key = '' - auth.save() - logger.info("Deleting all UserTSgroup models") - UserTSgroup.objects.all().delete() - logger.info("Deleting all AuthTS models") - AuthTS.objects.all().delete() - logger.info("Deleting all TSgroup models") - TSgroup.objects.all().delete() - logger.info("Teamspeak3 disabled") - - -def disable_forum(): - if settings.ENABLE_AUTH_FORUM: - logger.warn("ENABLE_AUTH_FORUM still True, after disabling users will still be able to create forum accounts") - if settings.ENABLE_BLUE_FORUM: - logger.warn("ENABLE_BLUE_FORUM still True, after disabling blues will still be able to create forum accounts") - for auth in AuthServicesInfo.objects.all(): - if auth.forum_username: - logger.info("Clearing %s forum username" % auth.user) - auth.forum_username = '' - auth.save() - - -def disable_jabber(): - if settings.ENABLE_AUTH_JABBER: - logger.warn("ENABLE_AUTH_JABBER still True, after disabling users will still be able to create jabber accounts") - if settings.ENABLE_BLUE_JABBER: - logger.warn("ENABLE_BLUE_JABBER still True, after disabling blues will still be able to create jabber accounts") - for auth in AuthServicesInfo.objects.all(): - if auth.jabber_username: - logger.info("Clearing %s jabber username" % auth.user) - auth.jabber_username = '' - auth.save() - - -def disable_mumble(): - if settings.ENABLE_AUTH_MUMBLE: - logger.warn("ENABLE_AUTH_MUMBLE still True, after disabling users will still be able to create mumble accounts") - if settings.ENABLE_BLUE_MUMBLE: - logger.warn("ENABLE_BLUE_MUMBLE still True, after disabling blues will still be able to create mumble accounts") - for auth in AuthServicesInfo.objects.all(): - if auth.mumble_username: - logger.info("Clearing %s mumble username" % auth.user) - auth.mumble_username = '' - auth.save() - logger.info("Deleting all MumbleUser models") - MumbleUser.objects.all().delete() - - -def disable_ipboard(): - if settings.ENABLE_AUTH_IPBOARD: - logger.warn( - "ENABLE_AUTH_IPBOARD still True, after disabling users will still be able to create IPBoard accounts") - if settings.ENABLE_BLUE_IPBOARD: - logger.warn( - "ENABLE_BLUE_IPBOARD still True, after disabling blues will still be able to create IPBoard accounts") - for auth in AuthServicesInfo.objects.all(): - if auth.ipboard_username: - logger.info("Clearing %s ipboard username" % auth.user) - auth.ipboard_username = '' - auth.save() - - -def disable_discord(): - if settings.ENABLE_AUTH_DISCORD: - logger.warn("ENABLE_AUTH_DISCORD still True, after disabling users will still be able to link Discord accounts") - if settings.ENABLE_BLUE_DISCORD: - logger.warn("ENABLE_BLUE_DISCORD still True, after disabling blues will still be able to link Discord accounts") - for auth in AuthServicesInfo.objects.all(): - if auth.discord_uid: - logger.info("Clearing %s Discord UID" % auth.user) - auth.discord_uid = '' - auth.save() - - -def disable_market(): - if settings.ENABLE_AUTH_MARKET: - logger.warn("ENABLE_AUTH_MARKET still True, after disabling users will still be able to activate Market accounts") - if settings.ENABLE_BLUE_DISCORD: - logger.warn("ENABLE_BLUE_MARKET still True, after disabling blues will still be able to activate Market accounts") - for auth in AuthServicesInfo.objects.all(): - if auth.market_username: - logger.info("Clearing %s market username" % auth.user) - auth.market_username = '' - auth.save() - - def deactivate_services(user): change = False logger.debug("Deactivating services for user %s" % user) - authinfo = AuthServicesInfo.objects.get(user=user) - if authinfo.mumble_username and authinfo.mumble_username != "": - logger.debug("User %s has mumble account %s. Deleting." % (user, authinfo.mumble_username)) - MumbleManager.delete_user(authinfo.mumble_username) - AuthServicesInfoManager.update_user_mumble_info("", user) - change = True - if authinfo.jabber_username and authinfo.jabber_username != "": - logger.debug("User %s has jabber account %s. Deleting." % (user, authinfo.jabber_username)) - OpenfireManager.delete_user(authinfo.jabber_username) - AuthServicesInfoManager.update_user_jabber_info("", user) - change = True - if authinfo.forum_username and authinfo.forum_username != "": - logger.debug("User %s has forum account %s. Deleting." % (user, authinfo.forum_username)) - Phpbb3Manager.disable_user(authinfo.forum_username) - AuthServicesInfoManager.update_user_forum_info("", user) - change = True - if authinfo.ipboard_username and authinfo.ipboard_username != "": - logger.debug("User %s has ipboard account %s. Deleting." % (user, authinfo.ipboard_username)) - IPBoardManager.disable_user(authinfo.ipboard_username) - AuthServicesInfoManager.update_user_ipboard_info("", user) - change = True - if authinfo.teamspeak3_uid and authinfo.teamspeak3_uid != "": - logger.debug("User %s has mumble account %s. Deleting." % (user, authinfo.teamspeak3_uid)) - Teamspeak3Manager.delete_user(authinfo.teamspeak3_uid) - AuthServicesInfoManager.update_user_teamspeak3_info("", "", user) - change = True - if authinfo.discord_uid and authinfo.discord_uid != "": - logger.debug("User %s has discord account %s. Deleting." % (user, authinfo.discord_uid)) - DiscordOAuthManager.delete_user(authinfo.discord_uid) - AuthServicesInfoManager.update_user_discord_info("", user) - change = True - if authinfo.xenforo_username and authinfo.xenforo_password != "": - logger.debug("User %s has a XenForo account %s. Deleting." % (user, authinfo.xenforo_username)) - XenForoManager.disable_user(authinfo.xenforo_username) - AuthServicesInfoManager.update_user_xenforo_info("", user) - change = True - if authinfo.market_username and authinfo.market_username != "": - logger.debug("User %s has a Market account %s. Deleting." % (user, authinfo.market_username)) - marketManager.disable_user(authinfo.market_username) - AuthServicesInfoManager.update_user_market_info("", user) - change = True - if authinfo.discourse_enabled: - logger.debug("User %s has a Discourse account. Disabling login." % user) - DiscourseManager.disable_user(user) - authinfo.discourse_enabled = False - authinfo.save() - change = True - if authinfo.smf_username and authinfo.smf_username != "": - logger.debug("User %s has a SMF account %s. Deleting." % (user, authinfo.smf_username)) - smfManager.disable_user(authinfo.smf_username) - AuthServicesInfoManager.update_user_smf_info("", user) - change = True + # Iterate through service hooks and disable users + for fn in get_hooks('services_hook'): + svc = fn() + try: + if svc.delete_user(user): + change = True + except: + logger.exception('Exception running delete_user for services module %s on user %s' % (svc, user)) if change: notify(user, "Services Disabled", message="Your services accounts have been disabled.", level="danger") @@ -235,273 +66,10 @@ def validate_services(self, user, state): deactivate_services(user) return logger.debug('Ensuring user %s services are available to state %s' % (user, state)) - auth = AuthServicesInfo.objects.get(user=user) - if auth.mumble_username and not getattr(settings, 'ENABLE_%s_MUMBLE' % setting_string, False): - MumbleManager.delete_user(auth.mumble_username) - AuthServicesInfoManager.update_user_mumble_info("", user) - notify(user, 'Mumble Account Disabled', level='danger') - if auth.jabber_username and not getattr(settings, 'ENABLE_%s_JABBER' % setting_string, False): - OpenfireManager.delete_user(auth.jabber_username) - AuthServicesInfoManager.update_user_jabber_info("", user) - notify(user, 'Jabber Account Disabled', level='danger') - if auth.forum_username and not getattr(settings, 'ENABLE_%s_FORUM' % setting_string, False): - Phpbb3Manager.disable_user(auth.forum_username) - AuthServicesInfoManager.update_user_forum_info("", user) - notify(user, 'Forum Account Disabled', level='danger') - if auth.ipboard_username and not getattr(settings, 'ENABLE_%s_IPBOARD' % setting_string, False): - IPBoardManager.disable_user(auth.ipboard_username) - AuthServicesInfoManager.update_user_ipboard_info("", user) - notify(user, 'IPBoard Account Disabled', level='danger') - if auth.teamspeak3_uid and not getattr(settings, 'ENABLE_%s_TEAMSPEAK' % setting_string, False): - Teamspeak3Manager.delete_user(auth.teamspeak3_uid) - AuthServicesInfoManager.update_user_teamspeak3_info("", "", user) - notify(user, 'TeamSpeak3 Account Disabled', level='danger') - if auth.discord_uid and not getattr(settings, 'ENABLE_%s_DISCORD' % setting_string, False): - DiscordOAuthManager.delete_user(auth.discord_uid) - AuthServicesInfoManager.update_user_discord_info("", user) - notify(user, 'Discord Account Disabled', level='danger') - if auth.xenforo_username and not getattr(settings, 'ENABLE_%s_XENFORO' % setting_string, False): - XenForoManager.disable_user(auth.xenforo_username) - AuthServicesInfoManager.update_user_xenforo_info("", user) - notify(user, 'XenForo Account Disabled', level='danger') - if auth.market_username and not getattr(settings, 'ENABLE_%s_MARKET' % setting_string, False): - marketManager.disable_user(auth.market_username) - AuthServicesInfoManager.update_user_market_info("", user) - notify(user, 'Alliance Market Account Disabled', level='danger') - if auth.discourse_enabled and not getattr(settings, 'ENABLE_%s_DISCOURSE' % setting_string, False): - DiscourseManager.disable_user(user) - authinfo.discourse_enabled = False - authinfo.save() - notify(user, 'Discourse Account Disabled', level='danger') - if auth.smf_username and not getattr(settings, 'ENABLE_%s_SMF' % setting_string, False): - smfManager.disable_user(auth.smf_username) - AuthServicesInfoManager.update_user_smf_info(auth.smf_username, user) - notify(user, "SMF Account Disabled", level='danger') - - -@task(bind=True) -def update_jabber_groups(self, pk): - user = User.objects.get(pk=pk) - logger.debug("Updating jabber groups for user %s" % user) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - groups = [] - for group in user.groups.all(): - groups.append(str(group.name)) - if len(groups) == 0: - groups.append('empty') - logger.debug("Updating user %s jabber groups to %s" % (user, groups)) - try: - OpenfireManager.update_user_groups(authserviceinfo.jabber_username, groups) - except: - logger.exception("Jabber group sync failed for %s, retrying in 10 mins" % user) - raise self.retry(countdown=60 * 10) - logger.debug("Updated user %s jabber groups." % user) - - -@task -def update_all_jabber_groups(): - logger.debug("Updating ALL jabber groups") - for user in AuthServicesInfo.objects.exclude(jabber_username__exact=''): - update_jabber_groups.delay(user.user_id) - - -@task(bind=True) -def update_mumble_groups(self, pk): - user = User.objects.get(pk=pk) - logger.debug("Updating mumble groups for user %s" % user) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - groups = [] - for group in user.groups.all(): - groups.append(str(group.name)) - if len(groups) == 0: - groups.append('empty') - logger.debug("Updating user %s mumble groups to %s" % (user, groups)) - try: - MumbleManager.update_groups(authserviceinfo.mumble_username, groups) - except: - logger.exception("Mumble group sync failed for %s, retrying in 10 mins" % user) - raise self.retry(countdown=60 * 10) - logger.debug("Updated user %s mumble groups." % user) - - -@task -def update_all_mumble_groups(): - logger.debug("Updating ALL mumble groups") - for user in AuthServicesInfo.objects.exclude(mumble_username__exact=''): - update_mumble_groups.delay(user.user_id) - - -@task(bind=True) -def update_forum_groups(self, pk): - user = User.objects.get(pk=pk) - logger.debug("Updating forum groups for user %s" % user) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - groups = [] - for group in user.groups.all(): - groups.append(str(group.name)) - if len(groups) == 0: - groups.append('empty') - logger.debug("Updating user %s forum groups to %s" % (user, groups)) - try: - Phpbb3Manager.update_groups(authserviceinfo.forum_username, groups) - except: - logger.exception("Phpbb group sync failed for %s, retrying in 10 mins" % user) - raise self.retry(countdown=60 * 10) - logger.debug("Updated user %s forum groups." % user) - - -@task -def update_all_forum_groups(): - logger.debug("Updating ALL forum groups") - for user in AuthServicesInfo.objects.exclude(forum_username__exact=''): - update_forum_groups.delay(user.user_id) - - -@task(bind=True) -def update_smf_groups(self, pk): - user = User.objects.get(pk=pk) - logger.debug("Updating smf groups for user %s" % user) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - groups = [] - for group in user.groups.all(): - groups.append(str(group.name)) - if len(groups) == 0: - groups.append('empty') - logger.debug("Updating user %s smf groups to %s" % (user, groups)) - try: - smfManager.update_groups(authserviceinfo.smf_username, groups) - except: - logger.exception("smf group sync failed for %s, retrying in 10 mins" % user) - raise self.retry(countdown=60 * 10) - logger.debug("Updated user %s smf groups." % user) - - -@task -def update_all_smf_groups(): - logger.debug("Updating ALL smf groups") - for user in AuthServicesInfo.objects.exclude(smf_username__exact=''): - update_smf_groups.delay(user.user_id) - - -@task(bind=True) -def update_ipboard_groups(self, pk): - user = User.objects.get(pk=pk) - logger.debug("Updating user %s ipboard groups." % user) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - groups = [] - for group in user.groups.all(): - groups.append(str(group.name)) - if len(groups) == 0: - groups.append('empty') - logger.debug("Updating user %s ipboard groups to %s" % (user, groups)) - try: - IPBoardManager.update_groups(authserviceinfo.ipboard_username, groups) - except: - logger.exception("IPBoard group sync failed for %s, retrying in 10 mins" % user) - raise self.retry(countdown=60 * 10) - logger.debug("Updated user %s ipboard groups." % user) - - -@task -def update_all_ipboard_groups(): - logger.debug("Updating ALL ipboard groups") - for user in AuthServicesInfo.objects.exclude(ipboard_username__exact=''): - update_ipboard_groups.delay(user.user_id) - - -@task(bind=True) -def update_teamspeak3_groups(self, pk): - user = User.objects.get(pk=pk) - logger.debug("Updating user %s teamspeak3 groups" % user) - usergroups = user.groups.all() - authserviceinfo = AuthServicesInfo.objects.get(user=user) - groups = {} - for usergroup in usergroups: - filtered_groups = AuthTS.objects.filter(auth_group=usergroup) - if filtered_groups: - for filtered_group in filtered_groups: - for ts_group in filtered_group.ts_group.all(): - groups[ts_group.ts_group_name] = ts_group.ts_group_id - logger.debug("Updating user %s teamspeak3 groups to %s" % (user, groups)) - try: - Teamspeak3Manager.update_groups(authserviceinfo.teamspeak3_uid, groups) - logger.debug("Updated user %s teamspeak3 groups." % user) - except TeamspeakError as e: - logger.error("Error occured while syncing TS groups for %s: %s" % (user, str(e))) - raise self.retry(countdown=60*10) - - -@task -def update_all_teamspeak3_groups(): - logger.debug("Updating ALL teamspeak3 groups") - for user in AuthServicesInfo.objects.exclude(teamspeak3_uid__exact=''): - update_teamspeak3_groups.delay(user.user_id) - - -@task(bind=True) -@only_one(key="Discord", timeout=60*5) -def update_discord_groups(self, pk): - user = User.objects.get(pk=pk) - logger.debug("Updating discord groups for user %s" % user) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - groups = [] - for group in user.groups.all(): - groups.append(str(group.name)) - if len(groups) == 0: - logger.debug("No syncgroups found for user. Adding empty group.") - groups.append('empty') - logger.debug("Updating user %s discord groups to %s" % (user, groups)) - try: - DiscordOAuthManager.update_groups(authserviceinfo.discord_uid, groups) - except: - logger.exception("Discord group sync failed for %s, retrying in 10 mins" % user) - raise self.retry(countdown=60 * 10) - logger.debug("Updated user %s discord groups." % user) - - -@task -def update_all_discord_groups(): - logger.debug("Updating ALL discord groups") - for user in AuthServicesInfo.objects.exclude(discord_uid__exact=''): - update_discord_groups.delay(user.user_id) - - -@task(bind=True) -def update_discord_nickname(self, pk): - user = User.objects.get(pk=pk) - logger.debug("Updating discord nickname for user %s" % user) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - character = EveManager.get_character_by_id(authserviceinfo.main_char_id) - logger.debug("Updating user %s discord nickname to %s" % (user, character.character_name)) - try: - DiscordOAuthManager.update_nickname(authserviceinfo.discord_uid, character.character_name) - except: - logger.exception("Discord nickname sync failed for %s, retrying in 10 mins" % user) - raise self.retry(countdown=60 * 10) - logger.debug("Updated user %s discord nickname." % user) - - -@task -def update_all_discord_nicknames(): - logger.debug("Updating ALL discord nicknames") - for user in AuthServicesInfo.objects.exclude(discord_uid__exact=''): - update_discord_nickname(user.user_id) - - -@task(bind=True) -def update_discourse_groups(self, pk): - user = User.objects.get(pk=pk) - logger.debug("Updating discourse groups for user %s" % user) - try: - DiscourseManager.update_groups(user) - except: - logger.warn("Discourse group sync failed for %s, retrying in 10 mins" % user) - raise self.retry(countdown=60 * 10) - logger.debug("Updated user %s discourse groups." % user) - - -@task -def update_all_discourse_groups(): - logger.debug("Updating ALL discourse groups") - for user in AuthServicesInfo.objects.filter(discourse_enabled=True): - update_discourse_groups.delay(user.pk) + # Iterate through services hooks and have them check the validity of the user + for fn in get_hooks('services_hook'): + svc = fn() + try: + svc.validate_user(user) + except: + logger.exception('Exception running validate_user for services module %s on user %s' % (svc, user)) diff --git a/services/templates/public/menublock.html b/services/templates/public/menublock.html new file mode 100644 index 00000000..a0a4637a --- /dev/null +++ b/services/templates/public/menublock.html @@ -0,0 +1,3 @@ +{% for item in menu_items %} + {{ item }} +{% endfor %} diff --git a/services/templates/public/menuitem.html b/services/templates/public/menuitem.html new file mode 100644 index 00000000..12493751 --- /dev/null +++ b/services/templates/public/menuitem.html @@ -0,0 +1,8 @@ +{% load i18n %} +{% load navactive %} + +
  • + + {% trans item.text %} + +
  • diff --git a/services/templatetags/__init__.py b/services/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/templatetags/menu_items.py b/services/templatetags/menu_items.py new file mode 100644 index 00000000..5928ca6e --- /dev/null +++ b/services/templatetags/menu_items.py @@ -0,0 +1,43 @@ +from __future__ import unicode_literals + +from django import template + +from alliance_auth.hooks import get_hooks + +register = template.Library() + + +def process_menu_items(hooks, request): + _menu_items = list() + items = [fn() for fn in hooks] + items.sort(key=lambda i: i.order) + for item in items: + _menu_items.append(item.render(request)) + return _menu_items + + +@register.inclusion_tag('public/menublock.html', takes_context=True) +def menu_main(context): + request = context['request'] + + return { + 'menu_items': process_menu_items(get_hooks('menu_main_hook'), request), + } + + +@register.inclusion_tag('public/menublock.html', takes_context=True) +def menu_aux(context): + request = context['request'] + + return { + 'menu_items': process_menu_items(get_hooks('menu_aux_hook'), request), + } + + +@register.inclusion_tag('public/menublock.html', takes_context=True) +def menu_util(context): + request = context['request'] + + return { + 'menu_items': process_menu_items(get_hooks('menu_util_hook'), request), + } diff --git a/services/tests.py b/services/tests.py deleted file mode 100644 index a39b155a..00000000 --- a/services/tests.py +++ /dev/null @@ -1 +0,0 @@ -# Create your tests here. diff --git a/services/tests/__init__.py b/services/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/tests/test_signals.py b/services/tests/test_signals.py new file mode 100644 index 00000000..302e9a62 --- /dev/null +++ b/services/tests/test_signals.py @@ -0,0 +1,91 @@ +from __future__ import unicode_literals + +try: + # Py3 + from unittest import mock +except ImportError: + # Py2 + import mock + +from django.test import TestCase +from django.contrib.auth.models import Group + +from alliance_auth.tests.auth_utils import AuthUtils + + +class ServicesSignalsTestCase(TestCase): + def setUp(self): + self.member = AuthUtils.create_member('auth_member') + self.none_user = AuthUtils.create_user('none_user', disconnect_signals=True) + + @mock.patch('services.signals.transaction') + @mock.patch('services.signals.get_hooks') + def test_m2m_changed_user_groups(self, get_hooks, transaction): + """ + Test that update_groups hook function is called on user groups change + """ + svc = mock.Mock() + svc.update_groups.return_value = None + + get_hooks.return_value = [lambda: svc] + + # Overload transaction.on_commit so everything happens synchronously + transaction.on_commit = lambda fn: fn() + + test_group = Group.objects.create(name="Test group") + + # Act, should trigger m2m change + self.member.groups.add(test_group) + self.member.save() + + # Assert + self.assertTrue(get_hooks.called) + args, kwargs = get_hooks.call_args + self.assertEqual('services_hook', args[0]) + + self.assertTrue(svc.update_groups.called) + args, kwargs = svc.update_groups.call_args + self.assertEqual(self.member, args[0]) + + @mock.patch('services.signals.disable_member') + def test_pre_delete_user(self, disable_member): + """ + Test that disable_member is called when a user is deleted + """ + self.none_user.delete() + + self.assertTrue(disable_member.called) + args, kwargs = disable_member.call_args + self.assertEqual(self.none_user, args[0]) + + @mock.patch('services.signals.disable_member') + def test_pre_save_user_inactivation(self, disable_member): + """ + Test a user set inactive has disable_member called + """ + self.member.is_active = False + self.member.save() # Signal Trigger + + self.assertTrue(disable_member.called) + args, kwargs = disable_member.call_args + self.assertEqual(self.member, args[0]) + + @mock.patch('services.signals.set_state') + def test_pre_save_user_activation(self, set_state): + """ + Test a user set inactive has disable_member called + """ + # Arrange, set user inactive first + self.member.is_active = False + self.member.save() # Signal Trigger (but not the one we want) + + set_state.reset_mock() + + # Act + self.member.is_active = True + self.member.save() # Signal Trigger + + # Assert + self.assertTrue(set_state.called) + args, kwargs = set_state.call_args + self.assertEqual(self.member, args[0]) diff --git a/services/tests/test_tasks.py b/services/tests/test_tasks.py new file mode 100644 index 00000000..bb307d0a --- /dev/null +++ b/services/tests/test_tasks.py @@ -0,0 +1,81 @@ +from __future__ import unicode_literals + +try: + # Py3 + from unittest import mock +except ImportError: + # Py2 + import mock + +from django.test import TestCase + +from authentication.states import MEMBER_STATE, BLUE_STATE, NONE_STATE +from alliance_auth.tests.auth_utils import AuthUtils + +from services.tasks import deactivate_services, validate_services + + +class ServicesTasksTestCase(TestCase): + def setUp(self): + self.member = AuthUtils.create_member('auth_member') + self.none_user = AuthUtils.create_user('none_user', disconnect_signals=True) + + @mock.patch('services.tasks.get_hooks') + @mock.patch('services.tasks.deactivate_services') + def test_validate_services_deactivate(self, deactivate_services, get_hooks): + """ + Test validate services will call deactivate on a None state user + """ + + validate_services.delay(user=self.none_user, state=NONE_STATE) + + self.assertTrue(deactivate_services.called) + args, kwargs = deactivate_services.call_args + self.assertEqual(self.none_user, args[0]) # Assert correct user is passed + self.assertFalse(get_hooks.called) + + @mock.patch('services.tasks.get_hooks') + @mock.patch('services.tasks.deactivate_services') + def test_validate_services_valid_member(self, deactivate_services, get_hooks): + """ + Test that validate_services is called for a valid member + """ + svc = mock.Mock() + svc.validate_user.return_value = None + + get_hooks.return_value = [lambda: svc] + + validate_services.delay(user=self.member, state=MEMBER_STATE) + + self.assertTrue(get_hooks.called) + args, kwargs = get_hooks.call_args + self.assertEqual('services_hook', args[0]) + self.assertTrue(svc.validate_user.called) + args, kwargs = svc.validate_user.call_args + self.assertEqual(self.member, args[0]) # Assert correct user is passed to service hook function + self.assertFalse(deactivate_services.called) + + @mock.patch('services.tasks.notify') + @mock.patch('services.tasks.get_hooks') + def test_deactivate_services(self, get_hooks, notify): + """ + Test that hooks delete_user function is called by deactivate_services + """ + svc = mock.Mock() + svc.delete_user.return_value = True + + get_hooks.return_value = [lambda: svc] + + deactivate_services(self.member) + + self.assertTrue(get_hooks.called) + args, kwargs = get_hooks.call_args + self.assertEqual('services_hook', args[0]) + self.assertTrue(svc.delete_user.called) + args, kwargs = svc.delete_user.call_args + self.assertEqual(self.member, args[0]) # Assert correct user is passed to service hook function + self.assertTrue(notify.called) + args, kwargs = notify.call_args + self.assertEqual(self.member, args[0]) # Assert user is passed to the notification system + self.assertEqual("Services Disabled", args[1]) + self.assertEqual("danger", kwargs['level']) diff --git a/services/views.py b/services/views.py index d25fd2b7..7f891530 100755 --- a/services/views.py +++ b/services/views.py @@ -1,56 +1,14 @@ from __future__ import unicode_literals -from django.shortcuts import render, redirect -from django.contrib.auth.decorators import login_required -from django.contrib.auth.decorators import permission_required -from django.contrib.auth.decorators import user_passes_test -from django.contrib.auth.models import Group -from django.conf import settings + from django.contrib import messages -from eveonline.models import EveCharacter -from eveonline.models import EveAllianceInfo -from authentication.models import AuthServicesInfo -from services.managers.openfire_manager import OpenfireManager -from services.managers.phpbb3_manager import Phpbb3Manager -from services.managers.mumble_manager import MumbleManager -from services.managers.ipboard_manager import IPBoardManager -from services.managers.xenforo_manager import XenForoManager -from services.managers.teamspeak3_manager import Teamspeak3Manager -from services.managers.discord_manager import DiscordOAuthManager -from services.managers.discourse_manager import DiscourseManager -from services.managers.ips4_manager import Ips4Manager -from services.managers.smf_manager import smfManager -from services.managers.market_manager import marketManager -from authentication.managers import AuthServicesInfoManager -from eveonline.managers import EveManager -from services.tasks import update_jabber_groups -from services.tasks import update_mumble_groups -from services.tasks import update_forum_groups -from services.tasks import update_ipboard_groups -from services.tasks import update_smf_groups -from services.tasks import update_teamspeak3_groups -from services.tasks import update_discord_groups -from services.tasks import update_discord_nickname -from services.tasks import update_discourse_groups -from services.forms import JabberBroadcastForm -from services.forms import FleetFormatterForm -from services.forms import ServicePasswordForm -from services.forms import TeamspeakJoinForm +from django.contrib.auth.decorators import login_required +from django.shortcuts import render + +from alliance_auth.hooks import get_hooks from authentication.decorators import members_and_blues -from authentication.states import MEMBER_STATE, BLUE_STATE, NONE_STATE - -import base64 -import hmac -import hashlib -try: - from urllib import unquote, urlencode -except ImportError: #py3 - from urllib.parse import unquote, urlencode -try: - from urlparse import parse_qs -except ImportError: #py3 - from urllib.parse import parse_qs - -import datetime +from authentication.models import AuthServicesInfo +from eveonline.models import EveCharacter +from services.forms import FleetFormatterForm import logging @@ -87,57 +45,6 @@ def fleet_formatter_view(request): return render(request, 'registered/fleetformattertool.html', context=context) -@login_required -@permission_required('auth.jabber_broadcast') -def jabber_broadcast_view(request): - logger.debug("jabber_broadcast_view called by user %s" % request.user) - allchoices = [] - if request.user.has_perm('auth.jabber_broadcast_all'): - allchoices.append(('all', 'all')) - for g in Group.objects.all(): - allchoices.append((str(g.name), str(g.name))) - else: - for g in request.user.groups.all(): - allchoices.append((str(g.name), str(g.name))) - if request.method == 'POST': - form = JabberBroadcastForm(request.POST) - form.fields['group'].choices = allchoices - logger.debug("Received POST request containing form, valid: %s" % form.is_valid()) - if form.is_valid(): - user_info = AuthServicesInfo.objects.get(user=request.user) - main_char = EveCharacter.objects.get(character_id=user_info.main_char_id) - logger.debug("Processing jabber broadcast for user %s with main character %s" % (user_info, main_char)) - if user_info.main_char_id != "": - message_to_send = form.cleaned_data[ - 'message'] + "\n##### SENT BY: " + "[" + main_char.corporation_ticker + "]" + \ - main_char.character_name + " TO: " + \ - form.cleaned_data['group'] + " WHEN: " + datetime.datetime.utcnow().strftime( - "%Y-%m-%d %H:%M:%S") + " #####\n##### Replies are NOT monitored #####\n" - group_to_send = form.cleaned_data['group'] - - OpenfireManager.send_broadcast_threaded(group_to_send, message_to_send, ) - - else: - message_to_send = form.cleaned_data[ - 'message'] + "\n##### SENT BY: " + "No character but can send pings?" + " TO: " + \ - form.cleaned_data['group'] + " WHEN: " + datetime.datetime.utcnow().strftime( - "%Y-%m-%d %H:%M:%S") + " #####\n##### Replies are NOT monitored #####\n" - group_to_send = form.cleaned_data['group'] - - OpenfireManager.send_broadcast_threaded(group_to_send, message_to_send, ) - - messages.success(request, 'Sent jabber broadcast to %s' % group_to_send) - logger.info("Sent jabber broadcast on behalf of user %s" % request.user) - else: - form = JabberBroadcastForm() - form.fields['group'].choices = allchoices - logger.debug("Generated broadcast form for user %s containing %s groups" % ( - request.user, len(form.fields['group'].choices))) - - context = {'form': form} - return render(request, 'registered/jabberbroadcast.html', context=context) - - @login_required @members_and_blues() def services_view(request): @@ -150,1060 +57,15 @@ def services_view(request): except EveCharacter.DoesNotExist: messages.warning(request, "There's a problem with your main character. Please select a new one.") - services = [ - 'FORUM', - 'JABBER', - 'MUMBLE', - 'IPBOARD', - 'TEAMSPEAK3', - 'DISCORD', - 'DISCOURSE', - 'IPS4', - 'SMF', - 'MARKET', - 'XENFORO', - ] - - context = { - 'authinfo': auth, - 'char': char, - } - - for s in services: - context['SHOW_' + s] = (getattr(settings, 'ENABLE_AUTH_' + s) and ( - auth.state == MEMBER_STATE or request.user.is_superuser)) or (getattr(settings, 'ENABLE_BLUE_' + s) and ( - auth.state == BLUE_STATE or request.user.is_superuser)) + context = {'service_ctrls': []} + for fn in get_hooks('services_hook'): + # Render hooked services controls + svc = fn() + if svc.show_service_ctrl(request.user, auth.state): + context['service_ctrls'].append(svc.render_services_ctrl(request)) return render(request, 'registered/services.html', context=context) def superuser_test(user): return user.is_superuser - - -@login_required -@members_and_blues() -def activate_forum(request): - logger.debug("activate_forum called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - # Valid now we get the main characters - character = EveManager.get_character_by_id(authinfo.main_char_id) - logger.debug("Adding phpbb user for user %s with main character %s" % (request.user, character)) - result = Phpbb3Manager.add_user(character.character_name, request.user.email, ['REGISTERED'], authinfo.main_char_id) - # if empty we failed - if result[0] != "": - AuthServicesInfoManager.update_user_forum_info(result[0], request.user) - logger.debug("Updated authserviceinfo for user %s with forum credentials. Updating groups." % request.user) - update_forum_groups.delay(request.user.pk) - logger.info("Successfully activated forum for user %s" % request.user) - messages.success(request, 'Activated forum account.') - credentials = { - 'username': result[0], - 'password': result[1], - } - return render(request, 'registered/service_credentials.html', - context={'credentials': credentials, 'service': 'Forum'}) - else: - logger.error("Unsuccessful attempt to activate forum for user %s" % request.user) - messages.error(request, 'An error occurred while processing your forum account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def deactivate_forum(request): - logger.debug("deactivate_forum called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = Phpbb3Manager.disable_user(authinfo.forum_username) - # false we failed - if result: - AuthServicesInfoManager.update_user_forum_info("", request.user) - logger.info("Successfully deactivated forum for user %s" % request.user) - messages.success(request, 'Deactivated forum account.') - else: - logger.error("Unsuccessful attempt to activate forum for user %s" % request.user) - messages.error(request, 'An error occurred while processing your forum account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def reset_forum_password(request): - logger.debug("reset_forum_password called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = Phpbb3Manager.update_user_password(authinfo.forum_username, authinfo.main_char_id) - # false we failed - if result != "": - logger.info("Successfully reset forum password for user %s" % request.user) - messages.success(request, 'Reset forum password.') - credentials = { - 'username': authinfo.forum_username, - 'password': result, - } - return render(request, 'registered/service_credentials.html', - context={'credentials': credentials, 'service': 'Forum'}) - else: - logger.error("Unsuccessful attempt to reset forum password for user %s" % request.user) - messages.error(request, 'An error occurred while processing your forum account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def activate_xenforo_forum(request): - logger.debug("activate_xenforo_forum called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - character = EveManager.get_character_by_id(authinfo.main_char_id) - logger.debug("Adding XenForo user for user %s with main character %s" % (request.user, character)) - result = XenForoManager.add_user(character.character_name, request.user.email) - # Based on XenAPI's response codes - if result['response']['status_code'] == 200: - logger.info("Updated authserviceinfo for user %s with XenForo credentials. Updating groups." % request.user) - AuthServicesInfoManager.update_user_xenforo_info(result['username'], request.user) - messages.success(request, 'Activated XenForo account.') - credentials = { - 'username': result['username'], - 'password': result['password'], - } - return render(request, 'registered/service_credentials.html', - context={'credentials': credentials, 'service': 'XenForo'}) - - else: - logger.error("UnSuccessful attempt to activate xenforo for user %s" % request.user) - messages.error(request, 'An error occurred while processing your XenForo account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def deactivate_xenforo_forum(request): - logger.debug("deactivate_xenforo_forum called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = XenForoManager.disable_user(authinfo.xenforo_username) - if result.status_code == 200: - AuthServicesInfoManager.update_user_xenforo_info("", request.user) - logger.info("Successfully deactivated XenForo for user %s" % request.user) - messages.success(request, 'Deactivated XenForo account.') - else: - messages.error(request, 'An error occurred while processing your XenForo account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def reset_xenforo_password(request): - logger.debug("reset_xenforo_password called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = XenForoManager.reset_password(authinfo.xenforo_username) - # Based on XenAPI's response codes - if result['response']['status_code'] == 200: - logger.info("Successfully reset XenForo password for user %s" % request.user) - messages.success(request, 'Reset XenForo account password.') - credentials = { - 'username': authinfo.xenforo_username, - 'password': result['password'], - } - return render(request, 'registered/service_credentials.html', - context={'credentials': credentials, 'service': 'XenForo'}) - else: - logger.error("Unsuccessful attempt to reset XenForo password for user %s" % request.user) - messages.error(request, 'An error occurred while processing your XenForo account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def set_xenforo_password(request): - logger.debug("set_xenforo_password called by user %s" % request.user) - if request.method == 'POST': - logger.debug("Received POST request with form.") - form = ServicePasswordForm(request.POST) - logger.debug("Form is valid: %s" % form.is_valid()) - if form.is_valid(): - password = form.cleaned_data['password'] - logger.debug("Form contains password of length %s" % len(password)) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = XenForoManager.update_user_password(authinfo.xenforo_username, password) - if result['response']['status_code'] == 200: - logger.info("Successfully reset XenForo password for user %s" % request.user) - messages.success(request, 'Changed XenForo password.') - else: - logger.error("Failed to install custom XenForo password for user %s" % request.user) - messages.error(request, 'An error occurred while processing your XenForo account.') - return redirect('auth_services') - else: - logger.debug("Request is not type POST - providing empty form.") - form = ServicePasswordForm() - - logger.debug("Rendering form for user %s" % request.user) - context = {'form': form, 'service': 'Forum'} - return render(request, 'registered/service_password.html', context=context) - - -@login_required -@members_and_blues() -def activate_ipboard_forum(request): - logger.debug("activate_ipboard_forum called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - # Valid now we get the main characters - character = EveManager.get_character_by_id(authinfo.main_char_id) - logger.debug("Adding ipboard user for user %s with main character %s" % (request.user, character)) - result = IPBoardManager.add_user(character.character_name, request.user.email) - if result[0] != "": - AuthServicesInfoManager.update_user_ipboard_info(result[0], request.user) - logger.debug("Updated authserviceinfo for user %s with ipboard credentials. Updating groups." % request.user) - update_ipboard_groups.delay(request.user.pk) - logger.info("Successfully activated ipboard for user %s" % request.user) - messages.success(request, 'Activated IPBoard account.') - credentials = { - 'username': result[0], - 'password': result[1], - } - return render(request, 'registered/service_credentials.html', - context={'credentials': credentials, 'service': 'IPBoard'}) - else: - logger.error("UnSuccessful attempt to activate ipboard for user %s" % request.user) - messages.error(request, 'An error occurred while processing your IPBoard account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def deactivate_ipboard_forum(request): - logger.debug("deactivate_ipboard_forum called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = IPBoardManager.disable_user(authinfo.ipboard_username) - # false we failed - if result: - AuthServicesInfoManager.update_user_ipboard_info("", request.user) - logger.info("Successfully deactivated ipboard for user %s" % request.user) - messages.success(request, 'Deactivated IPBoard account.') - else: - logger.error("Unsuccessful attempt to deactviate ipboard for user %s" % request.user) - messages.error(request, 'An error occurred while processing your IPBoard account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def reset_ipboard_password(request): - logger.debug("reset_ipboard_password called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = IPBoardManager.update_user_password(authinfo.ipboard_username, request.user.email) - if result != "": - logger.info("Successfully reset ipboard password for user %s" % request.user) - messages.success(request, 'Reset IPBoard password.') - credentials = { - 'username': authinfo.ipboard_username, - 'password': result, - } - return render(request, 'registered/service_credentials.html', - context={'credentials': credentials, 'service': 'IPBoard'}) - else: - logger.error("UnSuccessful attempt to reset ipboard password for user %s" % request.user) - messages.error(request, 'An error occurred while processing your IPBoard account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def activate_jabber(request): - logger.debug("activate_jabber called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - character = EveManager.get_character_by_id(authinfo.main_char_id) - logger.debug("Adding jabber user for user %s with main character %s" % (request.user, character)) - info = OpenfireManager.add_user(character.character_name) - # If our username is blank means we already had a user - if info[0] is not "": - AuthServicesInfoManager.update_user_jabber_info(info[0], request.user) - logger.debug("Updated authserviceinfo for user %s with jabber credentials. Updating groups." % request.user) - update_jabber_groups.delay(request.user.pk) - logger.info("Successfully activated jabber for user %s" % request.user) - messages.success(request, 'Activated jabber account.') - credentials = { - 'username': info[0], - 'password': info[1], - } - return render(request, 'registered/service_credentials.html', - context={'credentials': credentials, 'service': 'Jabber'}) - else: - logger.error("UnSuccessful attempt to activate jabber for user %s" % request.user) - messages.error(request, 'An error occurred while processing your jabber account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def deactivate_jabber(request): - logger.debug("deactivate_jabber called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = OpenfireManager.delete_user(authinfo.jabber_username) - # If our username is blank means we failed - if result: - AuthServicesInfoManager.update_user_jabber_info("", request.user) - logger.info("Successfully deactivated jabber for user %s" % request.user) - messages.success(request, 'Deactivated jabber account.') - else: - logger.error("UnSuccessful attempt to deactivate jabber for user %s" % request.user) - messages.error(request, 'An error occurred while processing your jabber account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def reset_jabber_password(request): - logger.debug("reset_jabber_password called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = OpenfireManager.update_user_pass(authinfo.jabber_username) - # If our username is blank means we failed - if result != "": - AuthServicesInfoManager.update_user_jabber_info(authinfo.jabber_username, request.user) - logger.info("Successfully reset jabber password for user %s" % request.user) - messages.success(request, 'Reset jabber password.') - credentials = { - 'username': authinfo.jabber_username, - 'password': result, - } - return render(request, 'registered/service_credentials.html', - context={'credentials': credentials, 'service': 'Jabber'}) - else: - logger.error("Unsuccessful attempt to reset jabber for user %s" % request.user) - messages.error(request, 'An error occurred while processing your jabber account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def activate_mumble(request): - logger.debug("activate_mumble called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - character = EveManager.get_character_by_id(authinfo.main_char_id) - ticker = character.corporation_ticker - - if authinfo.state == BLUE_STATE: - logger.debug("Adding mumble user for blue user %s with main character %s" % (request.user, character)) - # Blue members should have alliance ticker (if in alliance) - if EveAllianceInfo.objects.filter(alliance_id=character.alliance_id).exists(): - alliance = EveAllianceInfo.objects.filter(alliance_id=character.alliance_id)[0] - ticker = alliance.alliance_ticker - result = MumbleManager.create_blue_user(ticker, character.character_name) - else: - logger.debug("Adding mumble user for user %s with main character %s" % (request.user, character)) - result = MumbleManager.create_user(ticker, character.character_name) - # if its empty we failed - if result[0] is not "": - AuthServicesInfoManager.update_user_mumble_info(result[0], request.user) - logger.debug("Updated authserviceinfo for user %s with mumble credentials. Updating groups." % request.user) - update_mumble_groups.delay(request.user.pk) - logger.info("Successfully activated mumble for user %s" % request.user) - messages.success(request, 'Activated Mumble account.') - credentials = { - 'username': result[0], - 'password': result[1], - } - return render(request, 'registered/service_credentials.html', - context={'credentials': credentials, 'service': 'Mumble'}) - else: - logger.error("Unsuccessful attempt to activate mumble for user %s" % request.user) - messages.error(request, 'An error occurred while processing your Mumble account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def deactivate_mumble(request): - logger.debug("deactivate_mumble called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - # if we successfully remove the user or the user is already removed - if MumbleManager.delete_user(authinfo.mumble_username) or not MumbleManager.user_exists(authinfo.mumble_username): - AuthServicesInfoManager.update_user_mumble_info("", request.user) - logger.info("Successfully deactivated mumble for user %s" % request.user) - messages.success(request, 'Deactivated Mumble account.') - else: - logger.error("Unsuccessful attempt to deactivate mumble for user %s" % request.user) - messages.error(request, 'An error occurred while processing your Mumble account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def reset_mumble_password(request): - logger.debug("reset_mumble_password called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = MumbleManager.update_user_password(authinfo.mumble_username) - - # if blank we failed - if result != "": - logger.info("Successfully reset mumble password for user %s" % request.user) - messages.success(request, 'Reset Mumble password.') - credentials = { - 'username': authinfo.mumble_username, - 'password': result, - } - return render(request, 'registered/service_credentials.html', - context={'credentials': credentials, 'service': 'Mumble'}) - else: - logger.error("UnSuccessful attempt to reset mumble password for user %s" % request.user) - messages.error(request, 'An error occurred while processing your Mumble account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def activate_teamspeak3(request): - logger.debug("activate_teamspeak3 called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - character = EveManager.get_character_by_id(authinfo.main_char_id) - ticker = character.corporation_ticker - - if authinfo.state == BLUE_STATE: - logger.debug("Adding TS3 user for blue user %s with main character %s" % (request.user, character)) - # Blue members should have alliance ticker (if in alliance) - if EveAllianceInfo.objects.filter(alliance_id=character.alliance_id).exists(): - alliance = EveAllianceInfo.objects.filter(alliance_id=character.alliance_id)[0] - ticker = alliance.alliance_ticker - result = Teamspeak3Manager.add_blue_user(character.character_name, ticker) - else: - logger.debug("Adding TS3 user for user %s with main character %s" % (request.user, character)) - result = Teamspeak3Manager.add_user(character.character_name, ticker) - - # if its empty we failed - if result[0] is not "": - AuthServicesInfoManager.update_user_teamspeak3_info(result[0], result[1], request.user) - logger.debug("Updated authserviceinfo for user %s with TS3 credentials. Updating groups." % request.user) - logger.info("Successfully activated TS3 for user %s" % request.user) - messages.success(request, 'Activated TeamSpeak3 account.') - return redirect("auth_verify_teamspeak3") - logger.error("Unsuccessful attempt to activate TS3 for user %s" % request.user) - messages.error(request, 'An error occurred while processing your TeamSpeak3 account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def verify_teamspeak3(request): - logger.debug("verify_teamspeak3 called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - if not authinfo.teamspeak3_uid: - logger.warn("Unable to validate user %s teamspeak: no teamspeak data" % request.user) - return redirect("auth_services") - if request.method == "POST": - form = TeamspeakJoinForm(request.POST) - if form.is_valid(): - update_teamspeak3_groups.delay(request.user.pk) - logger.debug("Validated user %s joined TS server" % request.user) - return redirect("auth_services") - else: - form = TeamspeakJoinForm({'username': authinfo.teamspeak3_uid}) - context = { - 'form': form, - 'authinfo': authinfo, - } - return render(request, 'registered/teamspeakjoin.html', context=context) - - -@login_required -@members_and_blues() -def deactivate_teamspeak3(request): - logger.debug("deactivate_teamspeak3 called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = Teamspeak3Manager.delete_user(authinfo.teamspeak3_uid) - - # if false we failed - if result: - AuthServicesInfoManager.update_user_teamspeak3_info("", "", request.user) - logger.info("Successfully deactivated TS3 for user %s" % request.user) - messages.success(request, 'Deactivated TeamSpeak3 account.') - else: - logger.error("Unsuccessful attempt to deactivate TS3 for user %s" % request.user) - messages.error(request, 'An error occurred while processing your TeamSpeak3 account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def reset_teamspeak3_perm(request): - logger.debug("reset_teamspeak3_perm called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - character = EveManager.get_character_by_id(authinfo.main_char_id) - logger.debug("Deleting TS3 user for user %s" % request.user) - Teamspeak3Manager.delete_user(authinfo.teamspeak3_uid) - - if authinfo.state == BLUE_STATE: - logger.debug( - "Generating new permission key for blue user %s with main character %s" % (request.user, character)) - result = Teamspeak3Manager.generate_new_blue_permissionkey(authinfo.teamspeak3_uid, character.character_name, - character.corporation_ticker) - else: - logger.debug("Generating new permission key for user %s with main character %s" % (request.user, character)) - result = Teamspeak3Manager.generate_new_permissionkey(authinfo.teamspeak3_uid, character.character_name, - character.corporation_ticker) - - # if blank we failed - if result != "": - AuthServicesInfoManager.update_user_teamspeak3_info(result[0], result[1], request.user) - logger.debug("Updated authserviceinfo for user %s with TS3 credentials. Updating groups." % request.user) - update_teamspeak3_groups.delay(request.user.pk) - logger.info("Successfully reset TS3 permission key for user %s" % request.user) - messages.success(request, 'Reset TeamSpeak3 permission key.') - else: - logger.error("Unsuccessful attempt to reset TS3 permission key for user %s" % request.user) - messages.error(request, 'An error occurred while processing your TeamSpeak3 account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def deactivate_discord(request): - logger.debug("deactivate_discord called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = DiscordOAuthManager.delete_user(authinfo.discord_uid) - if result: - AuthServicesInfoManager.update_user_discord_info("", request.user) - logger.info("Successfully deactivated discord for user %s" % request.user) - messages.success(request, 'Deactivated Discord account.') - else: - logger.error("UnSuccessful attempt to deactivate discord for user %s" % request.user) - messages.error(request, 'An error occurred while processing your Discord account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def reset_discord(request): - logger.debug("reset_discord called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = DiscordOAuthManager.delete_user(authinfo.discord_uid) - if result: - AuthServicesInfoManager.update_user_discord_info("", request.user) - logger.info("Successfully deleted discord user for user %s - forwarding to discord activation." % request.user) - return redirect("auth_activate_discord") - logger.error("Unsuccessful attempt to reset discord for user %s" % request.user) - messages.error(request, 'An error occurred while processing your Discord account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def activate_discord(request): - logger.debug("activate_discord called by user %s" % request.user) - return redirect(DiscordOAuthManager.generate_oauth_redirect_url()) - - -@login_required -@members_and_blues() -def discord_callback(request): - logger.debug("Received Discord callback for activation of user %s" % request.user) - code = request.GET.get('code', None) - if not code: - logger.warn("Did not receive OAuth code from callback of user %s" % request.user) - return redirect("auth_services") - user_id = DiscordOAuthManager.add_user(code) - if user_id: - AuthServicesInfoManager.update_user_discord_info(user_id, request.user) - if settings.DISCORD_SYNC_NAMES: - update_discord_nickname.delay(request.user.pk) - update_discord_groups.delay(request.user.pk) - logger.info("Successfully activated Discord for user %s" % request.user) - messages.success(request, 'Activated Discord account.') - else: - logger.error("Failed to activate Discord for user %s" % request.user) - messages.error(request, 'An error occurred while processing your Discord account.') - return redirect("auth_services") - - -@login_required -@user_passes_test(superuser_test) -def discord_add_bot(request): - return redirect(DiscordOAuthManager.generate_bot_add_url()) - - -@login_required -@members_and_blues() -def set_forum_password(request): - logger.debug("set_forum_password called by user %s" % request.user) - if request.method == 'POST': - logger.debug("Received POST request with form.") - form = ServicePasswordForm(request.POST) - logger.debug("Form is valid: %s" % form.is_valid()) - if form.is_valid(): - password = form.cleaned_data['password'] - logger.debug("Form contains password of length %s" % len(password)) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = Phpbb3Manager.update_user_password(authinfo.forum_username, authinfo.main_char_id, - password=password) - if result != "": - logger.info("Successfully set forum password for user %s" % request.user) - messages.success(request, 'Set forum password.') - else: - logger.error("Failed to install custom forum password for user %s" % request.user) - messages.error(request, 'An error occurred while processing your forum account.') - return redirect("auth_services") - else: - logger.debug("Request is not type POST - providing empty form.") - form = ServicePasswordForm() - - logger.debug("Rendering form for user %s" % request.user) - context = {'form': form, 'service': 'Forum'} - return render(request, 'registered/service_password.html', context=context) - - -@login_required -@members_and_blues() -def set_mumble_password(request): - logger.debug("set_mumble_password called by user %s" % request.user) - if request.method == 'POST': - logger.debug("Received POST request with form.") - form = ServicePasswordForm(request.POST) - logger.debug("Form is valid: %s" % form.is_valid()) - if form.is_valid(): - password = form.cleaned_data['password'] - logger.debug("Form contains password of length %s" % len(password)) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = MumbleManager.update_user_password(authinfo.mumble_username, password=password) - if result != "": - logger.info("Successfully reset forum password for user %s" % request.user) - messages.success(request, 'Set Mumble password.') - else: - logger.error("Failed to install custom mumble password for user %s" % request.user) - messages.error(request, 'An error occurred while processing your Mumble account.') - return redirect("auth_services") - else: - logger.debug("Request is not type POST - providing empty form.") - form = ServicePasswordForm() - - logger.debug("Rendering form for user %s" % request.user) - context = {'form': form, 'service': 'Mumble'} - return render(request, 'registered/service_password.html', context=context) - - -@login_required -@members_and_blues() -def set_jabber_password(request): - logger.debug("set_jabber_password called by user %s" % request.user) - if request.method == 'POST': - logger.debug("Received POST request with form.") - form = ServicePasswordForm(request.POST) - logger.debug("Form is valid: %s" % form.is_valid()) - if form.is_valid(): - password = form.cleaned_data['password'] - logger.debug("Form contains password of length %s" % len(password)) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = OpenfireManager.update_user_pass(authinfo.jabber_username, password=password) - if result != "": - logger.info("Successfully set jabber password for user %s" % request.user) - messages.success(request, 'Set jabber password.') - else: - logger.error("Failed to install custom jabber password for user %s" % request.user) - messages.error(request, 'An error occurred while processing your jabber account.') - return redirect("auth_services") - else: - logger.debug("Request is not type POST - providing empty form.") - form = ServicePasswordForm() - - logger.debug("Rendering form for user %s" % request.user) - context = {'form': form, 'service': 'Jabber'} - return render(request, 'registered/service_password.html', context=context) - - -@login_required -@members_and_blues() -def set_ipboard_password(request): - logger.debug("set_ipboard_password called by user %s" % request.user) - error = None - if request.method == 'POST': - logger.debug("Received POST request with form.") - form = ServicePasswordForm(request.POST) - logger.debug("Form is valid: %s" % form.is_valid()) - if form.is_valid(): - password = form.cleaned_data['password'] - logger.debug("Form contains password of length %s" % len(password)) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = IPBoardManager.update_user_password(authinfo.ipboard_username, request.user.email, - plain_password=password) - if result != "": - logger.info("Successfully set IPBoard password for user %s" % request.user) - messages.success(request, 'Set IPBoard password.') - else: - logger.error("Failed to install custom ipboard password for user %s" % request.user) - messages.error(request, 'An error occurred while processing your IPBoard account.') - return redirect("auth_services") - else: - logger.debug("Request is not type POST - providing empty form.") - form = ServicePasswordForm() - - logger.debug("Rendering form for user %s" % request.user) - context = {'form': form, 'service': 'IPBoard', 'error': error} - return render(request, 'registered/service_password.html', context=context) - - -@login_required -@members_and_blues() -def activate_ips4(request): - logger.debug("activate_ips4 called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - # Valid now we get the main characters - character = EveManager.get_character_by_id(authinfo.main_char_id) - logger.debug("Adding IPS4 user for user %s with main character %s" % (request.user, character)) - result = Ips4Manager.add_user(character.character_name, request.user.email) - # if empty we failed - if result[0] != "": - AuthServicesInfoManager.update_user_ips4_info(result[0], result[2], request.user) - logger.debug("Updated authserviceinfo for user %s with IPS4 credentials." % request.user) - # update_ips4_groups.delay(request.user.pk) - logger.info("Successfully activated IPS4 for user %s" % request.user) - messages.success(request, 'Activated IPSuite4 account.') - credentials = { - 'username': result[0], - 'password': result[1], - } - return render(request, 'registered/service_credentials.html', - context={'credentials': credentials, 'service': 'IPSuite4'}) - else: - logger.error("UnSuccessful attempt to activate IPS4 for user %s" % request.user) - messages.error(request, 'An error occurred while processing your IPSuite4 account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def reset_ips4_password(request): - logger.debug("reset_ips4_password called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = Ips4Manager.update_user_password(authinfo.ips4_username) - # false we failed - if result != "": - logger.info("Successfully reset IPS4 password for user %s" % request.user) - messages.success(request, 'Reset IPSuite4 password.') - credentials = { - 'username': authinfo.ips4_username, - 'password': result, - } - return render(request, 'registered/service_credentials.html', - context={'credentials': credentials, 'service': 'IPSuite4'}) - else: - logger.error("Unsuccessful attempt to reset IPS4 password for user %s" % request.user) - messages.error(request, 'An error occurred while processing your IPSuite4 account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def set_ips4_password(request): - logger.debug("set_ips4_password called by user %s" % request.user) - if request.method == 'POST': - logger.debug("Received POST request with form.") - form = ServicePasswordForm(request.POST) - logger.debug("Form is valid: %s" % form.is_valid()) - if form.is_valid(): - password = form.cleaned_data['password'] - logger.debug("Form contains password of length %s" % len(password)) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = Ips4Manager.update_custom_password(authinfo.ips4_username, plain_password=password) - if result != "": - logger.info("Successfully set IPS4 password for user %s" % request.user) - messages.success(request, 'Set IPSuite4 password.') - else: - logger.error("Failed to install custom IPS4 password for user %s" % request.user) - messages.error(request, 'An error occurred while processing your IPSuite4 account.') - return redirect('auth_services') - else: - logger.debug("Request is not type POST - providing empty form.") - form = ServicePasswordForm() - - logger.debug("Rendering form for user %s" % request.user) - context = {'form': form, 'service': 'IPS4'} - return render(request, 'registered/service_password.html', context=context) - - -@login_required -@members_and_blues() -def deactivate_ips4(request): - logger.debug("deactivate_ips4 called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = Ips4Manager.delete_user(authinfo.ips4_id) - if result != "": - AuthServicesInfoManager.update_user_ips4_info("", "", request.user) - logger.info("Successfully deactivated IPS4 for user %s" % request.user) - messages.success(request, 'Deactivated IPSuite4 account.') - else: - logger.error("UnSuccessful attempt to deactivate IPS4 for user %s" % request.user) - messages.error(request, 'An error occurred while processing your IPSuite4 account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def activate_smf(request): - logger.debug("activate_smf called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - # Valid now we get the main characters - character = EveManager.get_character_by_id(authinfo.main_char_id) - logger.debug("Adding smf user for user %s with main character %s" % (request.user, character)) - result = smfManager.add_user(character.character_name, request.user.email, ['Member'], authinfo.main_char_id) - # if empty we failed - if result[0] != "": - AuthServicesInfoManager.update_user_smf_info(result[0], request.user) - logger.debug("Updated authserviceinfo for user %s with smf credentials. Updating groups." % request.user) - update_smf_groups.delay(request.user.pk) - logger.info("Successfully activated smf for user %s" % request.user) - messages.success(request, 'Activated SMF account.') - credentials = { - 'username': result[0], - 'password': result[1], - } - return render(request, 'registered/service_credentials.html', - context={'credentials': credentials, 'service': 'SMF'}) - else: - logger.error("UnSuccessful attempt to activate smf for user %s" % request.user) - messages.error(request, 'An error occurred while processing your SMF account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def deactivate_smf(request): - logger.debug("deactivate_smf called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = smfManager.disable_user(authinfo.smf_username) - # false we failed - if result: - AuthServicesInfoManager.update_user_smf_info("", request.user) - logger.info("Successfully deactivated smf for user %s" % request.user) - messages.success(request, 'Deactivated SMF account.') - else: - logger.error("UnSuccessful attempt to activate smf for user %s" % request.user) - messages.error(request, 'An error occurred while processing your SMF account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def reset_smf_password(request): - logger.debug("reset_smf_password called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = smfManager.update_user_password(authinfo.smf_username, authinfo.main_char_id) - # false we failed - if result != "": - logger.info("Successfully reset smf password for user %s" % request.user) - messages.success(request, 'Reset SMF password.') - credentials = { - 'username': authinfo.smf_username, - 'password': result, - } - return render(request, 'registered/service_credentials.html', - context={'credentials': credentials, 'service': 'SMF'}) - else: - logger.error("Unsuccessful attempt to reset smf password for user %s" % request.user) - messages.error(request, 'An error occurred while processing your SMF account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def set_smf_password(request): - logger.debug("set_smf_password called by user %s" % request.user) - if request.method == 'POST': - logger.debug("Received POST request with form.") - form = ServicePasswordForm(request.POST) - logger.debug("Form is valid: %s" % form.is_valid()) - if form.is_valid(): - password = form.cleaned_data['password'] - logger.debug("Form contains password of length %s" % len(password)) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = smfManager.update_user_password(authinfo.smf_username, authinfo.main_char_id, password=password) - if result != "": - logger.info("Successfully set smf password for user %s" % request.user) - messages.success(request, 'Set SMF password.') - else: - logger.error("Failed to install custom smf password for user %s" % request.user) - messages.error(request, 'An error occurred while processing your SMF account.') - return redirect("auth_services") - else: - logger.debug("Request is not type POST - providing empty form.") - form = ServicePasswordForm() - - logger.debug("Rendering form for user %s" % request.user) - context = {'form': form, 'service': 'SMF'} - return render(request, 'registered/service_password.html', context=context) - - -@login_required -@members_and_blues() -def activate_market(request): - logger.debug("activate_market called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - # Valid now we get the main characters - character = EveManager.get_character_by_id(authinfo.main_char_id) - logger.debug("Adding market user for user %s with main character %s" % (request.user, character)) - result = marketManager.add_user(character.character_name, request.user.email, authinfo.main_char_id, - character.character_name) - # if empty we failed - if result[0] != "": - AuthServicesInfoManager.update_user_market_info(result[0], request.user) - logger.debug("Updated authserviceinfo for user %s with market credentials." % request.user) - logger.info("Successfully activated market for user %s" % request.user) - messages.success(request, 'Activated Alliance Market account.') - credentials = { - 'username': result[0], - 'password': result[1], - } - return render(request, 'registered/service_credentials.html', - context={'credentials': credentials, 'service': 'Alliance Market'}) - else: - logger.error("UnSuccessful attempt to activate market for user %s" % request.user) - messages.error(request, 'An error occurred while processing your Alliance Market account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def deactivate_market(request): - logger.debug("deactivate_market called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = marketManager.disable_user(authinfo.market_username) - # false we failed - if result: - AuthServicesInfoManager.update_user_market_info("", request.user) - logger.info("Successfully deactivated market for user %s" % request.user) - messages.success(request, 'Deactivated Alliance Market account.') - else: - logger.error("UnSuccessful attempt to activate market for user %s" % request.user) - messages.error(request, 'An error occurred while processing your Alliance Market account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def reset_market_password(request): - logger.debug("reset_market_password called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = marketManager.update_user_password(authinfo.market_username) - # false we failed - if result != "": - logger.info("Successfully reset market password for user %s" % request.user) - messages.success(request, 'Reset Alliance Market password.') - credentials = { - 'username': authinfo.market_username, - 'password': result, - } - return render(request, 'registered/service_credentials.html', - context={'credentials': credentials, 'service': 'Alliance Market'}) - else: - logger.error("Unsuccessful attempt to reset market password for user %s" % request.user) - messages.error(request, 'An error occurred while processing your Alliance Market account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def set_market_password(request): - logger.debug("set_market_password called by user %s" % request.user) - if request.method == 'POST': - logger.debug("Received POST request with form.") - form = ServicePasswordForm(request.POST) - logger.debug("Form is valid: %s" % form.is_valid()) - if form.is_valid(): - password = form.cleaned_data['password'] - logger.debug("Form contains password of length %s" % len(password)) - authinfo = AuthServicesInfo.objects.get(user=request.user) - result = marketManager.update_custom_password(authinfo.market_username, password) - if result != "": - logger.info("Successfully reset market password for user %s" % request.user) - messages.success(request, 'Set Alliance Market password.') - else: - logger.error("Failed to install custom market password for user %s" % request.user) - messages.error(request, 'An error occurred while processing your Alliance Market account.') - return redirect("auth_services") - else: - logger.debug("Request is not type POST - providing empty form.") - form = ServicePasswordForm() - - logger.debug("Rendering form for user %s" % request.user) - context = {'form': form, 'service': 'Market'} - return render(request, 'registered/service_password.html', context=context) - - -@login_required -def discourse_sso(request): - - ## Check if user has access - - auth = AuthServicesInfo.objects.get(user=request.user) - if not request.user.is_superuser: - if not settings.ENABLE_AUTH_DISCOURSE and auth.state == MEMBER_STATE: - messages.error(request, 'Members are not authorized to access Discourse.') - return redirect('auth_dashboard') - elif not settings.ENABLE_BLUE_DISCOURSE and auth.state == BLUE_STATE: - messages.error(request, 'Blues are not authorized to access Discourse.') - return redirect('auth_dashboard') - elif auth.state == NONE_STATE: - messages.error(request, 'You are not authorized to access Discourse.') - return redirect('auth_dashboard') - - if not auth.main_char_id: - messages.error(request, "You must have a main character set to access Discourse.") - return redirect('auth_characters') - try: - main_char = EveCharacter.objects.get(character_id=auth.main_char_id) - except EveCharacter.DoesNotExist: - messages.error(request, "Your main character is missing a database model. Please select a new one.") - return redirect('auth_characters') - - payload = request.GET.get('sso') - signature = request.GET.get('sig') - - if None in [payload, signature]: - messages.error(request, 'No SSO payload or signature. Please contact support if this problem persists.') - return redirect('auth_dashboard') - - ## Validate the payload - - try: - payload = unquote(payload).encode('utf-8') - decoded = base64.decodestring(payload).decode('utf-8') - assert 'nonce' in decoded - assert len(payload) > 0 - except AssertionError: - messages.error(request, 'Invalid payload. Please contact support if this problem persists.') - return redirect('auth_dashboard') - - key = str(settings.DISCOURSE_SSO_SECRET).encode('utf-8') - h = hmac.new(key, payload, digestmod=hashlib.sha256) - this_signature = h.hexdigest() - - if this_signature != signature: - messages.error(request, 'Invalid payload. Please contact support if this problem persists.') - return redirect('auth_dashboard') - - ## Build the return payload - - username = DiscourseManager._sanitize_username(main_char.character_name) - - qs = parse_qs(decoded) - params = { - 'nonce': qs['nonce'][0], - 'email': request.user.email, - 'external_id': request.user.pk, - 'username': username, - 'name': username, - } - - if auth.main_char_id: - params['avatar_url'] = 'https://image.eveonline.com/Character/%s_256.jpg' % auth.main_char_id - - return_payload = base64.encodestring(urlencode(params).encode('utf-8')) - h = hmac.new(key, return_payload, digestmod=hashlib.sha256) - query_string = urlencode({'sso': return_payload, 'sig': h.hexdigest()}) - - ## Record activation and queue group sync - - if not auth.discourse_enabled: - auth.discourse_enabled = True - auth.save() - update_discourse_groups.apply_async(args=[request.user.pk], countdown=30) # wait 30s for new user creation on Discourse - - ## Redirect back to Discourse - - url = '%s/session/sso_login' % settings.DISCOURSE_URL - return redirect('%s?%s' % (url, query_string)) diff --git a/stock/templates/public/base.html b/stock/templates/public/base.html index 50bf7fb9..6b8db608 100755 --- a/stock/templates/public/base.html +++ b/stock/templates/public/base.html @@ -1,6 +1,7 @@ {% load staticfiles %} {% load i18n %} {% load navactive %} +{% load menu_items %} @@ -87,7 +88,7 @@ {% else %}
  • {% trans "Login" %}
  • {% endif %} - + @@ -119,7 +120,7 @@ {% trans " Help" %} - + {% menu_main %}
  • {% trans "Aux Navigation" %}
    @@ -194,7 +195,7 @@
  • {% endif %} - + {% menu_aux %}
  • {% trans "Util" %}
  • @@ -211,14 +212,7 @@ {% endif %} - - {% if perms.auth.jabber_broadcast %} -
  • - - {% trans " Jabber Broadcast" %} - -
  • - {% endif %} + {% menu_util %} diff --git a/stock/templates/registered/services.html b/stock/templates/registered/services.html index e3e3423c..11c358b3 100755 --- a/stock/templates/registered/services.html +++ b/stock/templates/registered/services.html @@ -10,14 +10,6 @@ {% block content %}

    {% trans "Available Services" %}

    - {% if SHOW_DISCORD %} - {% if request.user.is_superuser %} - -
    - {% endif %} - {% endif %} @@ -25,263 +17,9 @@ - {% if SHOW_FORUM %} - - - - - - - {% endif %} - {% if SHOW_SMF %} - - - - - - - {% endif %} - {% if SHOW_IPBOARD %} - - - - - - - {% endif %} - {% if SHOW_XENFORO %} - - - - - - - {% endif %} - {% if SHOW_MARKET %} - - - - - - - {% endif %} - {% if SHOW_JABBER %} - - - - - - - {% endif %} - {% if SHOW_MUMBLE %} - - - - - - - {% endif %} - {% if SHOW_IPS4 %} - - - - - {% endif %} - {% if SHOW_DISCORD %} - - - - - - - {% endif %} - {% if SHOW_DISCOURSE %} - - - - - {% endif %} - {% if SHOW_TEAMSPEAK3 %} - - - - - - - - - - - - - - - {% endif %} + {% for svc in service_ctrls %} + {{ svc }} + {% endfor %}
    {% trans "Service" %}{% trans "Domain" %} {% trans "Action" %}
    Forums{{ authinfo.forum_username }}{{ FORUM_URL }} - {% ifequal authinfo.forum_username "" %} - - - - {% else %} - - - - - - - - - - {% endifequal %} -
    SMF Forums{{ authinfo.smf_username }}{{ SMF_URL }} - {% ifequal authinfo.smf_username "" %} - - - - {% else %} - - - - - - - - - - {% endifequal %} -
    IPBoard Forums{{ authinfo.ipboard_username }}{{ FORUM_URL }} - {% ifequal authinfo.ipboard_username "" %} - - - - {% else %} - - - - - - - - - - {% endifequal %} -
    XenForo Forums{{ authinfo.xenforo_username }}{{ FORUM_URL }} - {% ifequal authinfo.xenforo_username "" %} - - - - {% else %} - - - - - - - - - - {% endifequal %} -
    Alliance Market{{ authinfo.market_username }}{{ MARKET_URL }} - {% ifequal authinfo.market_username "" %} - - - - {% else %} - - - - - - - - - - {% endifequal %} -
    Jabber{{ authinfo.jabber_username }}{{ JABBER_URL }} - {% ifequal authinfo.jabber_username "" %} - - - - {% else %} - - - - - - - - - - {% endifequal %} -
    Mumble{{ authinfo.mumble_username }}{{ MUMBLE_URL }} - {% ifequal authinfo.mumble_username "" %} - - - - {% else %} - - - - - - - - - - {% endifequal %} -
    IPS4{{ authinfo.ips4_username }}{{ IPS4_URL }} - {% ifequal authinfo.ips4_username "" %} - - - - {% else %} - - - - - - - - - - {% endifequal %} -
    Discordhttps://discordapp.com - {% ifequal authinfo.discord_uid "" %} - - - - {% else %} - - - - - - - {% endifequal %} -
    Discourse{{ char.character_name }}{{ DISCOURSE_URL }} - -
    {% trans "Service" %}{% trans "Unique ID" %}PermissionKey{% trans "Quick Link" %}{% trans "Action" %}
    Teamspeak 3{{ authinfo.teamspeak3_uid }}{{ authinfo.teamspeak3_perm_key }} - {% ifequal authinfo.teamspeak3_uid "" %} - {% else %} - Teamspeak3 - Link - {% endifequal %} - - {% ifequal authinfo.teamspeak3_uid "" %} - - - - {% else %} - - - - - - - - - {% endifequal %} -
    diff --git a/stock/templates/registered/services_ctrl.html b/stock/templates/registered/services_ctrl.html new file mode 100644 index 00000000..272cf101 --- /dev/null +++ b/stock/templates/registered/services_ctrl.html @@ -0,0 +1,32 @@ +{% load i18n %} + + + {{ service_name }} + {{ username }} + {{ service_url }} + + {% ifequal username "" %} + {% if urls.auth_activate %} + + + + {% endif %} + {% else %} + {% if urls.auth_set_password %} + + + + {% endif %} + {% if urls.auth_reset_password %} + + + + {% endif %} + {% if urls.auth_deactivate %} + + + + {% endif %} + {% endifequal %} + + diff --git a/testing-requirements.txt b/testing-requirements.txt new file mode 100644 index 00000000..2593378d --- /dev/null +++ b/testing-requirements.txt @@ -0,0 +1,9 @@ +# Requirements for running unit tests on this project +# If you aren't intending to run unit tests you can ignore this file +# These packages should be installed in addition to requirements.txt + +mock==2.0.0; python_version < '3.0' +nose>=1.3.7 +django-nose>=1.4.4 +coverage>=4.3.1 +coveralls>=1.1 diff --git a/thirdparty/Mumble/authenticator.py b/thirdparty/Mumble/authenticator.py index 18363500..58275a73 100644 --- a/thirdparty/Mumble/authenticator.py +++ b/thirdparty/Mumble/authenticator.py @@ -521,7 +521,7 @@ def do_main_program(): return (FALL_THROUGH, None, None) try: - sql = 'SELECT id, pwhash, groups FROM %sservices_mumbleuser WHERE username = %%s' % cfg.database.prefix + sql = 'SELECT id, pwhash, groups FROM %smumble_mumbleuser WHERE username = %%s' % cfg.database.prefix cur = threadDB.execute(sql, [name]) except threadDbException: return (FALL_THROUGH, None, None) @@ -571,7 +571,7 @@ def do_main_program(): return FALL_THROUGH try: - sql = 'SELECT id FROM %sservices_mumbleuser WHERE username = %%s' % cfg.database.prefix + sql = 'SELECT id FROM %smumble_mumbleuser WHERE username = %%s' % cfg.database.prefix cur = threadDB.execute(sql, [name]) except threadDbException: return FALL_THROUGH @@ -600,7 +600,7 @@ def do_main_program(): # Fetch the user from the database try: - sql = 'SELECT username FROM %sservices_mumbleuser WHERE id = %%s' % cfg.database.prefix + sql = 'SELECT username FROM %smumble_mumbleuser WHERE id = %%s' % cfg.database.prefix cur = threadDB.execute(sql, [bbid]) except threadDbException: return FALL_THROUGH @@ -666,7 +666,7 @@ def do_main_program(): filter = '%' try: - sql = 'SELECT id, username FROM %sservices_mumbleuser WHERE username LIKE %%s' % cfg.database.prefix + sql = 'SELECT id, username FROM %smumble_mumbleuser WHERE username LIKE %%s' % cfg.database.prefix cur = threadDB.execute(sql, [filter]) except threadDbException: return {} diff --git a/thirdparty/Mumble/requirements.txt b/thirdparty/Mumble/requirements.txt new file mode 100644 index 00000000..b440a025 --- /dev/null +++ b/thirdparty/Mumble/requirements.txt @@ -0,0 +1,3 @@ +bcrypt +passlib +zeroc-ice