Browse Source

Add backend code for encrypting passworld profiles

closes #580
pull/583/head
Bianca Rosa 3 years ago
committed by Guillaume Vincent
parent
commit
68625c8455
10 changed files with 251 additions and 14 deletions
  1. +2
    -0
      containers/backend/.gitignore
  2. +18
    -0
      containers/backend/api/migrations/0007_lesspassuser_key.py
  3. +29
    -0
      containers/backend/api/migrations/0008_encryptedpasswordprofiles.py
  4. +28
    -10
      containers/backend/api/models.py
  5. +16
    -0
      containers/backend/api/serializers.py
  6. +11
    -4
      containers/backend/api/tests/factories.py
  7. +25
    -0
      containers/backend/api/tests/test_user.py
  8. +106
    -0
      containers/backend/api/tests/tests_encrypted_password_profiles.py
  9. +5
    -0
      containers/backend/api/urls.py
  10. +11
    -0
      containers/backend/api/views.py

+ 2
- 0
containers/backend/.gitignore View File

@@ -1,2 +1,4 @@
www
*.sqlite3
# Ignore virtual env
venv

+ 18
- 0
containers/backend/api/migrations/0007_lesspassuser_key.py View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-12-18 16:29

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0006_change_default_password_profile'),
]

operations = [
migrations.AddField(
model_name='lesspassuser',
name='key',
field=models.TextField(null=True),
),
]

+ 29
- 0
containers/backend/api/migrations/0008_encryptedpasswordprofiles.py View File

@@ -0,0 +1,29 @@
# Generated by Django 3.0.7 on 2020-12-18 16:29

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


class Migration(migrations.Migration):

dependencies = [
('api', '0007_lesspassuser_key'),
]

