closes #580pull/583/head
@@ -1,2 +1,4 @@ | |||
www | |||
*.sqlite3 | |||
# Ignore virtual env | |||
venv |
@@ -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), | |||
), | |||
] |
@@ -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, | |||
}, | |||
), | |||
] |
@@ -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) |
@@ -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) | |||
@@ -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) |
@@ -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) |
@@ -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, | |||
) |
@@ -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 = [ | |||
@@ -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 | |||