blezz-tech 1 dag sedan
förälder
incheckning
1c4e18f34f

+ 2 - 0
README.md

@@ -1,5 +1,7 @@
 # ShariX Open Webapp Base
 
+**!!! `master` branch is outdated. Use `unstable` instead, until this message are disappears !!!**
+
 The base Django project of a service web application to which other modules are connected.
 
 ## Installation / Upgrade

+ 147 - 5
core/settings.py

@@ -4,6 +4,9 @@ from django.contrib.messages import constants as message_constants
 from django.utils.translation import gettext_lazy as _
 from pathlib import Path
 
+import ldap
+from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
+
 ########
 #
 # Django
@@ -22,6 +25,9 @@ CSRF_TRUSTED_ORIGINS = sv.CSRF_TRUSTED_ORIGINS
 ALLOWED_HOSTS = sv.ALLOWED_HOSTS
 INTERNAL_IPS = sv.INTERNAL_IPS
 
+# https if nginx responce HTTP_X_FORWARDED_PROTO=https
+SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
+
 # Application definition
 INSTALLED_APPS = [
     'design_template',
@@ -39,8 +45,8 @@ INSTALLED_APPS = [
     'webservice_running.apps.WebserviceRunningConfig',
     'django_tables2',
     'schema_graph',
-    "django.contrib.sites",
-    "django.contrib.flatpages",
+    # "django.contrib.sites",
+    # "django.contrib.flatpages",
     "django_extensions",
     'rest_framework',
     "rest_framework_api_key",
@@ -138,6 +144,16 @@ AUTH_PASSWORD_VALIDATORS = [
     },
 ]
 
+#Password hashgen
+
+PASSWORD_HASHERS = [
+    'django.contrib.auth.hashers.Argon2PasswordHasher',
+    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
+    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
+    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
+    'django.contrib.auth.hashers.ScryptPasswordHasher',
+]
+
 # Internationalization
 # https://docs.djangoproject.com/en/4.0/topics/i18n/
 LANGUAGE_CODE = 'ru'
@@ -393,9 +409,16 @@ REST_FRAMEWORK = {
 }
 
 SPAGHETTI_SAUCE = {
-    'apps': ['auth', 'sharix_admin',
-             'tickets', 'admin',
-             'flatpages', 'sessions', 'sites', 'dbsynce'],
+    'apps': [
+        'auth',
+        'sharix_admin',
+        'tickets',
+        'admin',
+        # 'flatpages',
+        'sessions',
+        # 'sites',
+        'dbsynce'
+    ],
     'show_fields': False,
     'show_proxy': True,
 }
@@ -413,3 +436,122 @@ EMAIL_USE_TLS = sv.EMAIL_USE_TLS
 EMAIL_HOST_USER = sv.EMAIL_HOST_USER
 EMAIL_HOST_PASSWORD = sv.EMAIL_HOST_PASSWORD
 DEFAULT_FROM_EMAIL = sv.DEFAULT_FROM_EMAIL
+
+#LDAP
+# Baseline configuration.
+
+AUTH_LDAP_PROTO = "ldap"
+AUTH_LDAP_HOST = "ldap.sharix.ru"
+AUTH_LDAP_PORT = '389'  # must be str
+
+#AUTH_LDAP_GID = "502"  # group ID to add signed up users
+#AUTH_LDAP_BASE_UID = 1000  # Integer
+
+#AUTH_LDAP_SERVER_URI = LDAP_PROTO+"://"+LDAP_HOST+":"+LDAP_PORT
+AUTH_LDAP_SERVER_URI = AUTH_LDAP_PROTO+"://"+AUTH_LDAP_HOST
+
+#If we want to use there - we should create custom ldap login backend - https://django-auth-ldap.readthedocs.io/en/latest/custombehavior.html
+#LDAP_LOGIN_ATTEMPT_LIMIT = 100
+#LDAP_RESET_TIME = 15 * 60
+
+#AUTH_LDAP_BASE_DN = "dc=ldap,dc=sharix,dc=ru"
+AUTH_LDAP_BASE_DN = "dc=ldap,dc=sharix,dc=ru"
+AUTH_LDAP_BIND_DN = "cn=admin,dc=ldap,dc=sharix,dc=ru"
+AUTH_LDAP_BIND_PASSWORD = "secret"
+AUTH_LDAP_USER_SEARCH = LDAPSearch(
+    "ou=users,dc=ldap,dc=sharix,dc=ru", ldap.SCOPE_SUBTREE, "(telephoneNumber=%(user)s)"
+#    "ou=users,dc=ldap,dc=sharix,dc=ru", ldap.SCOPE_SUBTREE, "(uid=%(user)s)"
+)
+# Or:
+# AUTH_LDAP_USER_DN_TEMPLATE = 'uid=%(user)s,ou=users,dc=example,dc=com'
+
+# Set up the basic group parameters.
+AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
+    "ou=groups,dc=ldap,dc=sharix,dc=ru",
+#    "ou=apps,dc=ldap,dc=sharix,dc=ru",
+#TODO need to specify only django users
+    ldap.SCOPE_SUBTREE,
+    "(objectClass=groupOfNames)",
+)
+AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr="cn")
+
+# Simple group restrictions
+#AUTH_LDAP_REQUIRE_GROUP = "cn=client,ou=groups,dc=ldap,dc=sharix,dc=ru"
+#AUTH_LDAP_REQUIRE_GROUP = "cn=django,ou=apps,dc=ldap,dc=sharix,dc=ru"
+#AUTH_LDAP_DENY_GROUP = "cn=disabled,ou=django,ou=groups,dc=example,dc=com"
+#AUTH_LDAP_DENY_GROUP = "cn=handlers,ou=apps,dc=ldap,dc=sharix,dc=ru"
+
+# TODO - it duplicates local lib for ldap, decide where it's better to be placed.
+#Most likely left side - Django, right side - LDAP
+# Populate the Django user from the LDAP directory.
+AUTH_LDAP_USER_ATTR_MAP = {
+    "id": "uid",
+    "password": "userPassword",
+    "last_name": "sn",
+    "middle_name": "initials",
+    "first_name": "givenName",
+    "email": "mail",
+    "avatar": "jpegPhoto",
+    "phone": "telephoneNumber",
+    "username": "uid",
+}
+
+AUTH_LDAP_USER_FLAGS_BY_GROUP = {
+    "is_active": [
+                  "cn=PLATFORM-ADMIN,ou=groups,dc=ldap,dc=sharix,dc=ru",
+                  "cn=PLATFORM-SUPERVISOR,ou=groups,dc=ldap,dc=sharix,dc=ru",
+                  "cn=PLATFORM-SUPPORT,ou=groups,dc=ldap,dc=sharix,dc=ru",
+                  "cn=PLATFORM-TECHSUPPORT,ou=groups,dc=ldap,dc=sharix,dc=ru",
+                  "cn=METASERVICE-ADMIN,ou=groups,dc=ldap,dc=sharix,dc=ru",
+                  "cn=CLIENT,ou=groups,dc=ldap,dc=sharix,dc=ru",
+                  "cn=django_admin,ou=groups,dc=ldap,dc=sharix,dc=ru",
+                  ],
+    "is_staff": [
+                  "cn=PLATFORM-TECHSUPPORT,ou=groups,dc=ldap,dc=sharix,dc=ru",
+                  "cn=django_admin,ou=groups,dc=ldap,dc=sharix,dc=ru",
+                  ],
+    "is_superuser": "cn=django_admin,ou=groups,dc=ldap,dc=sharix,dc=ru",
+}
+
+#AUTH_LDAP_REQUIRE_GROUP = (
+#    LDAPGroupQuery("cn=client,ou=groups,dc=ldap,dc=sharix,dc=ru")
+#    | LDAPGroupQuery("cn=platform_admin,ou=groups,dc=ldap,dc=sharix,dc=ru")
+#) & ~LDAPGroupQuery("cn=metaservice_admin,ou=groups,dc=ldap,dc=sharix,dc=ru")
+
+# This is the default, but I like to be explicit.
+AUTH_LDAP_ALWAYS_UPDATE_USER = True
+
+# Use LDAP group membership to calculate group permissions.
+AUTH_LDAP_FIND_GROUP_PERMS = True
+
+# Cache distinguished names and group memberships for an hour to minimize
+# LDAP traffic.
+AUTH_LDAP_CACHE_TIMEOUT = 3600
+
+AUTH_LDAP_MIRROR_GROUPS = True
+
+# Keep ModelBackend around for per-user permissions and maybe a local
+# superuser.
+AUTHENTICATION_BACKENDS = (
+    "django_auth_ldap.backend.LDAPBackend",
+#    "django.contrib.auth.backends.ModelBackend",
+)
+
+LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': False,
+    'handlers': {
+        'file': {
+            'level': 'DEBUG',
+            'class': 'logging.FileHandler',
+            'filename': '/var/log/django-ldap.log',
+        },
+    },
+    'loggers': {
+        'django_auth_ldap': {
+            'handlers': ['file'],
+            'level': 'DEBUG',
+            'propagate': True,
+        },
+    },
+}

