@@ -47,7 +47,7 @@ class LessPassUserAdmin(BaseUserAdmin): | |||
form = UserChangeForm | |||
add_form = UserCreationForm | |||
list_display = ('email', 'is_admin', 'column_entries_count') | |||
list_display = ('email', 'is_admin', 'column_passwords_count') | |||
list_filter = ('is_admin', 'is_active') | |||
fieldsets = ( | |||
(None, {'fields': ('email', 'password')}), | |||
@@ -64,22 +64,22 @@ class LessPassUserAdmin(BaseUserAdmin): | |||
filter_horizontal = () | |||
def get_queryset(self, request): | |||
return models.LessPassUser.objects.annotate(entries_count=Count('entries')) | |||
return models.LessPassUser.objects.annotate(passwords_count=Count('passwords')) | |||
def column_entries_count(self, instance): | |||
return instance.entries_count | |||
def column_passwords_count(self, instance): | |||
return instance.passwords_count | |||
column_entries_count.short_description = 'Entries count' | |||
column_entries_count.admin_order_field = 'entries_count' | |||
column_passwords_count.short_description = 'Password count' | |||
column_passwords_count.admin_order_field = 'passwords_count' | |||
class EntryAdmin(admin.ModelAdmin): | |||
class PasswordAdmin(admin.ModelAdmin): | |||
list_display = ('id', 'user',) | |||
search_fields = ('user__email',) | |||
ordering = ('user',) | |||
class PasswordAdmin(admin.ModelAdmin): | |||
class EntryAdmin(admin.ModelAdmin): | |||
list_display = ('id', 'user',) | |||
search_fields = ('user__email',) | |||
ordering = ('user',) | |||
@@ -50,6 +50,10 @@ class LessPassUser(AbstractBaseUser): | |||
return True | |||
@property | |||
def is_superuser(self): | |||
return self.is_admin | |||
@property | |||
def is_staff(self): | |||
return self.is_admin | |||
@@ -56,3 +56,15 @@ class EntrySerializer(serializers.ModelSerializer): | |||
setattr(instance, attr, value) | |||
instance.save() | |||
return instance | |||
class PasswordSerializer(serializers.ModelSerializer): | |||
class Meta: | |||
model = models.Password | |||
fields = ('id', 'login', 'site', 'lowercase', 'uppercase', 'symbol', 'number', 'counter', 'length', | |||
'created', 'modified') | |||
read_only_fields = ('created', 'modified') | |||
def create(self, validated_data): | |||
user = self.context['request'].user | |||
return models.Password.objects.create(user=user, **validated_data) |
@@ -9,11 +9,11 @@ class UserFactory(factory.DjangoModelFactory): | |||
email = factory.Sequence(lambda n: 'u{0}@lesspass.com'.format(n)) | |||
password = factory.PostGenerationMethodCall('set_password', 'password') | |||
is_staff = False | |||
is_admin = False | |||
class AdminFactory(UserFactory): | |||
is_staff = True | |||
is_admin = True | |||
class PasswordInfoFactory(factory.DjangoModelFactory): | |||
@@ -0,0 +1,89 @@ | |||
from rest_framework.test import APITestCase, APIClient | |||
from api import models | |||
from api.tests import factories | |||
class LogoutApiTestCase(APITestCase): | |||
def test_get_passwords_401(self): | |||
response = self.client.get('/api/passwords/') | |||
self.assertEqual(401, response.status_code) | |||
class LoginApiTestCase(APITestCase): | |||
def setUp(self): | |||
self.user = factories.UserFactory() | |||
self.client = APIClient() | |||
self.client.force_authenticate(user=self.user) | |||
def test_get_empty_passwords(self): | |||
request = self.client.get('/api/passwords/') | |||
self.assertEqual(0, len(request.data['results'])) | |||
def test_retrieve_its_own_passwords(self): | |||
password = factories.PasswordFactory(user=self.user) | |||
request = self.client.get('/api/passwords/') | |||
self.assertEqual(1, len(request.data['results'])) | |||
self.assertEqual(password.site, request.data['results'][0]['site']) | |||
def test_cant_retrieve_other_passwords(self): | |||
not_my_password = factories.PasswordFactory(user=factories.UserFactory()) | |||
request = self.client.get('/api/passwords/%s/' % not_my_password.id) | |||
self.assertEqual(404, request.status_code) | |||
def test_delete_its_own_passwords(self): | |||
password = factories.PasswordFactory(user=self.user) | |||
self.assertEqual(1, models.Password.objects.all().count()) | |||
request = self.client.delete('/api/passwords/%s/' % password.id) | |||
self.assertEqual(204, request.status_code) | |||
self.assertEqual(0, models.Password.objects.all().count()) | |||
def test_cant_delete_other_password(self): | |||
not_my_password = factories.PasswordFactory(user=factories.UserFactory()) | |||
self.assertEqual(1, models.Password.objects.all().count()) | |||
request = self.client.delete('/api/passwords/%s/' % not_my_password.id) | |||
self.assertEqual(404, request.status_code) | |||
self.assertEqual(1, models.Password.objects.all().count()) | |||
def test_create_password(self): | |||
password = { | |||
"site": "lesspass.com", | |||
"login": "test@oslab.fr", | |||
"lowercase": True, | |||
"uppercase": True, | |||
"number": True, | |||
"symbol": True, | |||
"counter": 1, | |||
"length": 12 | |||
} | |||
self.assertEqual(0, models.Password.objects.count()) | |||
self.client.post('/api/passwords/', password) | |||
self.assertEqual(1, models.Password.objects.count()) | |||
def test_update_password(self): | |||
password = factories.PasswordFactory(user=self.user) | |||
self.assertNotEqual('facebook.com', password.site) | |||
new_password = { | |||
"site": "facebook.com", | |||
"login": "test@oslab.fr", | |||
"lowercase": True, | |||
"uppercase": True, | |||
"number": True, | |||
"symbol": True, | |||
"counter": 1, | |||
"length": 12 | |||
} | |||
request = self.client.put('/api/passwords/%s/' % password.id, new_password) | |||
self.assertEqual(200, request.status_code, request.content.decode('utf-8')) | |||
password_updated = models.Password.objects.get(id=password.id) | |||
self.assertEqual('facebook.com', password_updated.site) | |||
def test_cant_update_other_password(self): | |||
not_my_password = factories.PasswordFactory(user=factories.UserFactory()) | |||
self.assertEqual('lesspass.com', not_my_password.site) | |||
new_password = { | |||
"site": "facebook", | |||
} | |||
request = self.client.put('/api/passwords/%s/' % not_my_password.id, new_password) | |||
self.assertEqual(404, request.status_code) | |||
self.assertEqual(1, models.Password.objects.all().count()) |
@@ -6,11 +6,11 @@ from api import views | |||
router = DefaultRouter() | |||
router.register(r'entries', views.EntryViewSet, base_name='entries') | |||
router.register(r'passwords', views.PasswordViewSet, base_name='passwords') | |||
urlpatterns = [ | |||
url(r'^', include(router.urls)), | |||
url(r'^tokens/auth/', rest_framework_jwt.views.obtain_jwt_token), | |||
url(r'^tokens/verify/', rest_framework_jwt.views.verify_jwt_token), | |||
url(r'^tokens/refresh/', rest_framework_jwt.views.refresh_jwt_token), | |||
url(r'^auth/', include('djoser.urls')), | |||
] |
@@ -39,6 +39,16 @@ class AuthViewSet(viewsets.ViewSet): | |||
return Response(status=status.HTTP_401_UNAUTHORIZED) | |||
class PasswordViewSet(viewsets.ModelViewSet): | |||
serializer_class = serializers.PasswordSerializer | |||
permission_classes = (permissions.IsAuthenticated, IsOwner,) | |||
search_fields = ('site', 'email',) | |||
ordering_fields = ('site', 'email', 'created') | |||
def get_queryset(self): | |||
return models.Password.objects.filter(user=self.request.user) | |||
class EntryViewSet(viewsets.ModelViewSet): | |||
serializer_class = serializers.EntrySerializer | |||
permission_classes = (permissions.IsAuthenticated, IsOwner,) | |||
@@ -1,3 +0,0 @@ | |||
[DJANGO] | |||
secret_key = 1*5j2@2s1x12h(vm^yx_e^k5jz)n^l=bre4^tgd1*8m#oo5xxy | |||
@@ -1,37 +1,28 @@ | |||
import logging | |||
import os | |||
import random | |||
import sys | |||
import datetime | |||
from smartconfigparser import Config | |||
from envparse import env | |||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |||
CONFIG_PATH = os.path.join(BASE_DIR, 'config') | |||
if not os.path.exists(CONFIG_PATH): | |||
os.makedirs(CONFIG_PATH) | |||
CONFIG_FILE = os.path.join(CONFIG_PATH, 'config.ini') | |||
config = Config() | |||
config.read(CONFIG_FILE) | |||
def get_secret_key(secret_key): | |||
if not secret_key: | |||
return "".join([random.choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$^&*(-_=+)") for i in range(50)]) | |||
return secret_key | |||
try: | |||
SECRET_KEY = config.get('DJANGO', 'SECRET_KEY') | |||
except: | |||
print('SECRET_KEY not found! Generating a new one...') | |||
import random | |||
SECRET_KEY = env('SECRET_KEY', preprocessor=get_secret_key, default=None) | |||
SECRET_KEY = "".join([random.choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$^&*(-_=+)") for i in range(50)]) | |||
if not config.has_section('DJANGO'): | |||
config.add_section('DJANGO') | |||
config.set('DJANGO', 'SECRET_KEY', SECRET_KEY) | |||
with open(CONFIG_FILE, 'wt') as f: | |||
config.write(f) | |||
DEBUG = env.bool('DJANGO_DEBUG', default=True) | |||
DEBUG = config.getboolean('DJANGO', 'DEBUG', False) | |||
ALLOWED_HOSTS = [] | |||
ALLOWED_HOSTS = config.getlist('DJANGO', 'ALLOWED_HOSTS', ['localhost', '127.0.0.1', '.lesspass.com']) | |||
ADMIN = [('Guillaume Vincent', 'guillaume@oslab.fr'), ] | |||
INSTALLED_APPS = [ | |||
'api.apps.ApiConfig', | |||
'django.contrib.admin', | |||
'django.contrib.auth', | |||
'django.contrib.contenttypes', | |||
@@ -39,16 +30,15 @@ INSTALLED_APPS = [ | |||
'django.contrib.messages', | |||
'django.contrib.staticfiles', | |||
'rest_framework', | |||
'djoser' | |||
'api' | |||
] | |||
MIDDLEWARE_CLASSES = [ | |||
MIDDLEWARE = [ | |||
'django.middleware.security.SecurityMiddleware', | |||
'django.contrib.sessions.middleware.SessionMiddleware', | |||
'django.middleware.common.CommonMiddleware', | |||
'django.middleware.csrf.CsrfViewMiddleware', | |||
'django.contrib.auth.middleware.AuthenticationMiddleware', | |||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware', | |||
'django.contrib.messages.middleware.MessageMiddleware', | |||
'django.middleware.clickjacking.XFrameOptionsMiddleware', | |||
] | |||
@@ -75,28 +65,20 @@ WSGI_APPLICATION = 'lesspass.wsgi.application' | |||
DATABASES = { | |||
'default': { | |||
'ENGINE': os.getenv('DATABASE_ENGINE', 'django.db.backends.sqlite3'), | |||
'NAME': os.getenv('DATABASE_NAME', 'db.sqlite3'), | |||
'USER': os.getenv('DATABASE_USER', None), | |||
'PASSWORD': os.getenv('DATABASE_PASSWORD', None), | |||
'HOST': os.getenv('DATABASE_HOST', None), | |||
'PORT': os.getenv('DATABASE_PORT', None), | |||
'ENGINE': env('DATABASE_ENGINE', default='django.db.backends.sqlite3'), | |||
'NAME': env('DATABASE_NAME', default=os.path.join(BASE_DIR, 'db.sqlite3')), | |||
'USER': env('DATABASE_USER', default=None), | |||
'PASSWORD': env('DATABASE_PASSWORD', default=None), | |||
'HOST': env('DATABASE_HOST', default=None), | |||
'PORT': env('DATABASE_PORT', default=None), | |||
} | |||
} | |||
AUTH_PASSWORD_VALIDATORS = [ | |||
{ | |||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', | |||
}, | |||
{ | |||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', | |||
}, | |||
{ | |||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', | |||
}, | |||
{ | |||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', | |||
}, | |||
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, | |||
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, | |||
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, | |||
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, | |||
] | |||
LANGUAGE_CODE = 'en-us' | |||
@@ -107,22 +89,52 @@ USE_I18N = True | |||
USE_L10N = True | |||
USE_TZ = False | |||
USE_TZ = True | |||
STATIC_URL = '/static/' | |||
STATIC_ROOT = os.path.join(BASE_DIR, 'www', 'static') | |||
MEDIA_URL = '/media/' | |||
MEDIA_ROOT = os.path.join(BASE_DIR, 'www', 'media') | |||
LOGGING = { | |||
'version': 1, | |||
'disable_existing_loggers': False, | |||
'handlers': { | |||
'console': { | |||
'class': 'logging.StreamHandler', | |||
} | |||
}, | |||
'root': { | |||
'handlers': ['console'], | |||
'level': env('DJANGO_LOG_LEVEL', default='DEBUG'), | |||
} | |||
} | |||
AUTH_USER_MODEL = 'api.LessPassUser' | |||
JWT_AUTH = { | |||
'JWT_EXPIRATION_DELTA': datetime.timedelta(hours=12), | |||
'JWT_ALLOW_REFRESH': True, | |||
} | |||
EMAIL_BACKEND = 'django.api.mail.backends.console.EmailBackend' | |||
REST_FRAMEWORK = { | |||
'DEFAULT_PERMISSION_CLASSES': ( | |||
'rest_framework.permissions.IsAuthenticated', | |||
), | |||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', | |||
'PAGE_SIZE': 20, | |||
'PAGE_SIZE': 50, | |||
'DEFAULT_FILTER_BACKENDS': ( | |||
'rest_framework.filters.OrderingFilter', | |||
'rest_framework.filters.SearchFilter', | |||
), | |||
'DEFAULT_AUTHENTICATION_CLASSES': ( | |||
'rest_framework_jwt.authentication.JSONWebTokenAuthentication', | |||
'rest_framework.authentication.SessionAuthentication', | |||
), | |||
'DEFAULT_RENDERER_CLASSES': ( | |||
'rest_framework.renderers.JSONRenderer', | |||
@@ -134,31 +146,22 @@ REST_FRAMEWORK = { | |||
'TEST_REQUEST_DEFAULT_FORMAT': 'json' | |||
} | |||
AUTHENTICATION_BACKENDS = ( | |||
'django.contrib.auth.backends.ModelBackend', | |||
) | |||
JWT_AUTH = { | |||
'JWT_EXPIRATION_DELTA': datetime.timedelta(hours=12), | |||
'JWT_ALLOW_REFRESH': True, | |||
} | |||
class DisableMigrations(object): | |||
def __contains__(self, item): | |||
return True | |||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' | |||
def __getitem__(self, item): | |||
return "notmigrations" | |||
STATIC_ROOT = os.path.join(BASE_DIR, 'www', 'static') | |||
AUTH_USER_MODEL = 'api.LessPassUser' | |||
LOGGING = { | |||
'version': 1, | |||
'disable_existing_loggers': False, | |||
'handlers': { | |||
'console': { | |||
'class': 'logging.StreamHandler', | |||
} | |||
}, | |||
'root': { | |||
'handlers': ['console'], | |||
'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG'), | |||
} | |||
} | |||
TESTS_IN_PROGRESS = False | |||
if 'test' in sys.argv[1:] or 'jenkins' in sys.argv[1:]: | |||
logging.disable(logging.CRITICAL) | |||
PASSWORD_HASHERS = ( | |||
'django.contrib.auth.hashers.MD5PasswordHasher', | |||
) | |||
DEBUG = False | |||
TEMPLATE_DEBUG = False | |||
TESTS_IN_PROGRESS = True | |||
MIGRATION_MODULES = DisableMigrations() |