[WIP] phone attribution verification support (#66053)
gitea/authentic/pipeline/head Something is wrong with the build of this commit Details

This commit is contained in:
Paul Marillonnet 2023-01-24 16:37:13 +01:00
parent e9de5a8e29
commit 1d89b679a1
7 changed files with 99 additions and 11 deletions

View File

@ -28,7 +28,6 @@ from django.core.validators import RegexValidator
from django.db import IntegrityError, models
from django.db.transaction import atomic
from django.utils.encoding import force_bytes, force_str
from django.utils.timezone import now
from django.utils.translation import gettext as _
from authentic2 import app_settings
@ -500,6 +499,7 @@ class UserCsvImporter:
header.globally_unique = True
except FieldDoesNotExist:
pass
if not header.field:
try:
header.attribute = Attribute.objects.get(name=header.name)
@ -737,6 +737,8 @@ class UserCsvImporter:
if not user:
user = User(ou=self.ou)
user.set_random_password()
# we need user to have an id in order for its potential verified attributes to be set
user.save()
for cell in row.cells:
if not cell.header.field:
@ -749,7 +751,8 @@ class UserCsvImporter:
if cell.header.name == 'email' and cell.header.verified:
user.set_email_verified(True, source='csv')
if cell.header.name == 'phone' and cell.header.verified:
user.phone_verified_on = now()
user.verified_attributes.phone = cell.value
user.set_phone_verified(True, source='csv')
cell.action = 'updated'
continue
cell.action = 'nothing'

View File

@ -0,0 +1,25 @@
# Generated by Django 2.2.26 on 2023-01-25 08:08
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('custom_user', '0034_user_email_verified_sources'),
]
operations = [
migrations.AddField(
model_name='user',
name='phone_verified_sources',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(max_length=63),
blank=True,
null=True,
size=None,
verbose_name='phone verification sources',
),
),
]

View File