+ 112 - 0
core/utils/ldap.py

@@ -0,0 +1,112 @@
+import ldap
+import os,hashlib
+from base64 import urlsafe_b64encode as encode
+
+from ldap.modlist import addModlist
+
+from django.conf import settings
+
+class LDAPOperations():
+    def __init__(self):
+        self.connect()
+
+    def connect(self):
+        if settings.AUTH_LDAP_PROTO == 'ldaps':
+            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
+        self.con = ldap.initialize(settings.AUTH_LDAP_PROTO + '://' + settings.AUTH_LDAP_HOST + ':' + settings.AUTH_LDAP_PORT)
+        try:
+            self.con.simple_bind_s(settings.AUTH_LDAP_BIND_DN, settings.AUTH_LDAP_BIND_PASSWORD)
+        except ldap.SERVER_DOWN:
+            raise ldap.SERVER_DOWN('The LDAP library can’t contact the LDAP server. Contact the admin.')
+
+    def check_attribute(self, attribute, value):
+        """
+        Takes an attribute and value and checks it against the LDAP server for existence/availability.
+        This is mainly for checking unique attributes ie uid, mail, uidNumber
+
+        :param attribute:
+        :param value
+        :return: tuple
+        """
+        query = "(" + attribute + "=" + value + ")"
+        result = self.con.search_s(settings.AUTH_LDAP_BASE_DN, ldap.SCOPE_SUBTREE, query)
+        return result
+
+    def add_to_group(self, username, group):
+        user_dn = "uid=%s,ou=users,%s" % (username, settings.AUTH_LDAP_BASE_DN,) 
+        group_dn = "cn=%s,ou=groups,%s" % (group, settings.AUTH_LDAP_BASE_DN,)
+        return self.con.modify_s(
+            group_dn,
+            [
+                (ldap.MOD_ADD, 'member', [user_dn.encode("utf-8")]),
+            ],
+        )
+
+    def del_from_group(self, username, group):
+        user_dn = "uid=%s,ou=users,%s" % (username, settings.AUTH_LDAP_BASE_DN,)
+        group_dn = "cn=%s,ou=groups,%s" % (group, settings.AUTH_LDAP_BASE_DN,)
+        return self.con.modify_s(
+            group_dn,
+            [
+                (ldap.MOD_DELETE, 'member', [user_dn.encode("utf-8")]),
+            ],
+        )
+
+    def add_user(self, user):
+        
+        modlist = {
+            "objectClass": ["sharixAccount"],
+            "uid": [user.id],
+            "userPassword": ['{ARGON2}'+user.password[6:]],
+            "sn": [user.last_name],
+            "initials": [user.middle_name],
+            "givenName": [user.first_name],
+            "cn": [user.get_full_name()],
+            "displayName": [user.get_full_name()],
+#            "title": [user.title],
+            "mail": [user.email],
+#            "jpegPhoto": [user.avatar],
+#            "employeeType": [user.designation],
+#            "departmentNumber": [user.department],
+            "telephoneNumber": [user.phone_number],
+#            "registeredAddress": [user.address],
+#            "homePhone": [user.phone],
+#            "uidNumber": [uid_number],
+#            "gidNumber": [settings.LDAP_GID],
+#            "loginShell": ["/bin/bash"],
+#            "homeDirectory": ["/home/users/" + user.username]
+        }
+        
+        dn = 'uid=' + modlist['uid'][0] + ',ou=users,' + settings.AUTH_LDAP_BASE_DN
+
+        # convert modlist to bytes form ie b'abc'
+        modlist_bytes = {}
+        for key in modlist.keys():
+            modlist_bytes[key] = [i.encode('utf-8') for i in modlist[key] if i
+                                  is not None]
+
+        result = self.con.add_s(dn, addModlist(modlist_bytes))
+        return result
+
+    def set_password(self, username, password):
+        """
+        set user password
+        :param username:
+        :param password:
+        :return: ldap result
+        """
+        dn = "uid=%s,ou=users,%s" % (username, settings.AUTH_LDAP_BASE_DN,)
+        user_result = self.check_attribute('uid', username)  # get user
+        tmp_modlist = dict(user_result)
+        old_value = {"userPassword": [tmp_modlist[dn]['userPassword'][0]]}
+        new_value = {"userPassword": [('{ARGON2}'+password[6:]).encode()]}
+
+        modlist = ldap.modlist.modifyModlist(old_value, new_value)
+        result = self.con.modify_s(dn, modlist)
+        return result
+
+    def delete_user(self, username):
+        dn = "uid=%s,%s" % (username, settings.AUTH_LDAP_BASE_DN,)
+        response = self.con.delete_s(dn)
+        return response
+

