diff --git a/docs/changelog.rst b/docs/changelog.rst index 7e6be33..9dc3203 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,9 @@ Changelog ========= +* :feature:`-` add REST API to retrieve and set user information as admin +* :feature:`-` add support model for offline account reset codes in new help + app * :support:`-` remove unused PowerDNS support tables from domains app * :feature:`-` add impersonation support for superusers * :support:`-` remove django-braces dependency diff --git a/gnuviechadmin/domains/tests/test_models.py b/gnuviechadmin/domains/tests/test_models.py index fd6d645..ef3b0d8 100644 --- a/gnuviechadmin/domains/tests/test_models.py +++ b/gnuviechadmin/domains/tests/test_models.py @@ -7,19 +7,9 @@ from unittest.mock import patch from django.test import TestCase from django.contrib.auth import get_user_model -from domains.models import ( - DNSComment, - DNSCryptoKey, - DNSDomain, - DNSDomainMetadata, - DNSRecord, - DNSSupermaster, - DNSTSIGKey, - HostingDomain, - MailDomain, -) -from hostingpackages.models import CustomerHostingPackage, HostingPackageTemplate +from domains.models import HostingDomain, MailDomain +from hostingpackages.models import CustomerHostingPackage, HostingPackageTemplate User = get_user_model() @@ -77,49 +67,3 @@ class HostingDomainTest(TestCase): def test___str__(self): hostingdomain = HostingDomain(domain="test") self.assertEqual(str(hostingdomain), "test") - - -class DNSDomainTest(TestCase): - def test___str__(self): - dnsdomain = DNSDomain(domain="test") - self.assertEqual(str(dnsdomain), "test") - - -class DNSRecordTest(TestCase): - def test___str__(self): - dnsrecord = DNSRecord(name="localhost", recordtype="A", content="127.0.0.1") - self.assertEqual(str(dnsrecord), "localhost IN A 127.0.0.1") - - -class DNSSupermasterTest(TestCase): - def test___str__(self): - dnssupermaster = DNSSupermaster(ip="127.0.0.1", nameserver="dns.example.org") - self.assertEqual(str(dnssupermaster), "127.0.0.1 dns.example.org") - - -class DNSCommentTest(TestCase): - def test___str__(self): - dnscomment = DNSComment(name="localhost", commenttype="A", comment="good stuff") - self.assertEqual(str(dnscomment), "localhost IN A: good stuff") - - -class DNSDomainMetadataTest(TestCase): - def test___str__(self): - dnsdomain = DNSDomain(domain="test") - dnsdomainmetadata = DNSDomainMetadata( - domain=dnsdomain, kind="SOA-EDIT", content="INCEPTION" - ) - self.assertEqual(str(dnsdomainmetadata), "test SOA-EDIT INCEPTION") - - -class DNSCryptoKeyTest(TestCase): - def test___str__(self): - dnsdomain = DNSDomain(domain="test") - dnscryptokey = DNSCryptoKey(domain=dnsdomain, content="testvalue") - self.assertEqual(str(dnscryptokey), "test testvalue") - - -class DNSTSIGKeyTest(TestCase): - def test___str__(self): - dnstsigkey = DNSTSIGKey(name="testkey", algorithm="hmac-md5", secret="dummykey") - self.assertEqual(str(dnstsigkey), "testkey hmac-md5 XXXX") diff --git a/gnuviechadmin/gnuviechadmin/settings.py b/gnuviechadmin/gnuviechadmin/settings.py index 243b6e8..6d81694 100644 --- a/gnuviechadmin/gnuviechadmin/settings.py +++ b/gnuviechadmin/gnuviechadmin/settings.py @@ -207,6 +207,8 @@ DJANGO_APPS = ( "django.contrib.flatpages", "crispy_forms", "impersonate", + "rest_framework", + "rest_framework.authtoken", ) ALLAUTH_APPS = ( @@ -232,6 +234,7 @@ LOCAL_APPS = ( "userdbs", "hostingpackages", "websites", + "help", "contact_form", ) @@ -263,6 +266,17 @@ CRISPY_TEMPLATE_PACK = "bootstrap3" # ######### END CRISPY_FORMS CONFIGURATION +# ######### REST FRAMEWORK CONFIGURATION +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.BasicAuthentication", + "rest_framework.authentication.SessionAuthentication", + "rest_framework.authentication.TokenAuthentication", + ] +} +# ######### END REST FRAMEWORK CONFIGURATION + + # ######### LOGGING CONFIGURATION # See: https://docs.djangoproject.com/en/dev/ref/settings/#logging # A sample logging configuration. The only tangible logging @@ -276,7 +290,7 @@ LOGGING = { "formatters": { "verbose": { "format": "%(levelname)s %(asctime)s %(name)s " - "%(module)s:%(lineno)d %(process)d %(thread)d %(message)s" + "%(module)s:%(lineno)d %(process)d %(thread)d %(message)s" }, "simple": {"format": "%(levelname)s %(name)s:%(lineno)d %(message)s"}, }, @@ -404,23 +418,8 @@ if GVA_ENVIRONMENT == "local": dict( [ (key, {"handlers": ["console"], "level": "DEBUG", "propagate": True}) - for key in [ - "dashboard", - "domains", - "fileservertasks", - "gvacommon", - "gvawebcore", - "hostingpackages", - "ldaptasks", - "managemails", - "mysqltasks", - "osusers", - "pgsqltasks", - "taskresults", - "userdbs", - "websites", - ] - ] + for key in LOCAL_APPS + ], ) ) elif GVA_ENVIRONMENT == "test": @@ -439,22 +438,7 @@ elif GVA_ENVIRONMENT == "test": dict( [ (key, {"handlers": ["console"], "level": "ERROR", "propagate": True}) - for key in [ - "dashboard", - "domains", - "fileservertasks", - "gvacommon", - "gvawebcore", - "hostingpackages", - "ldaptasks", - "managemails", - "mysqltasks", - "osusers", - "pgsqltasks", - "taskresults", - "userdbs", - "websites", - ] + for key in LOCAL_APPS ] ) ) diff --git a/gnuviechadmin/gnuviechadmin/urls.py b/gnuviechadmin/gnuviechadmin/urls.py index 8bcfca5..34a7d7c 100644 --- a/gnuviechadmin/gnuviechadmin/urls.py +++ b/gnuviechadmin/gnuviechadmin/urls.py @@ -6,11 +6,20 @@ from django.contrib import admin from django.contrib.flatpages import views from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.urls import path, re_path +from rest_framework import routers + +from help import views as help_views admin.autodiscover() +router = routers.DefaultRouter() +router.register(r"users", help_views.UserViewSet) +router.register(r"help-users", help_views.HelpUserViewSet) + urlpatterns = [ re_path(r"", include("dashboard.urls")), + path("api/", include(router.urls)), + path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), re_path(r"^admin/", admin.site.urls), re_path(r"^impersonate/", include("impersonate.urls")), re_path(r"^accounts/", include("allauth.urls")), diff --git a/gnuviechadmin/help/__init__.py b/gnuviechadmin/help/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnuviechadmin/help/admin.py b/gnuviechadmin/help/admin.py new file mode 100644 index 0000000..b143c6c --- /dev/null +++ b/gnuviechadmin/help/admin.py @@ -0,0 +1,22 @@ +from django.contrib import admin +from django.contrib.auth import get_user_model +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.utils.translation import ugettext_lazy as _ + +from help.models import HelpUser + +User = get_user_model() + + +class HelpUserInline(admin.StackedInline): + model = HelpUser + can_delete = False + readonly_fields = ("offline_account_code",) + + +class UserAdmin(BaseUserAdmin): + inlines = (HelpUserInline,) + + +admin.site.unregister(User) +admin.site.register(User, UserAdmin) diff --git a/gnuviechadmin/help/apps.py b/gnuviechadmin/help/apps.py new file mode 100644 index 0000000..870ccf9 --- /dev/null +++ b/gnuviechadmin/help/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class HelpConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "help" + verbose_name = _("User self help") diff --git a/gnuviechadmin/help/management/__init__.py b/gnuviechadmin/help/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnuviechadmin/help/management/commands/__init__.py b/gnuviechadmin/help/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnuviechadmin/help/management/commands/populate.py b/gnuviechadmin/help/management/commands/populate.py new file mode 100644 index 0000000..77ee7c3 --- /dev/null +++ b/gnuviechadmin/help/management/commands/populate.py @@ -0,0 +1,17 @@ +from django.contrib.auth import get_user_model +from django.core.management import BaseCommand + +from help.models import HelpUser + +User = get_user_model() + + +class Command(BaseCommand): + help = "Populate help user information for existing users" + + def handle(self, *args, **options): + for user in User.objects.filter(helpuser=None): + help_user = HelpUser.objects.create(user_id=user.id, email_address=user.email) + help_user.generate_offline_account_code() + help_user.save() + self.stdout.write(f"created offline account code {help_user.offline_account_code} for {user}.") diff --git a/gnuviechadmin/help/management/commands/reset_offline_code.py b/gnuviechadmin/help/management/commands/reset_offline_code.py new file mode 100644 index 0000000..a93579d --- /dev/null +++ b/gnuviechadmin/help/management/commands/reset_offline_code.py @@ -0,0 +1,29 @@ +from django.contrib.auth import get_user_model +from django.core.management import BaseCommand, CommandError + +from help.models import HelpUser + +User = get_user_model() + + +class Command(BaseCommand): + help = "Reset offline account reset code for existing users" + + def add_arguments(self, parser): + parser.add_argument("users", nargs='+', type=str) + + def handle(self, *args, **options): + for name in options["users"]: + try: + user = User.objects.get(username=name) + except User.DoesNotExist: + raise CommandError(f'User {name} does not exist') + + help_user = user.helpuser + if help_user is None: + help_user = HelpUser.objects.create(email_address=user.email) + + help_user.generate_offline_account_code() + help_user.save() + + self.stdout.write(f"generated new offline account reset code {help_user.offline_account_code} for {name}") diff --git a/gnuviechadmin/help/migrations/0001_add_help_user_model_extension.py b/gnuviechadmin/help/migrations/0001_add_help_user_model_extension.py new file mode 100644 index 0000000..fb20210 --- /dev/null +++ b/gnuviechadmin/help/migrations/0001_add_help_user_model_extension.py @@ -0,0 +1,55 @@ +# Generated by Django 3.2.18 on 2023-04-16 09:32 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="HelpUser", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "email_address", + models.EmailField( + help_text="Contact email address", max_length=254 + ), + ), + ( + "postal_address", + models.TextField(blank=True, help_text="Contact postal address"), + ), + ( + "offline_account_code", + models.CharField( + default="", + help_text="Offline account reset code", + max_length=36, + ), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/gnuviechadmin/help/migrations/__init__.py b/gnuviechadmin/help/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gnuviechadmin/help/models.py b/gnuviechadmin/help/models.py new file mode 100644 index 0000000..5debb7e --- /dev/null +++ b/gnuviechadmin/help/models.py @@ -0,0 +1,17 @@ +import uuid + +from django.conf import settings +from django.db import models +from django.utils.translation import ugettext_lazy as _ + + +class HelpUser(models.Model): + user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + email_address = models.EmailField(help_text=_("Contact email address")) + postal_address = models.TextField(help_text=_("Contact postal address"), blank=True) + offline_account_code = models.CharField( + help_text=_("Offline account reset code"), max_length=36, default="" + ) + + def generate_offline_account_code(self): + self.offline_account_code = str(uuid.uuid4()) diff --git a/gnuviechadmin/help/serializers.py b/gnuviechadmin/help/serializers.py new file mode 100644 index 0000000..2052926 --- /dev/null +++ b/gnuviechadmin/help/serializers.py @@ -0,0 +1,30 @@ +""" +Serializers for the REST API +""" + +from django.contrib.auth import get_user_model +from rest_framework import serializers + +from help.models import HelpUser + +User = get_user_model() + + +class UserSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = User + fields = ["url", "username", "helpuser"] + read_only_fields = ["username", "helpuser"] + + +class HelpUserSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = HelpUser + fields = [ + "url", + "user", + "email_address", + "postal_address", + "offline_account_code", + ] + read_only_fields = ["user", "offline_account_code"] diff --git a/gnuviechadmin/help/tests.py b/gnuviechadmin/help/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/gnuviechadmin/help/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/gnuviechadmin/help/views.py b/gnuviechadmin/help/views.py new file mode 100644 index 0000000..cdfcff6 --- /dev/null +++ b/gnuviechadmin/help/views.py @@ -0,0 +1,29 @@ +from django.contrib.auth import get_user_model +from rest_framework import permissions, viewsets + +from help.models import HelpUser +from help.serializers import HelpUserSerializer, UserSerializer + +User = get_user_model() + + +class UserViewSet(viewsets.ReadOnlyModelViewSet): + """ + API endpoint that allows users to be viewed or edited. + + """ + + queryset = User.objects.all().order_by("-username") + serializer_class = UserSerializer + permission_classes = [permissions.IsAdminUser] + + +class HelpUserViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows user help profile to be viewed or edited. + + """ + + queryset = HelpUser.objects.all().order_by("-user__username") + serializer_class = HelpUserSerializer + permission_classes = [permissions.IsAdminUser] diff --git a/poetry.lock b/poetry.lock index 9825743..69ee4a3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -666,6 +666,21 @@ files = [ django = ">=3.2.4" sqlparse = ">=0.2" +[[package]] +name = "django-filter" +version = "23.1" +description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "django-filter-23.1.tar.gz", hash = "sha256:dee5dcf2cea4d7f767e271b6d01f767fce7500676d5e5dc58dac8154000b87df"}, + {file = "django_filter-23.1-py3-none-any.whl", hash = "sha256:e3c52ad83c32fb5882125105efb5fea2a1d6a85e7dc64b04ef52edbf14451b6c"}, +] + +[package.dependencies] +Django = ">=3.2" + [[package]] name = "django-impersonate" version = "1.9.1" @@ -692,6 +707,22 @@ files = [ [package.dependencies] Django = ">=3.2" +[[package]] +name = "djangorestframework" +version = "3.14.0" +description = "Web APIs for Django, made easy." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"}, + {file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"}, +] + +[package.dependencies] +django = ">=3.0" +pytz = "*" + [[package]] name = "docutils" version = "0.19" @@ -850,6 +881,24 @@ sqs = ["boto3 (>=1.9.12)", "pycurl (>=7.44.1,<7.45.0)", "urllib3 (>=1.26.7)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=1.3.1)"] +[[package]] +name = "markdown" +version = "3.4.3" +description = "Python implementation of John Gruber's Markdown." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Markdown-3.4.3-py3-none-any.whl", hash = "sha256:065fd4df22da73a625f14890dd77eb8040edcbd68794bcd35943be14490608b2"}, + {file = "Markdown-3.4.3.tar.gz", hash = "sha256:8bf101198e004dc93e84a12a7395e31aac6a9c9942848ae1d99b9d72cf9b3520"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[package.extras] +testing = ["coverage", "pyyaml"] + [[package]] name = "markupsafe" version = "2.1.2" @@ -1753,4 +1802,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "dd56e0233689448f08dfcae943871bf9d72c05ad7bfd326c69f9ecb33ea8a461" +content-hash = "12e95bb19c0dc9d4b1388423e1007628e37e5e13a217de01b27bb34b20d5ac34" diff --git a/pyproject.toml b/pyproject.toml index d28421e..5082329 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,9 @@ passlib = "^1.7.4" redis = "^4.5.1" requests-oauthlib = "^1.3.1" django-impersonate = "^1.9.1" +djangorestframework = "^3.14.0" +markdown = "^3.4.3" +django-filter = "^23.1" [tool.poetry.group.dev.dependencies]