Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into pkv/otp-sender-fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
pxwxnvermx committed Jun 25, 2024
2 parents 7348e6c + 3899814 commit fdc0ff5
Show file tree
Hide file tree
Showing 13 changed files with 413 additions and 7 deletions.
24 changes: 22 additions & 2 deletions connectid/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
'messaging',
'oauth2_provider',
'rest_framework',
'axes',
'fcm_django',
'django.contrib.sites',
]

MIDDLEWARE = [
Expand All @@ -46,6 +48,14 @@
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'axes.middleware.AxesMiddleware',
'utils.middleware.CurrentVersionMiddleware'
]

AUTHENTICATION_BACKENDS = [
# AxesStandaloneBackend should be the first backend in the AUTHENTICATION_BACKENDS list.
'axes.backends.AxesStandaloneBackend',
'django.contrib.auth.backends.ModelBackend',
]

ROOT_URLCONF = 'connectid.urls'
Expand Down Expand Up @@ -168,11 +178,19 @@
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day'
}

},
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.AcceptHeaderVersioning",
"DEFAULT_VERSION": "1.0",
"ALLOWED_VERSIONS": ["1.0"]
}


AXES_COOLOFF_TIME = 6
AXES_IPWARE_META_PRECEDENCE_ORDER = [
'HTTP_X_FORWARDED_FOR',
'REMOTE_ADDR',
]

LOGIN_URL = '/admin/login/'

OAUTH2_PROVIDER = {
Expand All @@ -196,6 +214,8 @@
"DELETE_INACTIVE_DEVICES": False,
}

SITE_ID = 1

from .localsettings import *

# Firebase
Expand Down
1 change: 1 addition & 0 deletions requirements/requirements.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Django
django-axes[ipware]
django-oauth-toolkit
django-otp
django-phonenumber-field
Expand Down
9 changes: 9 additions & 0 deletions requirements/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ deprecated==1.2.14
django==4.1.7
# via
# -r requirements.in
# django-axes
# django-otp
# django-phonenumber-field
# djangorestframework
django-axes[ipware]==6.0.3
# via -r requirements.in
django-ipware==5.0.0
# via django-axes
# django-oauth-toolkit
# django-otp
# django-phonenumber-field
Expand Down Expand Up @@ -166,5 +174,6 @@ pip==23.1.2
# via pip-tools
setuptools==67.8.0
# via
# django-axes
# gunicorn
# pip-tools
3 changes: 3 additions & 0 deletions users/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
class UsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'users'

def ready(self):
from users import signals # noqa
17 changes: 17 additions & 0 deletions users/migrations/0005_connectuser_recovery_pin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.1.7 on 2024-04-24 18:22

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("users", "0004_connectuser_ip_address_alter_connectuser_dob"),
]

operations = [
migrations.AddField(
model_name="connectuser",
name="recovery_pin",
field=models.CharField(max_length=128, null=True),
),
]
17 changes: 17 additions & 0 deletions users/migrations/0006_alter_connectuser_recovery_pin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.1.7 on 2024-04-24 19:11

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("users", "0005_connectuser_recovery_pin"),
]

