diff --git a/cvat/apps/authentication/adapter.py b/cvat/apps/authentication/adapter.py index 92bd268ad07f..c9eb00ad2cec 100644 --- a/cvat/apps/authentication/adapter.py +++ b/cvat/apps/authentication/adapter.py @@ -16,14 +16,18 @@ def save_user(self, request, user, form, commit=True): last_name = data.get("last_name") email = data.get("email") username = data.get("username") - signed_email = data.get("signed_email") - user.set_hashed_signed_email(signed_email) + if "signed_email" in data: + user.set_hashed_signed_email(data["signed_email"]) user_email(user, email) user_username(user, username) if first_name: user_field(user, "first_name", first_name) if last_name: user_field(user, "last_name", last_name) + if "password1" in data: + user.set_password(data["password1"]) + else: + user.set_unusable_password() self.populate_username(request, user) if commit: diff --git a/cvat/apps/authentication/auth.py b/cvat/apps/authentication/auth.py index 1e4b361c47c9..4d3cc6fa7ae8 100644 --- a/cvat/apps/authentication/auth.py +++ b/cvat/apps/authentication/auth.py @@ -1,8 +1,6 @@ # Copyright (C) 2018 Intel Corporation # # SPDX-License-Identifier: MIT -import base64 -import binascii from django.conf import settings from django.db.models import Q import rules @@ -12,9 +10,8 @@ from django.core import signing from rest_framework import authentication, exceptions from rest_framework.authentication import TokenAuthentication as _TokenAuthentication -from rest_framework.authentication import BasicAuthentication as _BasicAuthentication -from rest_framework.authentication import get_authorization_header, _, HTTP_HEADER_ENCODING -from django.contrib.auth import login, authenticate, get_user_model + +from django.contrib.auth import login # Even with token authorization it is very important to have a valid session id # in cookies because in some cases we cannot use token authorization (e.g. when @@ -75,52 +72,6 @@ def authenticate(self, request): return (user, None) -class BasicAuthentication(_BasicAuthentication): - def authenticate(self, request): - """ - Returns a `User` if a correct username and wallet address have been supplied - using HTTP Basic authentication. Otherwise returns `None`. - """ - auth = get_authorization_header(request).split() - - if not auth or auth[0].lower() != b'basic': - return None - - if len(auth) == 1: - msg = _('Invalid basic header. No credentials provided.') - raise exceptions.AuthenticationFailed(msg) - elif len(auth) > 2: - msg = _('Invalid basic header. Credentials string should not contain spaces.') - raise exceptions.AuthenticationFailed(msg) - - try: - auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':') - except (TypeError, UnicodeDecodeError, binascii.Error): - msg = _('Invalid basic header. Credentials not correctly base64 encoded.') - raise exceptions.AuthenticationFailed(msg) - - userid, wallet_address = auth_parts[0], auth_parts[2] - return self.authenticate_credentials(userid, wallet_address, request) - - def authenticate_credentials(self, userid, wallet_address, request=None): - """ - Authenticate the userid and wallet address against username and wallet_address - with optional request for context. - """ - credentials = { - get_user_model().USERNAME_FIELD: userid, - 'wallet_address': wallet_address - } - user = authenticate(request=request, **credentials) - - if user is None: - raise exceptions.AuthenticationFailed(_('Invalid username/password.')) - - if not user.is_active: - raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) - - return (user, None) - # AUTH PREDICATES has_admin_role = rules.is_group_member(str(AUTH_ROLE.ADMIN)) has_user_role = rules.is_group_member(str(AUTH_ROLE.USER)) @@ -291,8 +242,7 @@ def has_object_permission(self, request, view, obj): class TaskCreatePermission(BasePermission): # pylint: disable=no-self-use def has_permission(self, request, view): - return True - #return request.user.has_perm('engine.task.create') + return request.user.has_perm('engine.task.create') class TaskAccessPermission(BasePermission): # pylint: disable=no-self-use @@ -338,8 +288,7 @@ def get_queryset(self): class TaskChangePermission(BasePermission): # pylint: disable=no-self-use def has_object_permission(self, request, view, obj): - return True - #return request.user.has_perm('engine.task.change', obj) + return request.user.has_perm('engine.task.change', obj) class TaskDeletePermission(BasePermission): # pylint: disable=no-self-use @@ -349,14 +298,12 @@ def has_object_permission(self, request, view, obj): class JobAccessPermission(BasePermission): # pylint: disable=no-self-use def has_object_permission(self, request, view, obj): - return True - #return request.user.has_perm('engine.job.access', obj) + return request.user.has_perm('engine.job.access', obj) class JobChangePermission(BasePermission): # pylint: disable=no-self-use def has_object_permission(self, request, view, obj): - return True - #return request.user.has_perm('engine.job.change', obj) + return request.user.has_perm('engine.job.change', obj) class JobReviewPermission(BasePermission): # pylint: disable=no-self-use diff --git a/cvat/apps/authentication/authentication_backends.py b/cvat/apps/authentication/authentication_backends.py index c3be8dc62689..2cc10cd83500 100644 --- a/cvat/apps/authentication/authentication_backends.py +++ b/cvat/apps/authentication/authentication_backends.py @@ -7,6 +7,8 @@ from allauth.account.utils import filter_users_by_email, filter_users_by_username from django.contrib.auth import get_user_model +from cvat.apps.authentication.utils import validate_user_wallet_address + UserModel = get_user_model() class AuthenticationBackend(_AuthenticationBackend): @@ -19,6 +21,7 @@ def _authenticate_by_email(self, **credentials): # and use username as fallback email = credentials.get("email", credentials.get("username")) if email: + validate_user_wallet_address(credentials["wallet_address"], email, credentials["signed_email"]) for user in filter_users_by_email(email): if self._check_wallet_address(user, credentials["wallet_address"]): return user diff --git a/cvat/apps/authentication/migrations/0001_initial.py b/cvat/apps/authentication/migrations/0001_initial.py index 5a1b1f4f9ab3..3e9a03234289 100644 --- a/cvat/apps/authentication/migrations/0001_initial.py +++ b/cvat/apps/authentication/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1.10 on 2021-07-26 10:31 +# Generated by Django 3.1.10 on 2021-07-26 14:04 from django.conf import settings import django.contrib.auth.models @@ -21,6 +21,7 @@ class Migration(migrations.Migration): name='User', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), diff --git a/cvat/apps/authentication/models.py b/cvat/apps/authentication/models.py index 43e5f8070e58..1c48eab0a819 100644 --- a/cvat/apps/authentication/models.py +++ b/cvat/apps/authentication/models.py @@ -13,7 +13,6 @@ from cvat.apps.authentication.utils import hash_signed_email class User(AbstractUser): - password = None hashed_signed_email = models.CharField(_('hashed_signed_email'), max_length=128, default='') _hashed_signed_email = None diff --git a/cvat/apps/authentication/serializers.py b/cvat/apps/authentication/serializers.py index 952caad4aace..464afb7bb3b9 100644 --- a/cvat/apps/authentication/serializers.py +++ b/cvat/apps/authentication/serializers.py @@ -67,17 +67,18 @@ def get_email_options(self): class LoginSerializer(_LoginSerializer): password = None wallet_address = CharField(required=True) + signed_email = CharField(required=True) def authenticate(self, **kwargs): return authenticate(self.context['request'], **kwargs) - def _validate_email(self, email, wallet_address): + def _validate_email(self, email, wallet_address, signed_email): user = None - if email and wallet_address: - user = self.authenticate(email=email, wallet_address=wallet_address) + if email and wallet_address and signed_email: + user = self.authenticate(email=email, wallet_address=wallet_address, signed_email=signed_email) else: - msg = _('Must include "email" and "wallet_address".') + msg = _('Must include "email" and "wallet_address" and "signed_email".') raise exceptions.ValidationError(msg) return user @@ -109,6 +110,7 @@ def validate(self, attrs): username = attrs.get('username') email = attrs.get('email') wallet_address = attrs.get('wallet_address') + signed_email = attrs.get('signed_email') user = None @@ -117,7 +119,7 @@ def validate(self, attrs): # Authentication through email if app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.EMAIL: - user = self._validate_email(email, wallet_address) + user = self._validate_email(email, wallet_address, signed_email) # Authentication through username elif app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.USERNAME: diff --git a/cvat/settings/base.py b/cvat/settings/base.py index a89e6c2d4210..afa41dbe1116 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -143,7 +143,7 @@ def add_ssh_keys(): 'cvat.apps.authentication.auth.TokenAuthentication', 'cvat.apps.authentication.auth.SignatureAuthentication', 'rest_framework.authentication.SessionAuthentication', - 'cvat.apps.authentication.auth.BasicAuthentication' + 'rest_framework.authentication.BasicAuthentication' ], 'DEFAULT_VERSIONING_CLASS': # Don't try to use URLPathVersioning. It will give you /api/{version}