@ -205,6 +205,13 @@ class User(AbstractBaseUser):
default=None,
verbose_name=_('phone verification date'),
)
phone_verified_sources = ArrayField(
verbose_name=_('phone verification sources'),
base_field=models.CharField(max_length=63),
null=True,
blank=True,
)
is_staff = models.BooleanField(
_('staff status'),
default=False,
@ -625,12 +632,34 @@ class User(AbstractBaseUser):
self.email_verified_date = timezone.now()
if source and source not in self.email_verified_sources:
self.email_verified_sources.append(source)
Attribute.add_verification_source(self, 'email', self.email, source)
else:
if source and source in self.email_verified_sources:
self.email_verified_sources.remove(source)
if not source or not self.email_verified_sources:
self.email_verified = False
self.email_verified_date = None
Attribute.remove_verification_source(self, 'email', self.email, source)
self.save()
def set_phone_verified(self, value, source=None):
if self.phone_verified_sources is None:
self.phone_verified_sources = []
if bool(value):
if isinstance(value, datetime.datetime):
self.phone_verified_on = value
else:
self.phone_verified_on = timezone.now()
if source and source not in self.phone_verified_sources:
self.phone_verified_sources.append(source)
Attribute.add_verification_source(self, 'phone', self.phone, source)
else:
if source and source in self.phone_verified_sources:
self.phone_verified_sources.remove(source)
if not source or not self.phone_verified_sources:
self.phone_verified_on = None
Attribute.remove_verification_source(self, 'phone', self.phone, source)
self.save()
class DeletedUser(models.Model):

View File

@ -281,6 +281,38 @@ class Attribute(models.Model):
av.last_verified_on = timezone.now()
av.save()
@classmethod
def remove_verification_source(cls, owner, attribute_name, value, source):
'''
Remove verification source for an existing unitary attribute value,
typically the user's first_name and last_name extended attributes.
'''
attribute = None
try:
attribute = Attribute.objects.get(name=attribute_name)
except Attribute.DoesNotExist:
pass
if not attribute:
# caller should have checked that the attribute object has been created, silently failing.
return
avs = AttributeValue.objects.with_owner(owner).select_for_update()
with transaction.atomic():
try:
av = avs.get(attribute=attribute, content=value)
except AttributeValue.DoesNotExist:
# caller should have checked that the attribute value is defined, silently failing
# to add source.
pass
else:
sources = av.verification_sources or []
if source in sources:
sources.remove(source)
av.verification_sources = sources
if not sources:
av.last_verified_on = None
av.save()
def set_value(self, owner, value, verified=False, attribute_value=None, verification_source=None):
serialize = self.get_kind()['serialize']
# setting to None is to delete

View File

@ -66,6 +66,7 @@ USER_ATTRIBUTES_SET = {
'email_verified_sources',
'phone',
'phone_verified_on',
'phone_verified_sources',
'last_account_deletion_alert',
'deactivation',
'deactivation_reason',

View File

@ -76,6 +76,7 @@ class SerializerTests(TestCase):
'email': '',
'phone': None,
'phone_verified_on': None,
'phone_verified_sources': None,
'first_name': '',
'last_name': '',
'is_active': True,

View File

@ -181,14 +181,13 @@ tnoel@entrouvert.com,Thomas,Noël,0123456789
fpeters@entrouvert.com,Frédéric,Péters,+3281005678
x,x,x,x'''
importer = user_csv_importer_factory(content)
phone = Attribute.objects.get(name='phone')
importer.run()
assert importer.headers == [
CsvHeader(1, 'email', field=True, key=True, verified=True),
CsvHeader(2, 'first_name', field=True),
CsvHeader(3, 'last_name', field=True),
CsvHeader(4, 'phone', attribute=phone),
CsvHeader(4, 'phone', field=True),
]
assert importer.has_errors
assert len(importer.rows) == 3
@ -234,14 +233,13 @@ tnoel@entrouvert.com,Thomas,Noël,0123456789
fpeters@entrouvert.com,Frédéric,Péters,+3281005678
x,x,x,x'''
importer = user_csv_importer_factory(content)
phone = Attribute.objects.get(name='phone')
assert not importer.run(simulate=True)
assert importer.headers == [
CsvHeader(1, 'email', field=True, key=True, verified=True),
CsvHeader(2, 'first_name', field=True),
CsvHeader(3, 'last_name', field=True),
CsvHeader(4, 'phone', attribute=phone),
CsvHeader(4, 'phone', field=True),
]
assert importer.has_errors
assert len(importer.rows) == 3
@ -270,7 +268,7 @@ tnoel@entrouvert.com,Thomas,Noël,0123456789'''
user.attributes.phone = '+33123456789'
assert not importer.run()
assert importer.has_error
assert importer.all_errors
assert importer.created == 0
assert importer.updated == 0
assert len(importer.rows) == 1
@ -418,7 +416,6 @@ app1,1,tnoel@entrouvert.com,Thomas,Noël,0606060606
app1,2,tnoel@entrouvert.com,Thomas,Noël,0606060606
'''
importer = user_csv_importer_factory(content)
phone = Attribute.objects.get(name='phone')
assert importer.run(), importer.all_errors
assert importer.headers == [
@ -427,7 +424,7 @@ app1,2,tnoel@entrouvert.com,Thomas,Noël,0606060606
CsvHeader(3, 'email', field=True, verified=True),
CsvHeader(4, 'first_name', field=True),
CsvHeader(5, 'last_name', field=True),
CsvHeader(6, 'phone', attribute=phone),
CsvHeader(6, 'phone', field=True),
]
assert not importer.has_errors
assert len(importer.rows) == 2
@ -745,10 +742,10 @@ jsmith@nowhere.null,Jimmy,Smith,0202020202'''
]
jdoe = User.objects.get(email='jdoe@nowhere.null')
assert jdoe.verified_attributes.phone == '0101010101'
assert jdoe.verified_attributes.phone == '+33101010101'
jsmith = User.objects.get(email='jsmith@nowhere.null')
assert jsmith.verified_attributes.phone == '0202020202'
assert jsmith.verified_attributes.phone == '+33202020202'
for user in (
jdoe,