+ 4 - 2
requirements.txt

@@ -1,5 +1,5 @@
 aniso8601==9.0.1
-asgiref==3.5.2
+asgiref==3.*
 attrs==22.2.0
 bleach==5.0.1
 certifi==2022.12.7
@@ -9,7 +9,8 @@ coreapi==2.3.3
 coreschema==0.0.4
 cryptography==39.0.0
 defusedxml==0.7.1
-Django==4.1.3
+Django==4.2.*
+django-auth-ldap
 django-ckeditor==6.7.2
 django-debug-toolbar==4.1.0
 django-extensions==3.2.1
@@ -68,3 +69,4 @@ uritemplate==4.1.1
 urllib3==1.26.14
 webencodings==0.5.1
 xmpppy==0.7.1
+argon2-cffi

+ 41 - 2
sharix_admin/forms/auth.py

@@ -1,22 +1,43 @@
 from django import forms
 from django.contrib.auth import get_user_model
 from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, UsernameField
+from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm
 from django.contrib.auth.models import Group
 
 from sharix_admin.forms.base import BaseForm
 
+#TODO fix path
+from core.utils.ldap import LDAPOperations
+
 
 class ShariXSignUpForm(BaseForm, UserCreationForm):
     """
     Форма для регистрации пользователей. 
     """
