I am using DRF for storing user uploaded images to S3 and in S3 i can see that images are public accessible using the URL.
My concern over here is there any best way to secure this images and restrict them for owner of that image only to view it.
I am using Heroku to deploy my DRF API Framework but i see this as security concern for my user who are uploading image files to S3 bucket.
I am trying to isolate user images by their name it self.But still it is public so i can access this images for another user just figure it out there name.
Here is S3 URL for media Images
https://xxx.s3.amazonaws.com/media/persons/niravjoshi/20181218152410.jpg
Here is my settings.py for Django
import os
import pymysql # noqa: 402
pymysql.install_as_MySQLdb()
import dj_database_url
from decouple import config
import django_heroku
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
#SECRET_KEY = 'feufm)u(pvsvb%&_%%*)p_bpa+sv8zt$#_-do5q3(vou-j*d#p'
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
DATABASES = {
'default': dj_database_url.config(
default=config('DATABASE_URL')
)
}
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
#Django Project Apps
'persons',
'rest_framework',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.google',
#'social_django',
]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
AWS_REGION = os.environ.get('AWS_REGION', '') # e.g. eu-west-1
AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY', '')
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_KEY', '')
AWS_STORAGE_BUCKET_NAME = os.environ.get('S3_BUCKET', '')
AWS_QUERYSTRING_AUTH = False
AWS_S3_CUSTOM_DOMAIN = os.environ.get("AWS_S3_CUSTOM_DOMAIN", "")
MEDIAFILES_LOCATION = 'media'
DEFAULT_FILE_STORAGE = 'DjangoE2ISAapi.storage_backends.MediaStorage'
MEDIA_URL = "https://%s/%s/" % (AWS_S3_CUSTOM_DOMAIN, MEDIAFILES_LOCATION)
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
AWS_STATIC_LOCATION = 'static'
STATICFILES_STORAGE = 'DjangoE2ISAapi.storage_backends.StaticStorage'
STATIC_URL = "https://%s/%s/" % (AWS_S3_CUSTOM_DOMAIN, AWS_STATIC_LOCATION)
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]
django_heroku.settings(locals())
from DjangoE2ISAapi.restconf.main import *
Here is my storage_backends.py
from django.conf import settings
from storages.backends.s3boto3 import S3Boto3Storage
class StaticStorage(S3Boto3Storage):
location = settings.AWS_STATIC_LOCATION
class MediaStorage(S3Boto3Storage):
location = settings.MEDIAFILES_LOCATION
Here is my Person model.py.
from django.core.serializers import serialize
from django.db import models
from django.conf import settings
import json
from django.core.serializers.json import DjangoJSONEncoder
# Create your models here.
def upload_file(instance,filename):
import os
from django.utils.timezone import now
filename_base, filename_ext = os.path.splitext(filename)
return "persons/{user}/{filename}".format(user=instance.UserName, filename=now().strftime("%Y%m%d%H%M%S")+filename_ext.lower())
class PersonQuerySet(models.QuerySet):
def serialize(self):
list_values=list(self.values('UserName','PersonId','PersonName','Person_Image','Person_sex','Person_BDate'))
print (list_values)
return json.dumps(list_values,sort_keys=True,indent=1,cls=DjangoJSONEncoder)
class PersonManager(models.Manager):
def get_queryset(self):
return PersonQuerySet(self.model,using=self._db)
class Person(models.Model):
UserName = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,)
PersonId = models.AutoField(primary_key=True)
PersonName = models.CharField("person's first name", max_length=30,null=False)
Person_Image = models.ImageField(upload_to=upload_file,null=True, blank=True)
SEX = (('M','Male'),('F','Female'), ('N','None'), )
Person_sex = models.CharField(max_length=1,choices=SEX,null=False)
Person_BDate = models.DateField(null=False)
Person_CDate = models.DateField(null=False,auto_now_add=True)
objects = PersonManager()
def __str__(self):
return str(self.PersonName) or ""
def serialize(self):
data={
'UserName': self.UserName,
'PersonId': self.PersonId,
'PersonName': self.PersonName,
'Person_Image':self.Person_Image,
'Person_sex': self.Person_sex,
'Person_Bdate': self.Person_BDate
}
data = json.dumps(data,sort_keys=True,indent=1,cls=DjangoJSONEncoder)
return data
#property
def owner(self):
return self.UserName
Here is response of Person API View:
The docs for boto's ACLs are here. I suggest just using the private "canned policy" -- since your users don't have S3 accounts anyway, it's by far the simplest idea. Your app will of course have to keep track of which user "owns" which files (which should be a very, very simple Django model!).
In order to enforce users only being able to download through your own application, just pass a small value to the expires_in parameter when generating the URL. Users will only get a valid download link through your application, and that link will be invalidated after their download.
Here is an example of the code used to generate the link for downloading :
#login_required
def download_document(request, file_id):
'''
Request handler to download file
'''
file = Document.objects.get(pk=file_id)
s3 = get_aws_s3_client() #function to create s3 session
download_url = s3.generate_presigned_url(
'get_object',
Params= {'Bucket': file.bucket_name, 'Key': file.key},
ExpiresIn=5, #the url won't be valid after only 5 seconds
)
return redirect(download_url)
You can go further and make the view valid only for the file owner by adding this code :
if file.owner == request.user :
return redirect(download_url)
else :
# render 403.html since access denied.
Edit :
As requested, this solution requires using a specific model to store informations related to each document.
The model will look similar to this :
class Image(models.Model):
customer = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete = models.CASCADE,)
key = models.CharField(max_length=120) #uuid64 will be stored here and used for s3 urls
name = models.CharField(max_length=120, null=True, blank=True)
size = models.FloatField()
human_size = models.CharField(max_length=120, null=True, blank=True)
filetype = models.CharField(max_length=120, null=True, blank=True)
fextension = models.CharField(max_length=30, null=True, blank=True)
bucket_name = models.CharField(max_length=120, null=True, blank=True)
region = models.CharField(max_length=120, null=True, blank=True)
s3link = models.CharField(max_length=170, null=True, blank=True)
timestamp = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
uploaded = models.BooleanField(default=False)
active = models.BooleanField(default=True)
def __str__(self):
return self.name
I can't discuss parts related to serialization since I have never used DRF.
I would add uuid field to every user like.
import uuid
class Person(models.Model):
uuid = models.UUID(default=uuid.uuid4)
....
You can set it as primary key as well instead of AutoField.
and put that unique uuid into URL instead of name in order to look like:
https://xxx.s3.amazonaws.com/media/persons/b3810ec3-dd9d-4f11-a1e1-47835c0058ec/20181218152410.jpg
That image will be still public, but it's impossible to access image if you don't know uuid of particular user.
If you want more secure solution not depending on URL only you need to add some Authentication logic.
Related
I am using create view to create a model instance (product).
Evey thing is working fine bt after doing some new migrations
i can't get the uploaded image, instead getting default image.
I think upload_to method of models isn't working.
i also used this in my urls
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
This is my settigs vars:
STATIC_URL = '/static/'
MEDIA_ROOT = os.path.join(BASE_DIR,'media')
MEDIA_URL = '/media/'
This is my models.py:
class Product(models.Model):
TYPE = [
('Sell','Sell'),
('Rent','Rent'),
('Sell or Rent','Sell or Rent'),
]
owner = models.ForeignKey(Owner, on_delete=models.CASCADE)
title = models.CharField(max_length = 25)
type = models.CharField(max_length = 12, choices = TYPE, default = 'Sell')
price = models.IntegerField()
description = models.TextField(default="No Description Given")
image = models.ImageField(default='default.jpeg', upload_to='product')
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('store')
and this is my views.py:
class ProductCreateView(LoginRequiredMixin, CreateView):
model = Product
fields = ['title','type', 'price','description', 'image']
def form_valid(self, form):
print(self.request.POST)
owner , created = Owner.objects.get_or_create(user=self.request.user)
form.instance.owner = self.request.user.owner
return super().form_valid(form)
I am getting default image for every new product created.
Thanks
P.S. when i am adding product from admin panel then every thing is working fine.
I have a model that stores an image in a media subdirectory, "media/games/". The image uploads to the correct location but when I try to retrieve it in the admin page it's trying to retrieve it in the base media/ path, and doesn't seem to reach down into the games folder, so that if I look under:
localhost:8000/media/games/image.png
it will show the image, but if I am in admin and click on the image link for the preview it tries to find it at:
localhost:8000/media/image.png
Shouldn't the ImageField be "games/image.png" instead of just "image.png"? I don't think the image field is storing the path correctly.
Here are my files:
MODELS:
from django.db import models
from django.core.files.storage import FileSystemStorage
fs = FileSystemStorage(location="media/games/")
class Game(models.Model):
title = models.CharField(max_length=127, unique=True)
slug = models.SlugField(max_length=127, unique=True)
summary = models.TextField(null=True, blank=True)
description = models.TextField(null=True, blank=True)
release_date = models.DateField('date released', null=True, blank=True)
released = models.BooleanField(default=False)
purchase_link = models.URLField(max_length=255, null=True, blank=True)
card_image = models.ImageField(storage=fs, null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.TimeField(auto_now=True)
def __str__(self):
return str(self.title)
URLS:
from django.conf.urls import url, include
from django.contrib import admin
from InvenTorrey.settings import base
from django.conf.urls.static import static
from games import urls as game_urls
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^^api/v1/', include(game_urls)),
]
if base.DEBUG is True:
urlpatterns += static(base.MEDIA_URL, document_root=base.MEDIA_ROOT)
SETTINGS:
BASE_DIR =
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
MEDIA_ROOT = os.path.join(BASE_DIR, "media/")
STATIC_ROOT = os.path.join(BASE_DIR,"static/")
STATIC_URL = '/static/'
MEDIA_URL = '/media/'
Instead of storage argument try to use upload_to:
card_image = models.ImageField(upload_to='games/', null=True, blank=True)
This allows you to save images to media/games/ directory, and also will add games/ to image's url path.
So I boogered up a migrate and decided it would be easier to go with a db reset. So I removed the migrations folder, as well as the database. I then recreated the database, ran python manage.py migrate, createsuperuser, and went ahead and logged into the admin panel - but to my surprise the models that I have registered no longer show up in the panel. They were there before this whole thing. Can anyone give me some insight? I'm sure I've missed something small and stupid. The file structure, outside of the migrations folder, has been untouched.
Models.py
from django.db import models
from django.core.validators import RegexValidator
class DetailsModel(models.Model):
distance = models.SmallIntegerField()
rate = models.DecimalField(max_digits=5, decimal_places=2)
class PhoneModel(models.Model):
phone_regex = RegexValidator(regex=r'^\+?1?\d{9,15}$', message="Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed.")
phone_number = models.CharField(validators=[phone_regex], max_length=15, blank=True) # validators should be a list
class TravelModel(models.Model):
mileageRate = models.DecimalField(max_digits=4, decimal_places=3)
def __str__(self):
return '%s' % (self.mileageRate)
class CompanyModel(models.Model):
name = models.CharField(max_length=255)
details = models.ForeignKey(DetailsModel, on_delete=models.CASCADE)
def __str__(self):
return '%s' % (self.name)
class SiteModel(models.Model):
company = models.ForeignKey(CompanyModel, on_delete=models.CASCADE)
street1 = models.CharField(max_length=255)
street2 = models.CharField(max_length=255)
city = models.CharField(max_length=50)
state = models.CharField(max_length=2)
zipcode = models.IntegerField()
country = models.CharField(max_length=50)
class PersonModel(models.Model):
firstName = models.CharField(max_length=50)
lastName = models.CharField(max_length=50)
company = models.ForeignKey(CompanyModel, on_delete=models.CASCADE)
phone = models.ForeignKey(PhoneModel, on_delete=models.CASCADE )
email = models.EmailField()
def __str__(self):
return '%s %s' % (self.firstName, self.lastName)
Settings.py (INSTALLED_APPS)
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'main',
#'rest_framework',
'debug_toolbar',
]
Admin.py from 'main' App
from django.contrib import admin
from . import models
# Register your models here.
admin.site.register(models.CompanyModel)
admin.site.register(models.SiteModel)
admin.site.register(models.PersonModel)
Before running python manage.py migrate, you should run python manage.py makemigrations.
I see two possible problems:
Try installing your app like this instead:
'YOUR_PROJECT.main' #use your project name
Check the permissions of your created user. It won't show if it has not the permissions to change the model.
With Django I can use the OneToOneField like this to create a relationship:
user = models.OneToOneField('auth.User')
How can I make a model (example below) that would get the current site_id when trying to upload an image:
class Image(models.Model):
site = # something here
user = models.OneToOneField('auth.User')
image = models.ImageField(upload_to='headers')
image_thumbnail = ImageSpecField(source='image', processors=[ResizeToFit(1920)], format='JPEG', options={'quality': 90})
image_admin_thumb = ImageSpecField(source='image', processors=[ResizeToFit(300)], format='JPEG', options={'quality': 80})
name = models.CharField(max_length=50)
date = models.DateTimeField(auto_now=True)
You can do one of 2 things:
You can use the get_current_site shortcut, but this would require the request object
You can get the Site object from settings.SITE_ID - this would need an additional lookup though.
For the second case, the usage would be something like this:
from django.contrib.sites.models import Site
from django.conf import settings
def set_current_site():
return Site.objects.get(pk=settings.SITE_ID)
and in the models:
site = models.ForeignKey(Site, default=set_current_site)
I have an image field using django-rest-framework how to I handle the uploading of images over the API?
Is there any examples?
models.py
image = models.ImageField(
upload_to="profiles",
height_field="height",
width_field="width",
null=True,
blank=True,
editable=True,
help_text="Profile Picture",
verbose_name="Profile Picture"
)
height = models.PositiveIntegerField(null=True, blank=True, editable=False)
width = models.PositiveIntegerField(null=True, blank=True, editable=False)
Finally I am able to upload image using Django.
Here is my working code
views.py
class FileUploadView(APIView):
# parser_classes = (MultiPartParser, FormParser, )
parser_classes = (FileUploadParser, )
# media_type = 'multipart/form-data'
# format = 'jpg'
def post(self, request, format='jpg'):
up_file = request.FILES['file']
destination = open('/Users/Username/' + up_file.name, 'wb+')
for chunk in up_file.chunks():
destination.write(chunk)
destination.close()
# ...
# do some stuff with uploaded file
# ...
return Response(up_file.name, status.HTTP_201_CREATED)
urls.py
urlpatterns = patterns('',
url(r'^imageUpload', views.FileUploadView.as_view())
curl request to upload
curl -X POST -S -H -u "admin:password" -F "file=#img.jpg;type=image/jpg" 127.0.0.1:8000/resourceurl/imageUpload
The image field doesn't store the image but rather a link to it. Do you have MEDIA_URL set in settings? eg MEDIA_URL = '/media/' If you're running on localhost you should find the image at localhost/media/profiles/image_name.png