operations = [
migrations.CreateModel(
name='EncryptedPasswordProfile',
fields=[
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('password_profile', models.TextField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='encrypted_password_profiles', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
]

+ 28
- 10
containers/backend/api/models.py View File

@@ -1,14 +1,12 @@
import uuid
from django.db import models
from django.contrib.auth.models import (
BaseUserManager, AbstractBaseUser
)
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser


class LesspassUserManager(BaseUserManager):
def create_user(self, email, password=None):
if not email:
raise ValueError('Users must have an email address')
raise ValueError("Users must have an email address")

user = self.model(
email=self.normalize_email(email),
@@ -19,20 +17,25 @@ class LesspassUserManager(BaseUserManager):
return user

def create_superuser(self, email, password):
user = self.create_user(email, password=password, )
user = self.create_user(
email,
password=password,
)
user.is_admin = True
user.save(using=self._db)
return user


class LessPassUser(AbstractBaseUser):
email = models.EmailField(verbose_name='email address', max_length=255, unique=True)
email = models.EmailField(verbose_name="email address", max_length=255, unique=True)
is_active = models.BooleanField(default=True)
is_admin = models.BooleanField(default=False)
key = models.TextField(null=True)

objects = LesspassUserManager()

USERNAME_FIELD = 'email'
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["key"]

def get_full_name(self):
return self.email
@@ -62,8 +65,8 @@ class LessPassUser(AbstractBaseUser):


class DateMixin(models.Model):
created = models.DateTimeField(auto_now_add=True, verbose_name='created')
modified = models.DateTimeField(auto_now=True, verbose_name='modified')
created = models.DateTimeField(auto_now_add=True, verbose_name="created")
modified = models.DateTimeField(auto_now=True, verbose_name="modified")

class Meta:
abstract = True
@@ -71,7 +74,9 @@ class DateMixin(models.Model):

class Password(DateMixin):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(LessPassUser, on_delete=models.CASCADE, related_name='passwords')
user = models.ForeignKey(
LessPassUser, on_delete=models.CASCADE, related_name="passwords"
)
login = models.CharField(max_length=255, null=True, blank=True)
site = models.CharField(max_length=255, null=True, blank=True)

@@ -87,3 +92,16 @@ class Password(DateMixin):

def __str__(self):
return str(self.id)


class EncryptedPasswordProfile(DateMixin):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(
LessPassUser,
on_delete=models.CASCADE,
related_name="encrypted_password_profiles",
)
password_profile = models.TextField()

def __str__(self):
return str(self.id)

+ 16
- 0
containers/backend/api/serializers.py View File

@@ -27,6 +27,22 @@ class PasswordSerializer(serializers.ModelSerializer):
return models.Password.objects.create(user=user, **validated_data)


class EncryptedPasswordProfileSerializer(serializers.ModelSerializer):
class Meta:
model = models.EncryptedPasswordProfile
fields = (
"id",
"password_profile",
"created",
"modified",
)
read_only_fields = ("created", "modified")

def create(self, validated_data):
user = self.context["request"].user
return models.EncryptedPasswordProfile.objects.create(user=user, **validated_data)


class BackwardCompatibleTokenObtainPairSerializer(TokenObtainPairSerializer):
def validate(self, attrs):
data = super().validate(attrs)


+ 11
- 4
containers/backend/api/tests/factories.py View File

@@ -7,8 +7,8 @@ class UserFactory(factory.DjangoModelFactory):
class Meta:
model = models.LessPassUser

email = factory.Sequence(lambda n: 'u{0}@lesspass.com'.format(n))
password = factory.PostGenerationMethodCall('set_password', 'password')
email = factory.Sequence(lambda n: "u{0}@lesspass.com".format(n))
password = factory.PostGenerationMethodCall("set_password", "password")
is_admin = False


@@ -21,5 +21,12 @@ class PasswordFactory(factory.DjangoModelFactory):
model = models.Password

user = factory.SubFactory(UserFactory)
login = 'admin@oslab.fr'
site = 'lesspass.com'
login = "admin@oslab.fr"
site = "lesspass.com"


class EncryptedPasswordProfileFactory(factory.DjangoModelFactory):
class Meta:
model = models.EncryptedPasswordProfile

user = factory.SubFactory(UserFactory)

+ 25
- 0
containers/backend/api/tests/test_user.py View File

@@ -0,0 +1,25 @@
from rest_framework.test import APITestCase, APIClient

from api import models
from api.tests import factories


class UserTestCase(APITestCase):
def setUp(self):
self.user = factories.UserFactory()
self.client = APIClient()
self.client.force_authenticate(user=self.user)

def test_auth_user_me(self):
request = self.client.get("/api/auth/users/me/")
self.assertEqual(request.status_code, 200)
self.assertIsNone(request.data["key"])

def test_update_auth_user_me(self):
self.assertIsNone(models.LessPassUser.objects.first().key)
request = self.client.put("/api/auth/users/me/", {"key": "abc"})
self.assertEqual(request.status_code, 200)
self.assertEqual("abc", models.LessPassUser.objects.first().key)
request = self.client.patch("/api/auth/users/me/", {"key": "def"})
self.assertEqual(request.status_code, 200)
self.assertEqual("def", models.LessPassUser.objects.first().key)

+ 106
- 0
containers/backend/api/tests/tests_encrypted_password_profiles.py View File

@@ -0,0 +1,106 @@
from rest_framework.test import APITestCase, APIClient

from api import models
from api.tests import factories


class LogoutEncryptedPasswordProfileTestCase(APITestCase):
def test_get_password_profiles_401(self):
response = self.client.get("/api/encrypted_password_profiles/")
self.assertEqual(401, response.status_code)


class LoginEncryptedPasswordProfileTestCase(APITestCase):
def setUp(self):
self.user = factories.UserFactory()
self.client = APIClient()
self.client.force_authenticate(user=self.user)

def test_get_empty_password_profiless(self):
request = self.client.get("/api/encrypted_password_profiles/")
self.assertEqual(0, len(request.data["results"]))

def test_retrieve_its_own_password_profiles(self):
factories.EncryptedPasswordProfileFactory(
user=self.user, password_profile="encrypted_content"
)
request = self.client.get("/api/encrypted_password_profiles/")
self.assertEqual(1, len(request.data["results"]))
self.assertEqual(
"encrypted_content",
request.data["results"][0]["password_profile"],
)

def test_cant_retrieve_other_password_profiles(self):
not_my_password_profile = factories.EncryptedPasswordProfileFactory(
user=factories.UserFactory()
)
request = self.client.get(
"/api/encrypted_password_profiles/%s/" % not_my_password_profile.id
)
self.assertEqual(404, request.status_code)

def test_delete_its_own_password_profiles(self):
password_profile = factories.EncryptedPasswordProfileFactory(user=self.user)
self.assertEqual(1, models.EncryptedPasswordProfile.objects.all().count())
request = self.client.delete(
"/api/encrypted_password_profiles/%s/" % password_profile.id
)
self.assertEqual(204, request.status_code)
self.assertEqual(0, models.EncryptedPasswordProfile.objects.all().count())

def test_cant_delete_other_password_profiles(self):
not_my_password_profile = factories.EncryptedPasswordProfileFactory(
user=factories.UserFactory()
)
self.assertEqual(1, models.EncryptedPasswordProfile.objects.all().count())
request = self.client.delete(
"/api/encrypted_password_profiles/%s/" % not_my_password_profile.id
)
self.assertEqual(404, request.status_code)
self.assertEqual(1, models.EncryptedPasswordProfile.objects.all().count())

def test_create_password(self):
self.assertEqual(0, models.EncryptedPasswordProfile.objects.count())
self.client.post(
"/api/encrypted_password_profiles/",
{"password_profile": "test_create_password"},
)
self.assertEqual(1, models.EncryptedPasswordProfile.objects.count())
password_profile = models.EncryptedPasswordProfile.objects.first()
self.assertEqual(password_profile.password_profile, "test_create_password")

def test_update_password_profile(self):
password_profile = factories.EncryptedPasswordProfileFactory(user=self.user)
self.assertNotEqual(
"test_update_password_profile", password_profile.password_profile
)
request = self.client.put(
"/api/encrypted_password_profiles/%s/" % password_profile.id,
{"password_profile": "test_update_password_profile"},
)
self.assertEqual(200, request.status_code)
password_profile_updated = models.EncryptedPasswordProfile.objects.get(
id=password_profile.id
)
self.assertEqual(
"test_update_password_profile", password_profile_updated.password_profile
)

def test_cant_update_other_password(self):
not_my_password_profile = factories.EncryptedPasswordProfileFactory(
user=factories.UserFactory(),
password_profile="test_cant_update_other_password",
)
self.assertEqual(
"test_cant_update_other_password", not_my_password_profile.password_profile
)
request = self.client.put(
"/api/encrypted_password_profiles/%s/" % not_my_password_profile.id,
{"password_profile": "not_my_password_profile"},
)
self.assertEqual(404, request.status_code)
self.assertEqual(
"test_cant_update_other_password",
models.EncryptedPasswordProfile.objects.first().password_profile,
)

+ 5
- 0
containers/backend/api/urls.py View File

@@ -7,6 +7,11 @@ from api import views

router = DefaultRouter()
router.register(r"passwords", views.PasswordViewSet, basename="passwords")
router.register(
r"encrypted_password_profiles",
views.EncryptedPasswordProfileViewSet,
basename="encrypted_password_profiles",
)
router.register(r"auth/register", djoser.views.UserViewSet, basename="auth_register")

urlpatterns = [


+ 11
- 0
containers/backend/api/views.py View File

@@ -21,6 +21,17 @@ class PasswordViewSet(viewsets.ModelViewSet):
return models.Password.objects.filter(user=self.request.user)


class EncryptedPasswordProfileViewSet(viewsets.ModelViewSet):
serializer_class = serializers.EncryptedPasswordProfileSerializer
permission_classes = (
permissions.IsAuthenticated,
IsOwner,
)

def get_queryset(self):
return models.EncryptedPasswordProfile.objects.filter(user=self.request.user)


class BackwardCompatibleTokenObtainPairView(TokenObtainPairView):
serializer_class = serializers.BackwardCompatibleTokenObtainPairSerializer



Loading…
Cancel
Save