-
+    ldap_ops = LDAPOperations()
     def save(self, commit=True):
+        
         user = super().save(commit=False)
         user.username = self.cleaned_data['phone_number']  # FIXME: Имя пользователя = номер телефона
+        user.id = self.cleaned_data['phone_number']  # FIXME: Имя пользователя = номер телефона
+
+        # TODO Fix LDAP checks - better to insert in cleaned_data or smth like this
+        if self.ldap_ops.check_attribute('uid', user.phone_number):
+            raise forms.ValidationError(
+                f"Phone number {user.phone_number} is not available (in use)",
+                code='phone_number_exists_ldap'
+            )
+
+        if self.ldap_ops.check_attribute('mail', user.email):
+            raise forms.ValidationError(
+                f"Email {user.email} is not available (in use)",
+                code='email_exists_ldap')
         if commit:
+            self.ldap_ops.add_user(user)
+            # self.ldap_ops.set_password(user.username, user.password)
+            self.ldap_ops.add_to_group(user.username, "CLIENT")
             user.save()
-            user.groups.add(Group.objects.get(id=51))  # Добавляем всех пользователей по умолчанию в группу CLIENT
+            # user.groups.add(Group.objects.get(id=51))  # Добавляем всех пользователей по умолчанию в группу CLIENT
         return user
 
     class Meta:
