tox: add code-style target
This commit is contained in:
parent
283a0eae60
commit
2ce0cf927e
|
@ -0,0 +1,18 @@
|
|||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: ['--target-version', 'py37', '--skip-string-normalization', '--line-length', '110']
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.7.0
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ['--profile', 'black', '--line-length', '110']
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.20.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: ['--keep-percent-format', '--py37-plus']
|
|
@ -1,7 +1,7 @@
|
|||
# This file is sourced by "execfile" from petale.settings
|
||||
|
||||
import os
|
||||
import glob
|
||||
import os
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
|
|
|
@ -15,15 +15,15 @@
|
|||
DEBUG = False
|
||||
TEMPLATE_DEBUG = False
|
||||
|
||||
#ADMINS = (
|
||||
# ADMINS = (
|
||||
# # ('User 1', 'watchdog@example.net'),
|
||||
# # ('User 2', 'janitor@example.net'),
|
||||
#)
|
||||
# )
|
||||
|
||||
# ALLOWED_HOSTS must be correct in production!
|
||||
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
||||
ALLOWED_HOSTS = [
|
||||
'*',
|
||||
'*',
|
||||
]
|
||||
|
||||
# Databases
|
||||
|
|
|
@ -24,6 +24,7 @@ class PartnerAdmin(admin.ModelAdmin):
|
|||
list_display = ['name', 'admin_emails']
|
||||
readonly_fields = ['size']
|
||||
|
||||
|
||||
admin.site.register(models.Partner, PartnerAdmin)
|
||||
|
||||
|
||||
|
@ -31,6 +32,7 @@ class CUTAdmin(admin.ModelAdmin):
|
|||
search_fields = ['uuid']
|
||||
list_display = ['uuid']
|
||||
|
||||
|
||||
admin.site.register(models.CUT, CUTAdmin)
|
||||
|
||||
|
||||
|
@ -40,6 +42,7 @@ class PetalAdmin(admin.ModelAdmin):
|
|||
readonly_fields = ['etag', 'size']
|
||||
raw_id_fields = ['cut']
|
||||
|
||||
|
||||
admin.site.register(models.Petal, PetalAdmin)
|
||||
|
||||
|
||||
|
@ -47,4 +50,5 @@ class AccessControlListAdmin(admin.ModelAdmin):
|
|||
search_fields = ['partner__name', 'user__username']
|
||||
list_display = ['partner', 'user', 'order', 'methods', 'key']
|
||||
|
||||
|
||||
admin.site.register(models.AccessControlList, AccessControlListAdmin)
|
||||
|
|
|
@ -14,36 +14,41 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
try:
|
||||
from functools import reduce
|
||||
except ImportError:
|
||||
pass
|
||||
import requests
|
||||
|
||||
try:
|
||||
from time import process_time
|
||||
except ImportError:
|
||||
from time import clock as process_time
|
||||
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
|
||||
from django.db.models.query import Q, F
|
||||
from django.http import StreamingHttpResponse, HttpResponse
|
||||
from django.conf import settings
|
||||
from django.db.transaction import atomic
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
from atomicwrites import atomic_write
|
||||
from django.conf import settings
|
||||
from django.db.models.query import F, Q
|
||||
from django.db.transaction import atomic
|
||||
from django.http import HttpResponse, StreamingHttpResponse
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .models import CUT, Petal, Partner, AccessControlList
|
||||
from .utils import logit, StreamingHash
|
||||
from .exceptions import (PartnerNotFound, CutNotFound, KeyNotFound, NotFound, MissingContentType,
|
||||
ConcurrentAccess, PreconditionException)
|
||||
from .exceptions import (
|
||||
ConcurrentAccess,
|
||||
CutNotFound,
|
||||
KeyNotFound,
|
||||
MissingContentType,
|
||||
NotFound,
|
||||
PartnerNotFound,
|
||||
PreconditionException,
|
||||
)
|
||||
from .models import CUT, AccessControlList, Partner, Petal
|
||||
from .utils import StreamingHash, logit
|
||||
|
||||
|
||||
def cut_exists(request, cut_uuid):
|
||||
|
@ -55,25 +60,26 @@ def cut_exists(request, cut_uuid):
|
|||
|
||||
authentic_url = getattr(settings, 'PETALE_AUTHENTIC_URL', None)
|
||||
if not authentic_url:
|
||||
logger.warning(u'PETALE_AUTHENTIC SETTINGS improperly defined')
|
||||
logger.warning('PETALE_AUTHENTIC SETTINGS improperly defined')
|
||||
return False
|
||||
|
||||
url = urlparse.urljoin(authentic_url, 'api/users/synchronization/')
|
||||
try:
|
||||
response = requests.post(url, json={"known_uuids": [cut_uuid]},
|
||||
auth=request.user.credentials, verify=False)
|
||||
response = requests.post(
|
||||
url, json={"known_uuids": [cut_uuid]}, auth=request.user.credentials, verify=False
|
||||
)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
logger.warning(u'authentic synchro API failed: %s', e)
|
||||
logger.warning('authentic synchro API failed: %s', e)
|
||||
return False
|
||||
try:
|
||||
data = response.json()
|
||||
except ValueError as e:
|
||||
logger.warning(u'authentic synchro API failed to decode response: %s', e)
|
||||
logger.warning('authentic synchro API failed to decode response: %s', e)
|
||||
return False
|
||||
|
||||
if data.get("unknown_uuids"):
|
||||
logger.warning(u'unknown uuid : %s %s', request.user.credentials[0], data)
|
||||
logger.warning('unknown uuid : %s %s', request.user.credentials[0], data)
|
||||
return False
|
||||
|
||||
CUT.objects.get_or_create(uuid=cut_uuid)
|
||||
|
@ -122,9 +128,7 @@ class PetalAPIKeysView(APIView):
|
|||
if key_filter:
|
||||
qs = qs.filter(key_filter)
|
||||
|
||||
return Response({
|
||||
'keys': [petal.name for petal in qs]
|
||||
})
|
||||
return Response({'keys': [petal.name for petal in qs]})
|
||||
|
||||
@logit
|
||||
def delete(self, request, partner_name, cut_uuid):
|
||||
|
@ -146,11 +150,7 @@ class PetalAPIView(APIView):
|
|||
if_none_match = if_none_match and [x.strip() for x in if_none_match.split(',')]
|
||||
|
||||
try:
|
||||
qs = Petal.objects.filter(
|
||||
name=petal_name,
|
||||
partner_id__name=partner_name,
|
||||
cut_id__uuid=cut_uuid
|
||||
)
|
||||
qs = Petal.objects.filter(name=petal_name, partner_id__name=partner_name, cut_id__uuid=cut_uuid)
|
||||
qs = qs.select_related('partner', 'cut')
|
||||
petal = qs.get()
|
||||
except Petal.DoesNotExist:
|
||||
|
@ -193,18 +193,13 @@ class PetalAPIView(APIView):
|
|||
petal = self.get_petal(partner_name, cut_uuid, petal_name)
|
||||
if if_none_match:
|
||||
if if_none_match == ['*'] or petal.etag in if_none_match:
|
||||
return Response(
|
||||
status=status.HTTP_304_NOT_MODIFIED,
|
||||
headers={
|
||||
'ETag': petal.etag
|
||||
}
|
||||
)
|
||||
return Response(status=status.HTTP_304_NOT_MODIFIED, headers={'ETag': petal.etag})
|
||||
# verify file exists before creating a StreamingHttpResponse
|
||||
# as StreamingHttpResponse generate its content after the Django global try/catch
|
||||
try:
|
||||
petal.data.open(mode='rb')
|
||||
response = HttpResponse(petal.data.read(), content_type=petal.content_type)
|
||||
except IOError:
|
||||
except OSError:
|
||||
continue
|
||||
finally:
|
||||
petal.data.close()
|
||||
|
@ -227,9 +222,9 @@ class PetalAPIView(APIView):
|
|||
raise MissingContentType
|
||||
|
||||
try:
|
||||
petal = self.get_petal(partner_name, cut_uuid, petal_name,
|
||||
if_match=if_match,
|
||||
if_none_match=if_none_match)
|
||||
petal = self.get_petal(
|
||||
partner_name, cut_uuid, petal_name, if_match=if_match, if_none_match=if_none_match
|
||||
)
|
||||
created = False
|
||||
except PreconditionException:
|
||||
raise ConcurrentAccess
|
||||
|
@ -244,8 +239,8 @@ class PetalAPIView(APIView):
|
|||
raise CutNotFound
|
||||
|
||||
petal, created = Petal.objects.get_or_create(
|
||||
name=petal_name, partner=partner, cut=cut,
|
||||
defaults={'size': 0})
|
||||
name=petal_name, partner=partner, cut=cut, defaults={'size': 0}
|
||||
)
|
||||
if not created and if_none_match:
|
||||
raise ConcurrentAccess
|
||||
else:
|
||||
|
@ -283,12 +278,7 @@ class PetalAPIView(APIView):
|
|||
fd.write(block)
|
||||
update_meta()
|
||||
|
||||
return Response(
|
||||
{},
|
||||
status=status_code,
|
||||
headers={
|
||||
'ETag': petal.etag
|
||||
})
|
||||
return Response({}, status=status_code, headers={'ETag': petal.etag})
|
||||
|
||||
@logit
|
||||
def delete(self, request, partner_name, cut_uuid, petal_name):
|
||||
|
@ -297,9 +287,8 @@ class PetalAPIView(APIView):
|
|||
|
||||
try:
|
||||
petal = self.get_petal(
|
||||
partner_name, cut_uuid, petal_name,
|
||||
if_match=if_match,
|
||||
if_none_match=if_none_match)
|
||||
partner_name, cut_uuid, petal_name, if_match=if_match, if_none_match=if_none_match
|
||||
)
|
||||
except PreconditionException:
|
||||
raise ConcurrentAccess
|
||||
petal.delete()
|
||||
|
|
|
@ -17,52 +17,48 @@
|
|||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from rest_framework.authentication import BasicAuthentication
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
|
||||
|
||||
class PetalAuthentication(BasicAuthentication):
|
||||
|
||||
def authentic_proxy(self, userid, password):
|
||||
'''Check userid and password with configured Authentic IdP, and verify it is an OIDC
|
||||
client.
|
||||
client.
|
||||
'''
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
authentic_url = getattr(settings, 'PETALE_AUTHENTIC_URL', None)
|
||||
if not authentic_url:
|
||||
logger.warning(u'authentic check-password not configured')
|
||||
logger.warning('authentic check-password not configured')
|
||||
return False, ''
|
||||
|
||||
authentic_auth = getattr(settings, 'PETALE_AUTHENTIC_AUTH', None)
|
||||
if not authentic_auth:
|
||||
logger.warning(u'authentic check-password not configured')
|
||||
logger.warning('authentic check-password not configured')
|
||||
return False, ''
|
||||
|
||||
url = urlparse.urljoin(authentic_url, 'api/check-password/')
|
||||
try:
|
||||
response = requests.post(url, json={
|
||||
'username': userid,
|
||||
'password': password}, auth=authentic_auth, verify=False)
|
||||
response = requests.post(
|
||||
url, json={'username': userid, 'password': password}, auth=authentic_auth, verify=False
|
||||
)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
logger.warning(u'authentic check-password API failed: %s', e)
|
||||
logger.warning('authentic check-password API failed: %s', e)
|
||||
return False, 'authentic is down'
|
||||
try:
|
||||
response = response.json()
|
||||
except ValueError as e:
|
||||
logger.warning(u'authentic check-password API failed: %s, %r', e, response.content)
|
||||
logger.warning('authentic check-password API failed: %s, %r', e, response.content)
|
||||
return False, 'authentic is down'
|
||||
|
||||
if response.get('result') == 0:
|
||||
logger.warning(u'authentic check-password API failed')
|
||||
logger.warning('authentic check-password API failed')
|
||||
return False, response.get('errors', [''])[0]
|
||||
|
||||
return True, None
|
||||
|
@ -70,8 +66,7 @@ class PetalAuthentication(BasicAuthentication):
|
|||
def authenticate_credentials(self, userid, password, request=None):
|
||||
username = userid[:30]
|
||||
try:
|
||||
user, auth = super(PetalAuthentication, self).authenticate_credentials(username,
|
||||
password)
|
||||
user, auth = super().authenticate_credentials(username, password)
|
||||
except AuthenticationFailed:
|
||||
success, error = self.authentic_proxy(userid, password)
|
||||
if not success:
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.core.validators
|
||||
import petale.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import petale.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -17,25 +15,73 @@ class Migration(migrations.Migration):
|
|||
migrations.CreateModel(
|
||||
name='AccessControlList',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
(
|
||||
'id',
|
||||
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
|
||||
),
|
||||
('order', models.IntegerField(verbose_name='Order')),
|
||||
('methods', models.CharField(default='GET,PUT,DELETE', help_text='GET, PUT, DELETE', max_length=128, verbose_name='Allowed methods')),
|
||||
('key', models.CharField(default='*', max_length=128, verbose_name='Allowed keys', validators=[])),
|
||||
(
|
||||
'methods',
|
||||
models.CharField(
|
||||
default='GET,PUT,DELETE',
|
||||
help_text='GET, PUT, DELETE',
|
||||
max_length=128,
|
||||
verbose_name='Allowed methods',
|
||||
),
|
||||
),
|
||||
(
|
||||
'key',
|
||||
models.CharField(default='*', max_length=128, verbose_name='Allowed keys', validators=[]),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CUTIdentifier',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('uuid', models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$', message='Invalid uuid format')])),
|
||||
(
|
||||
'id',
|
||||
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
|
||||
),
|
||||
(
|
||||
'uuid',
|
||||
models.CharField(
|
||||
max_length=32,
|
||||
validators=[
|
||||
django.core.validators.RegexValidator(
|
||||
'^[A-Za-z0-9-_]+$', message='Invalid uuid format'
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Partner',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(max_length=64, verbose_name='Partner', validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$', message='Invalid name format')])),
|
||||
('admin_emails', models.CharField(help_text='List of admin emails separated by comma', max_length=256, verbose_name='Admin emails')),
|
||||
(
|
||||
'id',
|
||||
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
|
||||
),
|
||||
(
|
||||
'name',
|
||||
models.CharField(
|
||||
max_length=64,
|
||||
verbose_name='Partner',
|
||||
validators=[
|
||||
django.core.validators.RegexValidator(
|
||||
'^[A-Za-z0-9-_]+$', message='Invalid name format'
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
(
|
||||
'admin_emails',
|
||||
models.CharField(
|
||||
help_text='List of admin emails separated by comma',
|
||||
max_length=256,
|
||||
verbose_name='Admin emails',
|
||||
),
|
||||
),
|
||||
('hard_global_max_size', models.IntegerField(verbose_name='Hard max size')),
|
||||
('soft_global_max_size', models.IntegerField(verbose_name='Soft max size')),
|
||||
('hard_per_key_max_size', models.IntegerField(verbose_name='Hard max size per key')),
|
||||
|
@ -45,10 +91,24 @@ class Migration(migrations.Migration):
|
|||
migrations.CreateModel(
|
||||
name='Petal',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
(
|
||||
'id',
|
||||
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
|
||||
),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name', validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$', message='Invalid name format')])),
|
||||
(
|
||||
'name',
|
||||
models.CharField(
|
||||
max_length=128,
|
||||
verbose_name='Name',
|
||||
validators=[
|
||||
django.core.validators.RegexValidator(
|
||||
'^[A-Za-z0-9-_]+$', message='Invalid name format'
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
('etag', models.CharField(max_length=256, verbose_name='ETag', blank=True)),
|
||||
('data', models.FileField(upload_to='data', verbose_name='Data Content', blank=True)),
|
||||
('content_type', models.CharField(max_length=128, verbose_name='Content type', blank=True)),
|
||||
|
@ -69,6 +129,6 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='petal',
|
||||
unique_together=set([('name', 'partner_id', 'cut_id')]),
|
||||
unique_together={('name', 'partner_id', 'cut_id')},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import petale.models
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import petale.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -32,17 +30,25 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='accesscontrollist',
|
||||
name='user',
|
||||
field=models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
|
||||
field=models.ForeignKey(
|
||||
verbose_name='User', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cutidentifier',
|
||||
name='uuid',
|
||||
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')]),
|
||||
field=models.CharField(
|
||||
max_length=32, validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')]
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='partner',
|
||||
name='name',
|
||||
field=models.CharField(max_length=64, verbose_name='Partner', validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')]),
|
||||
field=models.CharField(
|
||||
max_length=64,
|
||||
verbose_name='Partner',
|
||||
validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='petal',
|
||||
|
@ -67,7 +73,11 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='petal',
|
||||
name='name',
|
||||
field=models.CharField(max_length=128, verbose_name='Name', validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')]),
|
||||
field=models.CharField(
|
||||
max_length=128,
|
||||
verbose_name='Name',
|
||||
validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='petal',
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -14,7 +11,11 @@ class Migration(migrations.Migration):
|
|||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='accesscontrollist',
|
||||
options={'ordering': ['partner__name', 'user__username', 'order', 'key', 'methods'], 'verbose_name': 'Access control list', 'verbose_name_plural': 'Access control lists'},
|
||||
options={
|
||||
'ordering': ['partner__name', 'user__username', 'order', 'key', 'methods'],
|
||||
'verbose_name': 'Access control list',
|
||||
'verbose_name_plural': 'Access control lists',
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='cut',
|
||||
|
@ -36,7 +37,12 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='partner',
|
||||
name='admin_emails',
|
||||
field=models.CharField(help_text='List of admin emails separated by comma', max_length=256, verbose_name='Admin emails', blank=True),
|
||||
field=models.CharField(
|
||||
help_text='List of admin emails separated by comma',
|
||||
max_length=256,
|
||||
verbose_name='Admin emails',
|
||||
blank=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='partner',
|
||||
|
@ -51,7 +57,12 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='partner',
|
||||
name='name',
|
||||
field=models.CharField(unique=True, max_length=64, verbose_name='Partner', validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')]),
|
||||
field=models.CharField(
|
||||
unique=True,
|
||||
max_length=64,
|
||||
verbose_name='Partner',
|
||||
validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')],
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='partner',
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import petale.models
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
import petale.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -16,11 +14,15 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='cut',
|
||||
name='uuid',
|
||||
field=models.CharField(max_length=255, validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')]),
|
||||
field=models.CharField(
|
||||
max_length=255, validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')]
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='petal',
|
||||
name='data',
|
||||
field=models.FileField(upload_to=petale.models.petal_directory, max_length=512, verbose_name='Data Content'),
|
||||
field=models.FileField(
|
||||
upload_to=petale.models.petal_directory, max_length=512, verbose_name='Data Content'
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -15,6 +12,10 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='cut',
|
||||
name='uuid',
|
||||
field=models.CharField(unique=True, max_length=255, validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')]),
|
||||
field=models.CharField(
|
||||
unique=True,
|
||||
max_length=255,
|
||||
validators=[django.core.validators.RegexValidator('^[A-Za-z0-9-_]+$')],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
151
petale/models.py
151
petale/models.py
|
@ -14,53 +14,39 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import hashlib
|
||||
|
||||
from django.utils import six
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.validators import RegexValidator
|
||||
from django.core.mail import send_mail
|
||||
|
||||
from .exceptions import GlobalSpaceExhausted, PetalSizeExhausted
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.utils import six
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from . import utils
|
||||
|
||||
from .exceptions import GlobalSpaceExhausted, PetalSizeExhausted
|
||||
|
||||
id_validator = RegexValidator('^[A-Za-z0-9-_]+$')
|
||||
|
||||
|
||||
@six.python_2_unicode_compatible
|
||||
class Partner(models.Model):
|
||||
name = models.CharField(
|
||||
verbose_name=_('Partner'),
|
||||
max_length=64,
|
||||
unique=True,
|
||||
validators=[id_validator])
|
||||
name = models.CharField(verbose_name=_('Partner'), max_length=64, unique=True, validators=[id_validator])
|
||||
admin_emails = models.CharField(
|
||||
verbose_name=_('Admin emails'),
|
||||
max_length=256,
|
||||
blank=True,
|
||||
help_text=_('List of admin emails separated by comma'))
|
||||
hard_global_max_size = models.IntegerField(
|
||||
verbose_name=_('Hard max size'),
|
||||
help_text=_('as kilobytes'))
|
||||
soft_global_max_size = models.IntegerField(
|
||||
verbose_name=_('Soft max size'),
|
||||
help_text=_('as kilobytes'))
|
||||
help_text=_('List of admin emails separated by comma'),
|
||||
)
|
||||
hard_global_max_size = models.IntegerField(verbose_name=_('Hard max size'), help_text=_('as kilobytes'))
|
||||
soft_global_max_size = models.IntegerField(verbose_name=_('Soft max size'), help_text=_('as kilobytes'))
|
||||
hard_per_key_max_size = models.IntegerField(
|
||||
verbose_name=_('Hard max size per key'),
|
||||
help_text=_('as kilobytes'))
|
||||
verbose_name=_('Hard max size per key'), help_text=_('as kilobytes')
|
||||
)
|
||||
soft_per_key_max_size = models.IntegerField(
|
||||
verbose_name=_('Soft max size per key'),
|
||||
help_text=_('as kilobytes'))
|
||||
size = models.BigIntegerField(
|
||||
verbose_name=_('Size'),
|
||||
default=0,
|
||||
help_text=_('as bytes'))
|
||||
verbose_name=_('Soft max size per key'), help_text=_('as kilobytes')
|
||||
)
|
||||
size = models.BigIntegerField(verbose_name=_('Size'), default=0, help_text=_('as bytes'))
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
@ -71,15 +57,15 @@ class Partner(models.Model):
|
|||
if new_size > self.hard_global_max_size * 1024:
|
||||
raise GlobalSpaceExhausted
|
||||
|
||||
if (self.size < self.soft_global_max_size * 1024
|
||||
and new_size > self.soft_global_max_size * 1024):
|
||||
if self.size < self.soft_global_max_size * 1024 and new_size > self.soft_global_max_size * 1024:
|
||||
self.notify_admins(
|
||||
subject=_('Partner %s space almost exhausted') % self.name,
|
||||
# pylint: disable=no-member
|
||||
body=_('Current size: {current_size}, Max size: {max_size}').format(
|
||||
current_size=new_size,
|
||||
max_size=self.hard_global_max_size * 1024),
|
||||
**kwargs)
|
||||
current_size=new_size, max_size=self.hard_global_max_size * 1024
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def notify_admins(self, subject, body, **kwargs):
|
||||
if kwargs:
|
||||
|
@ -94,12 +80,8 @@ class Partner(models.Model):
|
|||
ordering = ['name']
|
||||
|
||||
|
||||
@six.python_2_unicode_compatible
|
||||
class CUT(models.Model):
|
||||
uuid = models.CharField(
|
||||
max_length=255,
|
||||
validators=[id_validator],
|
||||
unique=True)
|
||||
uuid = models.CharField(max_length=255, validators=[id_validator], unique=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.uuid
|
||||
|
@ -116,44 +98,27 @@ def petal_directory(instance, filename):
|
|||
assert instance.cut
|
||||
assert instance.cut.uuid
|
||||
|
||||
return 'data/{0}/{1}/{2}/{3}'.format(
|
||||
return 'data/{}/{}/{}/{}'.format(
|
||||
instance.partner.name,
|
||||
hashlib.md5(instance.cut.uuid.encode('ascii')).hexdigest()[:3],
|
||||
instance.cut.uuid,
|
||||
instance.name)
|
||||
instance.name,
|
||||
)
|
||||
|
||||
|
||||
@six.python_2_unicode_compatible
|
||||
class Petal(models.Model):
|
||||
created_at = models.DateTimeField(
|
||||
_('Created'),
|
||||
auto_now_add=True)
|
||||
updated_at = models.DateTimeField(
|
||||
_('Updated'),
|
||||
auto_now=True)
|
||||
name = models.CharField(
|
||||
_('Name'),
|
||||
max_length=128,
|
||||
validators=[id_validator])
|
||||
etag = models.CharField(
|
||||
_('ETag'),
|
||||
max_length=256)
|
||||
data = models.FileField(
|
||||
_('Data Content'),
|
||||
max_length=512,
|
||||
upload_to=petal_directory)
|
||||
content_type = models.CharField(
|
||||
_('Content type'),
|
||||
max_length=128)
|
||||
size = models.IntegerField(
|
||||
_('Size'),
|
||||
default=0,
|
||||
help_text=_('as bytes'))
|
||||
created_at = models.DateTimeField(_('Created'), auto_now_add=True)
|
||||
updated_at = models.DateTimeField(_('Updated'), auto_now=True)
|
||||
name = models.CharField(_('Name'), max_length=128, validators=[id_validator])
|
||||
etag = models.CharField(_('ETag'), max_length=256)
|
||||
data = models.FileField(_('Data Content'), max_length=512, upload_to=petal_directory)
|
||||
content_type = models.CharField(_('Content type'), max_length=128)
|
||||
size = models.IntegerField(_('Size'), default=0, help_text=_('as bytes'))
|
||||
cut = models.ForeignKey(CUT, on_delete=models.CASCADE)
|
||||
partner = models.ForeignKey(Partner, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return u'%s/%s/%s' % (self.partner.name, self.cut.uuid, self.name)
|
||||
return '%s/%s/%s' % (self.partner.name, self.cut.uuid, self.name)
|
||||
|
||||
def clean(self):
|
||||
if self.data:
|
||||
|
@ -164,60 +129,46 @@ class Petal(models.Model):
|
|||
'''Delegate global limits check to partner, and check per key size limits'''
|
||||
size_delta = content_length - self.size
|
||||
|
||||
self.partner.check_limits(size_delta,
|
||||
partner=self.partner.name,
|
||||
cut=self.cut.uuid,
|
||||
key=self.name)
|
||||
self.partner.check_limits(size_delta, partner=self.partner.name, cut=self.cut.uuid, key=self.name)
|
||||
|
||||
if content_length > self.partner.hard_per_key_max_size * 1024:
|
||||
raise PetalSizeExhausted
|
||||
|
||||
if (self.size <= self.partner.soft_per_key_max_size * 1024
|
||||
and content_length > self.partner.soft_per_key_max_size * 1024):
|
||||
if (
|
||||
self.size <= self.partner.soft_per_key_max_size * 1024
|
||||
and content_length > self.partner.soft_per_key_max_size * 1024
|
||||
):
|
||||
self.partner.notify_admins(
|
||||
# pylint: disable=no-member
|
||||
_('Key {key} space of partner {partner} almost exhausted').format(
|
||||
key=self.name,
|
||||
partner=self.partner.name),
|
||||
key=self.name, partner=self.partner.name
|
||||
),
|
||||
# pylint: disable=no-member
|
||||
_('Current size: {current_size}, Max size: {max_size}').format(
|
||||
current_size=content_length,
|
||||
max_size=self.partner.hard_per_key_max_size * 1024),
|
||||
current_size=content_length, max_size=self.partner.hard_per_key_max_size * 1024
|
||||
),
|
||||
partner=self.partner.name,
|
||||
cut=self.cut.uuid,
|
||||
key=self.name)
|
||||
key=self.name,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('name', 'partner', 'cut'))
|
||||
unique_together = ('name', 'partner', 'cut')
|
||||
verbose_name = _('Petal')
|
||||
verbose_name_plural = _('Petals')
|
||||
|
||||
|
||||
@six.python_2_unicode_compatible
|
||||
class AccessControlList(models.Model):
|
||||
order = models.IntegerField(
|
||||
_('Order'))
|
||||
partner = models.ForeignKey(
|
||||
Partner,
|
||||
verbose_name=_('Partner'),
|
||||
on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
verbose_name=_('User'),
|
||||
on_delete=models.CASCADE)
|
||||
order = models.IntegerField(_('Order'))
|
||||
partner = models.ForeignKey(Partner, verbose_name=_('Partner'), on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, verbose_name=_('User'), on_delete=models.CASCADE)
|
||||
methods = models.CharField(
|
||||
_('Allowed methods'),
|
||||
max_length=128,
|
||||
default='GET,PUT,DELETE',
|
||||
help_text=("GET, PUT, DELETE"))
|
||||
key = models.CharField(
|
||||
_('Allowed keys'),
|
||||
max_length=128,
|
||||
default='*')
|
||||
_('Allowed methods'), max_length=128, default='GET,PUT,DELETE', help_text=("GET, PUT, DELETE")
|
||||
)
|
||||
key = models.CharField(_('Allowed keys'), max_length=128, default='*')
|
||||
|
||||
def __str__(self):
|
||||
return u'%s %s %s %s' % (
|
||||
self.partner.name, self.user.username, self.methods, self.key)
|
||||
return '%s %s %s %s' % (self.partner.name, self.user.username, self.methods, self.key)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Access control list')
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
from .models import AccessControlList
|
||||
from .exceptions import AccessForbidden
|
||||
from .models import AccessControlList
|
||||
|
||||
|
||||
class PetaleAccessPermission(BasePermission):
|
||||
|
@ -26,9 +26,8 @@ class PetaleAccessPermission(BasePermission):
|
|||
petal_name = view.kwargs.get('petal_name')
|
||||
|
||||
qs = AccessControlList.objects.filter(
|
||||
partner__name=partner_name,
|
||||
user=request.user,
|
||||
methods__contains=request.method)
|
||||
partner__name=partner_name, user=request.user, methods__contains=request.method
|
||||
)
|
||||
|
||||
if not qs.exists():
|
||||
raise AccessForbidden
|
||||
|
|
|
@ -10,6 +10,7 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
|
|||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||
|
||||
|
||||
|
@ -37,7 +38,7 @@ INSTALLED_APPS = (
|
|||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'petale'
|
||||
'petale',
|
||||
)
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -82,8 +83,7 @@ USE_TZ = True
|
|||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [
|
||||
],
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
|
@ -108,13 +108,11 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
|||
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'petale.authentication.PetalAuthentication',
|
||||
),
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': ('petale.authentication.PetalAuthentication',),
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
'petale.permissions.PetaleAccessPermission',
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
@ -147,14 +145,13 @@ LOGGING = {
|
|||
'handlers': ['syslog'],
|
||||
'level': 'DEBUG',
|
||||
'propagate': True,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local_settings_file = os.environ.get(
|
||||
'PETALE_SETTINGS_FILE',
|
||||
os.path.join(os.path.dirname(__file__), 'local_settings.py'))
|
||||
'PETALE_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')
|
||||
)
|
||||
if os.path.exists(local_settings_file):
|
||||
with open(local_settings_file) as fd:
|
||||
exec(fd.read())
|
||||
|
||||
|
|
|
@ -17,14 +17,18 @@
|
|||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
|
||||
from .api_views import PetalAPIView, PetalAPIKeysView
|
||||
|
||||
from .api_views import PetalAPIKeysView, PetalAPIView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin/', admin.site.urls),
|
||||
url(r'^api/(?P<partner_name>[\w,-]+)/(?P<cut_uuid>[\w,-]{,255})/$',
|
||||
url(
|
||||
r'^api/(?P<partner_name>[\w,-]+)/(?P<cut_uuid>[\w,-]{,255})/$',
|
||||
PetalAPIKeysView.as_view(),
|
||||
name='api-keys'),
|
||||
url(r'^api/(?P<partner_name>[\w,-]+)/(?P<cut_uuid>[\w,-]{,255})/(?P<petal_name>[\w,-]+)/$',
|
||||
PetalAPIView.as_view(), name='api')
|
||||
name='api-keys',
|
||||
),
|
||||
url(
|
||||
r'^api/(?P<partner_name>[\w,-]+)/(?P<cut_uuid>[\w,-]{,255})/(?P<petal_name>[\w,-]+)/$',
|
||||
PetalAPIView.as_view(),
|
||||
name='api',
|
||||
),
|
||||
]
|
||||
|
|
|
@ -18,7 +18,6 @@ import hashlib
|
|||
import logging
|
||||
from functools import wraps
|
||||
|
||||
|
||||
DEFAULT_HASH_ALGO = 'sha1'
|
||||
|
||||
|
||||
|
@ -28,19 +27,20 @@ def logit(func):
|
|||
logger = logging.getLogger('petale')
|
||||
req_url = '%s %s %s' % (request.method, request.path, request.GET.urlencode())
|
||||
logger.info(req_url, extra={'request_url': req_url})
|
||||
req_headers = ''.join(
|
||||
['%s: %s | ' % (k, v) for k, v in request.META.items() if k.isupper()])
|
||||
req_headers = ''.join(['%s: %s | ' % (k, v) for k, v in request.META.items() if k.isupper()])
|
||||
logger.debug('Request Headers: %s', req_headers, extra={'request_headers': req_headers})
|
||||
response = func(self, request, *args, **kwargs)
|
||||
resp_headers = ''.join(['%s: %s | ' % (k, v) for k, v in response.items()])
|
||||
logger.debug('Response Headers: %s', resp_headers,
|
||||
extra={'response_headers': resp_headers})
|
||||
logger.debug('Response Headers: %s', resp_headers, extra={'response_headers': resp_headers})
|
||||
if hasattr(response, 'data'):
|
||||
logger.debug('Response Data: %r', response.data,
|
||||
extra={'response_body': response.data})
|
||||
logger.debug('Response Status Code: %s', response.status_code,
|
||||
extra={'response_status_code': response.status_code})
|
||||
logger.debug('Response Data: %r', response.data, extra={'response_body': response.data})
|
||||
logger.debug(
|
||||
'Response Status Code: %s',
|
||||
response.status_code,
|
||||
extra={'response_status_code': response.status_code},
|
||||
)
|
||||
return response
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
|
@ -57,7 +57,7 @@ def etag(stream):
|
|||
return '"%s:%s"' % (DEFAULT_HASH_ALGO, digest.hexdigest())
|
||||
|
||||
|
||||
class StreamingHash(object):
|
||||
class StreamingHash:
|
||||
def __init__(self, readable, hash_algo=DEFAULT_HASH_ALGO):
|
||||
self.readable = readable
|
||||
self.hash_algo = hash_algo
|
||||
|
|
23
setup.py
23
setup.py
|
@ -1,15 +1,14 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from setuptools.command.install_lib import install_lib as _install_lib
|
||||
from distutils.cmd import Command
|
||||
from distutils.command.build import build as _build
|
||||
from distutils.command.sdist import sdist
|
||||
from distutils.cmd import Command
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
from setuptools.command.install_lib import install_lib as _install_lib
|
||||
|
||||
|
||||
class eo_sdist(sdist):
|
||||
|
@ -29,15 +28,17 @@ class eo_sdist(sdist):
|
|||
|
||||
def get_version():
|
||||
'''Use the VERSION, if absent generates a version with git describe, if not
|
||||
tag exists, take 0.0- and add the length of the commit log.
|
||||
tag exists, take 0.0- and add the length of the commit log.
|
||||
'''
|
||||
if os.path.exists('VERSION'):
|
||||
with open('VERSION', 'r') as v:
|
||||
with open('VERSION') as v:
|
||||
return v.read()
|
||||
if os.path.exists('.git'):
|
||||
p = subprocess.Popen(
|
||||
['git', 'describe', '--dirty=.dirty', '--match=v*'],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
result = p.communicate()[0]
|
||||
if p.returncode == 0:
|
||||
result = result.decode('ascii').strip()[1:] # strip spaces/newlines and initial v
|
||||
|
@ -48,9 +49,7 @@ def get_version():
|
|||
version = result
|
||||
return version
|
||||
else:
|
||||
return '0.0.post%s' % len(
|
||||
subprocess.check_output(
|
||||
['git', 'rev-list', 'HEAD']).splitlines())
|
||||
return '0.0.post%s' % len(subprocess.check_output(['git', 'rev-list', 'HEAD']).splitlines())
|
||||
return '0.0'
|
||||
|
||||
|
||||
|
@ -76,6 +75,7 @@ class compile_translations(Command):
|
|||
def run(self):
|
||||
try:
|
||||
from django.core.management import call_command
|
||||
|
||||
for path, dirs, files in os.walk('petale'):
|
||||
if 'locale' not in dirs:
|
||||
continue
|
||||
|
@ -94,7 +94,6 @@ class build(_build):
|
|||
|
||||
|
||||
class install_lib(_install_lib):
|
||||
|
||||
def run(self):
|
||||
self.run_command('compile_translations')
|
||||
_install_lib.run(self)
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from tempfile import mkdtemp
|
||||
import shutil
|
||||
from tempfile import mkdtemp
|
||||
|
||||
import pytest
|
||||
import django_webtest
|
||||
|
||||
from utils import (create_partner, create_petal, create_service, create_cut,
|
||||
get_service, create_acl_record, get_tests_file_content)
|
||||
import pytest
|
||||
from utils import (
|
||||
create_acl_record,
|
||||
create_cut,
|
||||
create_partner,
|
||||
create_petal,
|
||||
create_service,
|
||||
get_service,
|
||||
get_tests_file_content,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -19,6 +23,7 @@ def app(request, settings):
|
|||
|
||||
def fin():
|
||||
shutil.rmtree(settings.MEDIA_ROOT)
|
||||
|
||||
request.addfinalizer(fin)
|
||||
return django_webtest.DjangoTestApp()
|
||||
|
||||
|
@ -65,12 +70,18 @@ def acl(db, partner_southpark, partner_gotham):
|
|||
create_acl_record(1, partner_southpark, get_service('family'), 'invoice', methods='GET,HEAD,PUT'),
|
||||
create_acl_record(2, partner_southpark, get_service('library'), 'books', methods='GET,HEAD'),
|
||||
create_acl_record(3, partner_gotham, get_service('arkham'), 'taxes*', methods='GET,HEAD,PUT,DELETE'),
|
||||
create_acl_record(4, partner_southpark, get_service('library'), 'loans', methods='GET,HEAD,PUT,DELETE'),
|
||||
create_acl_record(
|
||||
4, partner_southpark, get_service('library'), 'loans', methods='GET,HEAD,PUT,DELETE'
|
||||
),
|
||||
create_acl_record(5, partner_southpark, get_service('library'), 'favourite*', methods='GET,PUT'),
|
||||
create_acl_record(6, partner_southpark, get_service('cityhall'), 'favourite*', methods='GET,PUT,DELETE'),
|
||||
create_acl_record(
|
||||
6, partner_southpark, get_service('cityhall'), 'favourite*', methods='GET,PUT,DELETE'
|
||||
),
|
||||
create_acl_record(3, partner_southpark, get_service('library'), 'profile', methods='GET,PUT,DELETE'),
|
||||
create_acl_record(1, partner_southpark, get_service('arkham'), 'ma*', methods='GET,HEAD,PUT,DELETE'),
|
||||
create_acl_record(4, partner_southpark, get_service(('a1b2' * 8)[:30]), 'profile*', methods='GET,PUT')
|
||||
create_acl_record(
|
||||
4, partner_southpark, get_service(('a1b2' * 8)[:30]), 'profile*', methods='GET,PUT'
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin(db):
|
||||
from django.contrib.auth.models import User
|
||||
user = User.objects.create(
|
||||
username='admin',
|
||||
email='admin@example.com',
|
||||
is_superuser=True,
|
||||
is_staff=True)
|
||||
|
||||
user = User.objects.create(username='admin', email='admin@example.com', is_superuser=True, is_staff=True)
|
||||
user.set_password('admin')
|
||||
user.save()
|
||||
return user
|
||||
|
@ -18,6 +13,7 @@ def admin(db):
|
|||
|
||||
def test_create_user(settings, app, db, admin):
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
settings.PETALE_CHECK_CUT_UUID = False
|
||||
|
||||
# Login
|
||||
|
@ -50,7 +46,7 @@ def test_create_user(settings, app, db, admin):
|
|||
response = response.click('Accueil')
|
||||
|
||||
# Création de la règle de contrôle d'accès
|
||||
response = response.click(u'Règles de cont')
|
||||
response = response.click('Règles de cont')
|
||||
response = response.click('Ajouter')
|
||||
response.form.set('order', '1')
|
||||
response.form.select('partner', text='partenaire1')
|
||||
|
@ -77,7 +73,7 @@ def test_create_user(settings, app, db, admin):
|
|||
response.form.set('password', 'admin')
|
||||
response = response.form.submit().follow()
|
||||
|
||||
response = response.click(u'Pétales')
|
||||
response = response.click('Pétales')
|
||||
row = [e.text() for e in response.pyquery('tbody tr > *').items()]
|
||||
assert row[:1] + row[2:] == ['', 'partenaire1', 'cut1', 'cle1']
|
||||
response = response.click(row[1])
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
import os
|
||||
import json
|
||||
from xml.etree import ElementTree as etree
|
||||
import os
|
||||
from multiprocessing.pool import ThreadPool
|
||||
from unittest import mock
|
||||
from xml.etree import ElementTree as etree
|
||||
|
||||
import pytest
|
||||
import mock
|
||||
|
||||
from utils import get_tests_file_content, FakedResponse
|
||||
|
||||
from django.utils.encoding import force_text
|
||||
from utils import FakedResponse, get_tests_file_content
|
||||
|
||||
from petale.utils import etag
|
||||
from petale.models import CUT, Petal
|
||||
from petale.utils import etag
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
@ -44,8 +42,7 @@ def test_resource_not_found(app, partner_southpark, cut_kevin_uuid, acl):
|
|||
app.head(url, status=404)
|
||||
|
||||
|
||||
def test_access_control_list(app, partner_southpark, cut_kevin_uuid,
|
||||
petal_books, petal_invoice, acl):
|
||||
def test_access_control_list(app, partner_southpark, cut_kevin_uuid, petal_books, petal_invoice, acl):
|
||||
app.authorization = ('Basic', ('arkham', 'arkham'))
|
||||
|
||||
# test permission on requested partner
|
||||
|
@ -79,16 +76,18 @@ def test_simple_api(app, partner_southpark, cut_kevin_uuid, acl, petal_invoice):
|
|||
|
||||
# test create key without content-type
|
||||
resp = app.put(
|
||||
url, params=json.dumps(payload),
|
||||
url,
|
||||
params=json.dumps(payload),
|
||||
headers={
|
||||
'If-None-Match': '*',
|
||||
'Content-Type': '',
|
||||
}, status=400)
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
assert resp.json['error'] == 'missing-content-type'
|
||||
|
||||
# test create key with cut uuid length over 32
|
||||
app.put_json('/api/southaprk/%s12/whatever/' % cut_kevin_uuid,
|
||||
params=json.dumps(payload), status=404)
|
||||
app.put_json('/api/southaprk/%s12/whatever/' % cut_kevin_uuid, params=json.dumps(payload), status=404)
|
||||
|
||||
# test create key
|
||||
resp = app.put_json(url, params=payload, headers={'If-None-Match': '*'}, status=201)
|
||||
|
@ -115,7 +114,7 @@ def test_simple_api(app, partner_southpark, cut_kevin_uuid, acl, petal_invoice):
|
|||
resp = app.head(cut_keys_url, status=405)
|
||||
|
||||
resp = app.get(cut_keys_url, status=200)
|
||||
assert set(resp.json['keys']) == set(['loans'])
|
||||
assert set(resp.json['keys']) == {'loans'}
|
||||
|
||||
# test get all keys and prefix
|
||||
resp = app.get('%s?prefix=invoice' % cut_keys_url, status=200)
|
||||
|
@ -154,8 +153,12 @@ def test_binary_data(app, partner_southpark, cut_kevin_uuid, acl):
|
|||
# test create xml data
|
||||
url = '/api/southpark/%s/profile-friends/' % cut_kevin_uuid
|
||||
content_type = 'text/xml'
|
||||
resp = app.put(url, params=get_tests_file_content('users.xml'),
|
||||
headers={'If-None-Match': '*', 'Content-Type': content_type}, status=201)
|
||||
resp = app.put(
|
||||
url,
|
||||
params=get_tests_file_content('users.xml'),
|
||||
headers={'If-None-Match': '*', 'Content-Type': content_type},
|
||||
status=201,
|
||||
)
|
||||
resp = app.get(url)
|
||||
assert resp.headers.get('Content-Type') == content_type
|
||||
xml_data = etree.fromstring(resp.content)
|
||||
|
@ -178,8 +181,9 @@ def test_binary_data(app, partner_southpark, cut_kevin_uuid, acl):
|
|||
url = '/api/southpark/%s/profile-picture/' % cut_kevin_uuid
|
||||
content_type = 'application/octet-stream'
|
||||
content = get_tests_file_content('fg.jpg') * 100
|
||||
resp = app.put(url, params=content,
|
||||
headers={'If-None-Match': '*', 'Content-Type': content_type}, status=201)
|
||||
resp = app.put(
|
||||
url, params=content, headers={'If-None-Match': '*', 'Content-Type': content_type}, status=201
|
||||
)
|
||||
resp = app.get(url)
|
||||
assert resp.headers.get('Content-Type') == content_type
|
||||
assert resp.content == content
|
||||
|
@ -187,8 +191,12 @@ def test_binary_data(app, partner_southpark, cut_kevin_uuid, acl):
|
|||
# test create binary data
|
||||
url = '/api/southpark/%s/profile-invoice/' % cut_kevin_uuid
|
||||
content_type = 'application/pdf'
|
||||
resp = app.put(url, params=get_tests_file_content('invoice.pdf'),
|
||||
headers={'If-None-Match': '*', 'Content-Type': content_type}, status=201)
|
||||
resp = app.put(
|
||||
url,
|
||||
params=get_tests_file_content('invoice.pdf'),
|
||||
headers={'If-None-Match': '*', 'Content-Type': content_type},
|
||||
status=201,
|
||||
)
|
||||
resp = app.get(url)
|
||||
assert resp.headers.get('Content-Type') == content_type
|
||||
|
||||
|
@ -224,7 +232,7 @@ def test_caching(app, partner_southpark, cut_kevin_uuid, acl):
|
|||
etags = [
|
||||
'"sha1:5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9"',
|
||||
'"sha1:c75ac194bf3ed4ef3f3e14343585c2fd7ed9b06bbdfbf0eb9f817b7337b966ea" ',
|
||||
' "sha1:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b"'
|
||||
' "sha1:6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b"',
|
||||
]
|
||||
etags.append(cache)
|
||||
resp = app.put_json(url, params=payload, headers={'If-None-Match': ','.join(etags)}, status=412)
|
||||
|
@ -235,11 +243,13 @@ def test_caching(app, partner_southpark, cut_kevin_uuid, acl):
|
|||
app.authorization = ('Basic', ('cityhall', 'cityhall'))
|
||||
resp = app.get(url, status=200)
|
||||
data = json.loads(resp.text)
|
||||
data['favourites'].append({
|
||||
"name": "best books",
|
||||
"url": "https://southpark.com/library",
|
||||
"items": ["Guide To Life by E. Cartman", "Gingers Have Sools by Kyle"]
|
||||
})
|
||||
data['favourites'].append(
|
||||
{
|
||||
"name": "best books",
|
||||
"url": "https://southpark.com/library",
|
||||
"items": ["Guide To Life by E. Cartman", "Gingers Have Sools by Kyle"],
|
||||
}
|
||||
)
|
||||
|
||||
etag = resp.headers['Etag']
|
||||
resp = app.put_json(url, params=data, headers={'If-Match': etag}, status=200)
|
||||
|
@ -249,7 +259,7 @@ def test_caching(app, partner_southpark, cut_kevin_uuid, acl):
|
|||
new_item = {
|
||||
"name": "Green zones",
|
||||
"url": "https://southpark.com/parks",
|
||||
"items": ["Main Street", "Emo Kids Street"]
|
||||
"items": ["Main Street", "Emo Kids Street"],
|
||||
}
|
||||
payload['favourites'].append(new_item)
|
||||
resp = app.put_json(url, params=payload, headers={'If-Match': cache}, status=412)
|
||||
|
@ -272,19 +282,23 @@ def test_partner_size_limit(app, cut_kevin_uuid, acl, petal_invoice, petal_books
|
|||
|
||||
# test sending data sized above per key hard max size
|
||||
url = '/api/gotham/%s/taxes-fail/' % cut_kevin_uuid
|
||||
resp = app.put(url,
|
||||
params='a' * 3 * 1024, # 3ko
|
||||
headers={'If-None-Match': '*'},
|
||||
content_type='text/plain',
|
||||
status=500)
|
||||
resp = app.put(
|
||||
url,
|
||||
params='a' * 3 * 1024, # 3ko
|
||||
headers={'If-None-Match': '*'},
|
||||
content_type='text/plain',
|
||||
status=500,
|
||||
)
|
||||
assert resp.json['error'] == 'key-space-exhausted'
|
||||
|
||||
# test sending data sized within soft and hard key limit
|
||||
resp = app.put(url,
|
||||
params='a' * (1024 + 1), # 1ko + 1
|
||||
headers={'If-None-Match': '*'},
|
||||
content_type='text/plain',
|
||||
status=201)
|
||||
resp = app.put(
|
||||
url,
|
||||
params='a' * (1024 + 1), # 1ko + 1
|
||||
headers={'If-None-Match': '*'},
|
||||
content_type='text/plain',
|
||||
status=201,
|
||||
)
|
||||
assert len(mailoutbox) == 1
|
||||
sent_mail = mailoutbox[0]
|
||||
assert sent_mail.to[0] == 'b.wayne@gotham.gov'
|
||||
|
@ -295,11 +309,7 @@ def test_partner_size_limit(app, cut_kevin_uuid, acl, petal_invoice, petal_books
|
|||
|
||||
for i in range(18):
|
||||
url = '/api/gotham/%s/taxes-%d/' % (cut_kevin_uuid, i)
|
||||
app.put(url,
|
||||
params='a' * 1024,
|
||||
headers={'If-None-Match': '*'},
|
||||
content_type='text/plain',
|
||||
status=201)
|
||||
app.put(url, params='a' * 1024, headers={'If-None-Match': '*'}, content_type='text/plain', status=201)
|
||||
|
||||
assert len(mailoutbox) == 2
|
||||
sent_mail = mailoutbox[1]
|
||||
|
@ -309,11 +319,13 @@ def test_partner_size_limit(app, cut_kevin_uuid, acl, petal_invoice, petal_books
|
|||
assert str(1024 * 20) in sent_mail.message().as_string()
|
||||
|
||||
url = '/api/gotham/%s/taxes-100/' % cut_kevin_uuid
|
||||
resp = app.put(url,
|
||||
params='a' * (1024 * 2 - 1),
|
||||
headers={'If-None-Match': '*'},
|
||||
content_type='text/plain',
|
||||
status=500)
|
||||
resp = app.put(
|
||||
url,
|
||||
params='a' * (1024 * 2 - 1),
|
||||
headers={'If-None-Match': '*'},
|
||||
content_type='text/plain',
|
||||
status=500,
|
||||
)
|
||||
assert resp.json['error'] == 'global-space-exhausted'
|
||||
|
||||
|
||||
|
@ -349,9 +361,7 @@ def test_idp_based_partner_authentication(mocked_post, app, cut_kevin_uuid, acl)
|
|||
payload = {"friends": [{"name": "Token", "age": 10}, {"name": "Kenny", "age": 10}]}
|
||||
url = '/api/southpark/%s/profile-whatever/' % cut_kevin_uuid
|
||||
# failure
|
||||
response = {
|
||||
"result": 0, "errors": ["Invalid username/password."]
|
||||
}
|
||||
response = {"result": 0, "errors": ["Invalid username/password."]}
|
||||
mocked_post.return_value = FakedResponse(content=json.dumps(response))
|
||||
app.put_json(url, params=payload, status=401, headers={'If-None-Match': '*'})
|
||||
response = {"result": 1, "errors": []}
|
||||
|
@ -376,15 +386,12 @@ def test_cut_uuid_idp_checking(mocked_post, settings, app, acl):
|
|||
|
||||
# failure with AUTHENTIC_PETALE improperly defined
|
||||
settings.PETALE_AUTHENTIC_URL = ''
|
||||
resp = app.put_json('/api/southpark/%s/profile/' % cut_uuid, params=payload,
|
||||
headers=headers, status=404)
|
||||
resp = app.put_json('/api/southpark/%s/profile/' % cut_uuid, params=payload, headers=headers, status=404)
|
||||
assert resp.json["error"] == "cut-not-found"
|
||||
# failure when cut uuid doesn't exist on idp
|
||||
response = {"unknown_uuids": [cut_uuid], "result": 1}
|
||||
mocked_post.return_value = FakedResponse(content=json.dumps(response),
|
||||
status_code=200)
|
||||
resp = app.put_json('/api/southpark/%s/profile/' % cut_uuid, params=payload,
|
||||
headers=headers, status=404)
|
||||
mocked_post.return_value = FakedResponse(content=json.dumps(response), status_code=200)
|
||||
resp = app.put_json('/api/southpark/%s/profile/' % cut_uuid, params=payload, headers=headers, status=404)
|
||||
assert resp.json["error"] == "cut-not-found"
|
||||
|
||||
resp = app.get('/api/southpark/%s/' % cut_uuid, headers=headers, status=404)
|
||||
|
@ -396,8 +403,7 @@ def test_cut_uuid_idp_checking(mocked_post, settings, app, acl):
|
|||
# sucess
|
||||
settings.PETALE_AUTHENTIC_URL = 'http://example.net/idp/'
|
||||
response = {"unknown_uuids": [], "result": 1}
|
||||
mocked_post.return_value = FakedResponse(content=json.dumps(response),
|
||||
status_code=200)
|
||||
mocked_post.return_value = FakedResponse(content=json.dumps(response), status_code=200)
|
||||
|
||||
resp = app.get('/api/southpark/%s/profile/' % cut_uuid, headers=headers, status=404)
|
||||
assert resp.json["error"] == "key-not-found"
|
||||
|
@ -405,8 +411,7 @@ def test_cut_uuid_idp_checking(mocked_post, settings, app, acl):
|
|||
resp = app.get('/api/southpark/%s/' % cut_uuid, headers=headers)
|
||||
assert resp.json["keys"] == []
|
||||
|
||||
resp = app.put_json('/api/southpark/%s/profile/' % cut_uuid, params=payload,
|
||||
headers=headers, status=201)
|
||||
resp = app.put_json('/api/southpark/%s/profile/' % cut_uuid, params=payload, headers=headers, status=201)
|
||||
assert CUT.objects.get(uuid=cut_uuid)
|
||||
assert Petal.objects.get(name='profile', cut__uuid=cut_uuid, partner__name='southpark')
|
||||
|
||||
|
@ -426,7 +431,7 @@ def test_storage_error(app, partner_southpark, cut_kevin_uuid, acl, caplog):
|
|||
|
||||
def test_concurrent_put(app, transactional_db, settings):
|
||||
'''Test concurrent PUT to the same key'''
|
||||
from utils import create_cut, create_partner, create_service, create_acl_record
|
||||
from utils import create_acl_record, create_cut, create_partner, create_service
|
||||
|
||||
uuid = create_cut('a' * 255).uuid
|
||||
southpark = create_partner('southpark', hg=20240, hk=19728)
|
||||
|
@ -446,6 +451,7 @@ def test_concurrent_put(app, transactional_db, settings):
|
|||
|
||||
def f(i):
|
||||
from django.db import connection
|
||||
|
||||
if i % 2 == 0:
|
||||
response = app.get(url, status=200)
|
||||
else:
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import os
|
||||
import json
|
||||
import os
|
||||
from io import BytesIO
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import mock
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.files import File
|
||||
from petale.models import Partner, CUT, AccessControlList, Petal
|
||||
|
||||
from petale.models import CUT, AccessControlList, Partner, Petal
|
||||
from petale.utils import etag
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
@ -39,9 +39,13 @@ def create_partner(name, admins=None, hg=2, sg=1, hk=1, sk=1):
|
|||
if not admins:
|
||||
admins = 'e.cartman@southpark.com,t.blakc@southpark.com'
|
||||
return Partner.objects.create(
|
||||
name=name, admin_emails=admins,
|
||||
hard_global_max_size=hg, soft_global_max_size=sg,
|
||||
hard_per_key_max_size=hk, soft_per_key_max_size=sk)
|
||||
name=name,
|
||||
admin_emails=admins,
|
||||
hard_global_max_size=hg,
|
||||
soft_global_max_size=sg,
|
||||
hard_per_key_max_size=hk,
|
||||
soft_per_key_max_size=sk,
|
||||
)
|
||||
|
||||
|
||||
def create_acl_record(order, partner, user, key, methods='*'):
|
||||
|
@ -57,12 +61,11 @@ def create_petal(cut_uuid, partner, name, data, content_type):
|
|||
size=len(data),
|
||||
etag=etag(data),
|
||||
data=File(BytesIO(data), name),
|
||||
content_type=content_type
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
|
||||
class FakedResponse(mock.Mock):
|
||||
|
||||
def json(self):
|
||||
return json.loads(self.content)
|
||||
|
||||
|
|
9
tox.ini
9
tox.ini
|
@ -2,10 +2,12 @@
|
|||
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/petale/
|
||||
envlist =
|
||||
py3-dj22-drf39
|
||||
code-style
|
||||
|
||||
[tox:jenkins]
|
||||
envlist =
|
||||
pylint
|
||||
code-style
|
||||
py3-dj22-drf39
|
||||
|
||||
[testenv]
|
||||
|
@ -62,6 +64,13 @@ deps =
|
|||
commands =
|
||||
/bin/bash -c "./pylint.sh petale/"
|
||||
|
||||
[testenv:code-style]
|
||||
skip_install = true
|
||||
deps =
|
||||
pre-commit
|
||||
commands =
|
||||
pre-commit run --all-files --show-diff-on-failure
|
||||
|
||||
[pytest]
|
||||
junit_family=xunit2
|
||||
filterwarnings =
|
||||
|
|
Loading…
Reference in New Issue