operations = [
migrations.AlterField(
model_name="connectuser",
name="recovery_pin",
field=models.CharField(blank=True, max_length=128, null=True),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 4.1.7 on 2024-05-17 01:17

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
dependencies = [
("users", "0006_alter_connectuser_recovery_pin"),
]

operations = [
migrations.AddField(
model_name="connectuser",
name="recovery_phone_validation_deadline",
field=models.DateField(blank=True, null=True),
),
migrations.CreateModel(
name="UserKey",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("key", models.CharField(max_length=60)),
("valid", models.BooleanField(default=True)),
("created", models.DateTimeField(auto_now_add=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
]
68 changes: 68 additions & 0 deletions users/migrations/0008_credential_usercredential.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Generated by Django 4.1.7 on 2024-05-20 02:10

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid


class Migration(migrations.Migration):
dependencies = [
("users", "0007_connectuser_recovery_phone_validation_deadline_and_more"),
]

operations = [
migrations.CreateModel(
name="Credential",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=300)),
("slug", models.CharField(max_length=100)),
("organization_slug", models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name="UserCredential",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("accepted", models.BooleanField(default=False)),
(
"invite_id",
models.CharField(default=uuid.uuid4, max_length=50, unique=True),
),
(
"credential",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="users.credential",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"unique_together": {("user", "credential")},
},
),
]
66 changes: 66 additions & 0 deletions users/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
from datetime import timedelta
import base64
import os

from uuid import uuid4

from django.contrib.auth.hashers import check_password, make_password
from django.contrib.auth.models import AbstractUser
from django.contrib.sites.models import Site
from django.db import models
from django.utils.timezone import now
from django.urls import reverse
from django_otp.models import SideChannelDevice

from phonenumber_field.modelfields import PhoneNumberField
Expand All @@ -21,13 +29,41 @@ class ConnectUser(AbstractUser):
name = models.TextField(max_length=150, blank=True)
dob = models.DateField(blank=True, null=True)
ip_address = models.GenericIPAddressField(blank=True, null=True)
# this is effectively a password so use set_recovery_pin to
# store a hashed value rather than setting it directly
recovery_pin = models.CharField(null=True, blank=True, max_length=128)
recovery_phone_validation_deadline = models.DateField(blank=True, null=True)

# removed from base class
first_name = None
last_name = None

REQUIRED_FIELDS = ["phone_number", "name"]

def set_recovery_pin(self, pin):
hashed_value = make_password(pin)
self.recovery_pin = hashed_value

def check_recovery_pin(self, pin):
return check_password(pin, self.recovery_pin)


class UserKey(models.Model):
user = models.ForeignKey(ConnectUser, on_delete=models.CASCADE)
key = models.CharField(max_length=60)
valid = models.BooleanField(default=True)
created = models.DateTimeField(auto_now_add=True)

@classmethod
def get_or_create_key_for_user(cls, user):
user_key = cls.objects.filter(user=user, valid=True).first()
if not user_key:
user_key = UserKey(user=user)
bin_key = os.urandom(32)
user_key.key = base64.b64encode(bin_key).decode('utf-8')
user_key.save()
return user_key


class PhoneDevice(SideChannelDevice):
phone_number = PhoneNumberField()
Expand Down Expand Up @@ -69,3 +105,33 @@ class RecoverySteps(models.TextChoices):
secret_key = models.TextField()
user = models.ForeignKey(ConnectUser, on_delete=models.CASCADE, unique=True)
step = models.TextField(choices=RecoverySteps.choices)


class Credential(models.Model):
name = models.CharField(max_length=300)
slug = models.CharField(max_length=100)
organization_slug = models.CharField(max_length=255)


class UserCredential(models.Model):
user = models.ForeignKey(ConnectUser, on_delete=models.CASCADE)
credential = models.ForeignKey(Credential, on_delete=models.CASCADE)
accepted = models.BooleanField(default=False)
invite_id = models.CharField(max_length=50, default=uuid4, unique=True)

class Meta:
unique_together = ("user", "credential")

@classmethod
def add_credential(cls, user, credential, request):
user_credential, created = cls.objects.get_or_create(user=user, credential=credential)
if created:
domain = Site.objects.get_current().domain
location = reverse("accept_credential", args=(user_credential.invite_id,))
url = f"https://{domain}{location}"
message = (
f"You have been given credential '{credential.name}'."
f"Please click the following link to accept {url}"
)
sender = get_sms_sender(user.phone_number.country_code)
send_sms(user.phone_number.as_e164, message, sender)
9 changes: 9 additions & 0 deletions users/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.dispatch import receiver

from axes.signals import user_locked_out
from rest_framework.exceptions import PermissionDenied


@receiver(user_locked_out)
def raise_permission_denied(*args, **kwargs):
raise PermissionDenied("Too many failed login attempts")
8 changes: 8 additions & 0 deletions users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,15 @@
path('phone_available', views.phone_available, name='phone_available'),
path('change_phone', views.change_phone, name='change_phone'),
path('change_password', views.change_password, name='change_password'),
path('update_profile', views.update_profile, name='update_profile'),
path('fetch_users', views.FetchUsers.as_view(), name='fetch_users'),
path('heartbeat', views.heartbeat, name='heartbeat'),
path('demo_users', views.GetDemoUsers.as_view(), name='demo_users'),
path('recover/confirm_pin', views.confirm_recovery_pin, name='confirm_recovery_pin'),
path('set_recovery_pin', views.set_recovery_pin, name='set_recovery_pin'),
path('filter_users', views.FilterUsers.as_view(), name='filter_users'),
path('add_credential', views.AddCredential.as_view(), name='add_credential'),
path('accept_credential/<slug:invite_id>', views.accept_credential, name='accept_credential'),
path('fetch_credentials', views.FetchCredentials.as_view(), name='fetch_credentials'),
path('fetch_db_key', views.fetch_db_key, name='fetch_db_key'),
]
Loading

0 comments on commit fdc0ff5

Please sign in to comment.