@@ -33,3 +54,21 @@ class ShariXLoginForm(BaseForm, AuthenticationForm):
     Форма для авторизации пользователей.
     """
     pass
+
+
+class ShariXResetPasswordForm(BaseForm, PasswordResetForm):
+    pass
+
+
+class ShariXResetPasswordConfirmForm(BaseForm, SetPasswordForm):
+
+    ldap_ops = LDAPOperations()
+    def save(self, commit=True):
+        password = self.cleaned_data["new_password1"]
+        #ldap_ops.set_password(user.username, user.password)
+        self.user.set_password(password)
+        if commit:
+            self.ldap_ops.set_password(self.user.phone_number, self.user.password)
+            self.user.save()
+        return self.user
+

+ 18 - 6
sharix_admin/templates/sharix_admin/auth/reset_password.html

@@ -3,15 +3,27 @@
 
 {% block title %}ShariX Open - Восстановление пароля{% endblock %}
 
+
 {% block content %}
 <div class="d-flex vh-100">
-    <div class="m-auto w-100 p-4 d-flex flex-column justify-content-center align-items-center text-center">
-        <div class="mb-4">
-            <h1 class="fs-2">Приносим свои извинения, автоматическое восстановление паролей сейчас не работает!</h1>
-            <a href="{% url "sharix_admin:auth_login" %}">Вернуться на страницу входа</a>
-        </div>
+    <div class="m-auto w-100 p-3 d-flex flex-column justify-content-center align-items-center" style="min-width: 320px; max-width: 448px;">
+        <img style="height: 128px; padding: 0.5em;" src="{% static 'sharix_admin/img/logo.svg' %}" alt="Логотип">
+        <h1 class="fs-3">Восстановление пароля</h1>
+
+        <form class="my-4 w-100" method="post">
+            {% csrf_token %}
+            {% include "sharix_admin/include/form.html" %}
+
+            <div class="mt-4">
+                <button class="w-100 btn btn-primary mb-2" type="submit">Отправить</button>
+                <a class="d-block w-100 text-center" href="{% url "sharix_admin:auth_signup" %}">Регистрация</a>
+                <a class="d-block w-100 text-center" href="{% url "sharix_admin:auth_login" %}">Вернуться на страницу входа</a>
+            </div>
+        </form>
 
         <small class="text-muted">&#169; ShariX Open {% now "Y" %}</small>
     </div>
 </div>
-{% endblock %}
+{% endblock %}
+
+

+ 29 - 0
sharix_admin/templates/sharix_admin/auth/reset_password_confirm.html

@@ -0,0 +1,29 @@
+{% extends 'sharix_admin/base.html' %}
+{% load static %}
+
+{% block title %}ShariX Platform - Изменение пароля{% endblock %}
+
+
+{% block content %}
+<div class="d-flex vh-100">
+    <div class="m-auto w-100 p-3 d-flex flex-column justify-content-center align-items-center" style="min-width: 320px; max-width: 448px;">
+        <img style="height: 128px; padding: 0.5em;" src="{% static 'sharix_admin/img/logo.svg' %}" alt="Логотип">
+        <h1 class="fs-3">Изменение пароля</h1>
+
+        <form class="my-4 w-100" method="post">
+            {% csrf_token %}
+            {% include "sharix_admin/include/form.html" %}
+
+            <div class="mt-4">
+                <button class="w-100 btn btn-primary mb-2" type="submit">Изменить</button>
+                <a class="d-block w-100 text-center" href="{% url "sharix_admin:auth_signup" %}">Регистрация</a>
+                <a class="d-block w-100 text-center" href="{% url "sharix_admin:auth_login" %}">Вернуться на страницу входа</a>
+            </div>
+        </form>
+
+        <small class="text-muted">&#169; ShariX Platform {% now "Y" %}</small>
+    </div>
+</div>
+{% endblock %}
+
+

+ 12 - 0
sharix_admin/templates/sharix_admin/auth/reset_password_email.html

@@ -0,0 +1,12 @@
+{% autoescape off %}
+  To initiate the password reset process for your {{ user.email }} ShariX Platform Registration/Login Account,
+  click the link below:
+
+  {{ protocol }}://{{ domain }}{% url 'sharix_admin:auth_reset_password_confirm' uidb64=uid token=token %}
+
+  If clicking the link above doesn't work, please copy and paste the URL in a new browser
+  window instead.
+
+  Sincerely,
+  ShariX Team
+{% endautoescape %}

+ 1 - 0
sharix_admin/templates/sharix_admin/auth/reset_password_subject

@@ -0,0 +1 @@
+ShariX Platform Password Reset

+ 1 - 0
sharix_admin/urls.py

@@ -83,6 +83,7 @@ urlpatterns = [
     path('auth/login/', ShariXLoginView.as_view(), name='auth_login'),
     path('auth/logout/', login_required(LogoutView.as_view()), name="auth_logout"),
     path('auth/reset-password/', ShariXResetPasswordView.as_view(), name='auth_reset_password'),
+    path('auth/reset-password-confirm/<uidb64>/<token>/', ShariXResetPasswordConfirmView.as_view(), name='auth_reset_password_confirm'),
 
     path('transactions/<int:trans_id>/', trans_id, name='transid'),
     path('balance/', balance, name='balance'),

+ 22 - 2
sharix_admin/views/auth.py

@@ -1,9 +1,11 @@
 from django.contrib.auth.views import LoginView
+from django.contrib.auth.views import PasswordResetView
+from django.contrib.auth.views import PasswordResetConfirmView
 from django.urls import reverse_lazy
-from django.views.generic import TemplateView
 from django.views.generic.edit import CreateView
 
 from sharix_admin.forms import ShariXSignUpForm, ShariXLoginForm
+from sharix_admin.forms import ShariXResetPasswordForm, ShariXResetPasswordConfirmForm
 
 
 class ShariXSignUpView(CreateView):
@@ -25,8 +27,26 @@ class ShariXLoginView(LoginView):
 
 
 # FIXME: Восстановления паролей сейчас не работает. Установлена заглушка.
-class ShariXResetPasswordView(TemplateView):
+class ShariXResetPasswordView(PasswordResetView):
     """
     Представление для восстановления пароля.
     """
+    form_class = ShariXResetPasswordForm
     template_name = "sharix_admin/auth/reset_password.html"
+    email_template_name = "sharix_admin/auth/reset_password_email.html"
+    subject_template_name = "sharix_admin/auth/reset_password_subject"
+    success_message = "We've emailed you instructions for setting your password, " \
+                      "if an account exists with the email you entered. You should receive them shortly." \
+                      " If you don't receive an email, " \
+                      "please make sure you've entered the address you registered with, and check your spam folder."
+    success_url = reverse_lazy('sharix_admin:auth_login')
+
+
+class ShariXResetPasswordConfirmView(PasswordResetConfirmView):
+    """
+    Представление для восстановления пароля после получения e-mail.
+    """
+    form_class = ShariXResetPasswordConfirmForm
+    template_name = "sharix_admin/auth/reset_password_confirm.html"
+    success_url = reverse_lazy('sharix_admin:auth_login')
+