@@ -1,2 +1,4 @@ | |||||
www | www | ||||
*.sqlite3 | *.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 | import uuid | ||||
from django.db import models | from django.db import models | ||||
from django.contrib.auth.models import ( | |||||
BaseUserManager, AbstractBaseUser | |||||
) | |||||
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser | |||||
class LesspassUserManager(BaseUserManager): | class LesspassUserManager(BaseUserManager): | ||||
def create_user(self, email, password=None): | def create_user(self, email, password=None): | ||||
if not email: | if not email: | ||||
raise ValueError('Users must have an email address') | |||||
raise ValueError("Users must have an email address") | |||||
user = self.model( | user = self.model( | ||||
email=self.normalize_email(email), | email=self.normalize_email(email), | ||||
@@ -19,20 +17,25 @@ class LesspassUserManager(BaseUserManager): | |||||
return user | return user | ||||
def create_superuser(self, email, password): | 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.is_admin = True | ||||
user.save(using=self._db) | user.save(using=self._db) | ||||
return user | return user | ||||
class LessPassUser(AbstractBaseUser): | 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_active = models.BooleanField(default=True) | ||||
is_admin = models.BooleanField(default=False) | is_admin = models.BooleanField(default=False) | ||||
key = models.TextField(null=True) | |||||
objects = LesspassUserManager() | objects = LesspassUserManager() | ||||
USERNAME_FIELD = 'email' | |||||
USERNAME_FIELD = "email" | |||||
REQUIRED_FIELDS = ["key"] | |||||
def get_full_name(self): | def get_full_name(self): | ||||
return self.email | return self.email | ||||
@@ -62,8 +65,8 @@ class LessPassUser(AbstractBaseUser): | |||||
class DateMixin(models.Model): | 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: | class Meta: | ||||
abstract = True | abstract = True | ||||
@@ -71,7 +74,9 @@ class DateMixin(models.Model): | |||||
class Password(DateMixin): | class Password(DateMixin): | ||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) | 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) | login = models.CharField(max_length=255, null=True, blank=True) | ||||
site = 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): | def __str__(self): | ||||
return str(self.id) | 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) | 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): | class BackwardCompatibleTokenObtainPairSerializer(TokenObtainPairSerializer): | ||||
def validate(self, attrs): | def validate(self, attrs): | ||||
data = super().validate(attrs) | data = super().validate(attrs) | ||||
@@ -7,8 +7,8 @@ class UserFactory(factory.DjangoModelFactory): | |||||
class Meta: | class Meta: | ||||
model = models.LessPassUser | 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 | is_admin = False | ||||
@@ -21,5 +21,12 @@ class PasswordFactory(factory.DjangoModelFactory): | |||||
model = models.Password | model = models.Password | ||||
user = factory.SubFactory(UserFactory) | 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 = DefaultRouter() | ||||
router.register(r"passwords", views.PasswordViewSet, basename="passwords") | 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") | router.register(r"auth/register", djoser.views.UserViewSet, basename="auth_register") | ||||
urlpatterns = [ | urlpatterns = [ | ||||
@@ -21,6 +21,17 @@ class PasswordViewSet(viewsets.ModelViewSet): | |||||
return models.Password.objects.filter(user=self.request.user) | 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): | class BackwardCompatibleTokenObtainPairView(TokenObtainPairView): | ||||
serializer_class = serializers.BackwardCompatibleTokenObtainPairSerializer | serializer_class = serializers.BackwardCompatibleTokenObtainPairSerializer | ||||