From a7b617216414f1db06b8810f17ffc6f42053ef9c Mon Sep 17 00:00:00 2001 From: Jorge Date: Fri, 24 May 2024 19:09:43 -0400 Subject: [PATCH 01/11] First implementation of the permission models --- chris_backend/core/api.py | 24 + ...file_public_chrisfolder_public_and_more.py | 134 +++++ chris_backend/core/models.py | 481 +++++++++++++++- ..._feed_owner_feeduserpermission_and_more.py | 59 ++ chris_backend/feeds/models.py | 146 ++++- chris_backend/feeds/permissions.py | 92 +-- chris_backend/feeds/serializers.py | 143 +++-- chris_backend/feeds/views.py | 287 +++++++--- chris_backend/filebrowser/permissions.py | 16 +- chris_backend/filebrowser/serializers.py | 263 ++++++++- chris_backend/filebrowser/services.py | 2 +- chris_backend/filebrowser/views.py | 526 +++++++++++++++++- chris_backend/plugininstances/models.py | 5 +- chris_backend/plugininstances/permissions.py | 20 +- chris_backend/plugininstances/serializers.py | 15 +- .../plugininstances/services/manager.py | 15 +- chris_backend/users/views.py | 2 +- chris_backend/workflows/serializers.py | 7 +- 18 files changed, 2033 insertions(+), 204 deletions(-) create mode 100755 chris_backend/core/migrations/0002_chrisfile_public_chrisfolder_public_and_more.py create mode 100755 chris_backend/feeds/migrations/0002_remove_feed_owner_feeduserpermission_and_more.py diff --git a/chris_backend/core/api.py b/chris_backend/core/api.py index 88d2856a..ffcc15bb 100755 --- a/chris_backend/core/api.py +++ b/chris_backend/core/api.py @@ -61,6 +61,30 @@ path('v1/note/', feed_views.NoteDetail.as_view(), name='note-detail'), + path('v1//grouppermissions/', + feed_views.FeedGroupPermissionList.as_view(), + name='feed-group-permission-list'), + + path('v1//grouppermissions/search/', + feed_views.FeedGroupPermissionListListQuerySearch.as_view(), + name='feed-group-permission-list-query-search'), + + path('v1/grouppermissions//', + feed_views.FeedGroupPermissionDetail.as_view(), + name='feed-group-permission-detail'), + + path('v1//userpermissions/', + feed_views.FeedUserPermissionList.as_view(), + name='feed-user-permission-list'), + + path('v1//userpermissions/search/', + feed_views.FeedUserPermissionListListQuerySearch.as_view(), + name='feed-user-permission-list-query-search'), + + path('v1/userpermissions//', + feed_views.FeedUserPermissionDetail.as_view(), + name='feed-user-permission-detail'), + path('v1//comments/', feed_views.CommentList.as_view(), name='comment-list'), diff --git a/chris_backend/core/migrations/0002_chrisfile_public_chrisfolder_public_and_more.py b/chris_backend/core/migrations/0002_chrisfile_public_chrisfolder_public_and_more.py new file mode 100755 index 00000000..8404143f --- /dev/null +++ b/chris_backend/core/migrations/0002_chrisfile_public_chrisfolder_public_and_more.py @@ -0,0 +1,134 @@ +# Generated by Django 4.2.5 on 2024-05-07 21:44 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('auth', '0012_alter_user_first_name_max_length'), + ('core', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='chrisfile', + name='public', + field=models.BooleanField(blank=True, db_index=True, default=False), + ), + migrations.AddField( + model_name='chrisfolder', + name='public', + field=models.BooleanField(blank=True, db_index=True, default=False), + ), + migrations.AddField( + model_name='chrislinkfile', + name='public', + field=models.BooleanField(blank=True, db_index=True, default=False), + ), + migrations.CreateModel( + name='LinkFileUserPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('permission', models.CharField(choices=[('r', 'Read'), ('w', 'Write')], default='r', max_length=1)), + ('link_file', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.chrislinkfile')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('link_file', 'user')}, + }, + ), + migrations.CreateModel( + name='LinkFileGroupPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('permission', models.CharField(choices=[('r', 'Read'), ('w', 'Write')], default='r', max_length=1)), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')), + ('link_file', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.chrislinkfile')), + ], + options={ + 'unique_together': {('link_file', 'group')}, + }, + ), + migrations.CreateModel( + name='FolderUserPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('permission', models.CharField(choices=[('r', 'Read'), ('w', 'Write')], default='r', max_length=1)), + ('folder', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.chrisfolder')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('folder', 'user')}, + }, + ), + migrations.CreateModel( + name='FolderGroupPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('permission', models.CharField(choices=[('r', 'Read'), ('w', 'Write')], default='r', max_length=1)), + ('folder', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.chrisfolder')), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')), + ], + options={ + 'unique_together': {('folder', 'group')}, + }, + ), + migrations.CreateModel( + name='FileUserPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('permission', models.CharField(choices=[('r', 'Read'), ('w', 'Write')], default='r', max_length=1)), + ('file', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.chrisfile')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('file', 'user')}, + }, + ), + migrations.CreateModel( + name='FileGroupPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('permission', models.CharField(choices=[('r', 'Read'), ('w', 'Write')], default='r', max_length=1)), + ('file', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.chrisfile')), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')), + ], + options={ + 'unique_together': {('file', 'group')}, + }, + ), + migrations.AddField( + model_name='chrisfile', + name='shared_groups', + field=models.ManyToManyField(related_name='shared_files', through='core.FileGroupPermission', to='auth.group'), + ), + migrations.AddField( + model_name='chrisfile', + name='shared_users', + field=models.ManyToManyField(related_name='shared_files', through='core.FileUserPermission', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='chrisfolder', + name='shared_groups', + field=models.ManyToManyField(related_name='shared_folders', through='core.FolderGroupPermission', to='auth.group'), + ), + migrations.AddField( + model_name='chrisfolder', + name='shared_users', + field=models.ManyToManyField(related_name='shared_folders', through='core.FolderUserPermission', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='chrislinkfile', + name='shared_groups', + field=models.ManyToManyField(related_name='shared_link_files', through='core.LinkFileGroupPermission', to='auth.group'), + ), + migrations.AddField( + model_name='chrislinkfile', + name='shared_users', + field=models.ManyToManyField(related_name='shared_link_files', through='core.LinkFileUserPermission', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/chris_backend/core/models.py b/chris_backend/core/models.py index 3a0fdb0f..7a564db0 100755 --- a/chris_backend/core/models.py +++ b/chris_backend/core/models.py @@ -8,7 +8,7 @@ from django.db.models.signals import post_delete from django.dispatch import receiver from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Group import django_filters from django_filters.rest_framework import FilterSet @@ -20,6 +20,20 @@ logger = logging.getLogger(__name__) +PERMISSION_CHOICES = [("r", "Read"), ("w", "Write")] + + +def validate_permission(permission): + """ + Custom function to determine whether a permission value is valid. + """ + perm_list = [p[0] for p in PERMISSION_CHOICES] + if permission not in perm_list: + raise ValueError(f"Invalid permission '{permission}'. Allowed values " + f"are: {perm_list}.") + return permission + + class ChrisInstance(models.Model): """ Model class that defines a singleton representing a ChRIS instance. @@ -59,9 +73,14 @@ def load(cls): class ChrisFolder(models.Model): creation_date = models.DateTimeField(auto_now_add=True) path = models.CharField(max_length=1024, unique=True) # folder's path + public = models.BooleanField(blank=True, default=False, db_index=True) parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, related_name='children') owner = models.ForeignKey('auth.User', on_delete=models.CASCADE) + shared_groups = models.ManyToManyField(Group, related_name='shared_folders', + through='FolderGroupPermission') + shared_users = models.ManyToManyField(User, related_name='shared_folders', + through='FolderUserPermission') class Meta: ordering = ('-path',) @@ -95,6 +114,93 @@ def get_descendants(self): path = self.path.rstrip('/') + '/' return list(ChrisFolder.objects.filter(path__startswith=path)) + def has_group_permission(self, group, permission): + """ + Custom method to determine whether a group has been granted a permission + to access the folder. + """ + p = validate_permission(permission) + qs = FolderGroupPermission.objects.filter(group=group, folder=self, permission=p) + return qs.exists() + + def has_user_permission(self, user, permission): + """ + Custom method to determine whether a user has been granted a permission + to access the folder. + """ + p = validate_permission(permission) + lookup = models.Q(user=user) | models.Q(user__groups__shared_folders=self) + qs = FolderUserPermission.objects.filter(folder=self, permission=p).filter(lookup) + return qs.exists() + + def grant_group_permission(self, group, permission): + """ + Custom method to grant a group a permission to access the folder and all its + descendant folders, link files and files. + """ + FolderGroupPermission.objects.create(folder=self, group=group, + permission=permission) + + def remove_group_permission(self, group, permission): + """ + Custom method to remove a group's permission to access the folder and all its + descendant folders, link files and files. + """ + FolderGroupPermission.objects.get(folder=self, group=group, + permission=permission).delete() + + + def grant_user_permission(self, user, permission): + """ + Custom method to grant a user a permission to access the folder and all its + descendant folders, link files and files. + """ + FolderUserPermission.objects.create(folder=self, user=user, permission=permission) + + def remove_user_permission(self, user, permission): + """ + Custom method to remove a user's permission to access the folder and all its + descendant folders, link files and files. + """ + FolderUserPermission.objects.get(folder=self, user=user, + permission=permission).delete() + + def grant_public_access(self): + """ + Custom method to grant public access to the folder and all its descendant folders, + link files and files. + """ + self._update_public_access(True) + + def remove_public_access(self): + """ + Custom method to remove public access to the folder and all its descendant + folders, link files and files. + """ + self._update_public_access(False) + + def _update_public_access(self, public_tf): + """ + Internal method to update public access to the folder and all its descendant + folders, link files and files. + """ + path = self.path.rstrip('/') + '/' + + folders = list(ChrisFolder.objects.filter(path__startswith=path)) + for folder in folders: + folder.public = public_tf + ChrisFolder.objects.bulk_update(folders, ['public']) + + files = list(ChrisFile.objects.filter(path__startswith=path)) + for f in files: + f.public = public_tf + ChrisFile.objects.bulk_update(files, ['public']) + + link_files = list(ChrisLinkFile.objects.filter(path__startswith=path)) + for lf in link_files: + lf.public = public_tf + ChrisLinkFile.objects.bulk_update(link_files, ['public']) + class ChrisFolderFilter(FilterSet): path = django_filters.CharFilter(field_name='path') @@ -104,12 +210,178 @@ class Meta: fields = ['id', 'path'] +class FolderGroupPermission(models.Model): + permission = models.CharField(choices=PERMISSION_CHOICES, default='r', max_length=1) + folder = models.ForeignKey(ChrisFolder, on_delete=models.CASCADE) + group = models.ForeignKey(Group, on_delete=models.CASCADE) + + class Meta: + unique_together = ('folder', 'group',) + + def __str__(self): + return self.permission + + def save(self, *args, **kwargs): + """ + Overriden to grant the group permission to all the folders, files and link + files within the folder. + """ + super(FolderGroupPermission, self).save(*args, **kwargs) + + group = self.group + permission = self.permission + path = self.folder.path.rstrip('/') + '/' + + folders = ChrisFolder.objects.filter(path__startswith=path) + objs = [] + for folder in folders: + perm = FolderGroupPermission(folder=folder, group=group, + permission=permission) + objs.append(perm) + FolderGroupPermission.objects.bulk_create(objs, update_conflicts=True, + update_fields=['permission'], + unique_fields=['folder_id', 'group_id']) + + files = ChrisFile.objects.filter(fname__startswith=path) + objs = [] + for f in files: + perm = FileGroupPermission(file=f, group=group, permission=permission) + objs.append(perm) + FileGroupPermission.objects.bulk_create(objs, update_conflicts=True, + update_fields=['permission'], + unique_fields=['file_id', 'group_id']) + + link_files = ChrisLinkFile.objects.filter(fname__startswith=path) + objs = [] + for lf in link_files: + perm = LinkFileGroupPermission(link_file=lf, group=group, + permission=permission) + objs.append(perm) + LinkFileGroupPermission.objects.bulk_create(objs, update_conflicts=True, + update_fields=['permission'], + unique_fields=['link_file_id', + 'group_id']) + + def delete(self, *args, **kwargs): + """ + Overriden to remove the group permission to all the folders, files and + link files within the folder. + """ + super(FolderGroupPermission, self).delete(*args, **kwargs) + + group = self.group + permission = self.permission + path = self.folder.path.rstrip('/') + '/' + + FolderGroupPermission.objects.filter(folder__path__startswith=path, group=group, + permission=permission).delete() + + FileGroupPermission.objects.filter(file__fname__startswith=path, group=group, + permission=permission).delete() + + LinkFileGroupPermission.objects.filter(link_file__fname__startswith=path, + group=group, + permission=permission).delete() + + +class FolderGroupPermissionFilter(FilterSet): + group_name = django_filters.CharFilter(field_name='group__name', lookup_expr='exact') + + class Meta: + model = FolderGroupPermission + fields = ['id', 'group_name'] + + +class FolderUserPermission(models.Model): + permission = models.CharField(choices=PERMISSION_CHOICES, default='r', max_length=1) + folder = models.ForeignKey(ChrisFolder, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE) + + class Meta: + unique_together = ('folder', 'user',) + + def __str__(self): + return self.permission + + def save(self, *args, **kwargs): + """ + Overriden to grant the user permission to all the folders, files and link + files within the folder. + """ + super(FolderUserPermission, self).save(*args, **kwargs) + + user = self.user + permission = self.permission + path = self.folder.path.rstrip('/') + '/' + + folders = ChrisFolder.objects.filter(path__startswith=path) + objs = [] + for folder in folders: + perm = FolderUserPermission(folder=folder, user=user, permission=permission) + objs.append(perm) + FolderUserPermission.objects.bulk_create(objs, update_conflicts=True, + update_fields=['permission'], + unique_fields=['folder_id', 'group_id']) + + files = ChrisFile.objects.filter(path__startswith=path) + objs = [] + for f in files: + perm = FileUserPermission(file=f, user=user, permission=permission) + objs.append(perm) + FileUserPermission.objects.bulk_create(objs, update_conflicts=True, + update_fields=['permission'], + unique_fields=['file_id', 'group_id']) + + link_files = ChrisLinkFile.objects.filter(path__startswith=path) + objs = [] + for lf in link_files: + perm = LinkFileUserPermission(link_file=lf, user=user, permission=permission) + objs.append(perm) + LinkFileUserPermission.objects.bulk_create(objs, update_conflicts=True, + update_fields=['permission'], + unique_fields=['link_file_id', + 'group_id']) + + def delete(self, *args, **kwargs): + """ + Overriden to remove the user permission to all the folders, files and + link files within the folder. + """ + super(FolderUserPermission, self).delete(*args, **kwargs) + + user = self.user + permission = self.permission + path = self.folder.path.rstrip('/') + '/' + + FolderUserPermission.objects.filter(folder__path__startswith=path, user=user, + permission=permission).delete() + + FileUserPermission.objects.filter(file__fname__startswith=path, user=user, + permission=permission).delete() + + LinkFileUserPermission.objects.filter(link_file__fname__startswith=path, + user=user, permission=permission).delete() + + +class FolderUserPermissionFilter(FilterSet): + username = django_filters.CharFilter(field_name='user__username', lookup_expr='exact') + + class Meta: + model = FolderUserPermission + fields = ['id', 'username'] + + class ChrisFile(models.Model): creation_date = models.DateTimeField(auto_now_add=True) fname = models.FileField(max_length=1024, unique=True) + public = models.BooleanField(blank=True, default=False, db_index=True) parent_folder = models.ForeignKey(ChrisFolder, on_delete=models.CASCADE, related_name='chris_files') owner = models.ForeignKey('auth.User', on_delete=models.CASCADE) + shared_groups = models.ManyToManyField(Group, related_name='shared_files', + through='FileGroupPermission') + shared_users = models.ManyToManyField(User, related_name='shared_files', + through='FileUserPermission') class Meta: ordering = ('-fname',) @@ -117,6 +389,66 @@ class Meta: def __str__(self): return self.fname.name + def has_group_permission(self, group, permission): + """ + Custom method to determine whether a group has been granted permission to access + the file. + """ + p = validate_permission(permission) + qs = FileGroupPermission.objects.filter(group=group, file=self, permission=p) + return qs.exists() + + def has_user_permission(self, user, permission): + """ + Custom method to determine whether a user has been granted permission to access + the file. + """ + p = validate_permission(permission) + lookup = models.Q(user=user) | models.Q(user__groups__shared_files=self) + qs = FileUserPermission.objects.filter(file=self, permission=p).filter(lookup) + return qs.exists() + + def grant_group_permission(self, group, permission): + """ + Custom method to grant a group a permission to access the file. + """ + FileGroupPermission.objects.create(folder=self, group=group, + permission=permission) + + def remove_group_permission(self, group, permission): + """ + Custom method to remove a group's permission to access the file. + """ + FileGroupPermission.objects.get(folder=self, group=group, + permission=permission).delete() + + def grant_user_permission(self, user, permission): + """ + Custom method to grant a user a permission to access the file. + """ + FileUserPermission.objects.create(folder=self, user=user, permission=permission) + + def remove_user_permission(self, user, permission): + """ + Custom method to remove a user's permission to access the file. + """ + FileUserPermission.objects.get(folder=self, user=user, + permission=permission).delete() + + def grant_public_access(self): + """ + Custom method to grant public access to the file. + """ + self.public = True + self.save() + + def remove_public_access(self): + """ + Custom method to remove public access to the file. + """ + self.public = False + self.save() + @classmethod def get_base_queryset(cls): """ @@ -125,6 +457,7 @@ def get_base_queryset(cls): """ return cls.objects.all() + @receiver(post_delete, sender=ChrisFile) def auto_delete_file_from_storage(sender, instance, **kwargs): storage_path = instance.fname.name @@ -136,13 +469,58 @@ def auto_delete_file_from_storage(sender, instance, **kwargs): logger.error('Storage error, detail: %s' % str(e)) +class FileGroupPermission(models.Model): + permission = models.CharField(choices=PERMISSION_CHOICES, default='r', max_length=1) + file = models.ForeignKey(ChrisFile, on_delete=models.CASCADE) + group = models.ForeignKey(Group, on_delete=models.CASCADE) + + class Meta: + unique_together = ('file', 'group',) + + def __str__(self): + return self.permission + + +class FileGroupPermissionFilter(FilterSet): + group_name = django_filters.CharFilter(field_name='group__name', lookup_expr='exact') + + class Meta: + model = FileGroupPermission + fields = ['id', 'group_name'] + + +class FileUserPermission(models.Model): + permission = models.CharField(choices=PERMISSION_CHOICES, default='r', max_length=1) + file = models.ForeignKey(ChrisFile, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE) + + class Meta: + unique_together = ('file', 'user',) + + def __str__(self): + return self.permission + + +class FileUserPermissionFilter(FilterSet): + username = django_filters.CharFilter(field_name='user__username', lookup_expr='exact') + + class Meta: + model = FileUserPermission + fields = ['id', 'username'] + + class ChrisLinkFile(models.Model): creation_date = models.DateTimeField(auto_now_add=True) path = models.CharField(max_length=1024, db_index=True) # pointed path fname = models.FileField(max_length=1024, unique=True) + public = models.BooleanField(blank=True, default=False, db_index=True) parent_folder = models.ForeignKey(ChrisFolder, on_delete=models.CASCADE, related_name='chris_link_files') owner = models.ForeignKey('auth.User', on_delete=models.CASCADE) + shared_groups = models.ManyToManyField(Group, related_name='shared_link_files', + through='LinkFileGroupPermission') + shared_users = models.ManyToManyField(User, related_name='shared_link_files', + through='LinkFileUserPermission') def __str__(self): return self.fname.name @@ -167,6 +545,67 @@ def save(self, *args, **kwargs): self.fname.name = link_file_path super(ChrisLinkFile, self).save(*args, **kwargs) + def has_group_permission(self, group, permission): + """ + Custom method to determine whether a group has been granted permission to access + the link file. + """ + p = validate_permission(permission) + return LinkFileGroupPermission.objects.filter(group=group, link_file=self, + permission=p).exists() + + def has_user_permission(self, user, permission): + """ + Custom method to determine whether a user has been granted permission to access + the link file. + """ + p = validate_permission(permission) + lookup = models.Q(user=user) | models.Q(user__groups__shared_link_files=self) + return LinkFileUserPermission.objects.filter(link_file=self, + permission=p).filter(lookup).exists() + + def grant_group_permission(self, group, permission): + """ + Custom method to grant a group a permission to access the link file. + """ + LinkFileGroupPermission.objects.create(folder=self, group=group, + permission=permission) + + def remove_group_permission(self, group, permission): + """ + Custom method to remove a group's permission to access the link file. + """ + LinkFileGroupPermission.objects.get(folder=self, group=group, + permission=permission).delete() + + def grant_user_permission(self, user, permission): + """ + Custom method to grant a user a permission to access the link file. + """ + LinkFileUserPermission.objects.create(folder=self, user=user, + permission=permission) + + def remove_user_permission(self, user, permission): + """ + Custom method to remove a user's permission to access the link file. + """ + LinkFileUserPermission.objects.get(folder=self, user=user, + permission=permission).delete() + + def grant_public_access(self): + """ + Custom method to grant public access to the file. + """ + self.public = True + self.save() + + def remove_public_access(self): + """ + Custom method to remove public access to the file. + """ + self.public = False + self.save() + @receiver(post_delete, sender=ChrisLinkFile) def auto_delete_file_from_storage(sender, instance, **kwargs): @@ -179,6 +618,46 @@ def auto_delete_file_from_storage(sender, instance, **kwargs): logger.error('Storage error, detail: %s' % str(e)) +class LinkFileGroupPermission(models.Model): + permission = models.CharField(choices=PERMISSION_CHOICES, default='r', max_length=1) + link_file = models.ForeignKey(ChrisLinkFile, on_delete=models.CASCADE) + group = models.ForeignKey(Group, on_delete=models.CASCADE) + + class Meta: + unique_together = ('link_file', 'group',) + + def __str__(self): + return self.permission + + +class LinkFileGroupPermissionFilter(FilterSet): + group_name = django_filters.CharFilter(field_name='group__name', lookup_expr='exact') + + class Meta: + model = LinkFileGroupPermission + fields = ['id', 'group_name'] + + +class LinkFileUserPermission(models.Model): + permission = models.CharField(choices=PERMISSION_CHOICES, default='r', max_length=1) + link_file = models.ForeignKey(ChrisLinkFile, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE) + + class Meta: + unique_together = ('link_file', 'user',) + + def __str__(self): + return self.permission + + +class LinkFileUserPermissionFilter(FilterSet): + username = django_filters.CharFilter(field_name='user__username', lookup_expr='exact') + + class Meta: + model = LinkFileUserPermission + fields = ['id', 'username'] + + class FileDownloadToken(models.Model): creation_date = models.DateTimeField(auto_now_add=True) owner = models.ForeignKey('auth.User', on_delete=models.CASCADE) diff --git a/chris_backend/feeds/migrations/0002_remove_feed_owner_feeduserpermission_and_more.py b/chris_backend/feeds/migrations/0002_remove_feed_owner_feeduserpermission_and_more.py new file mode 100755 index 00000000..35421b2f --- /dev/null +++ b/chris_backend/feeds/migrations/0002_remove_feed_owner_feeduserpermission_and_more.py @@ -0,0 +1,59 @@ +# Generated by Django 4.2.5 on 2024-05-16 20:29 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('feeds', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='feed', + name='owner', + ), + migrations.CreateModel( + name='FeedUserPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('feed', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='feeds.feed')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('feed', 'user')}, + }, + ), + migrations.CreateModel( + name='FeedGroupPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('feed', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='feeds.feed')), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')), + ], + options={ + 'unique_together': {('feed', 'group')}, + }, + ), + migrations.AddField( + model_name='feed', + name='shared_groups', + field=models.ManyToManyField(related_name='shared_feeds', through='feeds.FeedGroupPermission', to='auth.group'), + ), + migrations.AddField( + model_name='feed', + name='shared_users', + field=models.ManyToManyField(related_name='shared_feeds', through='feeds.FeedUserPermission', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='feed', + name='owner', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + preserve_default=False, + ), + ] diff --git a/chris_backend/feeds/models.py b/chris_backend/feeds/models.py index 4b640de4..5cce48a8 100755 --- a/chris_backend/feeds/models.py +++ b/chris_backend/feeds/models.py @@ -1,6 +1,8 @@ from django.db import models from django.db.models.signals import post_delete +from django.contrib.auth.models import User, Group +from django.db.models import Q from django.dispatch import receiver import django_filters @@ -17,7 +19,11 @@ class Feed(models.Model): public = models.BooleanField(blank=True, default=False, db_index=True) folder = models.OneToOneField(ChrisFolder, on_delete=models.CASCADE, null=True, related_name='feed') - owner = models.ManyToManyField('auth.User', related_name='feed') + owner = models.ForeignKey('auth.User', on_delete=models.CASCADE) + shared_groups = models.ManyToManyField(Group, related_name='shared_feeds', + through='FeedGroupPermission') + shared_users = models.ManyToManyField(User, related_name='shared_feeds', + through='FeedUserPermission') class Meta: ordering = ('-creation_date',) @@ -41,13 +47,6 @@ def _save_note(self): note.feed = self note.save() - def get_creator(self): - """ - Custom method to get the user that created the feed. - """ - plg_inst = self.plugin_instances.filter(plugin__meta__type='fs')[0] - return plg_inst.owner - def get_plugin_instances_status_count(self, status): """ Custom method to get the number of associated plugin instances with a given @@ -55,6 +54,67 @@ def get_plugin_instances_status_count(self, status): """ return self.plugin_instances.filter(status=status).count() + def has_group_permission(self, group): + """ + Custom method to determine whether a group has been granted permission to access + the feed. + """ + return FeedGroupPermission.objects.filter(group=group, feed=self).exists() + + def has_user_permission(self, user): + """ + Custom method to determine whether a user has been granted permission to access + the feed. + """ + lookup = Q(user=user) | Q(user__groups__shared_feeds=self) + return FeedUserPermission.objects.filter(feed=self).filter(lookup).exists() + + def grant_group_permission(self, group): + """ + Custom method to grant a group write permission to access the feed and all its + folder's descendant folders, link files and files. + """ + FeedGroupPermission.objects.create(folder=self, group=group, permission='w') + + def remove_group_permission(self, group): + """ + Custom method to remove a group's permission to access the feed and all its + folder's descendant folders, link files and files. + """ + FeedGroupPermission.objects.get(folder=self, group=group, permission='w').delete() + + def grant_user_permission(self, user): + """ + Custom method to grant a user write permission to access the feed and all its + folder's descendant folders, link files and files. + """ + FeedUserPermission.objects.create(folder=self, user=user, permission='w') + + def remove_user_permission(self, user): + """ + Custom method to remove a user's permission to access the feed and all its + folder's descendant folders, link files and files. + """ + FeedUserPermission.objects.get(folder=self, user=user, permission='w').delete() + + def grant_public_access(self): + """ + Custom method to grant public access to the feed and all its folder's descendant + folders, link files and files. + """ + self.public = True + self.folder.grant_public_access() + self.save() + + def remove_public_access(self): + """ + Custom method to remove public access to the feed and all its folder's descendant + folders, link files and files. + """ + self.public = False + self.folder.remove_public_access() + self.save() + @receiver(post_delete, sender=Feed) def auto_delete_folder_with_feed(sender, instance, **kwargs): @@ -110,6 +170,76 @@ def filter_by_fname_icontains(self, queryset, name, value): return queryset.filter(pk__in=list(feed_ids)) +class FeedGroupPermission(models.Model): + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + group = models.ForeignKey(Group, on_delete=models.CASCADE) + + class Meta: + unique_together = ('feed', 'group',) + + def __str__(self): + return str(self.id) + + def save(self, *args, **kwargs): + """ + Overriden to grant the group write permission to all the folders, files and link + files within the feed. + """ + super(FeedGroupPermission, self).save(*args, **kwargs) + self.feed.folder.grant_group_permission(self.group, 'w') + + def delete(self, *args, **kwargs): + """ + Overriden to remove the group's write permission to all the folders, files and + link files within the feed. + """ + super(FeedGroupPermission, self).delete(*args, **kwargs) + self.feed.folder.remove_group_permission(self.group, 'w') + + +class FeedGroupPermissionFilter(FilterSet): + group_name = django_filters.CharFilter(field_name='group__name', lookup_expr='exact') + + class Meta: + model = FeedGroupPermission + fields = ['id', 'group_name'] + + +class FeedUserPermission(models.Model): + feed = models.ForeignKey(Feed, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE) + + class Meta: + unique_together = ('feed', 'user',) + + def __str__(self): + return str(self.id) + + def save(self, *args, **kwargs): + """ + Overriden to grant the user write permission to all the folders, files and link + files within the feed. + """ + super(FeedUserPermission, self).save(*args, **kwargs) + self.feed.folder.grant_user_permission(self.user, 'w') + + def delete(self, *args, **kwargs): + """ + Overriden to remove the user's write permission to all the folders, files and + link files within the feed. + """ + super(FeedUserPermission, self).delete(*args, **kwargs) + self.feed.folder.remove_user_permission(self.user, 'w') + + +class FeedUserPermissionFilter(FilterSet): + username = django_filters.CharFilter(field_name='user__username', lookup_expr='exact') + + class Meta: + model = FeedUserPermission + fields = ['id', 'username'] + + class Note(models.Model): creation_date = models.DateTimeField(auto_now_add=True) modification_date = models.DateTimeField(auto_now_add=True) diff --git a/chris_backend/feeds/permissions.py b/chris_backend/feeds/permissions.py index 524e9fba..a8457a4c 100755 --- a/chris_backend/feeds/permissions.py +++ b/chris_backend/feeds/permissions.py @@ -2,83 +2,95 @@ from rest_framework import permissions -class IsOwnerOrChris(permissions.BasePermission): +class IsChrisOrFeedOwnerOrHasFeedPermissionOrPublicFeedReadOnly(permissions.BasePermission): """ - Custom permission to only allow owners of an object or superuser - 'chris' to modify/edit it. + Custom permission to only allow superuser 'chris', the owner of a feed associated + to an object and users that have been granted the feed permission to modify/edit the + object (eg. a note). Read-only access is allowed to any user if the feed is public. """ def has_object_permission(self, request, view, obj): - # Read and write permissions are only allowed to - # the owner and superuser 'chris'. - if hasattr(obj.owner, 'all'): - return (request.user in obj.owner.all()) or (request.user.username == 'chris') - return (obj.owner == request.user) or (request.user.username == 'chris') + + if request.method in permissions.SAFE_METHODS and obj.feed.public: + return True + + return (request.user.username == 'chris' or obj.feed.owner == request.user or + obj.feed.has_user_permission(request.user)) -class IsFeedOwnerOrChrisOrPublicReadOnly(permissions.BasePermission): +class IsOwnerOrChrisOrReadOnly(permissions.BasePermission): """ Custom permission to only allow owners of an object or superuser 'chris' to - modify/edit it. Read only is allowed to other users if the object is public. + modify/edit it. Read-only is allowed to other users. """ def has_object_permission(self, request, view, obj): - return (request.method in permissions.SAFE_METHODS and obj.public) or ( - request.user in obj.owner.all()) or (request.user.username == 'chris') + + if request.method in permissions.SAFE_METHODS: + return True + + return (obj.owner == request.user) or (request.user.username == 'chris') -class IsRelatedFeedOwnerOrPublicReadOnlyOrChris(permissions.BasePermission): +class IsOwnerOrChrisOrHasPermissionOrPublicReadOnly(permissions.BasePermission): """ - Custom permission to only allow owners of a feed associated to an object or superuser - 'chris' to modify/edit the object (eg. a note). Read only is allowed to other users - if the related feed is public. + Custom permission to only allow superuser 'chris', the owner of a feed and + users that have been granted the feed permission to modify/edit the feed. + Read-only access is allowed to any user if the feed is public. """ def has_object_permission(self, request, view, obj): - if request.user.username == 'chris': + + if request.method in permissions.SAFE_METHODS and obj.public: return True - return (request.method in permissions.SAFE_METHODS and obj.feed.public) or ( - request.user in obj.feed.owner.all()) + return (obj.owner == request.user or request.user.username == 'chris' or + obj.has_user_permission(request.user)) -class IsOwnerOrChrisOrReadOnlyOrRelatedFeedPublicReadOnly(permissions.BasePermission): +class IsOwnerOrChrisOrHasPermissionReadOnly(permissions.BasePermission): """ - Custom permission to only allow owners of an object or superuser - 'chris' to modify/edit it. Read only is allowed to other authenticated users. - Read only is also allowed to unauthenticated users when the related feed is public. + Custom permission to only allow superuser 'chris' and the owner of a feed to + modify/edit the feed. Read-only access is allowed to other users that have been + granted the feed permission. """ def has_object_permission(self, request, view, obj): - user = request.user - if user.username == 'chris': + + if obj.owner == request.user or request.user.username == 'chris': return True - return obj.owner == user or (request.method in permissions.SAFE_METHODS and ( - user.is_authenticated or obj.feed.public)) + return (request.method in permissions.SAFE_METHODS and + obj.has_user_permission(request.user)) -class IsAuthenticatedOrRelatedFeedPublicReadOnly(permissions.BasePermission): +class IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly(permissions.BasePermission): """ - Custom permission to allow Read-only access to authenticated users. - Read only is also allowed to unauthenticated users when the related feed is public. + Custom permission to only allow superuser 'chris' and the owner of a feed associated + to an object to modify/edit the object. Read-only access is allowed to other users + that have been granted the feed permission. """ def has_object_permission(self, request, view, obj): - user = request.user - return request.method in permissions.SAFE_METHODS and ( - user.is_authenticated or obj.feed.public) + if obj.feed.owner == request.user or request.user.username == 'chris': + return True + + return (request.method in permissions.SAFE_METHODS and + obj.feed.has_user_permission(request.user)) -class IsRelatedTagOwnerOrChris(permissions.BasePermission): + +class IsOwnerOrChrisOrReadOnlyOrRelatedFeedPublicReadOnly(permissions.BasePermission): """ - Custom permission to only allow the owner of a tag associated to an object or - superuser 'chris' to access the object (eg. a tagging). + Custom permission to only allow owners of an object or superuser + 'chris' to modify/edit it. Read only is allowed to other authenticated users. + Read only is also allowed to unauthenticated users when the related feed is public. """ def has_object_permission(self, request, view, obj): - # Read and write permissions are only allowed to - # the owner and superuser 'chris'. - if (request.user.username == 'chris') or (request.user == obj.tag.owner): + user = request.user + if user.username == 'chris': return True - return False + + return obj.owner == user or (request.method in permissions.SAFE_METHODS and ( + user.is_authenticated or obj.feed.public)) diff --git a/chris_backend/feeds/serializers.py b/chris_backend/feeds/serializers.py index e42d5da0..288664d2 100755 --- a/chris_backend/feeds/serializers.py +++ b/chris_backend/feeds/serializers.py @@ -1,11 +1,12 @@ -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Group from django.core.exceptions import ObjectDoesNotExist from django.db.utils import IntegrityError from rest_framework import serializers from plugininstances.models import STATUS_CHOICES -from .models import Note, Feed, Tag, Tagging, Comment +from .models import (Note, Feed, Tag, Tagging, Comment, FeedGroupPermission, + FeedUserPermission) class NoteSerializer(serializers.HyperlinkedModelSerializer): @@ -27,7 +28,6 @@ class Meta: class TaggingSerializer(serializers.HyperlinkedModelSerializer): - owner_username = serializers.ReadOnlyField(source='tag.owner.username') tag_id = serializers.ReadOnlyField(source='tag.id') feed_id = serializers.ReadOnlyField(source='feed.id') feed = serializers.HyperlinkedRelatedField(view_name='feed-detail', read_only=True) @@ -35,7 +35,7 @@ class TaggingSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Tagging - fields = ('url', 'id', 'owner_username', 'tag_id', 'feed_id', 'tag', 'feed') + fields = ('url', 'id', 'tag_id', 'feed_id', 'tag', 'feed') def create(self, validated_data): """ @@ -53,8 +53,7 @@ def create(self, validated_data): def validate_tag(self, tag_id): """ - Custom method to check that a tag id is provided, exists in the DB and - owned by the user. + Custom method to check that a tag id is provided and exists in the DB. """ if not tag_id: raise serializers.ValidationError({'tag_id': ["A tag id is required."]}) @@ -64,16 +63,12 @@ def validate_tag(self, tag_id): except (ValueError, ObjectDoesNotExist): raise serializers.ValidationError( {'tag_id': ["Couldn't find any tag with id %s." % tag_id]}) - user = self.context['request'].user - if tag.owner != user: - raise serializers.ValidationError( - {'tag_id': ["User is not the owner of tag with tag_id %s." % tag_id]}) return tag def validate_feed(self, feed_id): """ Custom method to check that a feed id is provided, exists in the DB and - owned by the user. + the user has feed permission. """ if not feed_id: raise serializers.ValidationError({'feed_id': ["A feed id is required."]}) @@ -83,15 +78,18 @@ def validate_feed(self, feed_id): except (ValueError, ObjectDoesNotExist): raise serializers.ValidationError( {'feed_id': ["Couldn't find any feed with id %s." % feed_id]}) + user = self.context['request'].user - if user not in feed.owner.all(): + if not (feed.owner == user or user.username == 'chris' or + feed.has_user_permission(user)): raise serializers.ValidationError( - {'feed_id': ["User is not the owner of feed with feed_id %s." % feed_id]}) + {'feed_id': ["User does not have permission to tag feed with feed_id " + "%s." % feed_id]}) return feed class FeedSerializer(serializers.HyperlinkedModelSerializer): - creator_username = serializers.SerializerMethodField() + owner_username = serializers.ReadOnlyField(source='owner.username') folder_path = serializers.ReadOnlyField(source='folder.path') created_jobs = serializers.SerializerMethodField() waiting_jobs = serializers.SerializerMethodField() @@ -109,17 +107,29 @@ class FeedSerializer(serializers.HyperlinkedModelSerializer): comments = serializers.HyperlinkedIdentityField(view_name='comment-list') plugin_instances = serializers.HyperlinkedIdentityField( view_name='feed-plugininstance-list') - owner = serializers.HyperlinkedRelatedField(many=True, view_name='user-detail', - read_only=True) + owner = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True) class Meta: model = Feed fields = ('url', 'id', 'creation_date', 'modification_date', 'name', 'public', - 'creator_username', 'folder_path', 'created_jobs', 'waiting_jobs', + 'owner_username', 'folder_path', 'created_jobs', 'waiting_jobs', 'scheduled_jobs', 'started_jobs', 'registering_jobs', 'finished_jobs', 'errored_jobs', 'cancelled_jobs', 'folder', 'note', 'tags', 'taggings', 'comments', 'plugin_instances', 'owner') + def update(self, instance, validated_data): + """ + Overriden to grant or remove public access to the feed's folder and all its + descendant folders, link files and files depending on the new public status of + the feed. + """ + if 'public' in validated_data: + if instance.public and not validated_data['public']: + instance.folder.remove_public_access() + elif not instance.public and validated_data['public']: + instance.folder.grant_public_access() + return super(FeedSerializer, self).update(instance, validated_data) + def validate_name(self, name): """ Overriden to check that the feed's name does not contain forward slashes. @@ -129,24 +139,6 @@ def validate_name(self, name): ["This field may not contain forward slashes."]) return name - def validate_new_owner(self, username): - """ - Custom method to check whether a new feed owner is a system-registered user. - """ - try: - # check if user is a system-registered user - new_owner = User.objects.get(username=username) - except ObjectDoesNotExist: - raise serializers.ValidationError( - {'owner': ["User %s is not a registered user." % username]}) - return new_owner - - def get_creator_username(self, obj): - """ - Overriden to get the username of the creator of the feed. - """ - return obj.get_creator().username - def get_created_jobs(self, obj): """ Overriden to get the number of plugin instances in 'created' status. @@ -216,6 +208,87 @@ def get_cancelled_jobs(self, obj): return obj.get_plugin_instances_status_count('cancelled') +class FeedGroupPermissionSerializer(serializers.HyperlinkedModelSerializer): + grp_name = serializers.CharField(write_only=True) + feed_id = serializers.ReadOnlyField(source='feed.id') + feed_name = serializers.ReadOnlyField(source='feed.name') + group_id = serializers.ReadOnlyField(source='group.id') + group_name = serializers.ReadOnlyField(source='group.name') + + class Meta: + model = FeedGroupPermission + fields = ('url', 'id', 'feed_id', 'feed_name', 'group_id', 'group_name', + 'feed', 'group', 'name') + + def create(self, validated_data): + """ + Overriden to handle the error when trying to grant access permission to a group + that already has the permission granted. + """ + feed = validated_data['feed'] + group = validated_data['group'] + + try: + feed_perm = super(FeedGroupPermissionSerializer, self).create(validated_data) + except IntegrityError: + raise serializers.ValidationError( + {'non_field_errors': + [f"Group '{group.name}' already has permission to access feed " + f"with id {feed.id}"]}) + return feed_perm + + def validate_grp_name(self, grp_name): + """ + Custom method to check whether the provided group name exists in the DB. + """ + try: + group = Group.objects.get(name=grp_name) + except Group.DoesNotExist: + raise serializers.ValidationError( + {'grp_name': [f"Couldn't find any group with name '{grp_name}'."]}) + return group + +class FeedUserPermissionSerializer(serializers.HyperlinkedModelSerializer): + username = serializers.CharField(write_only=True, min_length=4, max_length=32) + feed_id = serializers.ReadOnlyField(source='feed.id') + feed_name = serializers.ReadOnlyField(source='feed.name') + user_id = serializers.ReadOnlyField(source='user.id') + user_username = serializers.ReadOnlyField(source='user.username') + + class Meta: + model = FeedUserPermission + fields = ('url', 'id', 'feed_id', 'feed_name', 'user_id', 'user_username', + 'feed', 'user', 'username') + + def create(self, validated_data): + """ + Overriden to handle the error when trying to grant access permission to a user + that already has the permission granted. + """ + feed = validated_data['feed'] + user = validated_data['user'] + + try: + feed_perm = super(FeedUserPermissionSerializer, self).create(validated_data) + except IntegrityError: + raise serializers.ValidationError( + {'non_field_errors': + [f"User '{user.username}' already has permission to access feed " + f"with id {feed.id}"]}) + return feed_perm + + def validate_username(self, username): + """ + Custom method to check whether the provided username exists in the DB. + """ + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + raise serializers.ValidationError( + {'username': [f"Couldn't find any user with username '{username}'."]}) + return user + + class CommentSerializer(serializers.HyperlinkedModelSerializer): owner_username = serializers.ReadOnlyField(source='owner.username') feed = serializers.HyperlinkedRelatedField(view_name='feed-detail', read_only=True) diff --git a/chris_backend/feeds/views.py b/chris_backend/feeds/views.py index 8dbe2e48..5b55c8e2 100755 --- a/chris_backend/feeds/views.py +++ b/chris_backend/feeds/views.py @@ -1,4 +1,5 @@ +from django.db.models import Q from django.shortcuts import get_object_or_404 from rest_framework import generics, permissions from rest_framework.reverse import reverse @@ -6,17 +7,18 @@ from collectionjson import services from plugininstances.serializers import PluginInstanceSerializer -from .models import Feed, FeedFilter -from .models import Tag, TagFilter -from .models import Comment, CommentFilter -from .models import Note, Tagging -from .serializers import FeedSerializer, NoteSerializer -from .serializers import TagSerializer, TaggingSerializer, CommentSerializer -from .permissions import (IsOwnerOrChris, IsRelatedTagOwnerOrChris, - IsFeedOwnerOrChrisOrPublicReadOnly, - IsRelatedFeedOwnerOrPublicReadOnlyOrChris, - IsOwnerOrChrisOrReadOnlyOrRelatedFeedPublicReadOnly, - IsAuthenticatedOrRelatedFeedPublicReadOnly) +from .models import (Feed, FeedFilter, FeedGroupPermission, FeedGroupPermissionFilter, + FeedUserPermission, FeedUserPermissionFilter, Tag, TagFilter, + Comment, CommentFilter, Note, Tagging) +from .serializers import (FeedSerializer, FeedGroupPermissionSerializer, + FeedUserPermissionSerializer, NoteSerializer, + TagSerializer, TaggingSerializer, CommentSerializer) +from .permissions import (IsChrisOrFeedOwnerOrHasFeedPermissionOrPublicFeedReadOnly, + IsOwnerOrChrisOrReadOnly, + IsOwnerOrChrisOrHasPermissionOrPublicReadOnly, + IsOwnerOrChrisOrHasPermissionReadOnly, + IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly, + IsOwnerOrChrisOrReadOnlyOrRelatedFeedPublicReadOnly) class NoteDetail(generics.RetrieveUpdateAPIView): @@ -26,7 +28,8 @@ class NoteDetail(generics.RetrieveUpdateAPIView): http_method_names = ['get', 'put'] queryset = Note.objects.all() serializer_class = NoteSerializer - permission_classes = (IsRelatedFeedOwnerOrPublicReadOnlyOrChris,) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsChrisOrFeedOwnerOrHasFeedPermissionOrPublicFeedReadOnly,) def retrieve(self, request, *args, **kwargs): """ @@ -39,22 +42,12 @@ def retrieve(self, request, *args, **kwargs): class TagList(generics.ListCreateAPIView): """ - A view for the collection of user-specific tags. + A view for the collection of tags. """ http_method_names = ['get', 'post'] serializer_class = TagSerializer - permission_classes = (permissions.IsAuthenticated, ) - - def get_queryset(self): - """ - Overriden to return a custom queryset that is only comprised by the tags - owned by the currently authenticated user. - """ - user = self.request.user - # if the user is chris then return all the tags in the system - if user.username == 'chris': - return Tag.objects.all() - return Tag.objects.filter(owner=user) + queryset = Tag.objects.all() + permission_classes = (permissions.IsAuthenticatedOrReadOnly, ) def perform_create(self, serializer): """ @@ -85,7 +78,6 @@ class TagListQuerySearch(generics.ListAPIView): http_method_names = ['get'] serializer_class = TagSerializer queryset = Tag.objects.all() - permission_classes = (permissions.IsAuthenticated,) filterset_class = TagFilter @@ -96,7 +88,7 @@ class TagDetail(generics.RetrieveUpdateDestroyAPIView): http_method_names = ['get', 'put', 'delete'] queryset = Tag.objects.all() serializer_class = TagSerializer - permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrChrisOrReadOnly) def retrieve(self, request, *args, **kwargs): """ @@ -114,7 +106,8 @@ class FeedTagList(generics.ListAPIView): http_method_names = ['get'] queryset = Feed.objects.all() serializer_class = TagSerializer - permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrHasPermissionOrPublicReadOnly) def list(self, request, *args, **kwargs): """ @@ -134,7 +127,7 @@ def get_tags_queryset(self, user): Custom method to get the actual tags queryset for the feed and user. """ feed = self.get_object() - return feed.tags.filter(owner=user) + return feed.tags.all() class TagFeedList(generics.ListAPIView): @@ -144,26 +137,32 @@ class TagFeedList(generics.ListAPIView): http_method_names = ['get'] queryset = Tag.objects.all() serializer_class = FeedSerializer - permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris) def list(self, request, *args, **kwargs): """ Overriden to return a list of the feeds for the queried tag. Document-level link relations are also added to the response. """ - queryset = self.get_feeds_queryset() + queryset = self.get_feeds_queryset(request.user) response = services.get_list_response(self, queryset) tag = self.get_object() links = {'tag': reverse('tag-detail', request=request, kwargs={"pk": tag.id})} return services.append_collection_links(response, links) - def get_feeds_queryset(self): + def get_feeds_queryset(self, user): """ - Custom method to get the actual feeds queryset for the tag. + Custom method to get the actual feeds queryset for the tag and user. """ tag = self.get_object() - return tag.feeds.all() + + if not user.is_authenticated: + return tag.feeds.filter(public=True) + + group_ids = [g.id for g in user.groups.all()] + lookup = Q(owner=user) | Q(public=True) | Q(shared_users=user) | Q( + shared_groups__in=group_ids) + return tag.feeds.filter(lookup) class FeedTaggingList(generics.ListCreateAPIView): @@ -173,7 +172,8 @@ class FeedTaggingList(generics.ListCreateAPIView): http_method_names = ['get', 'post'] queryset = Feed.objects.all() serializer_class = TaggingSerializer - permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrHasPermissionOrPublicReadOnly) def perform_create(self, serializer): """ @@ -207,7 +207,7 @@ def get_taggings_queryset(self, user): Custom method to get the actual taggings queryset for the feed. """ feed = self.get_object() - return Tagging.objects.filter(feed=feed, tag__owner=user) + return Tagging.objects.filter(feed=feed) class TagTaggingList(generics.ListCreateAPIView): @@ -217,7 +217,7 @@ class TagTaggingList(generics.ListCreateAPIView): http_method_names = ['get', 'post'] queryset = Tag.objects.all() serializer_class = TaggingSerializer - permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris) + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) def perform_create(self, serializer): """ @@ -237,7 +237,7 @@ def list(self, request, *args, **kwargs): Document-level link relations and a collection+json template are also added to the response. """ - queryset = self.get_taggings_queryset() + queryset = self.get_taggings_queryset(request.user) response = services.get_list_response(self, queryset) tag = self.get_object() links = {'tag': reverse('tag-detail', request=request, @@ -246,12 +246,19 @@ def list(self, request, *args, **kwargs): template_data = {"feed_id": ""} return services.append_collection_template(response, template_data) - def get_taggings_queryset(self): + def get_taggings_queryset(self, user): """ Custom method to get the actual taggings queryset for the tag. """ tag = self.get_object() - return Tagging.objects.filter(tag=tag) + + if not user.is_authenticated: + return Tagging.objects.filter(tag=tag).filter(feed__public=True) + + group_ids = [g.id for g in user.groups.all()] + lookup = Q(feed__owner=user) | (Q(feed__public=True) | Q( + feed__shared_users=user) | Q(feed__shared_groups__in=group_ids)) + return Tagging.objects.filter(tag=tag).filter(lookup) class TaggingDetail(generics.RetrieveDestroyAPIView): @@ -261,7 +268,8 @@ class TaggingDetail(generics.RetrieveDestroyAPIView): http_method_names = ['get', 'delete'] queryset = Tagging.objects.all() serializer_class = TaggingSerializer - permission_classes = (permissions.IsAuthenticated, IsRelatedTagOwnerOrChris) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsChrisOrFeedOwnerOrHasFeedPermissionOrPublicFeedReadOnly) class FeedList(generics.ListAPIView): @@ -274,15 +282,20 @@ class FeedList(generics.ListAPIView): def get_queryset(self): """ Overriden to return a custom queryset that is only comprised by the feeds - owned by the currently authenticated user. + owned by the currently authenticated user and those that have been shared with + the user. """ user = self.request.user if not user.is_authenticated: return [] + # if the user is chris then return all the feeds in the system if user.username == 'chris': return Feed.objects.all() - return Feed.objects.filter(owner=user) + + group_ids = [g.id for g in user.groups.all()] + lookup = Q(owner=user) | Q(shared_users=user) | Q(shared_groups__in=group_ids) + return Feed.objects.filter(lookup) def list(self, request, *args, **kwargs): """ @@ -336,13 +349,18 @@ class FeedListQuerySearch(generics.ListAPIView): def get_queryset(self): """ Overriden to return a custom queryset that is only comprised by the feeds - owned by the currently authenticated user. + owned by the currently authenticated user and those that have been shared with + the user. """ user = self.request.user + # if the user is chris then return all the feeds in the system if user.username == 'chris': return Feed.objects.all() - return Feed.objects.filter(owner=user) + + group_ids = [g.id for g in user.groups.all()] + lookup = Q(owner=user) | Q(shared_users=user) | Q(shared_groups__in=group_ids) + return Feed.objects.filter(lookup) class FeedDetail(generics.RetrieveUpdateDestroyAPIView): @@ -352,35 +370,15 @@ class FeedDetail(generics.RetrieveUpdateDestroyAPIView): http_method_names = ['get', 'put', 'delete'] queryset = Feed.objects.all() serializer_class = FeedSerializer - permission_classes = (IsFeedOwnerOrChrisOrPublicReadOnly,) - - def perform_update(self, serializer): - """ - Overriden to update feed's owners if requested by a PUT request. - """ - if 'owner' in self.request.data: - self.update_owners(serializer) - super(FeedDetail, self).perform_update(serializer) - - def update_owners(self, serializer): - """ - Custom method to update the feed's owners. - """ - feed = self.get_object() - owners = feed.owner.values('username') - username = self.request.data.pop('owner') - if {'username': username} not in owners: - new_owner = serializer.validate_new_owner(username) - owners = [o for o in feed.owner.all()] - owners.append(new_owner) - serializer.save(owner=owners) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrHasPermissionOrPublicReadOnly,) def retrieve(self, request, *args, **kwargs): """ Overriden to append a collection+json template. """ response = super(FeedDetail, self).retrieve(request, *args, **kwargs) - template_data = {"name": "", "public": "", "owner": ""} + template_data = {"name": "", "public": ""} return services.append_collection_template(response, template_data) @@ -425,6 +423,150 @@ def get_queryset(self): return Feed.objects.filter(public=True) +class FeedGroupPermissionList(generics.ListCreateAPIView): + """ + A view for a feed's collection of group permissions. + """ + http_method_names = ['get', 'post'] + queryset = Feed.objects.all() + serializer_class = FeedGroupPermissionSerializer + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasPermissionReadOnly) + + def perform_create(self, serializer): + """ + Overriden to provide a group and feed before first saving to the DB. + """ + group = serializer.validated_data.pop('name') + feed = self.get_object() + serializer.save(user=group, feed=feed) + + def list(self, request, *args, **kwargs): + """ + Overriden to return a list of the group permissions for the queried feed. + Document-level link relations and a collection+json template are also added + to the response. + """ + queryset = self.get_group_permissions_queryset() + response = services.get_list_response(self, queryset) + feed = self.get_object() + links = {'feed': reverse('feed-detail', request=request, + kwargs={"pk": feed.id})} + response = services.append_collection_links(response, links) + template_data = {"grp_name": ""} + return services.append_collection_template(response, template_data) + + def get_group_permissions_queryset(self): + """ + Custom method to get the actual group permissions queryset for the feed. + """ + feed = self.get_object() + return FeedGroupPermission.objects.filter(feed=feed) + + +class FeedGroupPermissionListQuerySearch(generics.ListAPIView): + """ + A view for the collection of feed-specific group permissions resulting from a query + search. + """ + http_method_names = ['get'] + serializer_class = FeedGroupPermissionSerializer + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasPermissionReadOnly) + filterset_class = FeedGroupPermissionFilter + + def get_queryset(self): + """ + Overriden to return a custom queryset that is comprised by the feed-specific + group permissions. + """ + feed = get_object_or_404(Feed, pk=self.kwargs['pk']) + return feed.shared_groups.all() + + +class FeedGroupPermissionDetail(generics.RetrieveDestroyAPIView): + """ + A view for a feed's group permission. + """ + http_method_names = ['get', 'delete'] + serializer_class = FeedGroupPermissionSerializer + queryset = FeedGroupPermission.objects.all() + permission_classes = (permissions.IsAuthenticated, + IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly) + + +class FeedUserPermissionList(generics.ListCreateAPIView): + """ + A view for a feed's collection of user permissions. + """ + http_method_names = ['get', 'post'] + queryset = Feed.objects.all() + serializer_class = FeedUserPermissionSerializer + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasPermissionReadOnly) + + def perform_create(self, serializer): + """ + Overriden to provide a user and feed before first saving to the DB. + """ + user = serializer.validated_data.pop('username') + feed = self.get_object() + serializer.save(user=user, feed=feed) + + def list(self, request, *args, **kwargs): + """ + Overriden to return a list of the user permissions for the queried feed. + Document-level link relations and a collection+json template are also added + to the response. + """ + queryset = self.get_user_permissions_queryset() + response = services.get_list_response(self, queryset) + feed = self.get_object() + links = {'feed': reverse('feed-detail', request=request, + kwargs={"pk": feed.id})} + response = services.append_collection_links(response, links) + template_data = {"username": ""} + return services.append_collection_template(response, template_data) + + def get_user_permissions_queryset(self): + """ + Custom method to get the actual user permissions queryset for the feed. + """ + feed = self.get_object() + return FeedUserPermission.objects.filter(feed=feed) + + +class FeedUserPermissionListQuerySearch(generics.ListAPIView): + """ + A view for the collection of feed-specific user permissions resulting from a query + search. + """ + http_method_names = ['get'] + serializer_class = FeedUserPermissionSerializer + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasPermissionReadOnly) + filterset_class = FeedUserPermissionFilter + + def get_queryset(self): + """ + Overriden to return a custom queryset that is comprised by the feed-specific + user permissions. + """ + feed = get_object_or_404(Feed, pk=self.kwargs['pk']) + return feed.shared_users.all() + + +class FeedUserPermissionDetail(generics.RetrieveDestroyAPIView): + """ + A view for a feed's user permission. + """ + http_method_names = ['get', 'delete'] + serializer_class = FeedUserPermissionSerializer + queryset = FeedUserPermission.objects.all() + permission_classes = (permissions.IsAuthenticated, + IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly) + + class CommentList(generics.ListCreateAPIView): """ A view for the collection of comments. @@ -432,7 +574,8 @@ class CommentList(generics.ListCreateAPIView): http_method_names = ['get', 'post'] queryset = Feed.objects.all() serializer_class = CommentSerializer - permission_classes = (IsFeedOwnerOrChrisOrPublicReadOnly,) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrHasPermissionOrPublicReadOnly) def perform_create(self, serializer): """ @@ -476,7 +619,8 @@ class CommentListQuerySearch(generics.ListAPIView): """ http_method_names = ['get'] serializer_class = CommentSerializer - permission_classes = (IsAuthenticatedOrRelatedFeedPublicReadOnly,) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsChrisOrFeedOwnerOrHasFeedPermissionOrPublicFeedReadOnly) filterset_class = CommentFilter def get_queryset(self): @@ -513,7 +657,8 @@ class FeedPluginInstanceList(generics.ListAPIView): http_method_names = ['get'] queryset = Feed.objects.all() serializer_class = PluginInstanceSerializer - permission_classes = (IsFeedOwnerOrChrisOrPublicReadOnly,) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrHasPermissionOrPublicReadOnly) def list(self, request, *args, **kwargs): """ diff --git a/chris_backend/filebrowser/permissions.py b/chris_backend/filebrowser/permissions.py index 2171c060..1032f540 100755 --- a/chris_backend/filebrowser/permissions.py +++ b/chris_backend/filebrowser/permissions.py @@ -1,13 +1,15 @@ +from django.db.models import Q +from django.contrib.auth.models import User from rest_framework import permissions from feeds.models import Feed -class IsOwnerOrChrisOrReadOnly(permissions.BasePermission): +class IsOwnerOrChrisOrWriteUserOrReadOnly(permissions.BasePermission): """ - Custom permission to only allow owners of an object or superuser - 'chris' to modify/edit it. Read only is allowed to other users. + Custom permission to only allow owners of an object or superuser 'chris' or users + with write permission to modify/edit it. Read-only is allowed to other users. """ def has_object_permission(self, request, view, obj): @@ -16,8 +18,12 @@ def has_object_permission(self, request, view, obj): if request.method in permissions.SAFE_METHODS: return True - # Write permissions are only allowed to the owner and superuser 'chris'. - return (request.user == obj.owner) or (request.user.username == 'chris') + # Write permissions are only allowed to the owner, superuser 'chris' and users + # with write permission. + user = request.user + lookup = Q(shared_folders=obj) | Q(groups__shared_folders=obj) + return (user == obj.owner or user.username == 'chris' or + User.objects.filter(username=user.username).filter(lookup).count()) class IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly(permissions.BasePermission): diff --git a/chris_backend/filebrowser/serializers.py b/chris_backend/filebrowser/serializers.py index b3e11d1a..96dab8ff 100755 --- a/chris_backend/filebrowser/serializers.py +++ b/chris_backend/filebrowser/serializers.py @@ -1,12 +1,16 @@ import os +from django.contrib.auth.models import User, Group +from django.db.utils import IntegrityError from rest_framework import serializers from rest_framework.reverse import reverse from collectionjson.fields import ItemLinkField from core.utils import get_file_resource_link -from core.models import ChrisFolder, ChrisFile, ChrisLinkFile +from core.models import (ChrisFolder, ChrisFile, ChrisLinkFile, FolderGroupPermission, + FolderUserPermission, FileGroupPermission, FileUserPermission, + LinkFileGroupPermission, LinkFileUserPermission) class FileBrowserFolderSerializer(serializers.HyperlinkedModelSerializer): @@ -21,7 +25,7 @@ class FileBrowserFolderSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = ChrisFolder - fields = ('url', 'id', 'creation_date', 'path', 'parent', 'children', + fields = ('url', 'id', 'creation_date', 'path', 'public', 'parent', 'children', 'files', 'link_files', 'owner') def create(self, validated_data): @@ -66,7 +70,90 @@ def validate_path(self, path): return path -class FileBrowserChrisFileSerializer(serializers.HyperlinkedModelSerializer): +class FileBrowserFolderGroupPermissionSerializer(serializers.HyperlinkedModelSerializer): + grp_name = serializers.CharField(write_only=True) + folder_id = serializers.ReadOnlyField(source='folder.id') + folder_name = serializers.ReadOnlyField(source='folder.name') + group_id = serializers.ReadOnlyField(source='group.id') + group_name = serializers.ReadOnlyField(source='group.name') + + class Meta: + model = FolderGroupPermission + fields = ('url', 'id', 'permission', 'folder_id', 'folder_name', 'group_id', + 'group_name', 'folder', 'group', 'grp_name') + + def create(self, validated_data): + """ + Overriden to handle the error when trying to create a permission for a group that + already has a permission granted. + """ + folder = validated_data['folder'] + group = validated_data['group'] + + try: + perm = super(FileBrowserFolderGroupPermissionSerializer, + self).create(validated_data) + except IntegrityError: + raise serializers.ValidationError( + {'non_field_errors': + [f"Group '{group.name}' already has a permission to access folder " + f"with id {folder.id}"]}) + return perm + + def validate_grp_name(self, grp_name): + """ + Custom method to check whether the provided group name exists in the DB. + """ + try: + group = Group.objects.get(name=grp_name) + except Group.DoesNotExist: + raise serializers.ValidationError( + {'grp_name': [f"Couldn't find any group with name '{grp_name}'."]}) + return group + +class FileBrowserFolderUserPermissionSerializer(serializers.HyperlinkedModelSerializer): + username = serializers.CharField(write_only=True, min_length=4, max_length=32) + folder_id = serializers.ReadOnlyField(source='folder.id') + folder_name = serializers.ReadOnlyField(source='folder.name') + user_id = serializers.ReadOnlyField(source='user.id') + user_username = serializers.ReadOnlyField(source='user.username') + + class Meta: + model = FolderUserPermission + fields = ('url', 'id', 'permission', 'folder_id', 'folder_name', 'user_id', + 'user_username', 'folder', 'user', 'username') + + def create(self, validated_data): + """ + Overriden to handle the error when trying to create a permission for a user that + already has a permission granted. + """ + folder = validated_data['folder'] + user = validated_data['user'] + + try: + perm = super(FileBrowserFolderUserPermissionSerializer, + self).create(validated_data) + except IntegrityError: + raise serializers.ValidationError( + {'non_field_errors': + [f"User '{user.username}' already has a permission to access " + f"folder with id {folder.id}"]}) + return perm + + def validate_username(self, username): + """ + Custom method to check whether the provided username exists in the DB. + """ + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + raise serializers.ValidationError( + {'username': [f"Couldn't find any user with username '{username}'."]}) + return user + + +class FileBrowserFileSerializer(serializers.HyperlinkedModelSerializer): fname = serializers.FileField(use_url=False) fsize = serializers.ReadOnlyField(source='fname.size') owner_username = serializers.ReadOnlyField(source='owner.username') @@ -87,7 +174,91 @@ def get_file_link(self, obj): return get_file_resource_link(self, obj) -class FileBrowserChrisLinkFileSerializer(serializers.HyperlinkedModelSerializer): +class FileBrowserFileGroupPermissionSerializer(serializers.HyperlinkedModelSerializer): + grp_name = serializers.CharField(write_only=True) + file_id = serializers.ReadOnlyField(source='file.id') + file_fname = serializers.ReadOnlyField(source='file.fname') + group_id = serializers.ReadOnlyField(source='group.id') + group_name = serializers.ReadOnlyField(source='group.name') + + class Meta: + model = FileGroupPermission + fields = ('url', 'id', 'permission', 'file_id', 'file_fname', 'group_id', + 'group_name', 'file', 'group', 'grp_name') + + def create(self, validated_data): + """ + Overriden to handle the error when trying to create a permission for a group that + already has a permission granted. + """ + f = validated_data['file'] + group = validated_data['group'] + + try: + perm = super(FileBrowserFileGroupPermissionSerializer, + self).create(validated_data) + except IntegrityError: + raise serializers.ValidationError( + {'non_field_errors': + [f"Group '{group.name}' already has a permission to access file " + f"with id {f.id}"]}) + return perm + + def validate_grp_name(self, grp_name): + """ + Custom method to check whether the provided group name exists in the DB. + """ + try: + group = Group.objects.get(name=grp_name) + except Group.DoesNotExist: + raise serializers.ValidationError( + {'grp_name': [f"Couldn't find any group with name '{grp_name}'."]}) + return group + + +class FileBrowserFileUserPermissionSerializer(serializers.HyperlinkedModelSerializer): + username = serializers.CharField(write_only=True, min_length=4, max_length=32) + file_id = serializers.ReadOnlyField(source='file.id') + file_fname = serializers.ReadOnlyField(source='file.fname') + user_id = serializers.ReadOnlyField(source='user.id') + user_username = serializers.ReadOnlyField(source='user.username') + + class Meta: + model = FileUserPermission + fields = ('url', 'id', 'permission', 'file_id', 'file_fname', 'user_id', + 'user_username', 'file', 'user', 'username') + + def create(self, validated_data): + """ + Overriden to handle the error when trying to create a permission for a user that + already has a permission granted. + """ + f = validated_data['file'] + user = validated_data['user'] + + try: + perm = super(FileBrowserFileUserPermissionSerializer, + self).create(validated_data) + except IntegrityError: + raise serializers.ValidationError( + {'non_field_errors': + [f"User '{user.username}' already has a permission to access " + f"file with id {f.id}"]}) + return perm + + def validate_username(self, username): + """ + Custom method to check whether the provided username exists in the DB. + """ + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + raise serializers.ValidationError( + {'username': [f"Couldn't find any user with username '{username}'."]}) + return user + + +class FileBrowserLinkFileSerializer(serializers.HyperlinkedModelSerializer): fname = serializers.FileField(use_url=False, required=False) fsize = serializers.ReadOnlyField(source='fname.size') owner_username = serializers.ReadOnlyField(source='owner.username') @@ -146,3 +317,87 @@ def get_linked_file_link(self, obj): request = self.context['request'] return reverse('chrisfile-detail', request=request, kwargs={'pk': linked_file.pk}) + + +class FileBrowserLinkFileGroupPermissionSerializer(serializers.HyperlinkedModelSerializer): + grp_name = serializers.CharField(write_only=True) + link_file_id = serializers.ReadOnlyField(source='link_file.id') + link_file_fname = serializers.ReadOnlyField(source='link_file.fname') + group_id = serializers.ReadOnlyField(source='group.id') + group_name = serializers.ReadOnlyField(source='group.name') + + class Meta: + model = FileGroupPermission + fields = ('url', 'id', 'permission', 'link_file_id', 'link_file_fname', + 'group_id', 'group_name', 'link_file', 'group', 'grp_name') + + def create(self, validated_data): + """ + Overriden to handle the error when trying to create a permission for a group that + already has a permission granted. + """ + lf = validated_data['link_file'] + group = validated_data['group'] + + try: + perm = super(FileBrowserLinkFileGroupPermissionSerializer, + self).create(validated_data) + except IntegrityError: + raise serializers.ValidationError( + {'non_field_errors': + [f"Group '{group.name}' already has a permission to access link " + f"file with id {lf.id}"]}) + return perm + + def validate_grp_name(self, grp_name): + """ + Custom method to check whether the provided group name exists in the DB. + """ + try: + group = Group.objects.get(name=grp_name) + except Group.DoesNotExist: + raise serializers.ValidationError( + {'grp_name': [f"Couldn't find any group with name '{grp_name}'."]}) + return group + + +class FileBrowserLinkFileUserPermissionSerializer(serializers.HyperlinkedModelSerializer): + username = serializers.CharField(write_only=True, min_length=4, max_length=32) + link_file_id = serializers.ReadOnlyField(source='link_file.id') + link_file_fname = serializers.ReadOnlyField(source='link_file.fname') + user_id = serializers.ReadOnlyField(source='user.id') + user_username = serializers.ReadOnlyField(source='user.username') + + class Meta: + model = FileUserPermission + fields = ('url', 'id', 'permission', 'link_file_id', 'link_file_fname', 'user_id', + 'user_username', 'link_file', 'user', 'username') + + def create(self, validated_data): + """ + Overriden to handle the error when trying to create a permission for a user that + already has a permission granted. + """ + lf = validated_data['link_file'] + user = validated_data['user'] + + try: + perm = super(FileBrowserLinkFileUserPermissionSerializer, + self).create(validated_data) + except IntegrityError: + raise serializers.ValidationError( + {'non_field_errors': + [f"User '{user.username}' already has a permission to access " + f"file with id {lf.id}"]}) + return perm + + def validate_username(self, username): + """ + Custom method to check whether the provided username exists in the DB. + """ + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + raise serializers.ValidationError( + {'username': [f"Couldn't find any user with username '{username}'."]}) + return user diff --git a/chris_backend/filebrowser/services.py b/chris_backend/filebrowser/services.py index 4a35c17e..bc280535 100755 --- a/chris_backend/filebrowser/services.py +++ b/chris_backend/filebrowser/services.py @@ -176,7 +176,7 @@ def get_shared_feed_creators_set(user=None): feeds_qs = Feed.objects.filter(models.Q(owner=user) | models.Q(public=True)) username = user.username for feed in feeds_qs.all(): - creator = feed.get_creator() + creator = feed.owner if creator.username != username: creators_set.add(creator.username) return creators_set diff --git a/chris_backend/filebrowser/views.py b/chris_backend/filebrowser/views.py index 4d1587f0..749343c5 100755 --- a/chris_backend/filebrowser/views.py +++ b/chris_backend/filebrowser/views.py @@ -2,22 +2,36 @@ import logging from django.http import Http404, FileResponse +from django.shortcuts import get_object_or_404 from rest_framework import generics, permissions from rest_framework.reverse import reverse from rest_framework.authentication import BasicAuthentication, SessionAuthentication -from core.models import ChrisFolder, ChrisFile, ChrisLinkFile +from core.models import (ChrisFolder, FolderGroupPermission, + FolderGroupPermissionFilter, FolderUserPermission, + FolderUserPermissionFilter, ChrisFile, FileGroupPermission, + FileGroupPermissionFilter, FileUserPermission, + FileUserPermissionFilter, ChrisLinkFile, + LinkFileGroupPermission, LinkFileGroupPermissionFilter, + LinkFileUserPermission, LinkFileUserPermissionFilter) from core.renderers import BinaryFileRenderer from core.views import TokenAuthSupportQueryString from collectionjson import services -from .serializers import (FileBrowserFolderSerializer, FileBrowserChrisFileSerializer, - FileBrowserChrisLinkFileSerializer) +from .serializers import (FileBrowserFolderSerializer, + FileBrowserFolderGroupPermissionSerializer, + FileBrowserFolderUserPermissionSerializer, + FileBrowserFileSerializer, + FileBrowserFileGroupPermissionSerializer, + FileBrowserFileUserPermissionSerializer, + FileBrowserLinkFileSerializer, + FileBrowserLinkFileGroupPermissionSerializer, + FileBrowserLinkFileUserPermissionSerializer) from .services import (get_authenticated_user_folder_queryset, get_unauthenticated_user_folder_queryset, get_authenticated_user_folder_children, get_unauthenticated_user_folder_children) -from .permissions import (IsOwnerOrChrisOrReadOnly, +from .permissions import (IsOwnerOrChrisOrWriteUserOrReadOnly, IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly) @@ -77,23 +91,26 @@ def get_queryset(self): user = self.request.user id = self.request.GET.get('id') pk_dict = {'id': id} + if id is None: path = self.request.GET.get('path', '') path = path.strip('/') pk_dict = {'path': path} + if user.is_authenticated: return get_authenticated_user_folder_queryset(pk_dict, user) return get_unauthenticated_user_folder_queryset(pk_dict) -class FileBrowserFolderDetail(generics.RetrieveDestroyAPIView): +class FileBrowserFolderDetail(generics.RetrieveUpdateDestroyAPIView): """ A ChRIS folder view. """ - http_method_names = ['get', 'delete'] + http_method_names = ['get', 'put', 'delete'] queryset = ChrisFolder.objects.all() serializer_class = FileBrowserFolderSerializer - permission_classes = (IsOwnerOrChrisOrReadOnly,) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrWriteUserOrReadOnly) def retrieve(self, request, *args, **kwargs): """ @@ -146,6 +163,7 @@ def get_children_queryset(self): """ user = self.request.user folder = self.get_object() + if user.is_authenticated: children = get_authenticated_user_folder_children(folder, user) else: @@ -153,13 +171,175 @@ def get_children_queryset(self): return self.filter_queryset(children) +class FileBrowserFolderGroupPermissionList(generics.ListCreateAPIView): + """ + A view for a folder's collection of group permissions. + """ + http_method_names = ['get', 'post'] + queryset = ChrisFolder.objects.all() + serializer_class = FileBrowserFolderGroupPermissionSerializer + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasPermissionReadOnly) + + def perform_create(self, serializer): + """ + Overriden to provide a group and folder before first saving to the DB. + """ + group = serializer.validated_data.pop('name') + folder = self.get_object() + serializer.save(user=group, folder=folder) + + def list(self, request, *args, **kwargs): + """ + Overriden to return a list of the group permissions for the queried folder. + Document-level link relations and a collection+json template are also added + to the response. + """ + queryset = self.get_group_permissions_queryset() + response = services.get_list_response(self, queryset) + folder = self.get_object() + links = {'folder': reverse('folder-detail', request=request, + kwargs={"pk": folder.id})} + response = services.append_collection_links(response, links) + template_data = {"grp_name": ""} + return services.append_collection_template(response, template_data) + + def get_group_permissions_queryset(self): + """ + Custom method to get the actual group permissions queryset for the folder. + """ + folder = self.get_object() + return FolderGroupPermission.objects.filter(folder=folder) + + +class FileBrowserFolderGroupPermissionListQuerySearch(generics.ListAPIView): + """ + A view for the collection of folder-specific group permissions resulting from a query + search. + """ + http_method_names = ['get'] + serializer_class = FileBrowserFolderGroupPermissionSerializer + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasPermissionReadOnly) + filterset_class = FolderGroupPermissionFilter + + def get_queryset(self): + """ + Overriden to return a custom queryset that is comprised by the folder-specific + group permissions. + """ + folder = get_object_or_404(ChrisFolder, pk=self.kwargs['pk']) + return folder.shared_groups.all() + + +class FileBrowserFolderGroupPermissionDetail(generics.RetrieveUpdateDestroyAPIView): + """ + A view for a folder's group permission. + """ + http_method_names = ['get', 'put', 'delete'] + serializer_class = FileBrowserFolderGroupPermissionSerializer + queryset = FolderGroupPermission.objects.all() + permission_classes = (permissions.IsAuthenticated, + IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly) + + def retrieve(self, request, *args, **kwargs): + """ + Overriden to append a collection+json template. + """ + response = super(FileBrowserFolderGroupPermissionDetail, + self).retrieve(request,*args, **kwargs) + template_data = {"permission": ""} + return services.append_collection_template(response, template_data) + + +class FileBrowserFolderUserPermissionList(generics.ListCreateAPIView): + """ + A view for a folder's collection of user permissions. + """ + http_method_names = ['get', 'post'] + queryset = ChrisFolder.objects.all() + serializer_class = FileBrowserFolderUserPermissionSerializer + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasPermissionReadOnly) + + def perform_create(self, serializer): + """ + Overriden to provide a user and folder before first saving to the DB. + """ + user = serializer.validated_data.pop('username') + folder = self.get_object() + serializer.save(user=user, folder=folder) + + def list(self, request, *args, **kwargs): + """ + Overriden to return a list of the user permissions for the queried folder. + Document-level link relations and a collection+json template are also added + to the response. + """ + queryset = self.get_user_permissions_queryset() + response = services.get_list_response(self, queryset) + folder = self.get_object() + links = {'folder': reverse('chrisfolder-detail', request=request, + kwargs={"pk": folder.id})} + response = services.append_collection_links(response, links) + template_data = {"username": ""} + return services.append_collection_template(response, template_data) + + def get_user_permissions_queryset(self): + """ + Custom method to get the actual user permissions queryset for the folder. + """ + folder = self.get_object() + return FolderUserPermission.objects.filter(folder=folder) + + +class FileBrowserFolderUserPermissionListQuerySearch(generics.ListAPIView): + """ + A view for the collection of folder-specific user permissions resulting from a query + search. + """ + http_method_names = ['get'] + serializer_class = FileBrowserFolderUserPermissionSerializer + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasPermissionReadOnly) + filterset_class = FolderUserPermissionFilter + + def get_queryset(self): + """ + Overriden to return a custom queryset that is comprised by the folder-specific + user permissions. + """ + folder = get_object_or_404(ChrisFolder, pk=self.kwargs['pk']) + return folder.shared_users.all() + + +class FileBrowserFolderUserPermissionDetail(generics.RetrieveUpdateDestroyAPIView): + """ + A view for a folder's user permission. + """ + http_method_names = ['get', 'put', 'delete'] + serializer_class = FileBrowserFolderUserPermissionSerializer + queryset = FolderUserPermission.objects.all() + permission_classes = (permissions.IsAuthenticated, + IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly) + + def retrieve(self, request, *args, **kwargs): + """ + Overriden to append a collection+json template. + """ + response = super(FileBrowserFolderUserPermissionDetail, + self).retrieve(request,*args, **kwargs) + template_data = {"permission": ""} + return services.append_collection_template(response, template_data) + + class FileBrowserFolderFileList(generics.ListAPIView): """ A view for the collection of all the files directly under this folder. """ http_method_names = ['get'] queryset = ChrisFolder.objects.all() - serializer_class = FileBrowserChrisFileSerializer + serializer_class = FileBrowserFileSerializer def list(self, request, *args, **kwargs): """ @@ -195,7 +375,7 @@ class FileBrowserFileDetail(generics.RetrieveAPIView): """ http_method_names = ['get'] queryset = ChrisFile.get_base_queryset() - serializer_class = FileBrowserChrisFileSerializer + serializer_class = FileBrowserFileSerializer permission_classes = (IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly,) @@ -218,13 +398,175 @@ def get(self, request, *args, **kwargs): return FileResponse(chris_file.fname) +class FileBrowserFileGroupPermissionList(generics.ListCreateAPIView): + """ + A view for a file's collection of group permissions. + """ + http_method_names = ['get', 'post'] + queryset = ChrisFile.objects.all() + serializer_class = FileBrowserFileGroupPermissionSerializer + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasPermissionReadOnly) + + def perform_create(self, serializer): + """ + Overriden to provide a group and file before first saving to the DB. + """ + group = serializer.validated_data.pop('name') + f = self.get_object() + serializer.save(user=group, file=f) + + def list(self, request, *args, **kwargs): + """ + Overriden to return a list of the group permissions for the queried file. + Document-level link relations and a collection+json template are also added + to the response. + """ + queryset = self.get_group_permissions_queryset() + response = services.get_list_response(self, queryset) + f = self.get_object() + links = {'file': reverse('chrisfile-detail', request=request, + kwargs={"pk": f.id})} + response = services.append_collection_links(response, links) + template_data = {"grp_name": ""} + return services.append_collection_template(response, template_data) + + def get_group_permissions_queryset(self): + """ + Custom method to get the actual group permissions queryset for the file. + """ + f = self.get_object() + return FileGroupPermission.objects.filter(file=f) + + +class FileBrowserFileGroupPermissionListQuerySearch(generics.ListAPIView): + """ + A view for the collection of file-specific group permissions resulting from a query + search. + """ + http_method_names = ['get'] + serializer_class = FileBrowserFileGroupPermissionSerializer + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasPermissionReadOnly) + filterset_class = FileGroupPermissionFilter + + def get_queryset(self): + """ + Overriden to return a custom queryset that is comprised by the file-specific + group permissions. + """ + f = get_object_or_404(ChrisFile, pk=self.kwargs['pk']) + return f.shared_groups.all() + + +class FileGroupPermissionDetail(generics.RetrieveUpdateDestroyAPIView): + """ + A view for a file's group permission. + """ + http_method_names = ['get', 'put', 'delete'] + serializer_class = FileBrowserFileGroupPermissionSerializer + queryset = FileGroupPermission.objects.all() + permission_classes = (permissions.IsAuthenticated, + IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly) + + def retrieve(self, request, *args, **kwargs): + """ + Overriden to append a collection+json template. + """ + response = super(FileGroupPermissionDetail, + self).retrieve(request,*args, **kwargs) + template_data = {"permission": ""} + return services.append_collection_template(response, template_data) + + +class FileBrowserFileUserPermissionList(generics.ListCreateAPIView): + """ + A view for a file's collection of user permissions. + """ + http_method_names = ['get', 'post'] + queryset = ChrisFile.objects.all() + serializer_class = FileBrowserFileUserPermissionSerializer + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasPermissionReadOnly) + + def perform_create(self, serializer): + """ + Overriden to provide a user and file before first saving to the DB. + """ + user = serializer.validated_data.pop('username') + f = self.get_object() + serializer.save(user=user, file=f) + + def list(self, request, *args, **kwargs): + """ + Overriden to return a list of the user permissions for the queried file. + Document-level link relations and a collection+json template are also added + to the response. + """ + queryset = self.get_user_permissions_queryset() + response = services.get_list_response(self, queryset) + f = self.get_object() + links = {'file': reverse('chrisfile-detail', request=request, + kwargs={"pk": f.id})} + response = services.append_collection_links(response, links) + template_data = {"username": ""} + return services.append_collection_template(response, template_data) + + def get_user_permissions_queryset(self): + """ + Custom method to get the actual user permissions queryset for the file. + """ + f = self.get_object() + return FileUserPermission.objects.filter(file=f) + + +class FileBrowserFileUserPermissionListQuerySearch(generics.ListAPIView): + """ + A view for the collection of file-specific user permissions resulting from a query + search. + """ + http_method_names = ['get'] + serializer_class = FileBrowserFileUserPermissionSerializer + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasPermissionReadOnly) + filterset_class = FileUserPermissionFilter + + def get_queryset(self): + """ + Overriden to return a custom queryset that is comprised by the file-specific + user permissions. + """ + f = get_object_or_404(ChrisFile, pk=self.kwargs['pk']) + return f.shared_users.all() + + +class FileBrowserFileUserPermissionDetail(generics.RetrieveUpdateDestroyAPIView): + """ + A view for a file's user permission. + """ + http_method_names = ['get', 'put', 'delete'] + serializer_class = FileBrowserFileUserPermissionSerializer + queryset = FileUserPermission.objects.all() + permission_classes = (permissions.IsAuthenticated, + IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly) + + def retrieve(self, request, *args, **kwargs): + """ + Overriden to append a collection+json template. + """ + response = super(FileBrowserFileUserPermissionDetail, + self).retrieve(request,*args, **kwargs) + template_data = {"permission": ""} + return services.append_collection_template(response, template_data) + + class FileBrowserFolderLinkFileList(generics.ListAPIView): """ A view for the collection of all the ChRIS link files directly under this folder. """ http_method_names = ['get'] queryset = ChrisFolder.objects.all() - serializer_class = FileBrowserChrisLinkFileSerializer + serializer_class = FileBrowserLinkFileSerializer def list(self, request, *args, **kwargs): """ @@ -261,7 +603,7 @@ class FileBrowserLinkFileDetail(generics.RetrieveAPIView): """ http_method_names = ['get'] queryset = ChrisLinkFile.objects.all() - serializer_class = FileBrowserChrisLinkFileSerializer + serializer_class = FileBrowserLinkFileSerializer permission_classes = (IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly,) @@ -282,3 +624,165 @@ def get(self, request, *args, **kwargs): """ chris_link_file = self.get_object() return FileResponse(chris_link_file.fname) + + +class FileBrowserLinkFileGroupPermissionList(generics.ListCreateAPIView): + """ + A view for a link file's collection of group permissions. + """ + http_method_names = ['get', 'post'] + queryset = ChrisLinkFile.objects.all() + serializer_class = FileBrowserLinkFileGroupPermissionSerializer + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasPermissionReadOnly) + + def perform_create(self, serializer): + """ + Overriden to provide a group and link file before first saving to the DB. + """ + group = serializer.validated_data.pop('name') + lf = self.get_object() + serializer.save(user=group, link_file=lf) + + def list(self, request, *args, **kwargs): + """ + Overriden to return a list of the group permissions for the queried link file. + Document-level link relations and a collection+json template are also added + to the response. + """ + queryset = self.get_group_permissions_queryset() + response = services.get_list_response(self, queryset) + lf = self.get_object() + links = {'link_file': reverse('chrislinkfile-detail', request=request, + kwargs={"pk": lf.id})} + response = services.append_collection_links(response, links) + template_data = {"grp_name": ""} + return services.append_collection_template(response, template_data) + + def get_group_permissions_queryset(self): + """ + Custom method to get the actual group permissions queryset for the link file. + """ + lf = self.get_object() + return LinkFileGroupPermission.objects.filter(link_file=lf) + + +class FileBrowserLinkFileGroupPermissionListQuerySearch(generics.ListAPIView): + """ + A view for the collection of link file-specific group permissions resulting from a + query search. + """ + http_method_names = ['get'] + serializer_class = FileBrowserLinkFileGroupPermissionSerializer + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasPermissionReadOnly) + filterset_class = LinkFileGroupPermissionFilter + + def get_queryset(self): + """ + Overriden to return a custom queryset that is comprised by the link file-specific + group permissions. + """ + lf = get_object_or_404(ChrisLinkFile, pk=self.kwargs['pk']) + return lf.shared_groups.all() + + +class LinkFileGroupPermissionDetail(generics.RetrieveUpdateDestroyAPIView): + """ + A view for a link file's group permission. + """ + http_method_names = ['get', 'put', 'delete'] + serializer_class = FileBrowserLinkFileGroupPermissionSerializer + queryset = LinkFileGroupPermission.objects.all() + permission_classes = (permissions.IsAuthenticated, + IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly) + + def retrieve(self, request, *args, **kwargs): + """ + Overriden to append a collection+json template. + """ + response = super(LinkFileGroupPermissionDetail, + self).retrieve(request,*args, **kwargs) + template_data = {"permission": ""} + return services.append_collection_template(response, template_data) + + +class FileBrowserLinkFileUserPermissionList(generics.ListCreateAPIView): + """ + A view for a link file's collection of user permissions. + """ + http_method_names = ['get', 'post'] + queryset = ChrisLinkFile.objects.all() + serializer_class = FileBrowserLinkFileUserPermissionSerializer + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasPermissionReadOnly) + + def perform_create(self, serializer): + """ + Overriden to provide a user and link file before first saving to the DB. + """ + user = serializer.validated_data.pop('username') + lf = self.get_object() + serializer.save(user=user, link_file=lf) + + def list(self, request, *args, **kwargs): + """ + Overriden to return a list of the user permissions for the queried link file. + Document-level link relations and a collection+json template are also added + to the response. + """ + queryset = self.get_user_permissions_queryset() + response = services.get_list_response(self, queryset) + lf = self.get_object() + links = {'link_file': reverse('chrislinkfile-detail', request=request, + kwargs={"pk": lf.id})} + response = services.append_collection_links(response, links) + template_data = {"username": ""} + return services.append_collection_template(response, template_data) + + def get_user_permissions_queryset(self): + """ + Custom method to get the actual user permissions queryset for the link file. + """ + lf = self.get_object() + return LinkFileUserPermission.objects.filter(link_file=lf) + + +class FileBrowserLinkFileUserPermissionListQuerySearch(generics.ListAPIView): + """ + A view for the collection of link file-specific user permissions resulting from a + query search. + """ + http_method_names = ['get'] + serializer_class = FileBrowserLinkFileUserPermissionSerializer + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasPermissionReadOnly) + filterset_class = LinkFileUserPermissionFilter + + def get_queryset(self): + """ + Overriden to return a custom queryset that is comprised by the link file-specific + user permissions. + """ + lf = get_object_or_404(ChrisLinkFile, pk=self.kwargs['pk']) + return lf.shared_users.all() + + +class FileBrowserLinkFileUserPermissionDetail(generics.RetrieveUpdateDestroyAPIView): + """ + A view for a link file's user permission. + """ + http_method_names = ['get', 'put', 'delete'] + serializer_class = FileBrowserLinkFileUserPermissionSerializer + queryset = LinkFileUserPermission.objects.all() + permission_classes = (permissions.IsAuthenticated, + IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly) + + def retrieve(self, request, *args, **kwargs): + """ + Overriden to append a collection+json template. + """ + response = super(FileBrowserLinkFileUserPermissionDetail, + self).retrieve(request,*args, **kwargs) + template_data = {"permission": ""} + return services.append_collection_template(response, template_data) diff --git a/chris_backend/plugininstances/models.py b/chris_backend/plugininstances/models.py index 7983a598..10d7f2e3 100755 --- a/chris_backend/plugininstances/models.py +++ b/chris_backend/plugininstances/models.py @@ -94,6 +94,7 @@ def _save_feed(self): """ feed = Feed() feed.name = self.title or self.plugin.meta.name + feed.owner = self.owner feed.save() # feed's folder path: SWIFT_CONTAINER_NAME/home//feeds/feed_ @@ -102,7 +103,6 @@ def _save_feed(self): folder.save() feed.folder = folder - feed.owner.set([self.owner]) feed.save() return feed @@ -124,7 +124,8 @@ def _save_output_folder(self): feed = current.feed # username = self.owner.username - username = feed.get_creator().username # use creator of the feed for shared feeds + username = feed.owner.username # use creator of the feed for shared + # feeds output_path = 'home/{0}/feeds/feed_{1}'.format(username, feed.id) + path folder = ChrisFolder(path=output_path, owner=self.owner) diff --git a/chris_backend/plugininstances/permissions.py b/chris_backend/plugininstances/permissions.py index 8bd65e86..91a6ac3d 100755 --- a/chris_backend/plugininstances/permissions.py +++ b/chris_backend/plugininstances/permissions.py @@ -1,23 +1,5 @@ -from rest_framework import permissions - - -class IsRelatedFeedOwnerOrPublicReadOnlyOrChris(permissions.BasePermission): - """ - Custom permission to only allow owners of a feed associated to an object or superuser - 'chris' to modify/edit the object. Read only is allowed to other users if the related - feed is public. - """ - def has_object_permission(self, request, view, obj): - if request.user.username == 'chris': - return True - - if hasattr(obj, 'plugin_inst'): - feed = obj.plugin_inst.feed - else: - feed = obj.feed - return (request.method in permissions.SAFE_METHODS and feed.public) or ( - request.user in feed.owner.all()) +from rest_framework import permissions class IsOwnerOrChrisOrAuthenticatedReadOnlyOrPublicReadOnly(permissions.BasePermission): diff --git a/chris_backend/plugininstances/serializers.py b/chris_backend/plugininstances/serializers.py index b9df37c9..5a742046 100755 --- a/chris_backend/plugininstances/serializers.py +++ b/chris_backend/plugininstances/serializers.py @@ -83,6 +83,7 @@ def validate_previous(self, previous_id): # as plugin instances are always created through the API plugin = self.context['view'].get_object() previous = None + if plugin.meta.type in ('ds', 'ts'): if not previous_id: raise serializers.ValidationError( @@ -94,10 +95,13 @@ def validate_previous(self, previous_id): err_str = "Couldn't find any 'previous' plugin instance with id %s." raise serializers.ValidationError( {'previous_id': [err_str % previous_id]}) + # check that the user can run plugins within this feed user = self.context['request'].user - if user not in previous.feed.owner.all(): - err_str = "User is not an owner of feed for previous instance with id %s." + feed = previous.feed + + if not (user == feed.owner or feed.has_user_permission(user)): + err_str = "Not allowed to write to feed for previous instance with id %s." raise serializers.ValidationError( {'previous_id': [err_str % previous_id]}) return previous @@ -314,16 +318,20 @@ def validate_paths(user, string): """ storage_manager = connect_storage(settings) path_list = [s.strip() for s in string.split(',')] + for path in path_list: path_parts = pathlib.Path(path).parts + if len(path_parts) == 0: # trying to access the root of the storage raise serializers.ValidationError( ["You do not have permission to access this path."]) + if len(path_parts) == 1 and path_parts[0] not in ('SERVICES', 'PIPELINES'): # trying to access the home folder or an unknown folder within the root folder raise serializers.ValidationError( ["You do not have permission to access this path."]) + if path_parts[0] == 'home' and path_parts[1] != user.username: if len(path_parts) <= 3: # trying to access another user's root or personal space @@ -341,7 +349,8 @@ def validate_paths(user, string): except (ValueError, Feed.DoesNotExist): raise serializers.ValidationError( ["This field may not be an invalid path."]) - if user not in feed.owner.all(): + + if not (user == feed.owner or feed.has_user_permission(user)): raise serializers.ValidationError( ["You do not have permission to access this path."]) else: diff --git a/chris_backend/plugininstances/services/manager.py b/chris_backend/plugininstances/services/manager.py index cff71496..724c36a5 100755 --- a/chris_backend/plugininstances/services/manager.py +++ b/chris_backend/plugininstances/services/manager.py @@ -658,11 +658,24 @@ def check_files_from_json_exist(self, json_file_content): def save_plugin_instance_final_status(self): """ - Save to the DB and log the final status of the plugin instance. + Set the plugin instance's output folder permissions recursively and log and + save the instance's final status to the DB. """ job_id = self.str_job_id + logger.info(f"Setting output folder's permissions for job {job_id} ...") + + for group in self.c_plugin_inst.feed.shared_groups.all(): + self.c_plugin_inst.output_folder.grant_group_permission(group, 'w') + + for user in self.c_plugin_inst.feed.shared_users.all(): + self.c_plugin_inst.output_folder.grant_user_permission(user, 'w') + + if self.c_plugin_inst.feed.public: + self.c_plugin_inst.output_folder.grant_public_access() + logger.info(f"Saving job {job_id} DB status as '{self.c_plugin_inst.status}'") self.c_plugin_inst.end_date = timezone.now() + logger.info(f"Saving job {job_id} DB end_date as '{self.c_plugin_inst.end_date}'") self.c_plugin_inst.save() diff --git a/chris_backend/users/views.py b/chris_backend/users/views.py index 97668c0e..ef1245e1 100755 --- a/chris_backend/users/views.py +++ b/chris_backend/users/views.py @@ -146,7 +146,7 @@ class GroupUserList(generics.ListCreateAPIView): def perform_create(self, serializer): """ - Overriden to associate an owner with the tag before first saving to the DB. + Overriden to provide a user and group before first saving to the DB. """ user = serializer.validated_data.pop('username') group = self.get_object() diff --git a/chris_backend/workflows/serializers.py b/chris_backend/workflows/serializers.py index 1acfdd16..bc9afe6b 100755 --- a/chris_backend/workflows/serializers.py +++ b/chris_backend/workflows/serializers.py @@ -62,10 +62,13 @@ def validate_previous_plugin_inst_id(self, previous_plugin_inst_id) -> PluginIns raise serializers.ValidationError( [f"Couldn't find any 'previous' plugin instance with id " f"{previous_plugin_inst_id}."]) + # check that the user can run plugins within this feed user = self.context['request'].user - if user not in previous_plugin_inst.feed.owner.all(): - raise serializers.ValidationError([f'User is not an owner of feed for ' + feed = previous_plugin_inst.feed + + if not (user == feed.owner or feed.has_user_permission(user)): + raise serializers.ValidationError([f'Not allowed to write to feed for ' f'previous instance with id {pk}.']) return previous_plugin_inst From 9f1f842d6c602b82f0f4c0788bd2672fa31a0ae7 Mon Sep 17 00:00:00 2001 From: Jorge Date: Thu, 30 May 2024 17:09:52 -0400 Subject: [PATCH 02/11] First implementation of serializers and views based on the new file/folder permission model --- chris_backend/core/api.py | 84 ++++- chris_backend/core/apps.py | 46 +++ chris_backend/core/models.py | 385 ++++++++++++++++++++--- chris_backend/feeds/models.py | 32 +- chris_backend/feeds/serializers.py | 15 +- chris_backend/feeds/views.py | 34 ++ chris_backend/filebrowser/permissions.py | 99 ++++-- chris_backend/filebrowser/serializers.py | 79 ++++- chris_backend/filebrowser/services.py | 202 +++--------- chris_backend/filebrowser/views.py | 324 +++++++++++++------ chris_backend/pacsfiles/serializers.py | 20 +- chris_backend/pipelines/serializers.py | 10 +- chris_backend/userfiles/serializers.py | 2 +- chris_backend/users/serializers.py | 29 +- chrisomatic/chrisomatic.yml | 2 +- 15 files changed, 989 insertions(+), 374 deletions(-) diff --git a/chris_backend/core/api.py b/chris_backend/core/api.py index ffcc15bb..f020823c 100755 --- a/chris_backend/core/api.py +++ b/chris_backend/core/api.py @@ -66,7 +66,7 @@ name='feed-group-permission-list'), path('v1//grouppermissions/search/', - feed_views.FeedGroupPermissionListListQuerySearch.as_view(), + feed_views.FeedGroupPermissionListQuerySearch.as_view(), name='feed-group-permission-list-query-search'), path('v1/grouppermissions//', @@ -78,7 +78,7 @@ name='feed-user-permission-list'), path('v1//userpermissions/search/', - feed_views.FeedUserPermissionListListQuerySearch.as_view(), + feed_views.FeedUserPermissionListQuerySearch.as_view(), name='feed-user-permission-list-query-search'), path('v1/userpermissions//', @@ -382,26 +382,98 @@ filebrowser_views.FileBrowserFolderChildList.as_view(), name='chrisfolder-child-list'), + path('v1/filebrowser//grouppermissions/', + filebrowser_views.FileBrowserFolderGroupPermissionList.as_view(), + name='foldergrouppermission-list'), + + path('v1/filebrowser//grouppermissions/search/', + filebrowser_views.FileBrowserFolderGroupPermissionListQuerySearch.as_view(), + name='chrisfoldergrouppermission-list-query-search'), + + path('v1/filebrowser/grouppermissions//', + filebrowser_views.FileBrowserFolderGroupPermissionDetail.as_view(), + name='foldergrouppermission-detail'), + + path('v1/filebrowser//userpermissions/', + filebrowser_views.FileBrowserFolderUserPermissionList.as_view(), + name='folderuserpermission-list'), + + path('v1/filebrowser//userpermissions/search/', + filebrowser_views.FileBrowserFolderUserPermissionListQuerySearch.as_view(), + name='folderuserpermission-list-query-search'), + + path('v1/filebrowser/userpermissions//', + filebrowser_views.FileBrowserFolderUserPermissionDetail.as_view(), + name='folderuserpermission-detail'), + path('v1/filebrowser//files/', filebrowser_views.FileBrowserFolderFileList.as_view(), name='chrisfolder-file-list'), - path('v1/filebrowser//linkfiles/', - filebrowser_views.FileBrowserFolderLinkFileList.as_view(), - name='chrisfolder-linkfile-list'), - path('v1/filebrowser/files//', filebrowser_views.FileBrowserFileDetail.as_view(), name='chrisfile-detail'), + path('v1/filebrowser/files//grouppermissions/', + filebrowser_views.FileBrowserFileGroupPermissionList.as_view(), + name='filegrouppermission-list'), + + path('v1/filebrowser/files//grouppermissions/search/', + filebrowser_views.FileBrowserFileGroupPermissionListQuerySearch.as_view(), + name='filegrouppermission-list-query-search'), + + path('v1/filebrowser/files/grouppermissions//', + filebrowser_views.FileBrowserFileGroupPermissionDetail.as_view(), + name='filegrouppermission-detail'), + + path('v1/filebrowser/files//userpermissions/', + filebrowser_views.FileBrowserFileUserPermissionList.as_view(), + name='fileuserpermission-list'), + + path('v1/filebrowser/files//userpermissions/search/', + filebrowser_views.FileBrowserFileUserPermissionListQuerySearch.as_view(), + name='fileuserpermission-list-query-search'), + + path('v1/filebrowser/files/userpermissions//', + filebrowser_views.FileBrowserFileUserPermissionDetail.as_view(), + name='fileuserpermission-detail'), + re_path(r'^v1/filebrowser/files/(?P[0-9]+)/.*$', filebrowser_views.FileBrowserFileResource.as_view(), name='chrisfile-resource'), + path('v1/filebrowser//linkfiles/', + filebrowser_views.FileBrowserFolderLinkFileList.as_view(), + name='chrisfolder-linkfile-list'), + path('v1/filebrowser/linkfiles//', filebrowser_views.FileBrowserLinkFileDetail.as_view(), name='chrislinkfile-detail'), + path('v1/filebrowser/linkfiles//grouppermissions/', + filebrowser_views.FileBrowserLinkFileGroupPermissionList.as_view(), + name='linkfilegrouppermission-list'), + + path('v1/filebrowser/linkfiles//grouppermissions/search/', + filebrowser_views.FileBrowserLinkFileGroupPermissionListQuerySearch.as_view(), + name='linkfilegrouppermission-list-query-search'), + + path('v1/filebrowser/linkfiles/grouppermissions//', + filebrowser_views.FileBrowserLinkFileGroupPermissionDetail.as_view(), + name='linkfilegrouppermission-detail'), + + path('v1/filebrowser/linkfiles//userpermissions/', + filebrowser_views.FileBrowserLinkFileUserPermissionList.as_view(), + name='linkfileuserpermission-list'), + + path('v1/filebrowser/linkfiles//userpermissions/search/', + filebrowser_views.FileBrowserLinkFileUserPermissionListQuerySearch.as_view(), + name='linkfileuserpermission-list-query-search'), + + path('v1/filebrowser/linkfiles/userpermissions//', + filebrowser_views.FileBrowserLinkFileUserPermissionDetail.as_view(), + name='linkfileuserpermission-detail'), + re_path(r'^v1/filebrowser/linkfiles/(?P[0-9]+)/.*$', filebrowser_views.FileBrowserLinkFileResource.as_view(), name='chrislinkfile-resource') diff --git a/chris_backend/core/apps.py b/chris_backend/core/apps.py index 6f8e932b..0ebf8d64 100755 --- a/chris_backend/core/apps.py +++ b/chris_backend/core/apps.py @@ -1,5 +1,51 @@ + from django.apps import AppConfig +from django.db.models.signals import post_migrate + + +def setup_chris(sender, **kwargs): + from django.contrib.auth.models import User, Group + from .models import ChrisInstance, ChrisFolder + + ChrisInstance.load() # create the ChRIS instance singleton + + # create superuser chris + try: + chris_user = User.objects.get(username='chris') + except User.DoesNotExist: + chris_user = User.objects.create_superuser('chris', 'dev@babymri.org', + 'chris1234') + # create required groups + (all_grp, _) = Group.objects.get_or_create(name='all_users') + (pacs_grp, _) = Group.objects.get_or_create(name='pacs_users') + + # create top level folders and their permissions + (folder, _) = ChrisFolder.objects.get_or_create(path='', owner=chris_user, + public=True) + + (folder, _) = ChrisFolder.objects.get_or_create(path='home', owner=chris_user) + if not folder.has_group_permission(all_grp): + folder.grant_group_permission(all_grp, 'r') + + (folder, _) = ChrisFolder.objects.get_or_create(path='SHARED', owner=chris_user) + if not folder.has_group_permission(all_grp): + folder.grant_group_permission(all_grp, 'r') + + ChrisFolder.objects.get_or_create(path='PUBLIC', owner=chris_user, public=True) + ChrisFolder.objects.get_or_create(path='PIPELINES', owner=chris_user, public=True) + + (folder, _) = ChrisFolder.objects.get_or_create(path='SERVICES', owner=chris_user) + if not folder.has_group_permission(all_grp): + folder.grant_group_permission(all_grp, 'r') + + (folder, _) = ChrisFolder.objects.get_or_create(path='SERVICES/PACS', + owner=chris_user) + if not folder.has_group_permission(pacs_grp): + folder.grant_group_permission(pacs_grp, 'r') class Core(AppConfig): name = 'core' + + def ready(self): + post_migrate.connect(setup_chris, sender=self) diff --git a/chris_backend/core/models.py b/chris_backend/core/models.py index 7a564db0..7be95315 100755 --- a/chris_backend/core/models.py +++ b/chris_backend/core/models.py @@ -95,6 +95,7 @@ def save(self, *args, **kwargs): """ if self.path: parent_path = os.path.dirname(self.path) + try: parent = ChrisFolder.objects.get(path=parent_path) except ChrisFolder.DoesNotExist: @@ -102,7 +103,8 @@ def save(self, *args, **kwargs): parent.save() # recursive call self.parent = parent - if self.path in ('', 'home') or self.path.startswith(('PIPELINES', 'SERVICES')): + if self.path in ('', 'home', 'PUBLIC', 'SHARED') or self.path.startswith( + ('PIPELINES', 'SERVICES')): self.owner = User.objects.get(username='chris') super(ChrisFolder, self).save(*args, **kwargs) @@ -114,23 +116,36 @@ def get_descendants(self): path = self.path.rstrip('/') + '/' return list(ChrisFolder.objects.filter(path__startswith=path)) - def has_group_permission(self, group, permission): + def has_group_permission(self, group, permission=''): """ Custom method to determine whether a group has been granted a permission to access the folder. """ - p = validate_permission(permission) - qs = FolderGroupPermission.objects.filter(group=group, folder=self, permission=p) + if not permission: + qs = FolderGroupPermission.objects.filter(group=group, folder=self) + else: + p = validate_permission(permission) + qs = FolderGroupPermission.objects.filter(group=group, folder=self, + permission=p) return qs.exists() - def has_user_permission(self, user, permission): + def has_user_permission(self, user, permission=''): """ Custom method to determine whether a user has been granted a permission - to access the folder. - """ - p = validate_permission(permission) - lookup = models.Q(user=user) | models.Q(user__groups__shared_folders=self) - qs = FolderUserPermission.objects.filter(folder=self, permission=p).filter(lookup) + to access the folder (perhaps through one of its groups). + """ + if not permission: + lookup = models.Q(shared_folders=self) | models.Q(groups__shared_folders=self) + qs = User.objects.filter(username=user.username).filter(lookup) + else: + p = validate_permission(permission) + if FolderUserPermission.objects.filter(folder=self, user=user, + permission=p).exists(): + return True + + user_grp_ids = [g.id for g in user.groups.all()] + qs = FolderGroupPermission.objects.filter(folder=self, permission=p, + group__pk__in=user_grp_ids) return qs.exists() def grant_group_permission(self, group, permission): @@ -149,7 +164,6 @@ def remove_group_permission(self, group, permission): FolderGroupPermission.objects.get(folder=self, group=group, permission=permission).delete() - def grant_user_permission(self, user, permission): """ Custom method to grant a user a permission to access the folder and all its @@ -179,6 +193,81 @@ def remove_public_access(self): """ self._update_public_access(False) + def get_shared_link(self): + """ + Custom method to get the link file in the SHARED folder pointing to + this folder if it exists. + """ + path = self.path.rstrip('/') + str_source_trace_dir = path.replace('/', '_') + fname = 'SHARED/' + str_source_trace_dir + '.chrislink' + + try: + lf = ChrisLinkFile.objects.get(fname=fname) + except ChrisLinkFile.DoesNotExist: + return None + return lf + + def create_shared_link(self): + """ + Custom method to create a link file in the SHARED folder pointing to + this folder. + """ + path = self.path.rstrip('/') + str_source_trace_dir = path.replace('/', '_') + fname = 'SHARED/' + str_source_trace_dir + '.chrislink' + + try: + lf = ChrisLinkFile.objects.get(fname=fname) + except ChrisLinkFile.DoesNotExist: + shared_folder = ChrisFolder.objects.get(path='SHARED') + lf = ChrisLinkFile(path=path, owner=self.owner, parent_folder=shared_folder) + lf.save(name=str_source_trace_dir) + return lf + + def remove_shared_link(self): + """ + Custom method to remove a link file in the SHARED folder pointing to + this folder if it exists. + """ + fname = 'SHARED/' + self.path.rstrip('/').replace('/', '_') + '.chrislink' + try: + lf = ChrisLinkFile.objects.get(fname=fname) + except ChrisLinkFile.DoesNotExist: + pass + else: + lf.delete() + + def create_public_link(self): + """ + Custom method to create a public link file in the PUBLIC folder pointing to + this folder. + """ + path = self.path.rstrip('/') + str_source_trace_dir = path.replace('/', '_') + fname = 'PUBLIC/' + str_source_trace_dir + '.chrislink' + + try: + lf = ChrisLinkFile.objects.get(fname=fname) + except ChrisLinkFile.DoesNotExist: + public_folder = ChrisFolder.objects.get(path='PUBLIC') + lf = ChrisLinkFile(path=path, owner=self.owner, public=True, + parent_folder=public_folder) + lf.save(name=str_source_trace_dir) + + def remove_public_link(self): + """ + Custom method to remove a public link file in the PUBLIC folder pointing to + this folder if it exists. + """ + fname = 'PUBLIC/' + self.path.rstrip('/').replace('/', '_') + '.chrislink' + try: + lf = ChrisLinkFile.objects.get(fname=fname) + except ChrisLinkFile.DoesNotExist: + pass + else: + lf.delete() + def _update_public_access(self, public_tf): """ Internal method to update public access to the folder and all its descendant @@ -389,51 +478,74 @@ class Meta: def __str__(self): return self.fname.name - def has_group_permission(self, group, permission): + def has_group_permission(self, group, permission=''): """ - Custom method to determine whether a group has been granted permission to access - the file. + Custom method to determine whether a group has been granted a permission to + access the file. """ - p = validate_permission(permission) - qs = FileGroupPermission.objects.filter(group=group, file=self, permission=p) + if not permission: + qs = FileGroupPermission.objects.filter(group=group, file=self) + else: + p = validate_permission(permission) + qs = FileGroupPermission.objects.filter(group=group, file=self, permission=p) return qs.exists() - def has_user_permission(self, user, permission): + def has_user_permission(self, user, permission=''): """ - Custom method to determine whether a user has been granted permission to access - the file. + Custom method to determine whether a user has been granted a permission to + access the file (perhaps through one of its groups). """ - p = validate_permission(permission) - lookup = models.Q(user=user) | models.Q(user__groups__shared_files=self) - qs = FileUserPermission.objects.filter(file=self, permission=p).filter(lookup) + if not permission: + lookup = models.Q(shared_files=self) | models.Q(groups__shared_files=self) + qs = User.objects.filter(username=user.username).filter(lookup) + else: + p = validate_permission(permission) + if FileUserPermission.objects.filter(file=self, user=user, + permission=p).exists(): + return True + + user_grp_ids = [g.id for g in user.groups.all()] + qs = FileGroupPermission.objects.filter(file=self, permission=p, + group__pk__in=user_grp_ids) return qs.exists() def grant_group_permission(self, group, permission): """ Custom method to grant a group a permission to access the file. """ - FileGroupPermission.objects.create(folder=self, group=group, - permission=permission) + FileGroupPermission.objects.update_or_create(file=self, group=group, + defaults={'permission': permission}) def remove_group_permission(self, group, permission): """ Custom method to remove a group's permission to access the file. """ - FileGroupPermission.objects.get(folder=self, group=group, - permission=permission).delete() + try: + perm = FileGroupPermission.objects.get(file=self, group=group, + permission=permission) + except FileGroupPermission.DoesNotExist: + pass + else: + perm.delete() def grant_user_permission(self, user, permission): """ Custom method to grant a user a permission to access the file. """ - FileUserPermission.objects.create(folder=self, user=user, permission=permission) + FileUserPermission.objects.update_or_create(file=self, user=user, + defaults={'permission': permission}) def remove_user_permission(self, user, permission): """ Custom method to remove a user's permission to access the file. """ - FileUserPermission.objects.get(folder=self, user=user, - permission=permission).delete() + try: + perm = FileUserPermission.objects.get(file=self, user=user, + permission=permission) + except FileUserPermission.DoesNotExist: + pass + else: + perm.delete() def grant_public_access(self): """ @@ -449,6 +561,81 @@ def remove_public_access(self): self.public = False self.save() + def get_shared_link(self): + """ + Custom method to get the link file in the SHARED folder pointing to + this file if it exists. + """ + path = self.fname.name + str_source_trace_dir = path.replace('/', '_') + fname = 'SHARED/' + str_source_trace_dir + '.chrislink' + + try: + lf = ChrisLinkFile.objects.get(fname=fname) + except ChrisLinkFile.DoesNotExist: + return None + return lf + + def create_shared_link(self): + """ + Custom method to create a link file in the SHARED folder pointing to + this file. + """ + path = self.fname.name + str_source_trace_dir = path.replace('/', '_') + fname = 'SHARED/' + str_source_trace_dir + '.chrislink' + + try: + lf = ChrisLinkFile.objects.get(fname=fname) + except ChrisLinkFile.DoesNotExist: + shared_folder = ChrisFolder.objects.get(path='SHARED') + lf = ChrisLinkFile(path=path, owner=self.owner, parent_folder=shared_folder) + lf.save(name=str_source_trace_dir) + return lf + + def remove_shared_link(self): + """ + Custom method to remove a link file in the SHARED folder pointing to + this file if it exists. + """ + fname = 'SHARED/' + self.fname.name.replace('/', '_') + '.chrislink' + try: + lf = ChrisLinkFile.objects.get(fname=fname) + except ChrisLinkFile.DoesNotExist: + pass + else: + lf.delete() + + def create_public_link(self): + """ + Custom method to create a public link file in the PUBLIC folder pointing to + this file. + """ + path = self.fname.name + str_source_trace_dir = path.replace('/', '_') + fname = 'PUBLIC/' + str_source_trace_dir + '.chrislink' + + try: + lf = ChrisLinkFile.objects.get(fname=fname) + except ChrisLinkFile.DoesNotExist: + public_folder = ChrisFolder.objects.get(path='PUBLIC') + lf = ChrisLinkFile(path=path, owner=self.owner, public=True, + parent_folder=public_folder) + lf.save(name=str_source_trace_dir) + + def remove_public_link(self): + """ + Custom method to remove a public link file in the PUBLIC folder pointing to + this file if it exists. + """ + fname = 'PUBLIC/' + self.fname.name.replace('/', '_') + '.chrislink' + try: + lf = ChrisLinkFile.objects.get(fname=fname) + except ChrisLinkFile.DoesNotExist: + pass + else: + lf.delete() + @classmethod def get_base_queryset(cls): """ @@ -545,52 +732,76 @@ def save(self, *args, **kwargs): self.fname.name = link_file_path super(ChrisLinkFile, self).save(*args, **kwargs) - def has_group_permission(self, group, permission): + def has_group_permission(self, group, permission=''): """ - Custom method to determine whether a group has been granted permission to access - the link file. + Custom method to determine whether a group has been granted a permission to + access the link file. """ - p = validate_permission(permission) - return LinkFileGroupPermission.objects.filter(group=group, link_file=self, - permission=p).exists() + if not permission: + qs = LinkFileGroupPermission.objects.filter(group=group, link_file=self) + else: + p = validate_permission(permission) + qs = LinkFileGroupPermission.objects.filter(group=group, link_file=self, + permission=p) + return qs.exists() - def has_user_permission(self, user, permission): + def has_user_permission(self, user, permission=''): """ - Custom method to determine whether a user has been granted permission to access - the link file. + Custom method to determine whether a user has been granted a permission to + access the link file (perhaps through one of its groups). """ - p = validate_permission(permission) - lookup = models.Q(user=user) | models.Q(user__groups__shared_link_files=self) - return LinkFileUserPermission.objects.filter(link_file=self, - permission=p).filter(lookup).exists() + if not permission: + lookup = models.Q(shared_link_files=self) | models.Q( + groups__shared_link_files=self) + qs = User.objects.filter(username=user.username).filter(lookup) + else: + p = validate_permission(permission) + if LinkFileUserPermission.objects.filter(link_file=self, user=user, + permission=p).exists(): + return True + + user_grp_ids = [g.id for g in user.groups.all()] + qs = LinkFileGroupPermission.objects.filter(link_file=self, permission=p, + group__pk__in=user_grp_ids) + return qs.exists() def grant_group_permission(self, group, permission): """ Custom method to grant a group a permission to access the link file. """ - LinkFileGroupPermission.objects.create(folder=self, group=group, - permission=permission) + LinkFileGroupPermission.objects.update_or_create(file=self, group=group, + defaults={'permission': permission}) def remove_group_permission(self, group, permission): """ Custom method to remove a group's permission to access the link file. """ - LinkFileGroupPermission.objects.get(folder=self, group=group, - permission=permission).delete() + try: + perm = LinkFileGroupPermission.objects.get(file=self, group=group, + permission=permission) + except LinkFileGroupPermission.DoesNotExist: + pass + else: + perm.delete() def grant_user_permission(self, user, permission): """ Custom method to grant a user a permission to access the link file. """ - LinkFileUserPermission.objects.create(folder=self, user=user, - permission=permission) + LinkFileUserPermission.objects.update_or_create(file=self, user=user, + defaults={'permission': permission}) def remove_user_permission(self, user, permission): """ Custom method to remove a user's permission to access the link file. """ - LinkFileUserPermission.objects.get(folder=self, user=user, - permission=permission).delete() + try: + perm = LinkFileUserPermission.objects.get(file=self, user=user, + permission=permission) + except LinkFileUserPermission.DoesNotExist: + pass + else: + perm.delete() def grant_public_access(self): """ @@ -606,6 +817,80 @@ def remove_public_access(self): self.public = False self.save() + def get_shared_link(self): + """ + Custom method to get the link file in the SHARED folder pointing to + this file if it exists. + """ + path = self.fname.name + str_source_trace_dir = path.replace('/', '_') + fname = 'SHARED/' + str_source_trace_dir + '.chrislink' + + try: + lf = ChrisLinkFile.objects.get(fname=fname) + except ChrisLinkFile.DoesNotExist: + return None + return lf + + def create_shared_link(self): + """ + Custom method to create a link file in the SHARED folder pointing to + this file. + """ + path = self.fname.name + str_source_trace_dir = path.replace('/', '_') + fname = 'SHARED/' + str_source_trace_dir + '.chrislink' + + try: + lf = ChrisLinkFile.objects.get(fname=fname) + except ChrisLinkFile.DoesNotExist: + shared_folder = ChrisFolder.objects.get(path='SHARED') + lf = ChrisLinkFile(path=path, owner=self.owner, parent_folder=shared_folder) + lf.save(name=str_source_trace_dir) + return lf + + def remove_shared_link(self): + """ + Custom method to remove a link file in the SHARED folder pointing to + this link file if it exists. + """ + fname = 'SHARED/' + self.fname.name.replace('/', '_') + '.chrislink' + try: + lf = ChrisLinkFile.objects.get(fname=fname) + except ChrisLinkFile.DoesNotExist: + pass + else: + lf.delete() + + def create_public_link(self): + """ + Custom method to create a public link file in the PUBLIC folder pointing to + this link file. + """ + path = self.fname.name + str_source_trace_dir = path.replace('/', '_') + fname = 'PUBLIC/' + str_source_trace_dir + '.chrislink' + + try: + lf = ChrisLinkFile.objects.get(fname=fname) + except ChrisLinkFile.DoesNotExist: + public_folder = ChrisFolder.objects.get(path='PUBLIC') + lf = ChrisLinkFile(path=path, owner=self.owner, public=True, + parent_folder=public_folder) + lf.save(name=str_source_trace_dir) + + def remove_public_link(self): + """ + Custom method to remove a public link file in the PUBLIC folder pointing to + this link file if it exists. + """ + fname = 'PUBLIC/' + self.fname.name.replace('/', '_') + '.chrislink' + try: + lf = ChrisLinkFile.objects.get(fname=fname) + except ChrisLinkFile.DoesNotExist: + pass + else: + lf.delete() @receiver(post_delete, sender=ChrisLinkFile) def auto_delete_file_from_storage(sender, instance, **kwargs): diff --git a/chris_backend/feeds/models.py b/chris_backend/feeds/models.py index 5cce48a8..a4953523 100755 --- a/chris_backend/feeds/models.py +++ b/chris_backend/feeds/models.py @@ -59,43 +59,53 @@ def has_group_permission(self, group): Custom method to determine whether a group has been granted permission to access the feed. """ - return FeedGroupPermission.objects.filter(group=group, feed=self).exists() + return FeedGroupPermission.objects.filter(feed=self, group=group).exists() def has_user_permission(self, user): """ Custom method to determine whether a user has been granted permission to access - the feed. + the feed (perhaps through one of its groups). """ - lookup = Q(user=user) | Q(user__groups__shared_feeds=self) - return FeedUserPermission.objects.filter(feed=self).filter(lookup).exists() + lookup = models.Q(shared_feeds=self) | models.Q(groups__shared_feeds=self) + return User.objects.filter(username=user.username).filter(lookup).exists() def grant_group_permission(self, group): """ - Custom method to grant a group write permission to access the feed and all its + Custom method to grant a group permission to access the feed and all its folder's descendant folders, link files and files. """ - FeedGroupPermission.objects.create(folder=self, group=group, permission='w') + FeedGroupPermission.objects.get_or_create(feed=self, group=group) def remove_group_permission(self, group): """ Custom method to remove a group's permission to access the feed and all its folder's descendant folders, link files and files. """ - FeedGroupPermission.objects.get(folder=self, group=group, permission='w').delete() + try: + perm = FeedGroupPermission.objects.get(feed=self, group=group) + except FeedGroupPermission.DoesNotExist: + pass + else: + perm.delete() def grant_user_permission(self, user): """ - Custom method to grant a user write permission to access the feed and all its + Custom method to grant a user permission to access the feed and all its folder's descendant folders, link files and files. """ - FeedUserPermission.objects.create(folder=self, user=user, permission='w') + FeedUserPermission.objects.get_or_create(feed=self, user=user) def remove_user_permission(self, user): """ Custom method to remove a user's permission to access the feed and all its folder's descendant folders, link files and files. """ - FeedUserPermission.objects.get(folder=self, user=user, permission='w').delete() + try: + perm = FeedUserPermission.objects.get(feed=self, user=user) + except FeedUserPermission.DoesNotExist: + pass + else: + perm.delete() def grant_public_access(self): """ @@ -104,6 +114,7 @@ def grant_public_access(self): """ self.public = True self.folder.grant_public_access() + self.folder.create_public_link() self.save() def remove_public_access(self): @@ -112,6 +123,7 @@ def remove_public_access(self): folders, link files and files. """ self.public = False + self.folder.remove_public_link() self.folder.remove_public_access() self.save() diff --git a/chris_backend/feeds/serializers.py b/chris_backend/feeds/serializers.py index 288664d2..51de39c1 100755 --- a/chris_backend/feeds/serializers.py +++ b/chris_backend/feeds/serializers.py @@ -125,9 +125,12 @@ def update(self, instance, validated_data): """ if 'public' in validated_data: if instance.public and not validated_data['public']: + instance.folder.remove_public_link() instance.folder.remove_public_access() + elif not instance.public and validated_data['public']: instance.folder.grant_public_access() + instance.folder.create_public_link() return super(FeedSerializer, self).update(instance, validated_data) def validate_name(self, name): @@ -223,7 +226,8 @@ class Meta: def create(self, validated_data): """ Overriden to handle the error when trying to grant access permission to a group - that already has the permission granted. + that already has the permission granted. Also a link file in the SHARED folder + pointing to the feed's folder is created if it doesn't exist. """ feed = validated_data['feed'] group = validated_data['group'] @@ -235,6 +239,9 @@ def create(self, validated_data): {'non_field_errors': [f"Group '{group.name}' already has permission to access feed " f"with id {feed.id}"]}) + + lf = feed.folder.create_shared_link() + lf.grant_group_permission(group, 'r') return feed_perm def validate_grp_name(self, grp_name): @@ -263,7 +270,8 @@ class Meta: def create(self, validated_data): """ Overriden to handle the error when trying to grant access permission to a user - that already has the permission granted. + that already has the permission granted. Also a link file in the SHARED folder + pointing to the feed's folder is created if it doesn't exist. """ feed = validated_data['feed'] user = validated_data['user'] @@ -275,6 +283,9 @@ def create(self, validated_data): {'non_field_errors': [f"User '{user.username}' already has permission to access feed " f"with id {feed.id}"]}) + + lf = feed.folder.create_shared_link() + lf.grant_user_permission(user, 'r') return feed_perm def validate_username(self, username): diff --git a/chris_backend/feeds/views.py b/chris_backend/feeds/views.py index 5b55c8e2..1b804637 100755 --- a/chris_backend/feeds/views.py +++ b/chris_backend/feeds/views.py @@ -494,6 +494,23 @@ class FeedGroupPermissionDetail(generics.RetrieveDestroyAPIView): permission_classes = (permissions.IsAuthenticated, IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly) + def perform_destroy(self, instance): + """ + Overriden to remove the group permission for the link file in the SHARED folder + pointing to the feed's folder. The link file itself is removed if all + its permissions have been removed. + """ + feed = instance.feed + group = instance.group + + lf = feed.folder.get_shared_link() + if lf is not None: + lf.remove_group_permission(group, 'r') + + if not lf.shared_groups.all().exists() and not lf.shared_users.all().exists(): + feed.folder.remove_shared_link() + super(FeedGroupPermissionDetail, self).perform_destroy(instance) + class FeedUserPermissionList(generics.ListCreateAPIView): """ @@ -566,6 +583,23 @@ class FeedUserPermissionDetail(generics.RetrieveDestroyAPIView): permission_classes = (permissions.IsAuthenticated, IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly) + def perform_destroy(self, instance): + """ + Overriden to remove the group permission for the link file in the SHARED folder + pointing to the feed's folder. The link file itself is removed if all + its permissions have been removed. + """ + feed = instance.feed + user = instance.user + + lf = feed.folder.get_shared_link() + if lf is not None: + lf.remove_user_permission(user, 'r') + + if not lf.shared_groups.all().exists() and not lf.shared_users.all().exists(): + feed.folder.remove_shared_link() + super(FeedUserPermissionDetail, self).perform_destroy(instance) + class CommentList(generics.ListCreateAPIView): """ diff --git a/chris_backend/filebrowser/permissions.py b/chris_backend/filebrowser/permissions.py index 1032f540..e46d4475 100755 --- a/chris_backend/filebrowser/permissions.py +++ b/chris_backend/filebrowser/permissions.py @@ -1,12 +1,8 @@ -from django.db.models import Q -from django.contrib.auth.models import User from rest_framework import permissions -from feeds.models import Feed - -class IsOwnerOrChrisOrWriteUserOrReadOnly(permissions.BasePermission): +class IsOwnerOrChrisOrHasWritePermissionOrReadOnly(permissions.BasePermission): """ Custom permission to only allow owners of an object or superuser 'chris' or users with write permission to modify/edit it. Read-only is allowed to other users. @@ -21,44 +17,87 @@ def has_object_permission(self, request, view, obj): # Write permissions are only allowed to the owner, superuser 'chris' and users # with write permission. user = request.user - lookup = Q(shared_folders=obj) | Q(groups__shared_folders=obj) return (user == obj.owner or user.username == 'chris' or - User.objects.filter(username=user.username).filter(lookup).count()) + obj.has_user_permission(user, 'w')) + + +class IsOwnerOrChrisOrHasAnyPermissionReadOnly(permissions.BasePermission): + """ + Custom permission to only allow superuser 'chris' and the owner of an object to + modify/edit it. Read-only access is allowed to other users that have been + granted any permission. + """ + + def has_object_permission(self, request, view, obj): + user = request.user + + if obj.owner == user or user.username == 'chris': + return True + + return (request.method in permissions.SAFE_METHODS and + obj.has_user_permission(user)) + + +class IsOwnerOrChrisOrHasAnyPermissionOrObjIsPublic(permissions.BasePermission): + """ + Custom permission to only allow superuser 'chris', the owner of an object or any + users that have been granted any permission to access an object. Also access is + allowed to all users if the object is public. + """ + + def has_object_permission(self, request, view, obj): + user = request.user + + return (obj.owner == user or user.username == 'chris' or obj.public or + obj.has_user_permission(user)) -class IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly(permissions.BasePermission): +class IsFolderOwnerOrChrisOrHasAnyFolderPermissionReadOnly(permissions.BasePermission): """ - Custom permission to only allow owners of the object, owners of a feed associated - to an object or superuser 'chris' to modify/edit the object. Read only is allowed - to other users if the related feed is public. + Custom permission to only allow superuser 'chris' and the owner of an object's + related folder to modify/edit the object. Read-only access is allowed to other users + that have been granted any permission to the object's related folder. """ def has_object_permission(self, request, view, obj): user = request.user - path = obj.fname.name - path_tokens = path.split('/', 4) - if path_tokens[0] == 'PIPELINES': # accessible to everybody + if obj.folder.owner == user or user.username == 'chris': return True - if not user.is_authenticated: - if (len(path_tokens) > 3 and path_tokens[0] == 'home' and path_tokens[2] == - 'feeds'): - feed_id = int(path_tokens[3].split('_')[1]) - feed = Feed.objects.get(id=feed_id) - return request.method in permissions.SAFE_METHODS and feed.public - return False + return (request.method in permissions.SAFE_METHODS and + obj.folder.has_user_permission(user)) + + +class IsFileOwnerOrChrisOrHasAnyFilePermissionReadOnly(permissions.BasePermission): + """ + Custom permission to only allow superuser 'chris' and the owner of an object's + related file to modify/edit the object. Read-only access is allowed to other users + that have been granted any permission to the object's related file. + """ + + def has_object_permission(self, request, view, obj): + user = request.user - if path_tokens[0] == 'SERVICES': # accessible to all authenticated users + if obj.file.owner == user or user.username == 'chris': return True - if request.user.username == 'chris' or obj.owner == request.user: + return (request.method in permissions.SAFE_METHODS and + obj.file.has_user_permission(user)) + + +class IsLinkFileOwnerOrChrisOrHasAnyLinkFilePermissionReadOnly(permissions.BasePermission): + """ + Custom permission to only allow superuser 'chris' and the owner of an object's + related link file to modify/edit the object. Read-only access is allowed to other + users that have been granted any permission to the object's related link file. + """ + + def has_object_permission(self, request, view, obj): + user = request.user + + if obj.link_file.owner == user or user.username == 'chris': return True - if (len(path_tokens) > 3 and path_tokens[0] == 'home' and path_tokens[2] == - 'feeds'): - feed_id = int(path_tokens[3].split('_')[1]) - feed = Feed.objects.get(id=feed_id) - return request.user in feed.owner.all() or ( - request.method in permissions.SAFE_METHODS and feed.public) - return False + return (request.method in permissions.SAFE_METHODS and + obj.link_file.has_user_permission(user)) diff --git a/chris_backend/filebrowser/serializers.py b/chris_backend/filebrowser/serializers.py index 96dab8ff..72a4dd68 100755 --- a/chris_backend/filebrowser/serializers.py +++ b/chris_backend/filebrowser/serializers.py @@ -14,6 +14,7 @@ class FileBrowserFolderSerializer(serializers.HyperlinkedModelSerializer): + path = serializers.CharField(max_length=1024, required=False) parent = serializers.HyperlinkedRelatedField(view_name='chrisfolder-detail', read_only=True) children = serializers.HyperlinkedIdentityField( @@ -21,18 +22,25 @@ class FileBrowserFolderSerializer(serializers.HyperlinkedModelSerializer): files = serializers.HyperlinkedIdentityField(view_name='chrisfolder-file-list') link_files = serializers.HyperlinkedIdentityField( view_name='chrisfolder-linkfile-list') + group_permissions = serializers.HyperlinkedIdentityField( + view_name='foldergrouppermission-list') + user_permissions = serializers.HyperlinkedIdentityField( + view_name='folderuserpermission-list') owner = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True) class Meta: model = ChrisFolder fields = ('url', 'id', 'creation_date', 'path', 'public', 'parent', 'children', - 'files', 'link_files', 'owner') + 'files', 'link_files', 'group_permissions', 'user_permissions', 'owner') def create(self, validated_data): """ Overriden to set the parent folder. """ path = validated_data.get('path') + if path is None: + raise serializers.ValidationError({'path': ['This field is required.']}) + parent_path = os.path.dirname(path) owner = validated_data['owner'] @@ -41,6 +49,22 @@ def create(self, validated_data): validated_data['parent'] = parent_folder return super(FileBrowserFolderSerializer, self).create(validated_data) + def update(self, instance, validated_data): + """ + Overriden to grant or remove public access to the folder and all its + descendant folders, link files and files depending on the new public status of + the folder. + """ + if 'public' in validated_data: + if instance.public and not validated_data['public']: + instance.remove_public_link() + instance.remove_public_access() + + elif not instance.public and validated_data['public']: + instance.grant_public_access() + instance.create_public_link() + return super(FileBrowserFolderSerializer, self).update(instance, validated_data) + def validate_path(self, path): """ Overriden to check whether the provided path is under home// but not @@ -85,7 +109,8 @@ class Meta: def create(self, validated_data): """ Overriden to handle the error when trying to create a permission for a group that - already has a permission granted. + already has a permission granted. Also a link file in the SHARED folder + pointing to the folder is created if it doesn't exist. """ folder = validated_data['folder'] group = validated_data['group'] @@ -98,6 +123,9 @@ def create(self, validated_data): {'non_field_errors': [f"Group '{group.name}' already has a permission to access folder " f"with id {folder.id}"]}) + + lf = folder.create_shared_link() + lf.grant_group_permission(group, 'r') return perm def validate_grp_name(self, grp_name): @@ -126,7 +154,8 @@ class Meta: def create(self, validated_data): """ Overriden to handle the error when trying to create a permission for a user that - already has a permission granted. + already has a permission granted. Also a link file in the SHARED folder + pointing to the folder is created if it doesn't exist. """ folder = validated_data['folder'] user = validated_data['user'] @@ -139,6 +168,9 @@ def create(self, validated_data): {'non_field_errors': [f"User '{user.username}' already has a permission to access " f"folder with id {folder.id}"]}) + + lf = folder.create_shared_link() + lf.grant_user_permission(user, 'r') return perm def validate_username(self, username): @@ -160,12 +192,17 @@ class FileBrowserFileSerializer(serializers.HyperlinkedModelSerializer): file_resource = ItemLinkField('get_file_link') parent_folder = serializers.HyperlinkedRelatedField(view_name='chrisfolder-detail', read_only=True) + group_permissions = serializers.HyperlinkedIdentityField( + view_name='filegrouppermission-list') + user_permissions = serializers.HyperlinkedIdentityField( + view_name='fileuserpermission-list') owner = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True) class Meta: model = ChrisFile - fields = ('url', 'id', 'creation_date', 'fname', 'fsize', - 'owner_username', 'file_resource', 'parent_folder', 'owner') + fields = ('url', 'id', 'creation_date', 'fname', 'fsize', 'public', + 'owner_username', 'file_resource', 'parent_folder', 'group_permissions', + 'user_permissions', 'owner') def get_file_link(self, obj): """ @@ -189,7 +226,8 @@ class Meta: def create(self, validated_data): """ Overriden to handle the error when trying to create a permission for a group that - already has a permission granted. + already has a permission granted. Also a link file in the SHARED folder + pointing to the file is created if it doesn't exist. """ f = validated_data['file'] group = validated_data['group'] @@ -202,6 +240,9 @@ def create(self, validated_data): {'non_field_errors': [f"Group '{group.name}' already has a permission to access file " f"with id {f.id}"]}) + + lf = f.create_shared_link() + lf.grant_group_permission(group, 'r') return perm def validate_grp_name(self, grp_name): @@ -231,7 +272,8 @@ class Meta: def create(self, validated_data): """ Overriden to handle the error when trying to create a permission for a user that - already has a permission granted. + already has a permission granted. Also a link file in the SHARED folder + pointing to the file is created if it doesn't exist. """ f = validated_data['file'] user = validated_data['user'] @@ -244,6 +286,9 @@ def create(self, validated_data): {'non_field_errors': [f"User '{user.username}' already has a permission to access " f"file with id {f.id}"]}) + + lf = f.create_shared_link() + lf.grant_user_permission(user, 'r') return perm def validate_username(self, username): @@ -267,13 +312,17 @@ class FileBrowserLinkFileSerializer(serializers.HyperlinkedModelSerializer): linked_file = ItemLinkField('get_linked_file_link') parent_folder = serializers.HyperlinkedRelatedField(view_name='chrisfolder-detail', read_only=True) + group_permissions = serializers.HyperlinkedIdentityField( + view_name='linkfilegrouppermission-list') + user_permissions = serializers.HyperlinkedIdentityField( + view_name='linkfileuserpermission-list') owner = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True) class Meta: model = ChrisLinkFile - fields = ('url', 'id', 'creation_date', 'path', 'fname', 'fsize', + fields = ('url', 'id', 'creation_date', 'path', 'fname', 'fsize', 'public', 'owner_username', 'file_resource', 'linked_folder', 'linked_file', - 'parent_folder', 'owner') + 'parent_folder', 'group_permissions', 'user_permissions', 'owner') def get_file_link(self, obj): """ @@ -334,7 +383,8 @@ class Meta: def create(self, validated_data): """ Overriden to handle the error when trying to create a permission for a group that - already has a permission granted. + already has a permission granted. Also a link file in the SHARED folder + pointing to this link file is created if it doesn't exist. """ lf = validated_data['link_file'] group = validated_data['group'] @@ -347,6 +397,9 @@ def create(self, validated_data): {'non_field_errors': [f"Group '{group.name}' already has a permission to access link " f"file with id {lf.id}"]}) + + shared_lf = lf.create_shared_link() + shared_lf.grant_group_permission(group, 'r') return perm def validate_grp_name(self, grp_name): @@ -376,7 +429,8 @@ class Meta: def create(self, validated_data): """ Overriden to handle the error when trying to create a permission for a user that - already has a permission granted. + already has a permission granted. Also a link file in the SHARED folder + pointing to this link file is created if it doesn't exist. """ lf = validated_data['link_file'] user = validated_data['user'] @@ -389,6 +443,9 @@ def create(self, validated_data): {'non_field_errors': [f"User '{user.username}' already has a permission to access " f"file with id {lf.id}"]}) + + shared_lf = lf.create_shared_link() + shared_lf.grant_user_permission(user, 'r') return perm def validate_username(self, username): diff --git a/chris_backend/filebrowser/services.py b/chris_backend/filebrowser/services.py index bc280535..5430c69f 100755 --- a/chris_backend/filebrowser/services.py +++ b/chris_backend/filebrowser/services.py @@ -2,181 +2,71 @@ from django.db import models from core.models import ChrisFolder -from feeds.models import Feed -def get_authenticated_user_folder_queryset(pk_dict, user): +def get_folder_queryset(pk_dict, user=None): """ - Convenience function to get a folder queryset for the authenticated user. + Convenience function to get a single folder queryset. """ - try: - folder = ChrisFolder.objects.get(**pk_dict) - except ChrisFolder.DoesNotExist: - return ChrisFolder.objects.none() - qs = ChrisFolder.objects.filter(**pk_dict) - username = user.username - - if username == 'chris': # chris user can see every existing folder - return qs - - path = folder.path - if path in ('', 'home') or path.startswith(('PIPELINES', 'SERVICES')): - return qs - path_tokens = path.split('/', 4) - if path_tokens[1] == username: - return qs + if qs.exists(): + folder = qs.first() - shared_feed_creators = get_shared_feed_creators_set(user) + if user is None: # unauthenticated user + if not folder.public: + return ChrisFolder.objects.none() + else: + if not (folder.owner == user or user.username == 'chris' or folder.public + or folder.has_user_permission(user)): + return ChrisFolder.objects.none() + return qs - if path_tokens[1] in shared_feed_creators: - if len(path_tokens) == 2: - return qs - if path_tokens[2] == 'feeds': - if len(path_tokens) == 3: - return qs - if path_tokens[3] in [f'feed_{f.id}' for f in Feed.objects.filter( - owner__username=path_tokens[1]).filter( - models.Q(owner=user) | models.Q(public=True))]: - return qs - return ChrisFolder.objects.none() -def get_unauthenticated_user_folder_queryset(pk_dict): +def get_folder_children_queryset(folder, user=None): """ - Convenience function to get a folder queryset for the unauthenticated user. + Convenience function to get the queryset of the immediate subfolders under a folder. """ - try: - folder = ChrisFolder.objects.get(**pk_dict) - except ChrisFolder.DoesNotExist: - return ChrisFolder.objects.none() + if user is None: + return folder.children.filter(public=True) - path = folder.path - if path.startswith('SERVICES'): - return ChrisFolder.objects.none() + if user.username == 'chris': + return folder.children.all() - qs = ChrisFolder.objects.filter(**pk_dict) + lookup = models.Q(owner=user) | models.Q(public=True) | models.Q( + shared_users=user) | models.Q(shared_groups__pk__in=[g.id for g + in user.groups.all()]) + return folder.children.filter(lookup) - if path in ('', 'home') or path.startswith('PIPELINES'): - return qs - path_tokens = path.split('/', 4) - public_feed_creators = get_shared_feed_creators_set() +def get_folder_files_queryset(folder, user=None): + """ + Convenience function to get the queryset of the immediate files under a folder. + """ + if user is None: + return folder.chris_files.filter(public=True) - if path_tokens[1] in public_feed_creators: - if len(path_tokens) == 2: - return qs - if path_tokens[2] == 'feeds': - if len(path_tokens) == 3: - return qs - if path_tokens[3] in [f'feed_{f.id}' for f in Feed.objects.filter( - owner__username=path_tokens[1]).filter(public=True)]: - return qs - return ChrisFolder.objects.none() + if user.username == 'chris': + return folder.chris_files.all() + lookup = models.Q(owner=user) | models.Q(public=True) | models.Q( + shared_users=user) | models.Q(shared_groups__pk__in=[g.id for g + in user.groups.all()]) + return folder.chris_files.filter(lookup) -def get_authenticated_user_folder_children(folder, user): - """ - Convenience function to get the list of the immediate subfolders under a folder - for the authenticated user. - """ - username = user.username - if username == 'chris': # chris user can see every existing folder - return list(folder.children.all()) - - path = folder.path - if path == '' or path.startswith(('PIPELINES', 'SERVICES')): - return list(folder.children.all()) - - shared_feed_creators = set() - computed_shared_feed_creators = False - shared_feeds = [] - computed_shared_feeds = False - children = [] - - for child_folder in folder.children.all(): - path = child_folder.path - path_tokens = path.split('/', 4) - - if path_tokens[1] == username: - children.append(child_folder) - else: - if not computed_shared_feed_creators: - shared_feed_creators = get_shared_feed_creators_set(user) - computed_shared_feed_creators = True - - if path_tokens[1] in shared_feed_creators: - if len(path_tokens) == 2: - children.append(child_folder) - elif path_tokens[2] == 'feeds': - if len(path_tokens) == 3: - children.append(child_folder) - else: - if not computed_shared_feeds: - shared_feeds = [f'feed_{f.id}' for f in Feed.objects.filter( - owner__username=path_tokens[1]).filter( - models.Q(owner=user) | models.Q(public=True))] - computed_shared_feeds = True - - if path_tokens[3] in shared_feeds: - children.append(child_folder) - return children - - -def get_unauthenticated_user_folder_children(folder): - """ - Convenience function to get the list of the immediate subfolders under a folder - for the unauthenticated user. - """ - path = folder.path - if path == '': - return list(folder.children.filter( - models.Q(path='home') | models.Q(path='PIPELINES'))) - - if path.startswith('PIPELINES'): - return list(folder.children.all()) - - public_feed_creators = get_shared_feed_creators_set() - public_feeds = [] - computed_public_feeds = False - children = [] - - for child_folder in folder.children.all(): - path = child_folder.path - path_tokens = path.split('/', 4) - - if path_tokens[1] in public_feed_creators: - if len(path_tokens) == 2: - children.append(child_folder) - elif path_tokens[2] == 'feeds': - if len(path_tokens) == 3: - children.append(child_folder) - else: - if not computed_public_feeds: - public_feeds = [f'feed_{f.id}' for f in Feed.objects.filter( - owner__username=path_tokens[1]).filter(public=True)] - computed_public_feeds = True - - if path_tokens[3] in public_feeds: - children.append(child_folder) - return children - - -def get_shared_feed_creators_set(user=None): + +def get_folder_link_files_queryset(folder, user=None): """ - Convenience function to get the set of creators of the feeds that have been shared - with the passed user (including public feeds). + Convenience function to get the queryset of the immediate link files under a folder. """ - creators_set = set() if user is None: - feeds_qs = Feed.objects.filter(public=True) - username = '' - else: - feeds_qs = Feed.objects.filter(models.Q(owner=user) | models.Q(public=True)) - username = user.username - for feed in feeds_qs.all(): - creator = feed.owner - if creator.username != username: - creators_set.add(creator.username) - return creators_set + return folder.chris_link_files.filter(public=True) + + if user.username == 'chris': + return folder.chris_link_files.all() + + lookup = models.Q(owner=user) | models.Q(public=True) | models.Q( + shared_users=user) | models.Q(shared_groups__pk__in=[g.id for g + in user.groups.all()]) + return folder.chris_link_files.filter(lookup) diff --git a/chris_backend/filebrowser/views.py b/chris_backend/filebrowser/views.py index 749343c5..91ae5d87 100755 --- a/chris_backend/filebrowser/views.py +++ b/chris_backend/filebrowser/views.py @@ -27,12 +27,16 @@ FileBrowserLinkFileSerializer, FileBrowserLinkFileGroupPermissionSerializer, FileBrowserLinkFileUserPermissionSerializer) -from .services import (get_authenticated_user_folder_queryset, - get_unauthenticated_user_folder_queryset, - get_authenticated_user_folder_children, - get_unauthenticated_user_folder_children) -from .permissions import (IsOwnerOrChrisOrWriteUserOrReadOnly, - IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly) +from .services import (get_folder_queryset, + get_folder_children_queryset, + get_folder_files_queryset, + get_folder_link_files_queryset) +from .permissions import (IsOwnerOrChrisOrHasWritePermissionOrReadOnly, + IsOwnerOrChrisOrHasAnyPermissionReadOnly, + IsFolderOwnerOrChrisOrHasAnyFolderPermissionReadOnly, + IsOwnerOrChrisOrHasAnyPermissionOrObjIsPublic, + IsFileOwnerOrChrisOrHasAnyFilePermissionReadOnly, + IsLinkFileOwnerOrChrisOrHasAnyLinkFilePermissionReadOnly) logger = logging.getLogger(__name__) @@ -70,10 +74,20 @@ def list(self, request, *args, **kwargs): def get_queryset(self): """ - Overriden to return a custom queryset that is only comprised by the initial - path (empty path). + Overriden to return a custom queryset that is only comprised by the root + folder (empty path). """ - return ChrisFolder.objects.filter(path='') + user = self.request.user + pk_dict = {'path': ''} + + if user.is_authenticated: + qs = get_folder_queryset(pk_dict, user) + else: + qs = get_folder_queryset(pk_dict) + + if qs.count() == 0: + raise Http404('Not found.') + return qs class FileBrowserFolderListQuerySearch(generics.ListAPIView): @@ -98,8 +112,8 @@ def get_queryset(self): pk_dict = {'path': path} if user.is_authenticated: - return get_authenticated_user_folder_queryset(pk_dict, user) - return get_unauthenticated_user_folder_queryset(pk_dict) + return get_folder_queryset(pk_dict, user) + return get_folder_queryset(pk_dict) class FileBrowserFolderDetail(generics.RetrieveUpdateDestroyAPIView): @@ -110,25 +124,35 @@ class FileBrowserFolderDetail(generics.RetrieveUpdateDestroyAPIView): queryset = ChrisFolder.objects.all() serializer_class = FileBrowserFolderSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly, - IsOwnerOrChrisOrWriteUserOrReadOnly) + IsOwnerOrChrisOrHasWritePermissionOrReadOnly) def retrieve(self, request, *args, **kwargs): """ - Overriden to get the collection of file browser paths directly under a path. + Overriden to retrieve a file browser folder and append a collection+json template. """ user = request.user id = kwargs.get('pk') pk_dict = {'id': id} if user.is_authenticated: - qs = get_authenticated_user_folder_queryset(pk_dict, user) + qs = get_folder_queryset(pk_dict, user) else: - qs = get_unauthenticated_user_folder_queryset(pk_dict) + qs = get_folder_queryset(pk_dict) if qs.count() == 0: raise Http404('Not found.') - return super(FileBrowserFolderDetail, self).retrieve(request, *args, **kwargs) + response = super(FileBrowserFolderDetail, self).retrieve(request, *args, **kwargs) + template_data = {"public": ""} + return services.append_collection_template(response, template_data) + + def update(self, request, *args, **kwargs): + """ + Overriden to remove path if provided by the user before serializer validation. + """ + request.data.pop('path', None) # change path is not implemented yet + return super(FileBrowserFolderDetail, self).update(request, *args, **kwargs) + class FileBrowserFolderChildList(generics.ListAPIView): """ @@ -147,28 +171,19 @@ def list(self, request, *args, **kwargs): pk_dict = {'id': id} if user.is_authenticated: - qs = get_authenticated_user_folder_queryset(pk_dict, user) + qs = get_folder_queryset(pk_dict, user) else: - qs = get_unauthenticated_user_folder_queryset(pk_dict) + qs = get_folder_queryset(pk_dict) - if qs.count() == 0: + folder = qs.first() + if folder is None: raise Http404('Not found.') - queryset = self.get_children_queryset() - return services.get_list_response(self, queryset) - - def get_children_queryset(self): - """ - Custom method to get the actual queryset of the children. - """ - user = self.request.user - folder = self.get_object() - if user.is_authenticated: - children = get_authenticated_user_folder_children(folder, user) + children_qs = get_folder_children_queryset(folder, user) else: - children = get_unauthenticated_user_folder_children(folder) - return self.filter_queryset(children) + children_qs = get_folder_children_queryset(folder) + return services.get_list_response(self, children_qs) class FileBrowserFolderGroupPermissionList(generics.ListCreateAPIView): @@ -179,7 +194,7 @@ class FileBrowserFolderGroupPermissionList(generics.ListCreateAPIView): queryset = ChrisFolder.objects.all() serializer_class = FileBrowserFolderGroupPermissionSerializer permission_classes = (permissions.IsAuthenticated, - IsOwnerOrChrisOrHasPermissionReadOnly) + IsOwnerOrChrisOrHasAnyPermissionReadOnly) def perform_create(self, serializer): """ @@ -198,8 +213,10 @@ def list(self, request, *args, **kwargs): queryset = self.get_group_permissions_queryset() response = services.get_list_response(self, queryset) folder = self.get_object() - links = {'folder': reverse('folder-detail', request=request, + + links = {'folder': reverse('chrisfolder-detail', request=request, kwargs={"pk": folder.id})} + response = services.append_collection_links(response, links) template_data = {"grp_name": ""} return services.append_collection_template(response, template_data) @@ -219,8 +236,7 @@ class FileBrowserFolderGroupPermissionListQuerySearch(generics.ListAPIView): """ http_method_names = ['get'] serializer_class = FileBrowserFolderGroupPermissionSerializer - permission_classes = (permissions.IsAuthenticated, - IsOwnerOrChrisOrHasPermissionReadOnly) + permission_classes = (permissions.IsAuthenticated,) filterset_class = FolderGroupPermissionFilter def get_queryset(self): @@ -228,8 +244,14 @@ def get_queryset(self): Overriden to return a custom queryset that is comprised by the folder-specific group permissions. """ - folder = get_object_or_404(ChrisFolder, pk=self.kwargs['pk']) - return folder.shared_groups.all() + user = self.request.user + id = self.kwargs['pk'] + pk_dict = {'id': id} + + folder = get_folder_queryset(pk_dict, user).first() + if folder is None: + raise Http404('Not found.') + return FolderGroupPermission.objects.filter(folder=folder) class FileBrowserFolderGroupPermissionDetail(generics.RetrieveUpdateDestroyAPIView): @@ -240,7 +262,7 @@ class FileBrowserFolderGroupPermissionDetail(generics.RetrieveUpdateDestroyAPIVi serializer_class = FileBrowserFolderGroupPermissionSerializer queryset = FolderGroupPermission.objects.all() permission_classes = (permissions.IsAuthenticated, - IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly) + IsFolderOwnerOrChrisOrHasAnyFolderPermissionReadOnly) def retrieve(self, request, *args, **kwargs): """ @@ -251,6 +273,23 @@ def retrieve(self, request, *args, **kwargs): template_data = {"permission": ""} return services.append_collection_template(response, template_data) + def perform_destroy(self, instance): + """ + Overriden to remove the group permission for the link file in the SHARED folder + pointing to the folder. The link file itself is removed if all its permissions + have been removed. + """ + folder = instance.folder + group = instance.group + + lf = folder.get_shared_link() + if lf is not None: + lf.remove_group_permission(group, 'r') + + if not lf.shared_groups.all().exists() and not lf.shared_users.all().exists(): + folder.remove_shared_link() + super(FileBrowserFolderGroupPermissionDetail, self).perform_destroy(instance) + class FileBrowserFolderUserPermissionList(generics.ListCreateAPIView): """ @@ -260,7 +299,7 @@ class FileBrowserFolderUserPermissionList(generics.ListCreateAPIView): queryset = ChrisFolder.objects.all() serializer_class = FileBrowserFolderUserPermissionSerializer permission_classes = (permissions.IsAuthenticated, - IsOwnerOrChrisOrHasPermissionReadOnly) + IsOwnerOrChrisOrHasAnyPermissionReadOnly) def perform_create(self, serializer): """ @@ -279,8 +318,10 @@ def list(self, request, *args, **kwargs): queryset = self.get_user_permissions_queryset() response = services.get_list_response(self, queryset) folder = self.get_object() + links = {'folder': reverse('chrisfolder-detail', request=request, kwargs={"pk": folder.id})} + response = services.append_collection_links(response, links) template_data = {"username": ""} return services.append_collection_template(response, template_data) @@ -300,8 +341,7 @@ class FileBrowserFolderUserPermissionListQuerySearch(generics.ListAPIView): """ http_method_names = ['get'] serializer_class = FileBrowserFolderUserPermissionSerializer - permission_classes = (permissions.IsAuthenticated, - IsOwnerOrChrisOrHasPermissionReadOnly) + permission_classes = (permissions.IsAuthenticated,) filterset_class = FolderUserPermissionFilter def get_queryset(self): @@ -309,8 +349,14 @@ def get_queryset(self): Overriden to return a custom queryset that is comprised by the folder-specific user permissions. """ - folder = get_object_or_404(ChrisFolder, pk=self.kwargs['pk']) - return folder.shared_users.all() + user = self.request.user + id = self.kwargs['pk'] + pk_dict = {'id': id} + + folder = get_folder_queryset(pk_dict, user).first() + if folder is None: + raise Http404('Not found.') + return FolderUserPermission.objects.filter(folder=folder) class FileBrowserFolderUserPermissionDetail(generics.RetrieveUpdateDestroyAPIView): @@ -321,7 +367,7 @@ class FileBrowserFolderUserPermissionDetail(generics.RetrieveUpdateDestroyAPIVie serializer_class = FileBrowserFolderUserPermissionSerializer queryset = FolderUserPermission.objects.all() permission_classes = (permissions.IsAuthenticated, - IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly) + IsFolderOwnerOrChrisOrHasAnyFolderPermissionReadOnly) def retrieve(self, request, *args, **kwargs): """ @@ -332,6 +378,23 @@ def retrieve(self, request, *args, **kwargs): template_data = {"permission": ""} return services.append_collection_template(response, template_data) + def perform_destroy(self, instance): + """ + Overriden to remove the user permission for the link file in the SHARED folder + pointing to the folder. The link file itself is removed if all its permissions + have been removed. + """ + folder = instance.folder + user = instance.user + + lf = folder.get_shared_link() + if lf is not None: + lf.remove_user_permission(user, 'r') + + if not lf.shared_groups.all().exists() and not lf.shared_users.all().exists(): + folder.remove_shared_link() + super(FileBrowserFolderUserPermissionDetail, self).perform_destroy(instance) + class FileBrowserFolderFileList(generics.ListAPIView): """ @@ -350,23 +413,21 @@ def list(self, request, *args, **kwargs): pk_dict = {'id': id} if user.is_authenticated: - qs = get_authenticated_user_folder_queryset(pk_dict, user) + qs = get_folder_queryset(pk_dict, user) else: - qs = get_unauthenticated_user_folder_queryset(pk_dict) + qs = get_folder_queryset(pk_dict) - if qs.count() == 0: + folder = qs.first() + if folder is None: raise Http404('Not found.') - queryset = self.get_files_queryset() - response = services.get_list_response(self, queryset) - return response + if user.is_authenticated: + files_qs = get_folder_files_queryset(folder, user) + else: + files_qs = get_folder_files_queryset(folder) - def get_files_queryset(self): - """ - Custom method to get a queryset with all the files directly under this folder. - """ - folder = self.get_object() - return folder.chris_files.all() + response = services.get_list_response(self, files_qs) + return response class FileBrowserFileDetail(generics.RetrieveAPIView): @@ -376,7 +437,8 @@ class FileBrowserFileDetail(generics.RetrieveAPIView): http_method_names = ['get'] queryset = ChrisFile.get_base_queryset() serializer_class = FileBrowserFileSerializer - permission_classes = (IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly,) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrHasAnyPermissionOrObjIsPublic) class FileBrowserFileResource(generics.GenericAPIView): @@ -386,7 +448,8 @@ class FileBrowserFileResource(generics.GenericAPIView): http_method_names = ['get'] queryset = ChrisFile.get_base_queryset() renderer_classes = (BinaryFileRenderer,) - permission_classes = (IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly,) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrHasAnyPermissionOrObjIsPublic) authentication_classes = (TokenAuthSupportQueryString, BasicAuthentication, SessionAuthentication) @@ -406,7 +469,7 @@ class FileBrowserFileGroupPermissionList(generics.ListCreateAPIView): queryset = ChrisFile.objects.all() serializer_class = FileBrowserFileGroupPermissionSerializer permission_classes = (permissions.IsAuthenticated, - IsOwnerOrChrisOrHasPermissionReadOnly) + IsOwnerOrChrisOrHasAnyPermissionReadOnly) def perform_create(self, serializer): """ @@ -425,8 +488,10 @@ def list(self, request, *args, **kwargs): queryset = self.get_group_permissions_queryset() response = services.get_list_response(self, queryset) f = self.get_object() + links = {'file': reverse('chrisfile-detail', request=request, kwargs={"pk": f.id})} + response = services.append_collection_links(response, links) template_data = {"grp_name": ""} return services.append_collection_template(response, template_data) @@ -447,7 +512,7 @@ class FileBrowserFileGroupPermissionListQuerySearch(generics.ListAPIView): http_method_names = ['get'] serializer_class = FileBrowserFileGroupPermissionSerializer permission_classes = (permissions.IsAuthenticated, - IsOwnerOrChrisOrHasPermissionReadOnly) + IsOwnerOrChrisOrHasAnyPermissionReadOnly) filterset_class = FileGroupPermissionFilter def get_queryset(self): @@ -456,10 +521,10 @@ def get_queryset(self): group permissions. """ f = get_object_or_404(ChrisFile, pk=self.kwargs['pk']) - return f.shared_groups.all() + return FileGroupPermission.objects.filter(file=f) -class FileGroupPermissionDetail(generics.RetrieveUpdateDestroyAPIView): +class FileBrowserFileGroupPermissionDetail(generics.RetrieveUpdateDestroyAPIView): """ A view for a file's group permission. """ @@ -467,17 +532,34 @@ class FileGroupPermissionDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = FileBrowserFileGroupPermissionSerializer queryset = FileGroupPermission.objects.all() permission_classes = (permissions.IsAuthenticated, - IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly) + IsFileOwnerOrChrisOrHasAnyFilePermissionReadOnly) def retrieve(self, request, *args, **kwargs): """ Overriden to append a collection+json template. """ - response = super(FileGroupPermissionDetail, + response = super(FileBrowserFileGroupPermissionDetail, self).retrieve(request,*args, **kwargs) template_data = {"permission": ""} return services.append_collection_template(response, template_data) + def perform_destroy(self, instance): + """ + Overriden to remove the group permission for the link file in the SHARED folder + pointing to the file. The link file itself is removed if all its permissions + have been removed. + """ + f = instance.file + group = instance.group + + lf = f.get_shared_link() + if lf is not None: + lf.remove_group_permission(group, 'r') + + if not lf.shared_groups.all().exists() and not lf.shared_users.all().exists(): + f.remove_shared_link() + super(FileBrowserFileGroupPermissionDetail, self).perform_destroy(instance) + class FileBrowserFileUserPermissionList(generics.ListCreateAPIView): """ @@ -487,7 +569,7 @@ class FileBrowserFileUserPermissionList(generics.ListCreateAPIView): queryset = ChrisFile.objects.all() serializer_class = FileBrowserFileUserPermissionSerializer permission_classes = (permissions.IsAuthenticated, - IsOwnerOrChrisOrHasPermissionReadOnly) + IsOwnerOrChrisOrHasAnyPermissionReadOnly) def perform_create(self, serializer): """ @@ -506,8 +588,10 @@ def list(self, request, *args, **kwargs): queryset = self.get_user_permissions_queryset() response = services.get_list_response(self, queryset) f = self.get_object() + links = {'file': reverse('chrisfile-detail', request=request, kwargs={"pk": f.id})} + response = services.append_collection_links(response, links) template_data = {"username": ""} return services.append_collection_template(response, template_data) @@ -528,7 +612,7 @@ class FileBrowserFileUserPermissionListQuerySearch(generics.ListAPIView): http_method_names = ['get'] serializer_class = FileBrowserFileUserPermissionSerializer permission_classes = (permissions.IsAuthenticated, - IsOwnerOrChrisOrHasPermissionReadOnly) + IsOwnerOrChrisOrHasAnyPermissionReadOnly) filterset_class = FileUserPermissionFilter def get_queryset(self): @@ -537,7 +621,7 @@ def get_queryset(self): user permissions. """ f = get_object_or_404(ChrisFile, pk=self.kwargs['pk']) - return f.shared_users.all() + return FileUserPermission.objects.filter(file=f) class FileBrowserFileUserPermissionDetail(generics.RetrieveUpdateDestroyAPIView): @@ -548,7 +632,7 @@ class FileBrowserFileUserPermissionDetail(generics.RetrieveUpdateDestroyAPIView) serializer_class = FileBrowserFileUserPermissionSerializer queryset = FileUserPermission.objects.all() permission_classes = (permissions.IsAuthenticated, - IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly) + IsFileOwnerOrChrisOrHasAnyFilePermissionReadOnly) def retrieve(self, request, *args, **kwargs): """ @@ -559,6 +643,23 @@ def retrieve(self, request, *args, **kwargs): template_data = {"permission": ""} return services.append_collection_template(response, template_data) + def perform_destroy(self, instance): + """ + Overriden to remove the user permission for the link file in the SHARED folder + pointing to the file. The link file itself is removed if all its permissions + have been removed. + """ + f = instance.file + user = instance.user + + lf = f.get_shared_link() + if lf is not None: + lf.remove_user_permission(user, 'r') + + if not lf.shared_groups.all().exists() and not lf.shared_users.all().exists(): + f.remove_shared_link() + super(FileBrowserFileUserPermissionDetail, self).perform_destroy(instance) + class FileBrowserFolderLinkFileList(generics.ListAPIView): """ @@ -577,24 +678,21 @@ def list(self, request, *args, **kwargs): pk_dict = {'id': id} if user.is_authenticated: - qs = get_authenticated_user_folder_queryset(pk_dict, user) + qs = get_folder_queryset(pk_dict, user) else: - qs = get_unauthenticated_user_folder_queryset(pk_dict) + qs = get_folder_queryset(pk_dict) - if qs.count() == 0: + folder = qs.first() + if folder is None: raise Http404('Not found.') - queryset = self.get_link_files_queryset() - response = services.get_list_response(self, queryset) - return response + if user.is_authenticated: + link_files_qs = get_folder_link_files_queryset(folder, user) + else: + link_files_qs = get_folder_link_files_queryset(folder) - def get_link_files_queryset(self): - """ - Custom method to get a queryset with all the link files directly under this - folder. - """ - folder = self.get_object() - return folder.chris_link_files.all() + response = services.get_list_response(self, link_files_qs) + return response class FileBrowserLinkFileDetail(generics.RetrieveAPIView): @@ -604,7 +702,8 @@ class FileBrowserLinkFileDetail(generics.RetrieveAPIView): http_method_names = ['get'] queryset = ChrisLinkFile.objects.all() serializer_class = FileBrowserLinkFileSerializer - permission_classes = (IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly,) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrHasAnyPermissionOrObjIsPublic) class FileBrowserLinkFileResource(generics.GenericAPIView): @@ -614,7 +713,8 @@ class FileBrowserLinkFileResource(generics.GenericAPIView): http_method_names = ['get'] queryset = ChrisLinkFile.objects.all() renderer_classes = (BinaryFileRenderer,) - permission_classes = (IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly,) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrHasAnyPermissionOrObjIsPublic) authentication_classes = (TokenAuthSupportQueryString, BasicAuthentication, SessionAuthentication) @@ -634,7 +734,7 @@ class FileBrowserLinkFileGroupPermissionList(generics.ListCreateAPIView): queryset = ChrisLinkFile.objects.all() serializer_class = FileBrowserLinkFileGroupPermissionSerializer permission_classes = (permissions.IsAuthenticated, - IsOwnerOrChrisOrHasPermissionReadOnly) + IsOwnerOrChrisOrHasAnyPermissionReadOnly) def perform_create(self, serializer): """ @@ -653,8 +753,10 @@ def list(self, request, *args, **kwargs): queryset = self.get_group_permissions_queryset() response = services.get_list_response(self, queryset) lf = self.get_object() + links = {'link_file': reverse('chrislinkfile-detail', request=request, kwargs={"pk": lf.id})} + response = services.append_collection_links(response, links) template_data = {"grp_name": ""} return services.append_collection_template(response, template_data) @@ -675,7 +777,7 @@ class FileBrowserLinkFileGroupPermissionListQuerySearch(generics.ListAPIView): http_method_names = ['get'] serializer_class = FileBrowserLinkFileGroupPermissionSerializer permission_classes = (permissions.IsAuthenticated, - IsOwnerOrChrisOrHasPermissionReadOnly) + IsOwnerOrChrisOrHasAnyPermissionReadOnly) filterset_class = LinkFileGroupPermissionFilter def get_queryset(self): @@ -684,10 +786,10 @@ def get_queryset(self): group permissions. """ lf = get_object_or_404(ChrisLinkFile, pk=self.kwargs['pk']) - return lf.shared_groups.all() + return LinkFileGroupPermission.objects.filter(link_file=lf) -class LinkFileGroupPermissionDetail(generics.RetrieveUpdateDestroyAPIView): +class FileBrowserLinkFileGroupPermissionDetail(generics.RetrieveUpdateDestroyAPIView): """ A view for a link file's group permission. """ @@ -695,17 +797,34 @@ class LinkFileGroupPermissionDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = FileBrowserLinkFileGroupPermissionSerializer queryset = LinkFileGroupPermission.objects.all() permission_classes = (permissions.IsAuthenticated, - IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly) + IsLinkFileOwnerOrChrisOrHasAnyLinkFilePermissionReadOnly) def retrieve(self, request, *args, **kwargs): """ Overriden to append a collection+json template. """ - response = super(LinkFileGroupPermissionDetail, + response = super(FileBrowserLinkFileGroupPermissionDetail, self).retrieve(request,*args, **kwargs) template_data = {"permission": ""} return services.append_collection_template(response, template_data) + def perform_destroy(self, instance): + """ + Overriden to remove the group permission for the link file in the SHARED folder + pointing to this link file. The link file itself is removed if all its + permissions have been removed. + """ + link_file = instance.link_file + group = instance.group + + lf = link_file.get_shared_link() + if lf is not None: + lf.remove_group_permission(group, 'r') + + if not lf.shared_groups.all().exists() and not lf.shared_users.all().exists(): + link_file.remove_shared_link() + super(FileBrowserLinkFileGroupPermissionDetail, self).perform_destroy(instance) + class FileBrowserLinkFileUserPermissionList(generics.ListCreateAPIView): """ @@ -715,7 +834,7 @@ class FileBrowserLinkFileUserPermissionList(generics.ListCreateAPIView): queryset = ChrisLinkFile.objects.all() serializer_class = FileBrowserLinkFileUserPermissionSerializer permission_classes = (permissions.IsAuthenticated, - IsOwnerOrChrisOrHasPermissionReadOnly) + IsOwnerOrChrisOrHasAnyPermissionReadOnly) def perform_create(self, serializer): """ @@ -756,7 +875,7 @@ class FileBrowserLinkFileUserPermissionListQuerySearch(generics.ListAPIView): http_method_names = ['get'] serializer_class = FileBrowserLinkFileUserPermissionSerializer permission_classes = (permissions.IsAuthenticated, - IsOwnerOrChrisOrHasPermissionReadOnly) + IsOwnerOrChrisOrHasAnyPermissionReadOnly) filterset_class = LinkFileUserPermissionFilter def get_queryset(self): @@ -765,7 +884,7 @@ def get_queryset(self): user permissions. """ lf = get_object_or_404(ChrisLinkFile, pk=self.kwargs['pk']) - return lf.shared_users.all() + return LinkFileUserPermission.objects.filter(link_file=lf) class FileBrowserLinkFileUserPermissionDetail(generics.RetrieveUpdateDestroyAPIView): @@ -776,7 +895,7 @@ class FileBrowserLinkFileUserPermissionDetail(generics.RetrieveUpdateDestroyAPIV serializer_class = FileBrowserLinkFileUserPermissionSerializer queryset = LinkFileUserPermission.objects.all() permission_classes = (permissions.IsAuthenticated, - IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly) + IsLinkFileOwnerOrChrisOrHasAnyLinkFilePermissionReadOnly) def retrieve(self, request, *args, **kwargs): """ @@ -786,3 +905,20 @@ def retrieve(self, request, *args, **kwargs): self).retrieve(request,*args, **kwargs) template_data = {"permission": ""} return services.append_collection_template(response, template_data) + + def perform_destroy(self, instance): + """ + Overriden to remove the user permission for the link file in the SHARED folder + pointing to this link file. The link file itself is removed if all its + permissions have been removed. + """ + link_file = instance.link_file + user = instance.user + + lf = link_file.get_shared_link() + if lf is not None: + lf.remove_user_permission(user, 'r') + + if not lf.shared_groups.all().exists() and not lf.shared_users.all().exists(): + link_file.remove_shared_link() + super(FileBrowserLinkFileUserPermissionDetail, self).perform_destroy(instance) diff --git a/chris_backend/pacsfiles/serializers.py b/chris_backend/pacsfiles/serializers.py index a117ad17..e6bdbd42 100755 --- a/chris_backend/pacsfiles/serializers.py +++ b/chris_backend/pacsfiles/serializers.py @@ -3,6 +3,7 @@ import os import time +from django.contrib.auth.models import Group from django.conf import settings from rest_framework import serializers @@ -49,13 +50,17 @@ def create(self, validated_data): """ owner = validated_data.pop('owner') pacs_name = validated_data.pop('pacs_name') + (pacs_grp, _) = Group.objects.get_or_create(name='pacs_users') try: pacs = PACS.objects.get(identifier=pacs_name) except PACS.DoesNotExist: folder_path = f'SERVICES/PACS/{pacs_name}' - (pacs_folder, _) = ChrisFolder.objects.get_or_create(path=folder_path, - owner=owner) + (pacs_folder, tf) = ChrisFolder.objects.get_or_create(path=folder_path, + owner=owner) + if tf: + pacs_folder.grant_group_permission(pacs_grp, 'r') + pacs = PACS(folder=pacs_folder, identifier=pacs_name) pacs.save() # create a PACS object @@ -84,6 +89,13 @@ def create(self, validated_data): files.append(pacs_file) PACSFile.objects.bulk_create(files) + + # grant group permission from the highest folder ancestor without it + current = series_folder + while not current.parent.has_group_permission(pacs_grp): + current = current.parent + current.grant_group_permission(pacs_grp, 'r') + else: error_msg = (f'A DICOM series with SeriesInstanceUID={SeriesInstanceUID} ' f'already registered for pacs {pacs_name}') @@ -167,8 +179,8 @@ class PACSFileSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = PACSFile - fields = ('url', 'id', 'creation_date', 'fname', 'fsize', 'owner_username', - 'file_resource', 'parent_folder', 'owner') + fields = ('url', 'id', 'creation_date', 'fname', 'fsize', 'public', + 'owner_username', 'file_resource', 'parent_folder', 'owner') def get_file_link(self, obj): """ diff --git a/chris_backend/pipelines/serializers.py b/chris_backend/pipelines/serializers.py index 7a9969dd..0620a4bf 100755 --- a/chris_backend/pipelines/serializers.py +++ b/chris_backend/pipelines/serializers.py @@ -498,7 +498,7 @@ class PipelineSourceFileSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = PipelineSourceFile - fields = ('url', 'id', 'creation_date', 'fname', 'fsize', 'type', + fields = ('url', 'id', 'creation_date', 'fname', 'fsize', 'public', 'type', 'ftype', 'uploader_username', 'owner_username', 'pipeline_id', 'pipeline_name', 'file_resource', 'parent_folder', 'owner') @@ -526,12 +526,16 @@ def create(self, validated_data): # file will be stored to Swift at: # SWIFT_CONTAINER_NAME/PIPELINES// folder_path = f'PIPELINES/{uploader.username}' - (parent_folder, _) = ChrisFolder.objects.get_or_create(path=folder_path, - owner=owner) + (parent_folder, tf) = ChrisFolder.objects.get_or_create(path=folder_path, + owner=owner) + if tf: + parent_folder.grant_public_access() + fname = validated_data['fname'] filename = os.path.basename(fname.name) validated_data['parent_folder'] = parent_folder source_file = PipelineSourceFile(**validated_data) + source_file.public = True source_file.fname.name = f'{folder_path}/{filename}' source_file.save() diff --git a/chris_backend/userfiles/serializers.py b/chris_backend/userfiles/serializers.py index 1479668a..73b0551e 100755 --- a/chris_backend/userfiles/serializers.py +++ b/chris_backend/userfiles/serializers.py @@ -24,7 +24,7 @@ class UserFileSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = UserFile - fields = ('url', 'id', 'creation_date', 'upload_path', 'fname', 'fsize', + fields = ('url', 'id', 'creation_date', 'upload_path', 'fname', 'fsize', 'public', 'owner_username', 'file_resource', 'parent_folder', 'owner') def create(self, validated_data): diff --git a/chris_backend/users/serializers.py b/chris_backend/users/serializers.py index d9acaf2f..062d3948 100755 --- a/chris_backend/users/serializers.py +++ b/chris_backend/users/serializers.py @@ -7,7 +7,7 @@ from rest_framework import serializers from rest_framework.validators import UniqueValidator -from core.models import ChrisFolder +from core.models import ChrisFolder, ChrisLinkFile from core.storage import connect_storage from userfiles.models import UserFile @@ -34,19 +34,40 @@ def create(self, validated_data): Overriden to take care of the password hashing and create a welcome file and a feeds folder for the user in its personal storage space. """ + # retrieve predefined groups + try: + all_grp = Group.objects.get(name='all_users') + pacs_grp = Group.objects.get(name='pacs_users') + except Group.DoesNotExist: + logger.error(f"Error while retrieving groups: ['all_users', 'pacs_users']") + raise + username = validated_data.get('username') email = validated_data.get('email') password = validated_data.get('password') + + # create user taking care of the password hashing and assign the predefined groups user = User.objects.create_user(username, email, password) + user.groups.set([all_grp, pacs_grp]) home_path = f'home/{username}' uploads_path = f'{home_path}/uploads' feeds_path = f'{home_path}/feeds' + # create predefined folders under the home directory (uploads_folder, _) = ChrisFolder.objects.get_or_create(path=uploads_path, owner=user) (feeds_folder, _) = ChrisFolder.objects.get_or_create(path=feeds_path, owner=user) + # create predefined link files under the home directory + link_file = ChrisLinkFile(path='PUBLIC', owner=user, + parent_folder=uploads_folder.parent) + link_file.save(name='public') + link_file = ChrisLinkFile(path='SHARED', owner=user, + parent_folder=uploads_folder.parent) + link_file.save(name='shared') + + # create a welcome.txt file inside the uploads folder storage_manager = connect_storage(settings) welcome_file_path = f'{uploads_path}/welcome.txt' try: @@ -62,15 +83,11 @@ def create(self, validated_data): def validate_username(self, username): """ - Overriden to check that the username does not contain forward slashes and is - not the 'chris' special username. + Overriden to check that the username does not contain forward slashes. """ if '/' in username: raise serializers.ValidationError( ["This field may not contain forward slashes."]) - if username == 'chris': - raise serializers.ValidationError( - ["Username %s is not available." % username]) return username diff --git a/chrisomatic/chrisomatic.yml b/chrisomatic/chrisomatic.yml index ef2520dd..9fbfbee6 100755 --- a/chrisomatic/chrisomatic.yml +++ b/chrisomatic/chrisomatic.yml @@ -5,7 +5,7 @@ on: chris_superuser: username: chris password: chris1234 - email: dev@babymri.org + email: dev111@babymri.org cube: users: From f752fe285e8bf96d47a7e79cca3cf9cb9d568726 Mon Sep 17 00:00:00 2001 From: Jorge Date: Thu, 6 Jun 2024 17:02:18 -0400 Subject: [PATCH 03/11] Fix APIs' permissions --- chris_backend/config/settings/local.py | 3 +- chris_backend/config/settings/production.py | 3 +- chris_backend/config/urls.py | 21 -- chris_backend/core/api.py | 20 ++ chris_backend/feeds/models.py | 3 +- chris_backend/feeds/permissions.py | 19 +- chris_backend/feeds/serializers.py | 14 ++ chris_backend/feeds/views.py | 47 ++-- chris_backend/filebrowser/permissions.py | 35 +-- chris_backend/filebrowser/serializers.py | 259 ++++++++++++++++++-- chris_backend/filebrowser/views.py | 158 ++++++------ chris_backend/pacsfiles/permissions.py | 14 +- chris_backend/pacsfiles/serializers.py | 2 +- chris_backend/pacsfiles/views.py | 16 +- chris_backend/userfiles/permissions.py | 34 --- chris_backend/userfiles/serializers.py | 45 ++-- chris_backend/userfiles/views.py | 27 +- chris_backend/users/models.py | 80 +++++- chris_backend/users/serializers.py | 58 +---- chris_backend/users/views.py | 8 +- 20 files changed, 556 insertions(+), 310 deletions(-) diff --git a/chris_backend/config/settings/local.py b/chris_backend/config/settings/local.py index 58e49308..b96abe76 100755 --- a/chris_backend/config/settings/local.py +++ b/chris_backend/config/settings/local.py @@ -189,7 +189,8 @@ 'last_name': 'sn', 'email': 'mail' } + AUTHENTICATION_BACKENDS = ( - 'django_auth_ldap.backend.LDAPBackend', + 'users.models.CustomLDAPBackend', 'django.contrib.auth.backends.ModelBackend', ) diff --git a/chris_backend/config/settings/production.py b/chris_backend/config/settings/production.py index e9bf6856..4eb33eaa 100755 --- a/chris_backend/config/settings/production.py +++ b/chris_backend/config/settings/production.py @@ -162,7 +162,8 @@ def get_secret(setting, secret_type=env): 'last_name': 'sn', 'email': 'mail' } + AUTHENTICATION_BACKENDS = ( - 'django_auth_ldap.backend.LDAPBackend', + 'users.models.CustomLDAPBackend', 'django.contrib.auth.backends.ModelBackend', ) diff --git a/chris_backend/config/urls.py b/chris_backend/config/urls.py index 15107779..9722975b 100755 --- a/chris_backend/config/urls.py +++ b/chris_backend/config/urls.py @@ -20,7 +20,6 @@ from django.conf import settings from plugins import admin as plugin_admin_views -from users import views as group_admin_views urlpatterns = [ @@ -39,26 +38,6 @@ plugin_admin_views.ComputeResourceAdminDetail.as_view(), name='admin-computeresource-detail'), - path('chris-admin/api/v1/groups/', - group_admin_views.GroupList.as_view(), - name='group-list'), - - path('chris-admin/api/v1/groups/search/', - group_admin_views.GroupListQuerySearch.as_view(), - name='group-list-query-search'), - - path('chris-admin/api/v1/groups//', - group_admin_views.GroupDetail.as_view(), - name='group-detail'), - - path('chris-admin/api/v1/groups//users/', - group_admin_views.GroupUserList.as_view(), - name='group-user-list'), - - path('chris-admin/api/v1/groups/users//', - group_admin_views.GroupUserDetail.as_view(), - name='user_groups-detail'), - path('chris-admin/', admin.site.urls), path('api/', include('core.api')), diff --git a/chris_backend/core/api.py b/chris_backend/core/api.py index f020823c..964baa9a 100755 --- a/chris_backend/core/api.py +++ b/chris_backend/core/api.py @@ -31,6 +31,26 @@ path('v1/users//groups/', user_views.UserGroupList.as_view(), name='user-group-list'), + path('v1/groups/', + user_views.GroupList.as_view(), + name='group-list'), + + path('v1/groups/search/', + user_views.GroupListQuerySearch.as_view(), + name='group-list-query-search'), + + path('v1/groups//', + user_views.GroupDetail.as_view(), + name='group-detail'), + + path('v1/groups//users/', + user_views.GroupUserList.as_view(), + name='group-user-list'), + + path('v1/groups/users//', + user_views.GroupUserDetail.as_view(), + name='user_groups-detail'), + path('v1/downloadtokens/', core_views.FileDownloadTokenList.as_view(), diff --git a/chris_backend/feeds/models.py b/chris_backend/feeds/models.py index a4953523..8d7a5b26 100755 --- a/chris_backend/feeds/models.py +++ b/chris_backend/feeds/models.py @@ -150,8 +150,7 @@ class FeedFilter(FilterSet): class Meta: model = Feed fields = ['id', 'name', 'name_exact', 'name_startswith', 'min_id', 'max_id', - 'min_creation_date', 'max_creation_date', 'public', - 'files_fname_icontains'] + 'min_creation_date', 'max_creation_date', 'files_fname_icontains'] def filter_by_fname_icontains(self, queryset, name, value): """ diff --git a/chris_backend/feeds/permissions.py b/chris_backend/feeds/permissions.py index a8457a4c..fb6d640d 100755 --- a/chris_backend/feeds/permissions.py +++ b/chris_backend/feeds/permissions.py @@ -29,7 +29,7 @@ def has_object_permission(self, request, view, obj): if request.method in permissions.SAFE_METHODS: return True - return (obj.owner == request.user) or (request.user.username == 'chris') + return obj.owner == request.user or request.user.username == 'chris' class IsOwnerOrChrisOrHasPermissionOrPublicReadOnly(permissions.BasePermission): @@ -80,17 +80,20 @@ def has_object_permission(self, request, view, obj): obj.feed.has_user_permission(request.user)) -class IsOwnerOrChrisOrReadOnlyOrRelatedFeedPublicReadOnly(permissions.BasePermission): +class IsOwnerOrChrisOrFeedOwnerOrHasFeedPermissionReadOnlyOrPublicFeedReadOnly( + permissions.BasePermission): """ - Custom permission to only allow owners of an object or superuser - 'chris' to modify/edit it. Read only is allowed to other authenticated users. - Read only is also allowed to unauthenticated users when the related feed is public. + Custom permission to only allow superuser 'chris', the owner of an object and the + owner of the object's associated feed to modify/edit the object (eg. a comment). + Read-only access is allowed to users that have been granted the associated feed + access permission or to any user if the feed is public. """ def has_object_permission(self, request, view, obj): user = request.user - if user.username == 'chris': + + if obj.owner == user or user.username == 'chris' or obj.feed.owner == user: return True - return obj.owner == user or (request.method in permissions.SAFE_METHODS and ( - user.is_authenticated or obj.feed.public)) + return request.method in permissions.SAFE_METHODS and ( + obj.feed.public or obj.feed.has_user_permission(user)) diff --git a/chris_backend/feeds/serializers.py b/chris_backend/feeds/serializers.py index 51de39c1..d7ed42cf 100755 --- a/chris_backend/feeds/serializers.py +++ b/chris_backend/feeds/serializers.py @@ -142,6 +142,20 @@ def validate_name(self, name): ["This field may not contain forward slashes."]) return name + def validate_public(self, public): + """ + Overriden to check that only the owner or superuser chris can change a feed's + public status. + """ + if self.instance: # validation on update + user = self.context['request'].user + + if not (self.instance.owner == user or user.username == 'chris'): + raise serializers.ValidationError( + ["Public status of a feed can only be changed by its owner or" + "superuser 'chris'."]) + return public + def get_created_jobs(self, obj): """ Overriden to get the number of plugin instances in 'created' status. diff --git a/chris_backend/feeds/views.py b/chris_backend/feeds/views.py index 1b804637..dda29015 100755 --- a/chris_backend/feeds/views.py +++ b/chris_backend/feeds/views.py @@ -13,12 +13,11 @@ from .serializers import (FeedSerializer, FeedGroupPermissionSerializer, FeedUserPermissionSerializer, NoteSerializer, TagSerializer, TaggingSerializer, CommentSerializer) -from .permissions import (IsChrisOrFeedOwnerOrHasFeedPermissionOrPublicFeedReadOnly, - IsOwnerOrChrisOrReadOnly, - IsOwnerOrChrisOrHasPermissionOrPublicReadOnly, - IsOwnerOrChrisOrHasPermissionReadOnly, - IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly, - IsOwnerOrChrisOrReadOnlyOrRelatedFeedPublicReadOnly) +from .permissions import ( + IsChrisOrFeedOwnerOrHasFeedPermissionOrPublicFeedReadOnly, IsOwnerOrChrisOrReadOnly, + IsOwnerOrChrisOrHasPermissionOrPublicReadOnly, IsOwnerOrChrisOrHasPermissionReadOnly, + IsChrisOrFeedOwnerOrHasFeedPermissionReadOnly, + IsOwnerOrChrisOrFeedOwnerOrHasFeedPermissionReadOnlyOrPublicFeedReadOnly) class NoteDetail(generics.RetrieveUpdateAPIView): @@ -132,7 +131,7 @@ def get_tags_queryset(self, user): class TagFeedList(generics.ListAPIView): """ - A view for a tag-specific collection of feeds. + A view for the tag-specific collection of feeds. """ http_method_names = ['get'] queryset = Tag.objects.all() @@ -161,13 +160,13 @@ def get_feeds_queryset(self, user): group_ids = [g.id for g in user.groups.all()] lookup = Q(owner=user) | Q(public=True) | Q(shared_users=user) | Q( - shared_groups__in=group_ids) + shared_groups__pk__in=group_ids) return tag.feeds.filter(lookup) class FeedTaggingList(generics.ListCreateAPIView): """ - A view for the collection of feed-specific taggings. + A view for the feed-specific collection of taggings. """ http_method_names = ['get', 'post'] queryset = Feed.objects.all() @@ -257,7 +256,7 @@ def get_taggings_queryset(self, user): group_ids = [g.id for g in user.groups.all()] lookup = Q(feed__owner=user) | (Q(feed__public=True) | Q( - feed__shared_users=user) | Q(feed__shared_groups__in=group_ids)) + feed__shared_users=user) | Q(feed__shared_groups__pk__in=group_ids)) return Tagging.objects.filter(tag=tag).filter(lookup) @@ -287,14 +286,14 @@ def get_queryset(self): """ user = self.request.user if not user.is_authenticated: - return [] + return Feed.objects.none() - # if the user is chris then return all the feeds in the system + # if the user is chris then return all the non-public feeds in the system if user.username == 'chris': - return Feed.objects.all() + return Feed.objects.exclude(public=True) group_ids = [g.id for g in user.groups.all()] - lookup = Q(owner=user) | Q(shared_users=user) | Q(shared_groups__in=group_ids) + lookup = Q(owner=user) | Q(shared_users=user) | Q(shared_groups__pk__in=group_ids) return Feed.objects.filter(lookup) def list(self, request, *args, **kwargs): @@ -330,6 +329,7 @@ def list(self, request, *args, **kwargs): if user.is_authenticated: links['download_tokens'] = reverse('filedownloadtoken-list', request=request) + links['groups'] = reverse('group-list', request=request) links['user'] = reverse('user-detail', request=request, kwargs={"pk": user.id}) if user.is_staff: @@ -343,7 +343,6 @@ class FeedListQuerySearch(generics.ListAPIView): """ http_method_names = ['get'] serializer_class = FeedSerializer - permission_classes = (permissions.IsAuthenticated,) filterset_class = FeedFilter def get_queryset(self): @@ -353,13 +352,15 @@ def get_queryset(self): the user. """ user = self.request.user + if not user.is_authenticated: + return Feed.objects.none() - # if the user is chris then return all the feeds in the system + # if the user is chris then return all the non-public feeds in the system if user.username == 'chris': - return Feed.objects.all() + return Feed.objects.exclude(public=True) group_ids = [g.id for g in user.groups.all()] - lookup = Q(owner=user) | Q(shared_users=user) | Q(shared_groups__in=group_ids) + lookup = Q(owner=user) | Q(shared_users=user) | Q(shared_groups__pk__in=group_ids) return Feed.objects.filter(lookup) @@ -481,7 +482,7 @@ def get_queryset(self): group permissions. """ feed = get_object_or_404(Feed, pk=self.kwargs['pk']) - return feed.shared_groups.all() + return FeedGroupPermission.objects.filter(feed=feed) class FeedGroupPermissionDetail(generics.RetrieveDestroyAPIView): @@ -570,7 +571,7 @@ def get_queryset(self): user permissions. """ feed = get_object_or_404(Feed, pk=self.kwargs['pk']) - return feed.shared_users.all() + return FeedUserPermission.objects.filter(feed=feed) class FeedUserPermissionDetail(generics.RetrieveDestroyAPIView): @@ -654,7 +655,7 @@ class CommentListQuerySearch(generics.ListAPIView): http_method_names = ['get'] serializer_class = CommentSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly, - IsChrisOrFeedOwnerOrHasFeedPermissionOrPublicFeedReadOnly) + IsOwnerOrChrisOrHasPermissionOrPublicReadOnly) filterset_class = CommentFilter def get_queryset(self): @@ -673,7 +674,9 @@ class CommentDetail(generics.RetrieveUpdateDestroyAPIView): http_method_names = ['get', 'put', 'delete'] queryset = Comment.objects.all() serializer_class = CommentSerializer - permission_classes = (IsOwnerOrChrisOrReadOnlyOrRelatedFeedPublicReadOnly,) + permission_classes = ( + permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrFeedOwnerOrHasFeedPermissionReadOnlyOrPublicFeedReadOnly,) def retrieve(self, request, *args, **kwargs): """ diff --git a/chris_backend/filebrowser/permissions.py b/chris_backend/filebrowser/permissions.py index e46d4475..5cbc2234 100755 --- a/chris_backend/filebrowser/permissions.py +++ b/chris_backend/filebrowser/permissions.py @@ -2,23 +2,24 @@ from rest_framework import permissions -class IsOwnerOrChrisOrHasWritePermissionOrReadOnly(permissions.BasePermission): +class IsOwnerOrChrisOrCanWriteOrCanReadOnlyOrPublicReadOnly(permissions.BasePermission): """ Custom permission to only allow owners of an object or superuser 'chris' or users - with write permission to modify/edit it. Read-only is allowed to other users. + with write permission to modify/edit it. Read-only access is allowed to other users + that have been granted read permission or to any user if the object is public. """ def has_object_permission(self, request, view, obj): - # Read permissions are allowed to any request, - # so we'll always allow GET, HEAD or OPTIONS requests. - if request.method in permissions.SAFE_METHODS: + user = request.user + + if obj.owner == user or user.username == 'chris': return True - # Write permissions are only allowed to the owner, superuser 'chris' and users - # with write permission. - user = request.user - return (user == obj.owner or user.username == 'chris' or - obj.has_user_permission(user, 'w')) + if request.method in permissions.SAFE_METHODS and (obj.public or + obj.has_user_permission(user)): + return True + + return obj.has_user_permission(user, 'w') class IsOwnerOrChrisOrHasAnyPermissionReadOnly(permissions.BasePermission): @@ -38,20 +39,6 @@ def has_object_permission(self, request, view, obj): obj.has_user_permission(user)) -class IsOwnerOrChrisOrHasAnyPermissionOrObjIsPublic(permissions.BasePermission): - """ - Custom permission to only allow superuser 'chris', the owner of an object or any - users that have been granted any permission to access an object. Also access is - allowed to all users if the object is public. - """ - - def has_object_permission(self, request, view, obj): - user = request.user - - return (obj.owner == user or user.username == 'chris' or obj.public or - obj.has_user_permission(user)) - - class IsFolderOwnerOrChrisOrHasAnyFolderPermissionReadOnly(permissions.BasePermission): """ Custom permission to only allow superuser 'chris' and the owner of an object's diff --git a/chris_backend/filebrowser/serializers.py b/chris_backend/filebrowser/serializers.py index 72a4dd68..a85c1187 100755 --- a/chris_backend/filebrowser/serializers.py +++ b/chris_backend/filebrowser/serializers.py @@ -3,10 +3,12 @@ from django.contrib.auth.models import User, Group from django.db.utils import IntegrityError +from django.conf import settings from rest_framework import serializers from rest_framework.reverse import reverse from collectionjson.fields import ItemLinkField +from core.storage import connect_storage from core.utils import get_file_resource_link from core.models import (ChrisFolder, ChrisFile, ChrisLinkFile, FolderGroupPermission, FolderUserPermission, FileGroupPermission, FileUserPermission, @@ -67,32 +69,64 @@ def update(self, instance, validated_data): def validate_path(self, path): """ - Overriden to check whether the provided path is under home// but not - under home//feeds/. + Overriden to check whether the provided path is under a home/'s subdirectory + for which the user has write permission. Also to check whether the folder + already exists. """ # remove leading and trailing slashes path = path.strip(' ').strip('/') - user = self.context['request'].user - prefix = f'home/{user.username}/' - - if path.startswith(prefix + 'feeds/'): - error_msg = f"Invalid field value. Creating folders with a path under the " \ - f"feed's directory '{prefix + 'feeds/'}' is not allowed." - raise serializers.ValidationError([error_msg]) - - if not path.startswith(prefix): - error_msg = f"Invalid field value. Path must start with '{prefix}'." - raise serializers.ValidationError([error_msg]) + if not path.startswith('home/'): + raise serializers.ValidationError(["Invalid path. Path must start with " + "'home/'."]) try: ChrisFolder.objects.get(path=path) except ChrisFolder.DoesNotExist: pass else: - error_msg = f"Folder with path '{path}' already exists." - raise serializers.ValidationError([error_msg]) + raise serializers.ValidationError([f"Folder with path '{path}' already " + f"exists."]) + user = self.context['request'].user + parent_folder_path = os.path.dirname(path) + + while True: + try: + parent_folder = ChrisFolder.objects.get(path=parent_folder_path) + except ChrisFolder.DoesNotExist: + parent_folder_path = os.path.dirname(parent_folder_path) + else: + break + + if not (parent_folder.owner == user or parent_folder.public or + parent_folder.has_user_permission(user, 'w')): + raise serializers.ValidationError([f"Invalid path. User do not have write " + f"permission under the folder " + f"'{parent_folder_path}'."]) return path + def validate_public(self, public): + """ + Overriden to check that only the owner or superuser chris can change a folder's + public status. + """ + if self.instance: # on update + user = self.context['request'].user + + if not (self.instance.owner == user or user.username == 'chris'): + raise serializers.ValidationError( + ["Public status of a feed can only be changed by its owner or" + "superuser 'chris'."]) + return public + + def validate(self, data): + """ + Overriden to validate that the 'public' field is in data when updating a folder. + """ + if self.instance: # on update + if 'public' not in data: + raise serializers.ValidationError({'public': ['This field is required.']}) + return data + class FileBrowserFolderGroupPermissionSerializer(serializers.HyperlinkedModelSerializer): grp_name = serializers.CharField(write_only=True) @@ -186,6 +220,8 @@ def validate_username(self, username): class FileBrowserFileSerializer(serializers.HyperlinkedModelSerializer): + new_file_path = serializers.CharField(max_length=1024, write_only=True, + required=False) fname = serializers.FileField(use_url=False) fsize = serializers.ReadOnlyField(source='fname.size') owner_username = serializers.ReadOnlyField(source='owner.username') @@ -201,8 +237,41 @@ class FileBrowserFileSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = ChrisFile fields = ('url', 'id', 'creation_date', 'fname', 'fsize', 'public', - 'owner_username', 'file_resource', 'parent_folder', 'group_permissions', - 'user_permissions', 'owner') + 'new_file_path', 'owner_username', 'file_resource', 'parent_folder', + 'group_permissions', 'user_permissions', 'owner') + + def update(self, instance, validated_data): + """ + Overriden to set the file's saving path and parent folder and delete the old + path from storage. + """ + if 'public' in validated_data: + instance.public = validated_data['public'] + + new_file_path = validated_data.pop('new_file_path', None) + + if new_file_path: + # user file will be stored at: SWIFT_CONTAINER_NAME/ + # where must start with home/ + + old_storage_path = instance.fname.name + + storage_manager = connect_storage(settings) + if storage_manager.obj_exists(new_file_path): + storage_manager.delete_obj(new_file_path) + + storage_manager.copy_obj(old_storage_path, new_file_path) + storage_manager.delete_obj(old_storage_path) + + folder_path = os.path.dirname(new_file_path) + owner = instance.owner + (parent_folder, _) = ChrisFolder.objects.get_or_create(path=folder_path, + owner=owner) + instance.parent_folder = parent_folder + instance.fname.name = new_file_path + + instance.save() + return instance def get_file_link(self, obj): """ @@ -210,6 +279,64 @@ def get_file_link(self, obj): """ return get_file_resource_link(self, obj) + def validate_new_file_path(self, new_file_path): + """ + Overriden to check whether the provided path is under a home/'s subdirectory + for which the user has write permission. + """ + # remove leading and trailing slashes + new_file_path = new_file_path.strip(' ').strip('/') + + if new_file_path.endswith('.chrislink'): + raise serializers.ValidationError(["Invalid path. This is not a ChRIS link " + "file."]) + if not new_file_path.startswith('home/'): + raise serializers.ValidationError(["Invalid path. Path must start with " + "'home/'."]) + user = self.context['request'].user + folder_path = os.path.dirname(new_file_path) + + while True: + try: + folder = ChrisFolder.objects.get(path=folder_path) + except ChrisFolder.DoesNotExist: + folder_path = os.path.dirname(folder_path) + else: + break + + if not (folder.owner == user or folder.public or + folder.has_user_permission(user, 'w')): + raise serializers.ValidationError([f"Invalid path. User do not have write " + f"permission under the folder " + f"'{folder_path}'."]) + return new_file_path + + def validate_public(self, public): + """ + Overriden to check that only the owner or superuser chris can change a file's + public status. + """ + if self.instance: # on update + user = self.context['request'].user + + if not (self.instance.owner == user or user.username == 'chris'): + raise serializers.ValidationError( + ["Public status of a feed can only be changed by its owner or" + "superuser 'chris'."]) + return public + + def validate(self, data): + """ + Overriden to validate that at least one of two fields are in data when + updating a file. + """ + if self.instance: # on update + if 'public' not in data and 'new_file_path' not in data: + raise serializers.ValidationError( + {'non_field_errors': ["At least one of the fields 'public' " + "or 'new_file_path' must be provided."]}) + return data + class FileBrowserFileGroupPermissionSerializer(serializers.HyperlinkedModelSerializer): grp_name = serializers.CharField(write_only=True) @@ -304,6 +431,8 @@ def validate_username(self, username): class FileBrowserLinkFileSerializer(serializers.HyperlinkedModelSerializer): + new_link_file_path = serializers.CharField(max_length=1024, write_only=True, + required=False) fname = serializers.FileField(use_url=False, required=False) fsize = serializers.ReadOnlyField(source='fname.size') owner_username = serializers.ReadOnlyField(source='owner.username') @@ -321,8 +450,42 @@ class FileBrowserLinkFileSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = ChrisLinkFile fields = ('url', 'id', 'creation_date', 'path', 'fname', 'fsize', 'public', - 'owner_username', 'file_resource', 'linked_folder', 'linked_file', - 'parent_folder', 'group_permissions', 'user_permissions', 'owner') + 'new_link_file_path', 'owner_username', 'file_resource', + 'linked_folder', 'linked_file', 'parent_folder', 'group_permissions', + 'user_permissions', 'owner') + + def update(self, instance, validated_data): + """ + Overriden to set the link file's saving path and parent folder and delete + the old path from storage. + """ + if 'public' in validated_data: + instance.public = validated_data['public'] + + new_link_file_path = validated_data.pop('new_link_file_path', None) + + if new_link_file_path: + # user file will be stored at: SWIFT_CONTAINER_NAME/ + # where must start with home/ + + old_storage_path = instance.fname.name + + storage_manager = connect_storage(settings) + if storage_manager.obj_exists(new_link_file_path): + storage_manager.delete_obj(new_link_file_path) + + storage_manager.copy_obj(old_storage_path, new_link_file_path) + storage_manager.delete_obj(old_storage_path) + + folder_path = os.path.dirname(new_link_file_path) + owner = instance.owner + (parent_folder, _) = ChrisFolder.objects.get_or_create(path=folder_path, + owner=owner) + instance.parent_folder = parent_folder + instance.fname.name = new_link_file_path + + instance.save() + return instance def get_file_link(self, obj): """ @@ -367,6 +530,64 @@ def get_linked_file_link(self, obj): return reverse('chrisfile-detail', request=request, kwargs={'pk': linked_file.pk}) + def validate_new_link_file_path(self, new_link_file_path): + """ + Overriden to check whether the provided path is under a home/'s subdirectory + for which the user has write permission. + """ + # remove leading and trailing slashes + new_link_file_path = new_link_file_path.strip(' ').strip('/') + + if new_link_file_path.endswith('.chrislink'): + raise serializers.ValidationError(["Invalid path. This is not a ChRIS link " + "file."]) + if not new_link_file_path.startswith('home/'): + raise serializers.ValidationError(["Invalid path. Path must start with " + "'home/'."]) + user = self.context['request'].user + folder_path = os.path.dirname(new_link_file_path) + + while True: + try: + folder = ChrisFolder.objects.get(path=folder_path) + except ChrisFolder.DoesNotExist: + folder_path = os.path.dirname(folder_path) + else: + break + + if not (folder.owner == user or folder.public or + folder.has_user_permission(user, 'w')): + raise serializers.ValidationError([f"Invalid path. User do not have write " + f"permission under the folder " + f"'{folder_path}'."]) + return new_link_file_path + + def validate_public(self, public): + """ + Overriden to check that only the owner or superuser chris can change a link + file's public status. + """ + if self.instance: # on update + user = self.context['request'].user + + if not (self.instance.owner == user or user.username == 'chris'): + raise serializers.ValidationError( + ["Public status of a feed can only be changed by its owner or" + "superuser 'chris'."]) + return public + + def validate(self, data): + """ + Overriden to validate that at least one of two fields are in data when + updating a file. + """ + if self.instance: # on update + if 'public' not in data and 'new_link_file_path' not in data: + raise serializers.ValidationError( + {'non_field_errors': ["At least one of the fields 'public' " + "or 'new_link_file_path' must be provided."]}) + return data + class FileBrowserLinkFileGroupPermissionSerializer(serializers.HyperlinkedModelSerializer): grp_name = serializers.CharField(write_only=True) diff --git a/chris_backend/filebrowser/views.py b/chris_backend/filebrowser/views.py index 91ae5d87..2e2a13cc 100755 --- a/chris_backend/filebrowser/views.py +++ b/chris_backend/filebrowser/views.py @@ -31,10 +31,9 @@ get_folder_children_queryset, get_folder_files_queryset, get_folder_link_files_queryset) -from .permissions import (IsOwnerOrChrisOrHasWritePermissionOrReadOnly, +from .permissions import (IsOwnerOrChrisOrCanWriteOrCanReadOnlyOrPublicReadOnly, IsOwnerOrChrisOrHasAnyPermissionReadOnly, IsFolderOwnerOrChrisOrHasAnyFolderPermissionReadOnly, - IsOwnerOrChrisOrHasAnyPermissionOrObjIsPublic, IsFileOwnerOrChrisOrHasAnyFilePermissionReadOnly, IsLinkFileOwnerOrChrisOrHasAnyLinkFilePermissionReadOnly) @@ -85,7 +84,7 @@ def get_queryset(self): else: qs = get_folder_queryset(pk_dict) - if qs.count() == 0: + if not qs.exists(): raise Http404('Not found.') return qs @@ -124,24 +123,12 @@ class FileBrowserFolderDetail(generics.RetrieveUpdateDestroyAPIView): queryset = ChrisFolder.objects.all() serializer_class = FileBrowserFolderSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly, - IsOwnerOrChrisOrHasWritePermissionOrReadOnly) + IsOwnerOrChrisOrCanWriteOrCanReadOnlyOrPublicReadOnly) def retrieve(self, request, *args, **kwargs): """ Overriden to retrieve a file browser folder and append a collection+json template. """ - user = request.user - id = kwargs.get('pk') - pk_dict = {'id': id} - - if user.is_authenticated: - qs = get_folder_queryset(pk_dict, user) - else: - qs = get_folder_queryset(pk_dict) - - if qs.count() == 0: - raise Http404('Not found.') - response = super(FileBrowserFolderDetail, self).retrieve(request, *args, **kwargs) template_data = {"public": ""} return services.append_collection_template(response, template_data) @@ -161,29 +148,27 @@ class FileBrowserFolderChildList(generics.ListAPIView): http_method_names = ['get'] queryset = ChrisFolder.objects.all() serializer_class = FileBrowserFolderSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrCanWriteOrCanReadOnlyOrPublicReadOnly) def list(self, request, *args, **kwargs): """ - Overriden to return a list of the children ChRIS folders. + Overriden to return a list of the children folders and append document-level + link relations. """ user = request.user - id = kwargs.get('pk') - pk_dict = {'id': id} - - if user.is_authenticated: - qs = get_folder_queryset(pk_dict, user) - else: - qs = get_folder_queryset(pk_dict) - - folder = qs.first() - if folder is None: - raise Http404('Not found.') + folder = self.get_object() if user.is_authenticated: children_qs = get_folder_children_queryset(folder, user) else: children_qs = get_folder_children_queryset(folder) - return services.get_list_response(self, children_qs) + + response = services.get_list_response(self, children_qs) + + links = {'folder': reverse('chrisfolder-detail', request=request, + kwargs={"pk": folder.id})} + return services.append_collection_links(response, links) class FileBrowserFolderGroupPermissionList(generics.ListCreateAPIView): @@ -236,7 +221,8 @@ class FileBrowserFolderGroupPermissionListQuerySearch(generics.ListAPIView): """ http_method_names = ['get'] serializer_class = FileBrowserFolderGroupPermissionSerializer - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasAnyPermissionReadOnly) filterset_class = FolderGroupPermissionFilter def get_queryset(self): @@ -244,13 +230,7 @@ def get_queryset(self): Overriden to return a custom queryset that is comprised by the folder-specific group permissions. """ - user = self.request.user - id = self.kwargs['pk'] - pk_dict = {'id': id} - - folder = get_folder_queryset(pk_dict, user).first() - if folder is None: - raise Http404('Not found.') + folder = get_object_or_404(ChrisFolder, pk=self.kwargs['pk']) return FolderGroupPermission.objects.filter(folder=folder) @@ -341,7 +321,8 @@ class FileBrowserFolderUserPermissionListQuerySearch(generics.ListAPIView): """ http_method_names = ['get'] serializer_class = FileBrowserFolderUserPermissionSerializer - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (permissions.IsAuthenticated, + IsOwnerOrChrisOrHasAnyPermissionReadOnly) filterset_class = FolderUserPermissionFilter def get_queryset(self): @@ -349,13 +330,7 @@ def get_queryset(self): Overriden to return a custom queryset that is comprised by the folder-specific user permissions. """ - user = self.request.user - id = self.kwargs['pk'] - pk_dict = {'id': id} - - folder = get_folder_queryset(pk_dict, user).first() - if folder is None: - raise Http404('Not found.') + folder = get_object_or_404(ChrisFolder, pk=self.kwargs['pk']) return FolderUserPermission.objects.filter(folder=folder) @@ -403,23 +378,16 @@ class FileBrowserFolderFileList(generics.ListAPIView): http_method_names = ['get'] queryset = ChrisFolder.objects.all() serializer_class = FileBrowserFileSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrCanWriteOrCanReadOnlyOrPublicReadOnly) def list(self, request, *args, **kwargs): """ - Overriden to return a list with all the files directly under this folder. + Overriden to return a list of the files directly under this folder and + append document-level link relations. """ user = request.user - id = kwargs.get('pk') - pk_dict = {'id': id} - - if user.is_authenticated: - qs = get_folder_queryset(pk_dict, user) - else: - qs = get_folder_queryset(pk_dict) - - folder = qs.first() - if folder is None: - raise Http404('Not found.') + folder = self.get_object() if user.is_authenticated: files_qs = get_folder_files_queryset(folder, user) @@ -427,18 +395,37 @@ def list(self, request, *args, **kwargs): files_qs = get_folder_files_queryset(folder) response = services.get_list_response(self, files_qs) - return response + + links = {'folder': reverse('chrisfolder-detail', request=request, + kwargs={"pk": folder.id})} + return services.append_collection_links(response, links) -class FileBrowserFileDetail(generics.RetrieveAPIView): +class FileBrowserFileDetail(generics.RetrieveUpdateDestroyAPIView): """ A ChRIS file view. """ - http_method_names = ['get'] + http_method_names = ['get', 'put', 'delete'] queryset = ChrisFile.get_base_queryset() serializer_class = FileBrowserFileSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly, - IsOwnerOrChrisOrHasAnyPermissionOrObjIsPublic) + IsOwnerOrChrisOrCanWriteOrCanReadOnlyOrPublicReadOnly) + + def retrieve(self, request, *args, **kwargs): + """ + Overriden to retrieve a file and append a collection+json template. + """ + response = super(FileBrowserFileDetail, self).retrieve(request, *args, **kwargs) + template_data = {"public": "", "new_file_path": ""} + return services.append_collection_template(response, template_data) + + def update(self, request, *args, **kwargs): + """ + Overriden to include the current fname in the request. + """ + chris_file = self.get_object() + request.data['fname'] = chris_file .fname.file # fname required in the serializer + return super(FileBrowserFileDetail, self).update(request, *args, **kwargs) class FileBrowserFileResource(generics.GenericAPIView): @@ -449,7 +436,7 @@ class FileBrowserFileResource(generics.GenericAPIView): queryset = ChrisFile.get_base_queryset() renderer_classes = (BinaryFileRenderer,) permission_classes = (permissions.IsAuthenticatedOrReadOnly, - IsOwnerOrChrisOrHasAnyPermissionOrObjIsPublic) + IsOwnerOrChrisOrCanWriteOrCanReadOnlyOrPublicReadOnly) authentication_classes = (TokenAuthSupportQueryString, BasicAuthentication, SessionAuthentication) @@ -668,23 +655,16 @@ class FileBrowserFolderLinkFileList(generics.ListAPIView): http_method_names = ['get'] queryset = ChrisFolder.objects.all() serializer_class = FileBrowserLinkFileSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrCanWriteOrCanReadOnlyOrPublicReadOnly) def list(self, request, *args, **kwargs): """ - Overriden to return a list with all the link files directly under this folder. + Overriden to return a list of the files directly under this folder and + append document-level link relations. """ user = request.user - id = kwargs.get('pk') - pk_dict = {'id': id} - - if user.is_authenticated: - qs = get_folder_queryset(pk_dict, user) - else: - qs = get_folder_queryset(pk_dict) - - folder = qs.first() - if folder is None: - raise Http404('Not found.') + folder = self.get_object() if user.is_authenticated: link_files_qs = get_folder_link_files_queryset(folder, user) @@ -692,18 +672,38 @@ def list(self, request, *args, **kwargs): link_files_qs = get_folder_link_files_queryset(folder) response = services.get_list_response(self, link_files_qs) - return response + + links = {'folder': reverse('chrisfolder-detail', request=request, + kwargs={"pk": folder.id})} + return services.append_collection_links(response, links) -class FileBrowserLinkFileDetail(generics.RetrieveAPIView): +class FileBrowserLinkFileDetail(generics.RetrieveUpdateDestroyAPIView): """ A ChRIS link file view. """ - http_method_names = ['get'] + http_method_names = ['get', 'put', 'delete'] queryset = ChrisLinkFile.objects.all() serializer_class = FileBrowserLinkFileSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly, - IsOwnerOrChrisOrHasAnyPermissionOrObjIsPublic) + IsOwnerOrChrisOrCanWriteOrCanReadOnlyOrPublicReadOnly) + + def retrieve(self, request, *args, **kwargs): + """ + Overriden to retrieve a link file and append a collection+json template. + """ + response = super(FileBrowserLinkFileDetail, self).retrieve(request, *args, + **kwargs) + template_data = {"public": "", "new_link_file_path": ""} + return services.append_collection_template(response, template_data) + + def update(self, request, *args, **kwargs): + """ + Overriden to include the current fname in the request. + """ + chris_link_file = self.get_object() + request.data['fname'] = chris_link_file.fname.file # fname required in the serializer + return super(FileBrowserLinkFileDetail, self).update(request, *args, **kwargs) class FileBrowserLinkFileResource(generics.GenericAPIView): @@ -714,7 +714,7 @@ class FileBrowserLinkFileResource(generics.GenericAPIView): queryset = ChrisLinkFile.objects.all() renderer_classes = (BinaryFileRenderer,) permission_classes = (permissions.IsAuthenticatedOrReadOnly, - IsOwnerOrChrisOrHasAnyPermissionOrObjIsPublic) + IsOwnerOrChrisOrCanWriteOrCanReadOnlyOrPublicReadOnly) authentication_classes = (TokenAuthSupportQueryString, BasicAuthentication, SessionAuthentication) diff --git a/chris_backend/pacsfiles/permissions.py b/chris_backend/pacsfiles/permissions.py index 9afaad7d..a948bddb 100755 --- a/chris_backend/pacsfiles/permissions.py +++ b/chris_backend/pacsfiles/permissions.py @@ -2,17 +2,17 @@ from rest_framework import permissions -class IsChrisOrReadOnly(permissions.BasePermission): +class IsChrisOrIsPACSUserReadOnly(permissions.BasePermission): """ Custom permission to only allow superuser 'chris' to create it. - Read only is allowed to other users. + Read only is allowed to other users in the pacs_users group. """ def has_permission(self, request, view): - # Read permissions are allowed to any request, - # so we'll always allow GET, HEAD or OPTIONS requests. - if request.method in permissions.SAFE_METHODS: + user = request.user + + if user.username == 'chris': return True - # Write permissions are only allowed to the superuser 'chris'. - return request.user.username == 'chris' + return (request.method in permissions.SAFE_METHODS and user.groups.filter( + name='pacs_users').exists()) diff --git a/chris_backend/pacsfiles/serializers.py b/chris_backend/pacsfiles/serializers.py index e6bdbd42..f03ccf55 100755 --- a/chris_backend/pacsfiles/serializers.py +++ b/chris_backend/pacsfiles/serializers.py @@ -28,7 +28,7 @@ class Meta: class PACSSeriesSerializer(serializers.HyperlinkedModelSerializer): - path = serializers.CharField(write_only=True) + path = serializers.CharField(max_length=1024, write_only=True) ndicom = serializers.IntegerField(write_only=True) pacs_name = serializers.CharField(max_length=20, write_only=True) pacs_identifier = serializers.ReadOnlyField(source='pacs.identifier') diff --git a/chris_backend/pacsfiles/views.py b/chris_backend/pacsfiles/views.py index 3525dcd2..663e16ff 100755 --- a/chris_backend/pacsfiles/views.py +++ b/chris_backend/pacsfiles/views.py @@ -9,7 +9,7 @@ from core.views import TokenAuthSupportQueryString from .models import PACSSeries, PACSSeriesFilter, PACSFile, PACSFileFilter from .serializers import PACSSeriesSerializer, PACSFileSerializer -from .permissions import IsChrisOrReadOnly +from .permissions import IsChrisOrIsPACSUserReadOnly class PACSSeriesList(generics.ListCreateAPIView): @@ -19,7 +19,7 @@ class PACSSeriesList(generics.ListCreateAPIView): http_method_names = ['get', 'post'] queryset = PACSSeries.objects.all() serializer_class = PACSSeriesSerializer - permission_classes = (permissions.IsAuthenticated, IsChrisOrReadOnly,) + permission_classes = (permissions.IsAuthenticated, IsChrisOrIsPACSUserReadOnly,) def list(self, request, *args, **kwargs): """ @@ -53,7 +53,7 @@ class PACSSeriesListQuerySearch(generics.ListAPIView): http_method_names = ['get'] serializer_class = PACSSeriesSerializer queryset = PACSSeries.objects.all() - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (permissions.IsAuthenticated, IsChrisOrIsPACSUserReadOnly) filterset_class = PACSSeriesFilter @@ -64,7 +64,7 @@ class PACSSeriesDetail(generics.RetrieveAPIView): http_method_names = ['get'] queryset = PACSSeries.objects.all() serializer_class = PACSSeriesSerializer - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (permissions.IsAuthenticated, IsChrisOrIsPACSUserReadOnly) class PACSFileList(generics.ListAPIView): @@ -74,7 +74,7 @@ class PACSFileList(generics.ListAPIView): http_method_names = ['get', 'post'] queryset = PACSFile.get_base_queryset() serializer_class = PACSFileSerializer - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (permissions.IsAuthenticated, IsChrisOrIsPACSUserReadOnly) def list(self, request, *args, **kwargs): """ @@ -94,7 +94,7 @@ class PACSFileListQuerySearch(generics.ListAPIView): http_method_names = ['get'] serializer_class = PACSFileSerializer queryset = PACSFile.get_base_queryset() - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (permissions.IsAuthenticated, IsChrisOrIsPACSUserReadOnly) filterset_class = PACSFileFilter @@ -105,7 +105,7 @@ class PACSFileDetail(generics.RetrieveAPIView): http_method_names = ['get'] queryset = PACSFile.get_base_queryset() serializer_class = PACSFileSerializer - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (permissions.IsAuthenticated, IsChrisOrIsPACSUserReadOnly) class PACSFileResource(generics.GenericAPIView): @@ -115,7 +115,7 @@ class PACSFileResource(generics.GenericAPIView): http_method_names = ['get'] queryset = PACSFile.get_base_queryset() renderer_classes = (BinaryFileRenderer,) - permission_classes = (permissions.IsAuthenticated,) + permission_classes = (permissions.IsAuthenticated, IsChrisOrIsPACSUserReadOnly) authentication_classes = (TokenAuthSupportQueryString, BasicAuthentication, SessionAuthentication) diff --git a/chris_backend/userfiles/permissions.py b/chris_backend/userfiles/permissions.py index 4f907d30..1623cef3 100755 --- a/chris_backend/userfiles/permissions.py +++ b/chris_backend/userfiles/permissions.py @@ -1,8 +1,6 @@ from rest_framework import permissions -from feeds.models import Feed - class IsOwnerOrChris(permissions.BasePermission): """ @@ -16,35 +14,3 @@ def has_object_permission(self, request, view, obj): if hasattr(obj.owner, 'all'): return (request.user in obj.owner.all()) or (request.user.username == 'chris') return (obj.owner == request.user) or (request.user.username == 'chris') - - -class IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly(permissions.BasePermission): - """ - Custom permission to only allow owners of the object, owners of a feed associated - to an object or superuser 'chris' to modify/edit the object. Read only is allowed - to other users if the related feed is public. - """ - - def has_object_permission(self, request, view, obj): - user = request.user - path = obj.fname.name - path_tokens = path.split('/', 4) - - if not user.is_authenticated: - if (len(path_tokens) > 3 and path_tokens[0] == 'home' and path_tokens[2] == - 'feeds'): - feed_id = int(path_tokens[3].split('_')[1]) - feed = Feed.objects.get(id=feed_id) - return request.method in permissions.SAFE_METHODS and feed.public - return False - - if request.user.username == 'chris' or obj.owner == request.user: - return True - - if (len(path_tokens) > 3 and path_tokens[0] == 'home' and path_tokens[2] == - 'feeds'): - feed_id = int(path_tokens[3].split('_')[1]) - feed = Feed.objects.get(id=feed_id) - return request.user in feed.owner.all() or ( - request.method in permissions.SAFE_METHODS and feed.public) - return False diff --git a/chris_backend/userfiles/serializers.py b/chris_backend/userfiles/serializers.py index 73b0551e..8d3b3078 100755 --- a/chris_backend/userfiles/serializers.py +++ b/chris_backend/userfiles/serializers.py @@ -15,7 +15,7 @@ class UserFileSerializer(serializers.HyperlinkedModelSerializer): fname = serializers.FileField(use_url=False) fsize = serializers.ReadOnlyField(source='fname.size') - upload_path = serializers.CharField(write_only=True) + upload_path = serializers.CharField(max_length=1024, write_only=True) owner_username = serializers.ReadOnlyField(source='owner.username') file_resource = ItemLinkField('get_file_link') parent_folder = serializers.HyperlinkedRelatedField(view_name='chrisfolder-detail', @@ -47,17 +47,18 @@ def create(self, validated_data): def update(self, instance, validated_data): """ - Overriden to set the file's saving path and parent folder and delete the old + Overriden to set the file's saving path and parent folder and delete the old path from storage. """ # user file will be stored at: SWIFT_CONTAINER_NAME/ - # where must start with home// + # where must start with home/ upload_path = validated_data.pop('upload_path') old_storage_path = instance.fname.name storage_manager = connect_storage(settings) if storage_manager.obj_exists(upload_path): storage_manager.delete_obj(upload_path) + storage_manager.copy_obj(old_storage_path, upload_path) storage_manager.delete_obj(old_storage_path) @@ -78,24 +79,32 @@ def get_file_link(self, obj): def validate_upload_path(self, upload_path): """ - Overriden to check whether the provided path is under home// but not - under home//feeds/. + Overriden to check whether the provided path is under a home/'s subdirectory + for which the user has write permission. """ # remove leading and trailing slashes upload_path = upload_path.strip(' ').strip('/') - user = self.context['request'].user - prefix = f'home/{user.username}/' - - if upload_path.startswith(prefix + 'feeds/'): - error_msg = f"Invalid file path. Uploading files to a path under the " \ - f"feed's directory '{prefix + 'feeds/'}' is not allowed." - raise serializers.ValidationError([error_msg]) - - if not upload_path.startswith(prefix): - error_msg = f"Invalid file path. Path must start with '{prefix}'." - raise serializers.ValidationError([error_msg]) if upload_path.endswith('.chrislink'): - error_msg = 'Invalid file path. Uploading ChRIS link files is not allowed.' - raise serializers.ValidationError([error_msg]) + raise serializers.ValidationError(["Invalid path. Uploading ChRIS link " + "files is not allowed."]) + if not upload_path.startswith('home/'): + raise serializers.ValidationError(["Invalid path. Path must start with " + "'home/'."]) + user = self.context['request'].user + folder_path = os.path.dirname(upload_path) + + while True: + try: + folder = ChrisFolder.objects.get(path=folder_path) + except ChrisFolder.DoesNotExist: + folder_path = os.path.dirname(folder_path) + else: + break + + if not (folder.owner == user or folder.public or + folder.has_user_permission(user, 'w')): + raise serializers.ValidationError([f"Invalid path. User do not have write " + f"permission under the folder " + f"'{folder_path}'."]) return upload_path diff --git a/chris_backend/userfiles/views.py b/chris_backend/userfiles/views.py index aaba8ba9..292ec62f 100755 --- a/chris_backend/userfiles/views.py +++ b/chris_backend/userfiles/views.py @@ -10,7 +10,7 @@ from core.views import TokenAuthSupportQueryString from .models import UserFile, UserFileFilter from .serializers import UserFileSerializer -from .permissions import IsOwnerOrChris, IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly +from .permissions import IsOwnerOrChris class UserFileList(generics.ListCreateAPIView): @@ -18,9 +18,8 @@ class UserFileList(generics.ListCreateAPIView): A view for the collection of user files. """ http_method_names = ['get', 'post'] - queryset = UserFile.objects.all() serializer_class = UserFileSerializer - permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris) + permission_classes = (permissions.IsAuthenticated,) def get_queryset(self): """ @@ -28,9 +27,11 @@ def get_queryset(self): owned by the currently authenticated user. """ user = self.request.user + # if the user is chris then return all the files in the user space if user.username == 'chris': return UserFile.get_base_queryset() + return UserFile.get_base_queryset().filter(owner=user) def perform_create(self, serializer): @@ -45,9 +46,11 @@ def list(self, request, *args, **kwargs): collection+json template to the response. """ response = super(UserFileList, self).list(request, *args, **kwargs) + # append query list query_list = [reverse('userfile-list-query-search', request=request)] response = services.append_collection_querylist(response, query_list) + # append write template template_data = {'upload_path': "", 'fname': ""} return services.append_collection_template(response, template_data) @@ -59,10 +62,22 @@ class UserFileListQuerySearch(generics.ListAPIView): """ http_method_names = ['get'] serializer_class = UserFileSerializer - queryset = UserFile.get_base_queryset() permission_classes = (permissions.IsAuthenticated,) filterset_class = UserFileFilter + def get_queryset(self): + """ + Overriden to return a custom queryset that is only comprised by the files + owned by the currently authenticated user. + """ + user = self.request.user + + # if the user is chris then return all the files in the user space + if user.username == 'chris': + return UserFile.get_base_queryset() + + return UserFile.get_base_queryset().filter(owner=user) + class UserFileDetail(generics.RetrieveUpdateDestroyAPIView): """ @@ -71,7 +86,7 @@ class UserFileDetail(generics.RetrieveUpdateDestroyAPIView): http_method_names = ['get', 'put', 'delete'] queryset = UserFile.get_base_queryset() serializer_class = UserFileSerializer - permission_classes = (IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly,) + permission_classes = (IsOwnerOrChris,) def retrieve(self, request, *args, **kwargs): """ @@ -97,7 +112,7 @@ class UserFileResource(generics.GenericAPIView): http_method_names = ['get'] queryset = UserFile.get_base_queryset() renderer_classes = (BinaryFileRenderer,) - permission_classes = (IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly,) + permission_classes = (IsOwnerOrChris,) authentication_classes = (TokenAuthSupportQueryString, BasicAuthentication, SessionAuthentication) diff --git a/chris_backend/users/models.py b/chris_backend/users/models.py index 4d953ef1..2f05d0e2 100755 --- a/chris_backend/users/models.py +++ b/chris_backend/users/models.py @@ -1,9 +1,21 @@ -from django.contrib.auth.models import Group +import logging +import io + +from django.contrib.auth.models import User, Group +from django_auth_ldap.backend import LDAPBackend +from django.conf import settings import django_filters from django_filters.rest_framework import FilterSet +from core.models import ChrisFolder, ChrisLinkFile +from core.storage import connect_storage +from userfiles.models import UserFile + + +logger = logging.getLogger(__name__) + class GroupFilter(FilterSet): name_icontains = django_filters.CharFilter(field_name='name', lookup_expr='icontains') @@ -11,3 +23,69 @@ class GroupFilter(FilterSet): class Meta: model = Group fields = ['id', 'name', 'name_icontains'] + + +class UserProxy(User): + + class Meta: + ordering = ('-username',) + proxy = True + + def save(self, *args, **kwargs): + """ + Overriden to assign the user's default groups and setup its home folder the + first time it's saved to the DB. + """ + first_save = False if self.pk else True + super(UserProxy, self).save(*args, **kwargs) + + if first_save: # first time the model is being saved + # retrieve predefined groups + try: + all_grp = Group.objects.get(name='all_users') + pacs_grp = Group.objects.get(name='pacs_users') + except Group.DoesNotExist: + logger.error( + f"Error while retrieving groups: ['all_users', 'pacs_users']") + raise + + # assign predefined groups + user = self + user.groups.set([all_grp, pacs_grp]) + + home_path = f'home/{user.username}' + uploads_path = f'{home_path}/uploads' + feeds_path = f'{home_path}/feeds' + + # create predefined folders under the home directory + (uploads_folder, _) = ChrisFolder.objects.get_or_create(path=uploads_path, + owner=user) + (feeds_folder, _) = ChrisFolder.objects.get_or_create(path=feeds_path, + owner=user) + + # create predefined link files under the home directory + link_file = ChrisLinkFile(path='PUBLIC', owner=user, + parent_folder=uploads_folder.parent) + link_file.save(name='public') + link_file = ChrisLinkFile(path='SHARED', owner=user, + parent_folder=uploads_folder.parent) + link_file.save(name='shared') + + # create a welcome.txt file inside the uploads folder + storage_manager = connect_storage(settings) + welcome_file_path = f'{uploads_path}/welcome.txt' + try: + with io.StringIO('Welcome to ChRIS!') as f: + storage_manager.upload_obj(welcome_file_path, f.read(), + content_type='text/plain') + welcome_file = UserFile(parent_folder=uploads_folder, owner=user) + welcome_file.fname.name = welcome_file_path + welcome_file.save() + except Exception as e: + logger.error( + f'Could not create welcome file in user space, detail: {str(e)}') + + +class CustomLDAPBackend(LDAPBackend): + def get_user_model(self): + return UserProxy diff --git a/chris_backend/users/serializers.py b/chris_backend/users/serializers.py index 062d3948..15fe5feb 100755 --- a/chris_backend/users/serializers.py +++ b/chris_backend/users/serializers.py @@ -1,18 +1,9 @@ -import logging -import io - from django.contrib.auth.models import User, Group -from django.conf import settings from rest_framework import serializers from rest_framework.validators import UniqueValidator -from core.models import ChrisFolder, ChrisLinkFile -from core.storage import connect_storage -from userfiles.models import UserFile - - -logger = logging.getLogger(__name__) +from .models import UserProxy class UserSerializer(serializers.HyperlinkedModelSerializer): @@ -31,55 +22,14 @@ class Meta: def create(self, validated_data): """ - Overriden to take care of the password hashing and create a welcome file - and a feeds folder for the user in its personal storage space. + Overriden to take care of the password hashing. """ - # retrieve predefined groups - try: - all_grp = Group.objects.get(name='all_users') - pacs_grp = Group.objects.get(name='pacs_users') - except Group.DoesNotExist: - logger.error(f"Error while retrieving groups: ['all_users', 'pacs_users']") - raise - username = validated_data.get('username') email = validated_data.get('email') password = validated_data.get('password') - # create user taking care of the password hashing and assign the predefined groups - user = User.objects.create_user(username, email, password) - user.groups.set([all_grp, pacs_grp]) - - home_path = f'home/{username}' - uploads_path = f'{home_path}/uploads' - feeds_path = f'{home_path}/feeds' - - # create predefined folders under the home directory - (uploads_folder, _) = ChrisFolder.objects.get_or_create(path=uploads_path, - owner=user) - (feeds_folder, _) = ChrisFolder.objects.get_or_create(path=feeds_path, owner=user) - - # create predefined link files under the home directory - link_file = ChrisLinkFile(path='PUBLIC', owner=user, - parent_folder=uploads_folder.parent) - link_file.save(name='public') - link_file = ChrisLinkFile(path='SHARED', owner=user, - parent_folder=uploads_folder.parent) - link_file.save(name='shared') - - # create a welcome.txt file inside the uploads folder - storage_manager = connect_storage(settings) - welcome_file_path = f'{uploads_path}/welcome.txt' - try: - with io.StringIO('Welcome to ChRIS!') as f: - storage_manager.upload_obj(welcome_file_path, f.read(), - content_type='text/plain') - welcome_file = UserFile(parent_folder=uploads_folder, owner=user) - welcome_file.fname.name = welcome_file_path - welcome_file.save() - except Exception as e: - logger.error(f'Could not create welcome file in user space, detail: {str(e)}') - return user + # create user taking care of the password hashing and setup groups and home folder + return UserProxy.objects.create_user(username, email, password) def validate_username(self, username): """ diff --git a/chris_backend/users/views.py b/chris_backend/users/views.py index ef1245e1..86326707 100755 --- a/chris_backend/users/views.py +++ b/chris_backend/users/views.py @@ -96,7 +96,7 @@ class GroupList(generics.ListCreateAPIView): http_method_names = ['get', 'post'] serializer_class = GroupSerializer queryset = Group.objects.all() - permission_classes = (permissions.IsAdminUser,) + permission_classes = (permissions.IsAuthenticated, IsAdminOrReadOnly) def list(self, request, *args, **kwargs): """ @@ -120,7 +120,7 @@ class GroupListQuerySearch(generics.ListAPIView): http_method_names = ['get'] serializer_class = GroupSerializer queryset = Group.objects.all() - permission_classes = (permissions.IsAdminUser,) + permission_classes = (permissions.IsAuthenticated,) filterset_class = GroupFilter @@ -142,7 +142,7 @@ class GroupUserList(generics.ListCreateAPIView): http_method_names = ['get', 'post'] queryset = Group.objects.all() serializer_class = GroupUserSerializer - permission_classes = (permissions.IsAdminUser, ) + permission_classes = (permissions.IsAuthenticated, IsAdminOrReadOnly) def perform_create(self, serializer): """ @@ -187,4 +187,4 @@ class GroupUserDetail(generics.RetrieveDestroyAPIView): http_method_names = ['get', 'delete'] serializer_class = GroupUserSerializer queryset = User.groups.through.objects.all() - permission_classes = (permissions.IsAdminUser,) + permission_classes = (permissions.IsAuthenticated, IsAdminOrReadOnly) From 6396206f07c1fb09611b87643764be0794311a7d Mon Sep 17 00:00:00 2001 From: Jorge Date: Fri, 7 Jun 2024 20:03:32 -0400 Subject: [PATCH 04/11] Fix bugs across APIs --- chris_backend/config/settings/local.py | 3 + chris_backend/config/settings/production.py | 4 + chris_backend/core/api.py | 2 +- chris_backend/core/apps.py | 3 +- chris_backend/core/models.py | 22 +-- chris_backend/core/tests/test_views.py | 11 +- chris_backend/feeds/tests/test_models.py | 4 +- chris_backend/feeds/tests/test_serializers.py | 5 +- chris_backend/feeds/tests/test_views.py | 4 +- chris_backend/feeds/views.py | 27 +++- chris_backend/filebrowser/serializers.py | 135 +++++++++++++++-- .../filebrowser/tests/test_services.py | 8 +- chris_backend/filebrowser/tests/test_views.py | 6 +- chris_backend/filebrowser/views.py | 138 ++++++++++++++---- .../pacsfiles/tests/test_serializers.py | 8 +- chris_backend/pacsfiles/tests/test_views.py | 7 +- .../pipelines/tests/test_serializers.py | 5 +- chris_backend/pipelines/tests/test_views.py | 5 +- .../plugininstances/tests/test_manager.py | 5 +- .../plugininstances/tests/test_models.py | 5 +- .../plugininstances/tests/test_serializers.py | 5 +- .../plugininstances/tests/test_tasks.py | 5 +- .../plugininstances/tests/test_views.py | 9 +- .../userfiles/tests/test_serializers.py | 7 +- chris_backend/userfiles/tests/test_views.py | 7 +- chris_backend/users/tests/test_serializers.py | 10 +- chris_backend/users/tests/test_views.py | 6 +- .../workflows/tests/test_serializers.py | 5 +- chris_backend/workflows/tests/test_views.py | 5 +- 29 files changed, 337 insertions(+), 129 deletions(-) diff --git a/chris_backend/config/settings/local.py b/chris_backend/config/settings/local.py index b96abe76..29ce2c00 100755 --- a/chris_backend/config/settings/local.py +++ b/chris_backend/config/settings/local.py @@ -24,6 +24,9 @@ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'w1kxu^l=@pnsf!5piqz6!!5kdcdpo79y6jebbp+2244yjm*#+k' +# Superuser settings +CHRIS_SUPERUSER_PASSWORD = 'chris1234' + # Hosts/domain names that are valid for this site # See https://docs.djangoproject.com/en/4.2/ref/settings/#allowed-hosts ALLOWED_HOSTS = ['*'] diff --git a/chris_backend/config/settings/production.py b/chris_backend/config/settings/production.py index 4eb33eaa..94f04bcc 100755 --- a/chris_backend/config/settings/production.py +++ b/chris_backend/config/settings/production.py @@ -35,6 +35,10 @@ def get_secret(setting, secret_type=env): SECRET_KEY = get_secret('DJANGO_SECRET_KEY') +# SUPERUSER SETTINGS +CHRIS_SUPERUSER_PASSWORD = get_secret('CHRIS_SUPERUSER_PASSWORD') + + # SITE CONFIGURATION # ------------------------------------------------------------------------------ # Hosts/domain names that are valid for this site diff --git a/chris_backend/core/api.py b/chris_backend/core/api.py index 964baa9a..22132cc4 100755 --- a/chris_backend/core/api.py +++ b/chris_backend/core/api.py @@ -408,7 +408,7 @@ path('v1/filebrowser//grouppermissions/search/', filebrowser_views.FileBrowserFolderGroupPermissionListQuerySearch.as_view(), - name='chrisfoldergrouppermission-list-query-search'), + name='foldergrouppermission-list-query-search'), path('v1/filebrowser/grouppermissions//', filebrowser_views.FileBrowserFolderGroupPermissionDetail.as_view(), diff --git a/chris_backend/core/apps.py b/chris_backend/core/apps.py index 0ebf8d64..41304661 100755 --- a/chris_backend/core/apps.py +++ b/chris_backend/core/apps.py @@ -5,6 +5,7 @@ def setup_chris(sender, **kwargs): from django.contrib.auth.models import User, Group + from django.conf import settings from .models import ChrisInstance, ChrisFolder ChrisInstance.load() # create the ChRIS instance singleton @@ -14,7 +15,7 @@ def setup_chris(sender, **kwargs): chris_user = User.objects.get(username='chris') except User.DoesNotExist: chris_user = User.objects.create_superuser('chris', 'dev@babymri.org', - 'chris1234') + settings.CHRIS_SUPERUSER_PASSWORD) # create required groups (all_grp, _) = Group.objects.get_or_create(name='all_users') (pacs_grp, _) = Group.objects.get_or_create(name='pacs_users') diff --git a/chris_backend/core/models.py b/chris_backend/core/models.py index 7be95315..f450dd11 100755 --- a/chris_backend/core/models.py +++ b/chris_backend/core/models.py @@ -280,12 +280,12 @@ def _update_public_access(self, public_tf): folder.public = public_tf ChrisFolder.objects.bulk_update(folders, ['public']) - files = list(ChrisFile.objects.filter(path__startswith=path)) + files = list(ChrisFile.objects.filter(fname__startswith=path)) for f in files: f.public = public_tf ChrisFile.objects.bulk_update(files, ['public']) - link_files = list(ChrisLinkFile.objects.filter(path__startswith=path)) + link_files = list(ChrisLinkFile.objects.filter(fname__startswith=path)) for lf in link_files: lf.public = public_tf ChrisLinkFile.objects.bulk_update(link_files, ['public']) @@ -410,18 +410,18 @@ def save(self, *args, **kwargs): objs.append(perm) FolderUserPermission.objects.bulk_create(objs, update_conflicts=True, update_fields=['permission'], - unique_fields=['folder_id', 'group_id']) + unique_fields=['folder_id', 'user_id']) - files = ChrisFile.objects.filter(path__startswith=path) + files = ChrisFile.objects.filter(fname__startswith=path) objs = [] for f in files: perm = FileUserPermission(file=f, user=user, permission=permission) objs.append(perm) FileUserPermission.objects.bulk_create(objs, update_conflicts=True, update_fields=['permission'], - unique_fields=['file_id', 'group_id']) + unique_fields=['file_id', 'user_id']) - link_files = ChrisLinkFile.objects.filter(path__startswith=path) + link_files = ChrisLinkFile.objects.filter(fname__startswith=path) objs = [] for lf in link_files: perm = LinkFileUserPermission(link_file=lf, user=user, permission=permission) @@ -429,7 +429,7 @@ def save(self, *args, **kwargs): LinkFileUserPermission.objects.bulk_create(objs, update_conflicts=True, update_fields=['permission'], unique_fields=['link_file_id', - 'group_id']) + 'user_id']) def delete(self, *args, **kwargs): """ @@ -769,7 +769,7 @@ def grant_group_permission(self, group, permission): """ Custom method to grant a group a permission to access the link file. """ - LinkFileGroupPermission.objects.update_or_create(file=self, group=group, + LinkFileGroupPermission.objects.update_or_create(link_file=self, group=group, defaults={'permission': permission}) def remove_group_permission(self, group, permission): @@ -777,7 +777,7 @@ def remove_group_permission(self, group, permission): Custom method to remove a group's permission to access the link file. """ try: - perm = LinkFileGroupPermission.objects.get(file=self, group=group, + perm = LinkFileGroupPermission.objects.get(link_file=self, group=group, permission=permission) except LinkFileGroupPermission.DoesNotExist: pass @@ -788,7 +788,7 @@ def grant_user_permission(self, user, permission): """ Custom method to grant a user a permission to access the link file. """ - LinkFileUserPermission.objects.update_or_create(file=self, user=user, + LinkFileUserPermission.objects.update_or_create(link_file=self, user=user, defaults={'permission': permission}) def remove_user_permission(self, user, permission): @@ -796,7 +796,7 @@ def remove_user_permission(self, user, permission): Custom method to remove a user's permission to access the link file. """ try: - perm = LinkFileUserPermission.objects.get(file=self, user=user, + perm = LinkFileUserPermission.objects.get(link_file=self, user=user, permission=permission) except LinkFileUserPermission.DoesNotExist: pass diff --git a/chris_backend/core/tests/test_views.py b/chris_backend/core/tests/test_views.py index 1b5b50ac..a95f1c92 100755 --- a/chris_backend/core/tests/test_views.py +++ b/chris_backend/core/tests/test_views.py @@ -1,21 +1,20 @@ import logging -import json from django.test import TestCase, tag from django.contrib.auth.models import User from django.urls import reverse -from django.conf import settings from django.utils import timezone +from django.conf import settings from rest_framework import status import jwt -from userfiles.models import UserFile from core.models import FileDownloadToken -from core.storage.helpers import mock_storage, connect_storage +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD + class CoreViewTests(TestCase): """ @@ -28,9 +27,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.content_type = 'application/vnd.collection+json' self.username = 'cube' diff --git a/chris_backend/feeds/tests/test_models.py b/chris_backend/feeds/tests/test_models.py index 83a708f9..1f57ecec 100755 --- a/chris_backend/feeds/tests/test_models.py +++ b/chris_backend/feeds/tests/test_models.py @@ -11,6 +11,7 @@ COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD class FeedModelTests(TestCase): @@ -21,8 +22,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.feed_name = "Feed1" self.plugin_name = "pacspull" diff --git a/chris_backend/feeds/tests/test_serializers.py b/chris_backend/feeds/tests/test_serializers.py index 5f37d88e..7be0a971 100755 --- a/chris_backend/feeds/tests/test_serializers.py +++ b/chris_backend/feeds/tests/test_serializers.py @@ -14,6 +14,7 @@ COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD class SerializerTests(TestCase): @@ -24,9 +25,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.username = 'foo' self.password = 'bar' diff --git a/chris_backend/feeds/tests/test_views.py b/chris_backend/feeds/tests/test_views.py index 96072728..87fc1097 100755 --- a/chris_backend/feeds/tests/test_views.py +++ b/chris_backend/feeds/tests/test_views.py @@ -14,6 +14,7 @@ COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD class ViewTests(TestCase): @@ -24,8 +25,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.content_type='application/vnd.collection+json' diff --git a/chris_backend/feeds/views.py b/chris_backend/feeds/views.py index dda29015..7ff0b0bb 100755 --- a/chris_backend/feeds/views.py +++ b/chris_backend/feeds/views.py @@ -445,15 +445,21 @@ def perform_create(self, serializer): def list(self, request, *args, **kwargs): """ Overriden to return a list of the group permissions for the queried feed. - Document-level link relations and a collection+json template are also added - to the response. + A query list, document-level link relations and a collection+json template are + also added to the response. """ queryset = self.get_group_permissions_queryset() response = services.get_list_response(self, queryset) feed = self.get_object() + + query_list = [reverse('feedgrouppermission-list-query-search', + request=request, kwargs={"pk": feed.id})] + response = services.append_collection_querylist(response, query_list) + links = {'feed': reverse('feed-detail', request=request, kwargs={"pk": feed.id})} response = services.append_collection_links(response, links) + template_data = {"grp_name": ""} return services.append_collection_template(response, template_data) @@ -534,15 +540,21 @@ def perform_create(self, serializer): def list(self, request, *args, **kwargs): """ Overriden to return a list of the user permissions for the queried feed. - Document-level link relations and a collection+json template are also added - to the response. + A query list, document-level link relations and a collection+json template are + also added to the response. """ queryset = self.get_user_permissions_queryset() response = services.get_list_response(self, queryset) feed = self.get_object() + + query_list = [reverse('feeduserpermission-list-query-search', + request=request, kwargs={"pk": feed.id})] + response = services.append_collection_querylist(response, query_list) + links = {'feed': reverse('feed-detail', request=request, kwargs={"pk": feed.id})} response = services.append_collection_links(response, links) + template_data = {"username": ""} return services.append_collection_template(response, template_data) @@ -622,20 +634,23 @@ def perform_create(self, serializer): def list(self, request, *args, **kwargs): """ Overriden to return a list of the comments for the queried feed. - A collection+json write template and document-level link relation are also - added to the response. + A query list, collection+json write template and document-level link relation + are also added to the response. """ queryset = self.get_comments_queryset() response = services.get_list_response(self, queryset) feed = self.get_object() + # append query list query_list = [reverse('comment-list-query-search', request=request, kwargs={"pk": feed.id})] response = services.append_collection_querylist(response, query_list) + # append document-level link relations links = {'feed': reverse('feed-detail', request=request, kwargs={"pk": feed.id})} response = services.append_collection_links(response, links) + # append write template template_data = {"title": "", "content": ""} return services.append_collection_template(response, template_data) diff --git a/chris_backend/filebrowser/serializers.py b/chris_backend/filebrowser/serializers.py index a85c1187..fd825cb2 100755 --- a/chris_backend/filebrowser/serializers.py +++ b/chris_backend/filebrowser/serializers.py @@ -39,10 +39,7 @@ def create(self, validated_data): """ Overriden to set the parent folder. """ - path = validated_data.get('path') - if path is None: - raise serializers.ValidationError({'path': ['This field is required.']}) - + path = validated_data['path'] parent_path = os.path.dirname(path) owner = validated_data['owner'] @@ -120,20 +117,27 @@ def validate_public(self, public): def validate(self, data): """ - Overriden to validate that the 'public' field is in data when updating a folder. + Overriden to validate that required fields are in data when creating or + updating a folder. """ if self.instance: # on update if 'public' not in data: raise serializers.ValidationError({'public': ['This field is required.']}) + else: + if 'path' not in data: # on create + raise serializers.ValidationError({'path': ['This field is required.']}) return data class FileBrowserFolderGroupPermissionSerializer(serializers.HyperlinkedModelSerializer): - grp_name = serializers.CharField(write_only=True) + grp_name = serializers.CharField(write_only=True, required=False) folder_id = serializers.ReadOnlyField(source='folder.id') folder_name = serializers.ReadOnlyField(source='folder.name') group_id = serializers.ReadOnlyField(source='group.id') group_name = serializers.ReadOnlyField(source='group.name') + folder = serializers.HyperlinkedRelatedField(view_name='chrisfolder-detail', + read_only=True) + group = serializers.HyperlinkedRelatedField(view_name='group-detail', read_only=True) class Meta: model = FolderGroupPermission @@ -173,12 +177,32 @@ def validate_grp_name(self, grp_name): {'grp_name': [f"Couldn't find any group with name '{grp_name}'."]}) return group + def validate(self, data): + """ + Overriden to validate that required fields are in data when creating or + updating a permission. + """ + if self.instance: # on update + if 'permission' not in data: + raise serializers.ValidationError({'permission': + ['This field is required.']}) + else: + if 'grp_name' not in data: # on create + raise serializers.ValidationError({'grp_name': + ['This field is required.']}) + return data + + class FileBrowserFolderUserPermissionSerializer(serializers.HyperlinkedModelSerializer): - username = serializers.CharField(write_only=True, min_length=4, max_length=32) + username = serializers.CharField(write_only=True, min_length=4, max_length=32, + required=False) folder_id = serializers.ReadOnlyField(source='folder.id') folder_name = serializers.ReadOnlyField(source='folder.name') user_id = serializers.ReadOnlyField(source='user.id') user_username = serializers.ReadOnlyField(source='user.username') + folder = serializers.HyperlinkedRelatedField(view_name='chrisfolder-detail', + read_only=True) + user = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True) class Meta: model = FolderUserPermission @@ -218,6 +242,21 @@ def validate_username(self, username): {'username': [f"Couldn't find any user with username '{username}'."]}) return user + def validate(self, data): + """ + Overriden to validate that required fields are in data when creating or + updating a permission. + """ + if self.instance: # on update + if 'permission' not in data: + raise serializers.ValidationError({'permission': + ['This field is required.']}) + else: + if 'username' not in data: # on create + raise serializers.ValidationError({'username': + ['This field is required.']}) + return data + class FileBrowserFileSerializer(serializers.HyperlinkedModelSerializer): new_file_path = serializers.CharField(max_length=1024, write_only=True, @@ -339,11 +378,14 @@ def validate(self, data): class FileBrowserFileGroupPermissionSerializer(serializers.HyperlinkedModelSerializer): - grp_name = serializers.CharField(write_only=True) + grp_name = serializers.CharField(write_only=True, required=False) file_id = serializers.ReadOnlyField(source='file.id') file_fname = serializers.ReadOnlyField(source='file.fname') group_id = serializers.ReadOnlyField(source='group.id') group_name = serializers.ReadOnlyField(source='group.name') + file = serializers.HyperlinkedRelatedField(view_name='chrisfile-detail', + read_only=True) + group = serializers.HyperlinkedRelatedField(view_name='group-detail', read_only=True) class Meta: model = FileGroupPermission @@ -383,13 +425,32 @@ def validate_grp_name(self, grp_name): {'grp_name': [f"Couldn't find any group with name '{grp_name}'."]}) return group + def validate(self, data): + """ + Overriden to validate that required fields are in data when creating or + updating a permission. + """ + if self.instance: # on update + if 'permission' not in data: + raise serializers.ValidationError({'permission': + ['This field is required.']}) + else: + if 'grp_name' not in data: # on create + raise serializers.ValidationError({'grp_name': + ['This field is required.']}) + return data + class FileBrowserFileUserPermissionSerializer(serializers.HyperlinkedModelSerializer): - username = serializers.CharField(write_only=True, min_length=4, max_length=32) + username = serializers.CharField(write_only=True, min_length=4, max_length=32, + required=False) file_id = serializers.ReadOnlyField(source='file.id') file_fname = serializers.ReadOnlyField(source='file.fname') user_id = serializers.ReadOnlyField(source='user.id') user_username = serializers.ReadOnlyField(source='user.username') + file = serializers.HyperlinkedRelatedField(view_name='chrisfile-detail', + read_only=True) + user = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True) class Meta: model = FileUserPermission @@ -429,6 +490,21 @@ def validate_username(self, username): {'username': [f"Couldn't find any user with username '{username}'."]}) return user + def validate(self, data): + """ + Overriden to validate that required fields are in data when creating or + updating a permission. + """ + if self.instance: # on update + if 'permission' not in data: + raise serializers.ValidationError({'permission': + ['This field is required.']}) + else: + if 'username' not in data: # on create + raise serializers.ValidationError({'username': + ['This field is required.']}) + return data + class FileBrowserLinkFileSerializer(serializers.HyperlinkedModelSerializer): new_link_file_path = serializers.CharField(max_length=1024, write_only=True, @@ -590,11 +666,14 @@ def validate(self, data): class FileBrowserLinkFileGroupPermissionSerializer(serializers.HyperlinkedModelSerializer): - grp_name = serializers.CharField(write_only=True) + grp_name = serializers.CharField(write_only=True, required=False) link_file_id = serializers.ReadOnlyField(source='link_file.id') link_file_fname = serializers.ReadOnlyField(source='link_file.fname') group_id = serializers.ReadOnlyField(source='group.id') group_name = serializers.ReadOnlyField(source='group.name') + link_file = serializers.HyperlinkedRelatedField(view_name='chrislinkfile-detail', + read_only=True) + group = serializers.HyperlinkedRelatedField(view_name='group-detail', read_only=True) class Meta: model = FileGroupPermission @@ -634,13 +713,32 @@ def validate_grp_name(self, grp_name): {'grp_name': [f"Couldn't find any group with name '{grp_name}'."]}) return group + def validate(self, data): + """ + Overriden to validate that required fields are in data when creating or + updating a permission. + """ + if self.instance: # on update + if 'permission' not in data: + raise serializers.ValidationError({'permission': + ['This field is required.']}) + else: + if 'grp_name' not in data: # on create + raise serializers.ValidationError({'grp_name': + ['This field is required.']}) + return data + class FileBrowserLinkFileUserPermissionSerializer(serializers.HyperlinkedModelSerializer): - username = serializers.CharField(write_only=True, min_length=4, max_length=32) + username = serializers.CharField(write_only=True, min_length=4, max_length=32, + required=False) link_file_id = serializers.ReadOnlyField(source='link_file.id') link_file_fname = serializers.ReadOnlyField(source='link_file.fname') user_id = serializers.ReadOnlyField(source='user.id') user_username = serializers.ReadOnlyField(source='user.username') + link_file = serializers.HyperlinkedRelatedField(view_name='chrislinkfile-detail', + read_only=True) + user = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True) class Meta: model = FileUserPermission @@ -679,3 +777,18 @@ def validate_username(self, username): raise serializers.ValidationError( {'username': [f"Couldn't find any user with username '{username}'."]}) return user + + def validate(self, data): + """ + Overriden to validate that required fields are in data when creating or + updating a permission. + """ + if self.instance: # on update + if 'permission' not in data: + raise serializers.ValidationError({'permission': + ['This field is required.']}) + else: + if 'username' not in data: # on create + raise serializers.ValidationError({'username': + ['This field is required.']}) + return data diff --git a/chris_backend/filebrowser/tests/test_services.py b/chris_backend/filebrowser/tests/test_services.py index 8928c493..90fb3b32 100755 --- a/chris_backend/filebrowser/tests/test_services.py +++ b/chris_backend/filebrowser/tests/test_services.py @@ -4,12 +4,16 @@ from django.test import TestCase from django.contrib.auth.models import User +from django.conf import settings from core.models import ChrisFolder from userfiles.models import UserFile from filebrowser import services +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD + + class ServiceTests(TestCase): """ Test top-level functions in the services module @@ -21,9 +25,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD # create users diff --git a/chris_backend/filebrowser/tests/test_views.py b/chris_backend/filebrowser/tests/test_views.py index cb4bcfc9..06968921 100755 --- a/chris_backend/filebrowser/tests/test_views.py +++ b/chris_backend/filebrowser/tests/test_views.py @@ -19,6 +19,7 @@ COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD class FileBrowserViewTests(TestCase): @@ -32,9 +33,8 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - chris_user = User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD + chris_user = User.objects.get(username=self.chris_username) self.content_type = 'application/vnd.collection+json' self.username = 'foo' diff --git a/chris_backend/filebrowser/views.py b/chris_backend/filebrowser/views.py index 2e2a13cc..ba4e4c57 100755 --- a/chris_backend/filebrowser/views.py +++ b/chris_backend/filebrowser/views.py @@ -185,25 +185,29 @@ def perform_create(self, serializer): """ Overriden to provide a group and folder before first saving to the DB. """ - group = serializer.validated_data.pop('name') + group = serializer.validated_data.pop('grp_name') folder = self.get_object() - serializer.save(user=group, folder=folder) + serializer.save(group=group, folder=folder) def list(self, request, *args, **kwargs): """ Overriden to return a list of the group permissions for the queried folder. - Document-level link relations and a collection+json template are also added - to the response. + A query list, document-level link relations and a collection+json template are + also added to the response. """ queryset = self.get_group_permissions_queryset() response = services.get_list_response(self, queryset) folder = self.get_object() + query_list = [reverse('foldergrouppermission-list-query-search', + request=request, kwargs={"pk": folder.id})] + response = services.append_collection_querylist(response, query_list) + links = {'folder': reverse('chrisfolder-detail', request=request, kwargs={"pk": folder.id})} - response = services.append_collection_links(response, links) - template_data = {"grp_name": ""} + + template_data = {"grp_name": "", "permission": ""} return services.append_collection_template(response, template_data) def get_group_permissions_queryset(self): @@ -253,6 +257,15 @@ def retrieve(self, request, *args, **kwargs): template_data = {"permission": ""} return services.append_collection_template(response, template_data) + def update(self, request, *args, **kwargs): + """ + Overriden to remove 'grp_name' if provided by the user before serializer + validation. + """ + request.data.pop('grp_name', None) # shoud not change on update + return super(FileBrowserFolderGroupPermissionDetail, self).update(request, + *args, **kwargs) + def perform_destroy(self, instance): """ Overriden to remove the group permission for the link file in the SHARED folder @@ -292,18 +305,22 @@ def perform_create(self, serializer): def list(self, request, *args, **kwargs): """ Overriden to return a list of the user permissions for the queried folder. - Document-level link relations and a collection+json template are also added - to the response. + A query list, document-level link relations and a collection+json template are + also added to the response. """ queryset = self.get_user_permissions_queryset() response = services.get_list_response(self, queryset) folder = self.get_object() + query_list = [reverse('folderuserpermission-list-query-search', + request=request, kwargs={"pk": folder.id})] + response = services.append_collection_querylist(response, query_list) + links = {'folder': reverse('chrisfolder-detail', request=request, kwargs={"pk": folder.id})} - response = services.append_collection_links(response, links) - template_data = {"username": ""} + + template_data = {"username": "", "permission": ""} return services.append_collection_template(response, template_data) def get_user_permissions_queryset(self): @@ -353,6 +370,15 @@ def retrieve(self, request, *args, **kwargs): template_data = {"permission": ""} return services.append_collection_template(response, template_data) + def update(self, request, *args, **kwargs): + """ + Overriden to remove 'username' if provided by the user before serializer + validation. + """ + request.data.pop('username', None) # shoud not change on update + return super(FileBrowserFolderUserPermissionDetail, self).update(request, + *args, **kwargs) + def perform_destroy(self, instance): """ Overriden to remove the user permission for the link file in the SHARED folder @@ -462,25 +488,29 @@ def perform_create(self, serializer): """ Overriden to provide a group and file before first saving to the DB. """ - group = serializer.validated_data.pop('name') + group = serializer.validated_data.pop('grp_name') f = self.get_object() - serializer.save(user=group, file=f) + serializer.save(group=group, file=f) def list(self, request, *args, **kwargs): """ Overriden to return a list of the group permissions for the queried file. - Document-level link relations and a collection+json template are also added - to the response. + A query list, document-level link relations and a collection+json template are + also added to the response. """ queryset = self.get_group_permissions_queryset() response = services.get_list_response(self, queryset) f = self.get_object() + query_list = [reverse('filegrouppermission-list-query-search', + request=request, kwargs={"pk": f.id})] + response = services.append_collection_querylist(response, query_list) + links = {'file': reverse('chrisfile-detail', request=request, kwargs={"pk": f.id})} - response = services.append_collection_links(response, links) - template_data = {"grp_name": ""} + + template_data = {"grp_name": "", "permission": ""} return services.append_collection_template(response, template_data) def get_group_permissions_queryset(self): @@ -530,6 +560,15 @@ def retrieve(self, request, *args, **kwargs): template_data = {"permission": ""} return services.append_collection_template(response, template_data) + def update(self, request, *args, **kwargs): + """ + Overriden to remove 'grp_name' if provided by the user before serializer + validation. + """ + request.data.pop('grp_name', None) # shoud not change on update + return super(FileBrowserFileGroupPermissionDetail, self).update(request, + *args, **kwargs) + def perform_destroy(self, instance): """ Overriden to remove the group permission for the link file in the SHARED folder @@ -569,18 +608,22 @@ def perform_create(self, serializer): def list(self, request, *args, **kwargs): """ Overriden to return a list of the user permissions for the queried file. - Document-level link relations and a collection+json template are also added - to the response. + A query list, document-level link relations and a collection+json template are + also added to the response. """ queryset = self.get_user_permissions_queryset() response = services.get_list_response(self, queryset) f = self.get_object() + query_list = [reverse('fileuserpermission-list-query-search', + request=request, kwargs={"pk": f.id})] + response = services.append_collection_querylist(response, query_list) + links = {'file': reverse('chrisfile-detail', request=request, kwargs={"pk": f.id})} - response = services.append_collection_links(response, links) - template_data = {"username": ""} + + template_data = {"username": "" , "permission": ""} return services.append_collection_template(response, template_data) def get_user_permissions_queryset(self): @@ -630,6 +673,15 @@ def retrieve(self, request, *args, **kwargs): template_data = {"permission": ""} return services.append_collection_template(response, template_data) + def update(self, request, *args, **kwargs): + """ + Overriden to remove 'username' if provided by the user before serializer + validation. + """ + request.data.pop('username', None) # shoud not change on update + return super(FileBrowserFileUserPermissionDetail, self).update(request, + *args, **kwargs) + def perform_destroy(self, instance): """ Overriden to remove the user permission for the link file in the SHARED folder @@ -740,25 +792,29 @@ def perform_create(self, serializer): """ Overriden to provide a group and link file before first saving to the DB. """ - group = serializer.validated_data.pop('name') + group = serializer.validated_data.pop('grp_name') lf = self.get_object() - serializer.save(user=group, link_file=lf) + serializer.save(group=group, link_file=lf) def list(self, request, *args, **kwargs): """ Overriden to return a list of the group permissions for the queried link file. - Document-level link relations and a collection+json template are also added - to the response. + A query list, document-level link relations and a collection+json template are + also added to the response. """ queryset = self.get_group_permissions_queryset() response = services.get_list_response(self, queryset) lf = self.get_object() + query_list = [reverse('linkfilegrouppermission-list-query-search', + request=request, kwargs={"pk": lf.id})] + response = services.append_collection_querylist(response, query_list) + links = {'link_file': reverse('chrislinkfile-detail', request=request, kwargs={"pk": lf.id})} - response = services.append_collection_links(response, links) - template_data = {"grp_name": ""} + + template_data = {"grp_name": "", "permission": ""} return services.append_collection_template(response, template_data) def get_group_permissions_queryset(self): @@ -808,6 +864,15 @@ def retrieve(self, request, *args, **kwargs): template_data = {"permission": ""} return services.append_collection_template(response, template_data) + def update(self, request, *args, **kwargs): + """ + Overriden to remove 'grp_name' if provided by the user before serializer + validation. + """ + request.data.pop('grp_name', None) # shoud not change on update + return super(FileBrowserLinkFileGroupPermissionDetail, self).update(request, + *args, **kwargs) + def perform_destroy(self, instance): """ Overriden to remove the group permission for the link file in the SHARED folder @@ -847,16 +912,22 @@ def perform_create(self, serializer): def list(self, request, *args, **kwargs): """ Overriden to return a list of the user permissions for the queried link file. - Document-level link relations and a collection+json template are also added - to the response. + A query list, document-level link relations and a collection+json template are + also added to the response. """ queryset = self.get_user_permissions_queryset() response = services.get_list_response(self, queryset) lf = self.get_object() + + query_list = [reverse('linkfileuserpermission-list-query-search', + request=request, kwargs={"pk": lf.id})] + response = services.append_collection_querylist(response, query_list) + links = {'link_file': reverse('chrislinkfile-detail', request=request, kwargs={"pk": lf.id})} response = services.append_collection_links(response, links) - template_data = {"username": ""} + + template_data = {"username": "", "permission": ""} return services.append_collection_template(response, template_data) def get_user_permissions_queryset(self): @@ -906,6 +977,15 @@ def retrieve(self, request, *args, **kwargs): template_data = {"permission": ""} return services.append_collection_template(response, template_data) + def update(self, request, *args, **kwargs): + """ + Overriden to remove 'username' if provided by the user before serializer + validation. + """ + request.data.pop('username', None) # shoud not change on update + return super(FileBrowserLinkFileUserPermissionDetail, self).update(request, + *args, **kwargs) + def perform_destroy(self, instance): """ Overriden to remove the user permission for the link file in the SHARED folder diff --git a/chris_backend/pacsfiles/tests/test_serializers.py b/chris_backend/pacsfiles/tests/test_serializers.py index c76bf55a..80985d5e 100755 --- a/chris_backend/pacsfiles/tests/test_serializers.py +++ b/chris_backend/pacsfiles/tests/test_serializers.py @@ -3,12 +3,16 @@ from django.contrib.auth.models import User from django.test import TestCase, tag +from django.conf import settings from unittest import mock from rest_framework import serializers from pacsfiles.serializers import PACSSeriesSerializer +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD + + class PACSSeriesSerializerTests(TestCase): def setUp(self): @@ -17,9 +21,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD def tearDown(self): # re-enable logging diff --git a/chris_backend/pacsfiles/tests/test_views.py b/chris_backend/pacsfiles/tests/test_views.py index 00961137..6cf8d65f 100755 --- a/chris_backend/pacsfiles/tests/test_views.py +++ b/chris_backend/pacsfiles/tests/test_views.py @@ -17,6 +17,9 @@ from pacsfiles import views +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD + + class PACSViewTests(TestCase): """ Generic pacs series view tests' setup and tearDown. @@ -28,9 +31,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.content_type = 'application/vnd.collection+json' self.username = 'test' diff --git a/chris_backend/pipelines/tests/test_serializers.py b/chris_backend/pipelines/tests/test_serializers.py index 4363c438..3993317c 100755 --- a/chris_backend/pipelines/tests/test_serializers.py +++ b/chris_backend/pipelines/tests/test_serializers.py @@ -20,6 +20,7 @@ COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD class SerializerTests(TestCase): @@ -30,9 +31,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.plugin_fs_name = "simplefsapp" self.plugin_fs_parameters = {'dir': {'type': 'string', 'optional': True, diff --git a/chris_backend/pipelines/tests/test_views.py b/chris_backend/pipelines/tests/test_views.py index 5597d50e..ac907b3a 100755 --- a/chris_backend/pipelines/tests/test_views.py +++ b/chris_backend/pipelines/tests/test_views.py @@ -20,6 +20,7 @@ COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD class ViewTests(TestCase): @@ -30,9 +31,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.content_type = 'application/vnd.collection+json' diff --git a/chris_backend/plugininstances/tests/test_manager.py b/chris_backend/plugininstances/tests/test_manager.py index e78ff4a8..ce2a1e72 100755 --- a/chris_backend/plugininstances/tests/test_manager.py +++ b/chris_backend/plugininstances/tests/test_manager.py @@ -19,6 +19,7 @@ COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD class PluginInstanceManagerTests(TestCase): @@ -29,9 +30,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.storage_manager = connect_storage(settings) diff --git a/chris_backend/plugininstances/tests/test_models.py b/chris_backend/plugininstances/tests/test_models.py index 0eb8df0a..0305e9b8 100755 --- a/chris_backend/plugininstances/tests/test_models.py +++ b/chris_backend/plugininstances/tests/test_models.py @@ -17,6 +17,7 @@ COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD class ModelTests(TestCase): @@ -27,9 +28,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.plugin_fs_name = "simplecopyapp" self.plugin_fs_parameters = {'dir': {'type': 'string', 'optional': True, diff --git a/chris_backend/plugininstances/tests/test_serializers.py b/chris_backend/plugininstances/tests/test_serializers.py index d83c6f12..c0008998 100755 --- a/chris_backend/plugininstances/tests/test_serializers.py +++ b/chris_backend/plugininstances/tests/test_serializers.py @@ -16,6 +16,7 @@ COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD class SerializerTests(TestCase): @@ -26,9 +27,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.username = 'foo' self.password = 'foopassword' diff --git a/chris_backend/plugininstances/tests/test_tasks.py b/chris_backend/plugininstances/tests/test_tasks.py index 480f4a6a..227b37fd 100755 --- a/chris_backend/plugininstances/tests/test_tasks.py +++ b/chris_backend/plugininstances/tests/test_tasks.py @@ -13,6 +13,7 @@ COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD class TasksTests(TestCase): @@ -23,9 +24,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.username = 'foo' self.password = 'bar' diff --git a/chris_backend/plugininstances/tests/test_views.py b/chris_backend/plugininstances/tests/test_views.py index 994c7d6b..d86733bb 100755 --- a/chris_backend/plugininstances/tests/test_views.py +++ b/chris_backend/plugininstances/tests/test_views.py @@ -28,6 +28,7 @@ COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD class ViewTests(TestCase): @@ -38,9 +39,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.username = 'foo' self.password = 'bar' @@ -109,9 +108,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.storage_manager = connect_storage(settings) self.username = 'foo' diff --git a/chris_backend/userfiles/tests/test_serializers.py b/chris_backend/userfiles/tests/test_serializers.py index 024b3531..ed6b4b92 100755 --- a/chris_backend/userfiles/tests/test_serializers.py +++ b/chris_backend/userfiles/tests/test_serializers.py @@ -14,6 +14,9 @@ from userfiles.serializers import UserFileSerializer +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD + + class UserFileSerializerTests(TestCase): def setUp(self): @@ -22,9 +25,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.username = 'test' self.password = 'testpass' diff --git a/chris_backend/userfiles/tests/test_views.py b/chris_backend/userfiles/tests/test_views.py index bf1d8283..23430c52 100755 --- a/chris_backend/userfiles/tests/test_views.py +++ b/chris_backend/userfiles/tests/test_views.py @@ -20,6 +20,9 @@ from userfiles import views +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD + + class UserFileViewTests(TestCase): """ Generic userfile view tests' setup and tearDown. @@ -31,9 +34,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.content_type = 'application/vnd.collection+json' diff --git a/chris_backend/users/tests/test_serializers.py b/chris_backend/users/tests/test_serializers.py index 678be04e..917bf1ab 100755 --- a/chris_backend/users/tests/test_serializers.py +++ b/chris_backend/users/tests/test_serializers.py @@ -2,8 +2,7 @@ import logging from django.test import TestCase -from django.contrib.auth.models import User - +from django.conf import settings from rest_framework import serializers from userfiles.models import UserFile @@ -11,6 +10,9 @@ from core.storage.helpers import mock_storage +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD + + class SerializerTests(TestCase): """ Generic serializers tests' setup and tearDown. @@ -22,9 +24,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.username = 'cube' self.password = 'cubepass' diff --git a/chris_backend/users/tests/test_views.py b/chris_backend/users/tests/test_views.py index e9f84bd7..1d4073d9 100755 --- a/chris_backend/users/tests/test_views.py +++ b/chris_backend/users/tests/test_views.py @@ -13,6 +13,8 @@ from core.storage.helpers import mock_storage, connect_storage +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD + class ViewTests(TestCase): """ @@ -25,9 +27,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password, is_staff=True) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.content_type = 'application/vnd.collection+json' self.username = 'cube' diff --git a/chris_backend/workflows/tests/test_serializers.py b/chris_backend/workflows/tests/test_serializers.py index 681337ed..8b72cff4 100755 --- a/chris_backend/workflows/tests/test_serializers.py +++ b/chris_backend/workflows/tests/test_serializers.py @@ -19,6 +19,7 @@ COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD class SerializerTests(TestCase): @@ -29,9 +30,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.plugin_fs_name = "simplefsapp" self.plugin_fs_parameters = {'dir': {'type': 'string', 'optional': True, diff --git a/chris_backend/workflows/tests/test_views.py b/chris_backend/workflows/tests/test_views.py index da9e4b0b..55ff2c84 100755 --- a/chris_backend/workflows/tests/test_views.py +++ b/chris_backend/workflows/tests/test_views.py @@ -18,6 +18,7 @@ from workflows.models import Workflow COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD class ViewTests(TestCase): @@ -28,9 +29,7 @@ def setUp(self): # create superuser chris (owner of root folders) self.chris_username = 'chris' - self.chris_password = 'chris1234' - User.objects.create_user(username=self.chris_username, - password=self.chris_password) + self.chris_password = CHRIS_SUPERUSER_PASSWORD self.content_type = 'application/vnd.collection+json' From e91506c4ec7435d6f9ce2d330c66f4a6b36b295b Mon Sep 17 00:00:00 2001 From: Jorge Date: Fri, 14 Jun 2024 19:53:41 -0400 Subject: [PATCH 05/11] Fix bugs and permissions --- chris_backend/feeds/models.py | 6 +- chris_backend/feeds/tests/test_models.py | 10 -- chris_backend/feeds/tests/test_serializers.py | 26 ---- chris_backend/feeds/tests/test_views.py | 89 +++--------- chris_backend/filebrowser/services.py | 1 + .../filebrowser/tests/test_services.py | 57 ++++---- chris_backend/filebrowser/tests/test_views.py | 112 ++++++--------- chris_backend/pacsfiles/models.py | 10 +- chris_backend/pacsfiles/tests/test_views.py | 6 +- chris_backend/pipelines/models.py | 5 +- chris_backend/plugininstances/models.py | 4 +- chris_backend/plugininstances/serializers.py | 89 +++++------- .../plugininstances/tests/test_models.py | 2 +- .../plugininstances/tests/test_serializers.py | 58 +++----- .../userfiles/tests/test_serializers.py | 24 ++-- chris_backend/users/permissions.py | 8 +- chris_backend/users/tests/test_models.py | 134 ++++++++++++++++++ chris_backend/users/tests/test_serializers.py | 16 +-- chris_backend/users/tests/test_views.py | 31 ++-- chris_backend/users/views.py | 2 +- 20 files changed, 342 insertions(+), 348 deletions(-) create mode 100755 chris_backend/users/tests/test_models.py diff --git a/chris_backend/feeds/models.py b/chris_backend/feeds/models.py index 8d7a5b26..71035011 100755 --- a/chris_backend/feeds/models.py +++ b/chris_backend/feeds/models.py @@ -2,7 +2,6 @@ from django.db import models from django.db.models.signals import post_delete from django.contrib.auth.models import User, Group -from django.db.models import Q from django.dispatch import receiver import django_filters @@ -130,7 +129,10 @@ def remove_public_access(self): @receiver(post_delete, sender=Feed) def auto_delete_folder_with_feed(sender, instance, **kwargs): - instance.folder.delete() + try: + instance.folder.delete() + except Exception: + pass class FeedFilter(FilterSet): diff --git a/chris_backend/feeds/tests/test_models.py b/chris_backend/feeds/tests/test_models.py index 1f57ecec..2fc6a111 100755 --- a/chris_backend/feeds/tests/test_models.py +++ b/chris_backend/feeds/tests/test_models.py @@ -69,16 +69,6 @@ def test_save_creates_new_note_just_after_feed_is_created(self): """ self.assertEqual(Note.objects.count(), 1) - def test_get_creator(self): - """ - Test whether custom get_creator method properly returns the user that created the - feed. - """ - user = User.objects.get(username=self.username) - feed = Feed.objects.get(name=self.feed_name) - feed_creator = feed.get_creator() - self.assertEqual(feed_creator, user) - def test_get_plugin_instances_status_count(self): """ Test whether custom get_plugin_instances_status_count method properly returns diff --git a/chris_backend/feeds/tests/test_serializers.py b/chris_backend/feeds/tests/test_serializers.py index 7be0a971..9bf169ff 100755 --- a/chris_backend/feeds/tests/test_serializers.py +++ b/chris_backend/feeds/tests/test_serializers.py @@ -105,11 +105,6 @@ def test_validate_tag(self): with self.assertRaises(serializers.ValidationError): tagging_serializer.validate_tag(tag.id + 1) # error if tag not found in DB - with self.assertRaises(serializers.ValidationError): - other_user = User.objects.get(username=self.other_username) - tagging_serializer.context['request'].user = other_user - tagging_serializer.validate_tag(tag.id) # error if users doesn't own tag - def test_validate_feed(self): """ Test whether custom validate_feed method returns a feed instance or @@ -154,27 +149,6 @@ def test_validate_name(self): name = self.feed_serializer.validate_name('myfeed') self.assertEqual(name, 'myfeed') - def test_validate_new_owner(self): - """ - Test whether custom validate_new_owner method returns a user instance - or raises a serializers.ValidationError when the proposed new owner is - not a system-registered user. - """ - new_owner = User.objects.get(username=self.other_username) - user_inst = self.feed_serializer.validate_new_owner(new_owner.username) - self.assertEqual(user_inst, new_owner) - - with self.assertRaises(serializers.ValidationError): - self.feed_serializer.validate_new_owner('not a registered user') - - def test_get_creator_username(self): - """ - Test whether overriden get_creator_username method returns the username of the - user that created the feed. - """ - creator_username = self.feed_serializer.get_creator_username(self.feed_serializer.instance) - self.assertEqual(creator_username, self.username) - def test_get_started_jobs(self): """ Test whether overriden get_created_jobs method returns the correct number of diff --git a/chris_backend/feeds/tests/test_views.py b/chris_backend/feeds/tests/test_views.py index 87fc1097..ce9e83db 100755 --- a/chris_backend/feeds/tests/test_views.py +++ b/chris_backend/feeds/tests/test_views.py @@ -205,9 +205,11 @@ def test_feed_list_query_search_success_chris_user_lists_all_matching_feeds(self response = self.client.get(self.list_url) self.assertContains(response, self.feedname) - def test_feed_list_query_search_failure_unauthenticated(self): + def test_feed_list_query_search_success_unauthenticated(self): response = self.client.get(self.list_url) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], []) + def test_feed_list_query_search_from_other_users_not_listed(self): self.client.login(username=self.other_username, password=self.other_password) @@ -227,7 +229,7 @@ def setUp(self): self.read_update_delete_url = reverse("feed-detail", kwargs={"pk": feed.id}) self.put = json.dumps({ "template": {"data": [{"name": "name", "value": "Updated"}, - {"name": "owner", "value": self.other_username}]}}) + {"name": "public", "value": True}]}}) def test_feed_detail_success(self): self.client.login(username=self.username, password=self.password) @@ -248,9 +250,8 @@ def test_feed_update_success(self): response = self.client.put(self.read_update_delete_url, data=self.put, content_type=self.content_type) self.assertContains(response, "Updated") - new_owner = User.objects.get(username=self.other_username) feed = Feed.objects.get(name="Updated") - self.assertIn(new_owner, feed.owner.all()) + self.assertTrue(feed.public) def test_feed_update_failure_unauthenticated(self): response = self.client.put(self.read_update_delete_url, data=self.put, @@ -263,14 +264,6 @@ def test_feed_update_failure_access_denied(self): content_type=self.content_type) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_feed_update_failure_new_unregistered_owner(self): - put = json.dumps({"template": {"data": [{"name": "owner", "value": "foouser"}]}}) - self.client.login(username=self.username, password=self.password) - response = self.client.put(self.read_update_delete_url, data=put, - content_type=self.content_type) - feed = Feed.objects.get(name=self.feedname) - self.assertFalse(len(feed.owner.all()) > 1) - def test_feed_delete_success(self): self.client.login(username=self.username, password=self.password) response = self.client.delete(self.read_update_delete_url) @@ -365,11 +358,10 @@ def test_comment_detail_success_related_feed_owner(self): self.assertContains(response, "Comment1") self.assertTrue(response.data["feed"].endswith(self.corresponding_feed_url)) - def test_comment_detail_success_not_related_feed_owner(self): + def test_comment_detail_failure_access_denied(self): self.client.login(username=self.other_username, password=self.other_password) response = self.client.get(self.read_update_delete_url) - self.assertContains(response, "Comment1") - self.assertTrue(response.data["feed"].endswith(self.corresponding_feed_url)) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_comment_detail_failure_unauthenticated(self): response = self.client.get(self.read_update_delete_url) @@ -438,20 +430,15 @@ def test_tag_create_failure_unauthenticated(self): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_tag_list_success(self): - self.client.login(username=self.username, password=self.password) + self.client.login(username=self.other_username, password=self.other_password) response = self.client.get(self.create_read_url) self.assertContains(response, "Tag2") self.assertContains(response, "Tag3") - def test_tag_list_failure_unauthenticated(self): + def test_tag_list_success_unauthenticated(self): response = self.client.get(self.create_read_url) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - def test_tag_list_for_other_users_not_listed(self): - self.client.login(username=self.other_username, password=self.other_password) - response = self.client.get(self.create_read_url) - self.assertNotContains(response, "Tag2") - self.assertNotContains(response, "Tag3") + self.assertContains(response, "Tag2") + self.assertContains(response, "Tag3") class TagDetailViewTests(ViewTests): @@ -472,18 +459,13 @@ def setUp(self): {"name": "color", "value": "black"}]}}) def test_tag_detail_success(self): - self.client.login(username=self.username, password=self.password) + self.client.login(username=self.other_username, password=self.other_password) response = self.client.get(self.read_update_delete_url) self.assertContains(response, "Tag1") - def test_tag_detail_failure_unauthenticated(self): + def test_tag_detail_success_unauthenticated(self): response = self.client.get(self.read_update_delete_url) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - def test_tag_detail_failure_not_owner(self): - self.client.login(username=self.other_username, password=self.other_password) - response = self.client.get(self.read_update_delete_url) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertContains(response, "Tag1") def test_tag_update_success(self): self.client.login(username=self.username, password=self.password) @@ -564,17 +546,6 @@ def test_feed_tag_list_failure_not_owner(self): self.client.login(username=self.other_username, password=self.other_password) response = self.client.get(self.list_url) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_feed_tag_list_from_other_feed_owners_not_listed(self): - self.client.login(username=self.other_username, password=self.other_password) - feed = Feed.objects.get(name=self.feedname) - owner = User.objects.get(username=self.username) - new_owner = User.objects.get(username=self.other_username) - # make new_owner an owner of the feed together with the feed's current owner - feed.owner.set([owner, new_owner]) - feed.save() - response = self.client.get(self.list_url) - self.assertNotContains(response, "Tag2") # a feed owner can not see another feed owner's tags class TagFeedListViewTests(ViewTests): @@ -615,14 +586,16 @@ def test_tag_feed_list_success(self): self.assertContains(response, self.feedname) self.assertNotContains(response, "new") # feed list is tag-specific - def test_tag_feed_list_failure_unauthenticated(self): + def test_tag_feed_list_success_unauthenticated(self): response = self.client.get(self.list_url) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], []) def test_tag_feed_list_failure_not_owner(self): self.client.login(username=self.other_username, password=self.other_password) response = self.client.get(self.list_url) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], []) class FeedTaggingListViewTests(ViewTests): @@ -680,18 +653,6 @@ def test_feed_tagging_list_failure_not_owner(self): response = self.client.get(self.create_read_url) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_feed_tagging_list_from_other_feed_owners_not_listed(self): - self.client.login(username=self.other_username, password=self.other_password) - feed = Feed.objects.get(name=self.feedname) - owner = User.objects.get(username=self.username) - new_owner = User.objects.get(username=self.other_username) - # make new_owner an owner of the feed together with the feed's current owner - feed.owner.set([owner, new_owner]) - feed.save() - response = self.client.get(self.create_read_url) - tag = Tag.objects.get(name="Tag2") - self.assertNotContains(response, tag.id) # a feed owner can not see another feed owner's taggings - class TagTaggingListViewTests(ViewTests): """ @@ -752,14 +713,10 @@ def test_tag_tagging_list_success(self): feed = Feed.objects.get(name=self.feedname) self.assertNotContains(response, feed.id) - def test_tag_tagging_list_failure_unauthenticated(self): + def test_tag_tagging_list_success_unauthenticated(self): response = self.client.get(self.create_read_url) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - def test_tag_tagging_list_failure_not_owner(self): - self.client.login(username=self.other_username, password=self.other_password) - response = self.client.get(self.create_read_url) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data['results'], []) class TaggingDetailViewTests(ViewTests): diff --git a/chris_backend/filebrowser/services.py b/chris_backend/filebrowser/services.py index 5430c69f..b8fbb202 100755 --- a/chris_backend/filebrowser/services.py +++ b/chris_backend/filebrowser/services.py @@ -1,4 +1,5 @@ + from django.db import models from core.models import ChrisFolder diff --git a/chris_backend/filebrowser/tests/test_services.py b/chris_backend/filebrowser/tests/test_services.py index 90fb3b32..be062826 100755 --- a/chris_backend/filebrowser/tests/test_services.py +++ b/chris_backend/filebrowser/tests/test_services.py @@ -7,6 +7,7 @@ from django.conf import settings from core.models import ChrisFolder +from users.models import UserProxy from userfiles.models import UserFile from filebrowser import services @@ -23,70 +24,65 @@ def setUp(self): # avoid cluttered console output (for instance logging all the http requests) logging.disable(logging.WARNING) - # create superuser chris (owner of root folders) + # superuser chris (owner of root folders) self.chris_username = 'chris' self.chris_password = CHRIS_SUPERUSER_PASSWORD - # create users self.username = 'foo' self.password = 'foopass' self.another_username = 'boo' self.another_password = 'boopass' - user = User.objects.create_user(username=self.username, password=self.password) - User.objects.create_user(username=self.another_username, password=self.another_password) - - # create a folder in the user space - path = f'home/{self.username}/uploads' - (self.folder, _) = ChrisFolder.objects.get_or_create(path=path, owner=user) - # create a file in the DB "already uploaded" to the server) - upload_path = f'{path}/file1.txt' - userfile = UserFile(owner=user, parent_folder=self.folder) - userfile.fname.name = upload_path - userfile.save() + # create users with their home folders setup + UserProxy.objects.create_user(username=self.username, password=self.password) + UserProxy.objects.create_user(username=self.another_username, + password=self.another_password) def tearDown(self): + User.objects.get(username=self.username).delete() + User.objects.get(username=self.another_username).delete() + # re-enable logging logging.disable(logging.NOTSET) - def test_get_authenticated_user_folder_queryset_folder_does_not_exist(self): + def test_get_folder_queryset_folder_does_not_exist(self): """ - Test whether the services.get_authenticated_user_folder_queryset function returns - an empty queryset for an authenticated user if a folder doesn't exist. + Test whether the services.get_folder_queryset function returns + an empty queryset if a folder doesn't exist. """ user = User.objects.get(username=self.username) path = f'home/{self.username}/uploads/crazyfolder' pk_dict = {'path': path} - qs = services.get_authenticated_user_folder_queryset(pk_dict, user) + qs = services.get_folder_queryset(pk_dict, user) self.assertEqual(qs.count(), 0) - def test_get_authenticated_user_folder_queryset_from_user_for_chris_user(self): + def test_get_folder_queryset_from_user_for_chris_user(self): """ - Test whether the services.get_authenticated_user_folder_queryset function + Test whether the services.get_folder_queryset function allows the chris user to see any existing folder. """ chris_user = User.objects.get(username=self.chris_username) path = f'home/{self.username}/uploads' pk_dict = {'path': path} - qs = services.get_authenticated_user_folder_queryset(pk_dict, chris_user) + qs = services.get_folder_queryset(pk_dict, chris_user) self.assertEqual(qs.count(), 1) self.assertEqual(qs.first().path, pk_dict['path']) - def test_get_authenticated_user_folder_queryset_from_user_prevent_another_user(self): + def test_get_folder_queryset_from_user_prevent_another_user(self): """ - Test whether the services.get_authenticated_user_folder_queryset function + Test whether the services.get_folder_queryset function doesn't allow a user to see other user's existing private folders. """ another_user = User.objects.get(username=self.another_username) path = f'home/{self.username}/uploads' pk_dict = {'path': path} - qs = services.get_authenticated_user_folder_queryset(pk_dict, another_user) + qs = services.get_folder_queryset(pk_dict, another_user) self.assertEqual(qs.count(), 0) - def test_get_authenticated_user_folder_queryset_top_level_folders(self): + def test_get_folder_queryset_top_level_folders(self): """ - Test whether the services.get_authenticated_user_folder_queryset function + Test whether the services.get_folder_queryset function returns the appropriate queryset for top-level folders for any user. """ chris_user = User.objects.get(username=self.chris_username) @@ -98,22 +94,21 @@ def test_get_authenticated_user_folder_queryset_top_level_folders(self): for path in ('', 'home', 'PIPELINES', 'SERVICES'): pk_dict = {'path': path} - qs = services.get_authenticated_user_folder_queryset(pk_dict, user) + qs = services.get_folder_queryset(pk_dict, user) self.assertEqual(qs.count(), 1) self.assertEqual(qs.first().path, pk_dict['path']) - qs = services.get_authenticated_user_folder_queryset(pk_dict, another_user) + qs = services.get_folder_queryset(pk_dict, another_user) self.assertEqual(qs.count(), 1) self.assertEqual(qs.first().path, pk_dict['path']) - def test_get_authenticated_user_folder_queryset_user_space(self): + def test_get_folder_queryset_user_space(self): """ - Test whether the services.get_authenticated_user_folder_queryset function + Test whether the services.get_folder_queryset function allows the chris user to see any existing folder. """ user = User.objects.get(username=self.username) path = f'home/{self.username}' pk_dict = {'path': path} - qs = services.get_authenticated_user_folder_queryset(pk_dict, user) + qs = services.get_folder_queryset(pk_dict, user) self.assertEqual(qs.count(), 1) self.assertEqual(qs.first().path, pk_dict['path']) - diff --git a/chris_backend/filebrowser/tests/test_views.py b/chris_backend/filebrowser/tests/test_views.py index 06968921..c42f333b 100755 --- a/chris_backend/filebrowser/tests/test_views.py +++ b/chris_backend/filebrowser/tests/test_views.py @@ -13,6 +13,7 @@ from core.models import ChrisFolder, ChrisLinkFile from core.storage import connect_storage +from users.models import UserProxy from userfiles.models import UserFile from plugins.models import PluginMeta, Plugin, ComputeResource from plugininstances.models import PluginInstance @@ -31,36 +32,27 @@ def setUp(self): # avoid cluttered console output (for instance logging all the http requests) logging.disable(logging.WARNING) - # create superuser chris (owner of root folders) + self.content_type = 'application/vnd.collection+json' + + # superuser chris (owner of root folders) self.chris_username = 'chris' self.chris_password = CHRIS_SUPERUSER_PASSWORD - chris_user = User.objects.get(username=self.chris_username) - self.content_type = 'application/vnd.collection+json' + # normal users self.username = 'foo' self.password = 'foopass' self.other_username = 'boo' self.other_password = 'boopass' - # create folders - ChrisFolder.objects.get_or_create(path='SERVICES/PACS', owner=chris_user) - ChrisFolder.objects.get_or_create(path=f'PIPELINES/{self.username}', owner=chris_user) - # create users - user = User.objects.create_user(username=self.username, password=self.password) - User.objects.create_user(username=self.other_username, password=self.other_password) - - # create a file in the DB "already uploaded" to the server) - upload_path = f'home/{self.username}/uploads/myfolder/file1.txt' - - folder_path = os.path.dirname(upload_path) - (file_parent_folder, _) = ChrisFolder.objects.get_or_create(path=folder_path, - owner=user) - userfile = UserFile(owner=user, parent_folder=file_parent_folder) - userfile.fname.name = upload_path - userfile.save() + UserProxy.objects.create_user(username=self.username, password=self.password) + UserProxy.objects.create_user(username=self.other_username, + password=self.other_password) def tearDown(self): + User.objects.get(username=self.username).delete() + User.objects.get(username=self.other_username).delete() + # re-enable logging logging.disable(logging.NOTSET) @@ -161,7 +153,7 @@ def test_filebrowserfolder_list_query_search_feeds_folder_success_shared_feed(se compute_resource=plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) self.client.login(username=self.username, password=self.password) read_url = reverse('chrisfolder-list-query-search') + f'?path=home/{self.other_username}/feeds' @@ -178,7 +170,7 @@ def test_filebrowserfolder_list_query_search_feeds_folder_failure_shared_feed_un compute_resource=plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) read_url = reverse('chrisfolder-list-query-search') + f'?path=home/{self.other_username}/feeds' response = self.client.get(read_url) @@ -194,7 +186,7 @@ def test_filebrowserfolder_list_query_search_feed_folder_success_shared_feed(sel compute_resource=plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) self.client.login(username=self.username, password=self.password) read_url = reverse('chrisfolder-list-query-search') + f'?path=home/{self.other_username}/feeds/feed_{pl_inst.feed.id}' @@ -212,7 +204,7 @@ def test_filebrowserfolder_list_query_search_feed_folder_failure_shared_feed_una compute_resource=plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) read_url = reverse('chrisfolder-list-query-search') + f'?path=home/{self.other_username}/feeds/feed_{pl_inst.feed.id}' @@ -348,7 +340,7 @@ def test_filebrowserfolder_feeds_folder_success_shared_feed(self): plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) self.client.login(username=self.username, password=self.password) folder = ChrisFolder.objects.get(path=f'home/{self.other_username}/feeds') @@ -367,7 +359,7 @@ def test_filebrowserfolder_feeds_folder_failure_not_found_shared_feed_unauthenti plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) folder = ChrisFolder.objects.get(path=f'home/{self.other_username}/feeds') read_url = reverse("chrisfolder-detail", kwargs={"pk": folder.id}) @@ -385,7 +377,7 @@ def test_filebrowserfolder_feed_folder_success_shared_feed(self): plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) self.client.login(username=self.username, password=self.password) folder = ChrisFolder.objects.get(path=f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}') @@ -406,14 +398,14 @@ def test_filebrowserfolder_feed_folder_failure_not_found_shared_feed_unauthentic plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) folder = ChrisFolder.objects.get( path=f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}') read_url = reverse("chrisfolder-detail", kwargs={"pk": folder.id}) response = self.client.get(read_url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_filebrowserfolder_feed_folder_success_public_feed(self): other_user = User.objects.get(username=self.other_username) @@ -520,7 +512,7 @@ def test_filebrowserfolderchild_list_home_folder_empty_unauthenticated(self): folder = ChrisFolder.objects.get(path='home') read_url = reverse("chrisfolder-child-list", kwargs={"pk": folder.id}) response = self.client.get(read_url) - self.assertEqual(len(response.data['results']), 0) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_filebrowserfolderchild_list_user_home_folder_success(self): folder = ChrisFolder.objects.get(path=f'home/{self.username}') @@ -572,7 +564,7 @@ def test_filebrowserfolderchild_list_feeds_folder_success_shared_feed(self): plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) self.client.login(username=self.username, password=self.password) folder = ChrisFolder.objects.get(path=f'home/{self.other_username}/feeds') @@ -580,7 +572,7 @@ def test_filebrowserfolderchild_list_feeds_folder_success_shared_feed(self): response = self.client.get(read_url) self.assertContains(response, f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}') - def test_filebrowserfolderchild_list_feeds_folder_failure_not_found_shared_feed_unauthenticated(self): + def test_filebrowserfolderchild_list_feeds_folder_failure_shared_feed_unauthenticated_access_denied(self): other_user = User.objects.get(username=self.other_username) plugin = self.plugin @@ -591,12 +583,12 @@ def test_filebrowserfolderchild_list_feeds_folder_failure_not_found_shared_feed_ plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) folder = ChrisFolder.objects.get(path=f'home/{self.other_username}/feeds') read_url = reverse("chrisfolder-child-list", kwargs={"pk": folder.id}) response = self.client.get(read_url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_filebrowserfolderchild_list_feed_folder_success_shared_feed(self): other_user = User.objects.get(username=self.other_username) @@ -609,7 +601,7 @@ def test_filebrowserfolderchild_list_feed_folder_success_shared_feed(self): plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) self.client.login(username=self.username, password=self.password) folder = ChrisFolder.objects.get( @@ -620,7 +612,7 @@ def test_filebrowserfolderchild_list_feed_folder_success_shared_feed(self): self.assertContains(response, f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}/') - def test_filebrowserfolderchild_list_feed_folder_failure_not_found_shared_feed_unauthenticated(self): + def test_filebrowserfolderchild_list_feed_folder_failure_shared_feed_unauthenticated_access_denied(self): other_user = User.objects.get(username=self.other_username) plugin = self.plugin @@ -631,14 +623,14 @@ def test_filebrowserfolderchild_list_feed_folder_failure_not_found_shared_feed_u plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) folder = ChrisFolder.objects.get( path=f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}') read_url = reverse("chrisfolder-child-list", kwargs={"pk": folder.id}) response = self.client.get(read_url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_filebrowserfolderchild_list_feed_folder_success_public_feed(self): other_user = User.objects.get(username=self.other_username) @@ -815,7 +807,7 @@ def test_fileBrowserfile_list_success_shared_feed(self): plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) file_path = f'{pl_inst.output_folder.path}/file3.txt' with io.StringIO("test file") as file3: @@ -845,7 +837,7 @@ def test_fileBrowserfile_list_failure_not_found_shared_feed_unauthenticated(self plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) file_path = f'{pl_inst.output_folder.path}/file3.txt' with io.StringIO("test file") as file3: @@ -901,8 +893,8 @@ def setUp(self): kwargs={"pk": parent_folder.id}) def tearDown(self): - super(FileBrowserFolderLinkFileListViewTests, self).tearDown() self.storage_manager.delete_obj(self.link_path) + super(FileBrowserFolderLinkFileListViewTests, self).tearDown() def test_filebrowserfolderlinkfile_list_success(self): self.client.login(username=self.username, password=self.password) @@ -977,7 +969,7 @@ def test_fileBrowserlinkfile_list_success_shared_feed(self): plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) # create link file in the output folder link_path = f'{pl_inst.output_folder.path}/SERVICES_PACS.chrislink' @@ -1004,7 +996,7 @@ def test_fileBrowserlinkfile_list_failure_not_found_shared_feed_unauthenticated( plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) # create link file in the output folder link_path = f'{pl_inst.output_folder.path}/SERVICES_PACS.chrislink' @@ -1058,8 +1050,8 @@ def setUp(self): kwargs={"pk": link_file.id}) def tearDown(self): - super(FileBrowserLinkFileDetailViewTests, self).tearDown() self.storage_manager.delete_obj(self.link_path) + super(FileBrowserLinkFileDetailViewTests, self).tearDown() def test_fileBrowserlinkfile_detail_success(self): self.client.login(username=self.username, password=self.password) @@ -1096,7 +1088,7 @@ def test_fileBrowserlinkfile_detail_success_shared_feed(self): plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) # create link file in the output folder link_path = f'{pl_inst.output_folder.path}/SERVICES_PACS.chrislink' @@ -1123,7 +1115,7 @@ def test_fileBrowserlinkfile_detail_failure_unauthorized_shared_feed_unauthentic plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) # create link file in the output folder link_path = f'{pl_inst.output_folder.path}/SERVICES_PACS.chrislink' @@ -1179,8 +1171,8 @@ def setUp(self): kwargs={"pk": link_file.id}) + 'SERVICES_PACS.chrislink' def tearDown(self): - super(FileBrowserLinkFileResourceViewTests, self).tearDown() self.storage_manager.delete_obj(self.link_path) + super(FileBrowserLinkFileResourceViewTests, self).tearDown() def test_fileBrowserlinkfile_resource_success(self): self.client.login(username=self.username, password=self.password) @@ -1205,33 +1197,25 @@ def test_fileBrowserlinkfile_resource_failure_unauthenticated(self): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_fileBrowserlinkfile_resource_success_public_feed_unauthenticated(self): - self.pl_inst.feed.public = True - self.pl_inst.feed.save() + self.pl_inst.feed.grant_public_access() response = self.client.get(self.download_url) self.assertEqual(response.status_code, 200) content = [c for c in response.streaming_content][0].decode('utf-8') self.assertEqual(content, 'SERVICES/PACS') - def test_fileBrowserlinkfile_resource_success_shared_feed(self): + def test_fileBrowserlinkfile_resource_success_shared_link_file(self): other_user = User.objects.get(username=self.other_username) - plugin = self.plugin - # create a feed by creating a "fs" plugin instance - pl_inst = PluginInstance.objects.create(plugin=plugin, owner=other_user, - title='test', - compute_resource= - plugin.compute_resources.all()[0]) - pl_inst.feed.name = 'shared_feed' - pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) - - # create link file in the output folder - link_path = f'{pl_inst.output_folder.path}/SERVICES_PACS.chrislink' + # create link file in the uploads folder + path = f'home/{self.other_username}/uploads' + uploads_folder = ChrisFolder.objects.get(path=path) link_file = ChrisLinkFile(path='SERVICES/PACS', owner=other_user, - parent_folder=pl_inst.output_folder) + parent_folder=uploads_folder) link_file.save(name='SERVICES_PACS') + link_file.grant_user_permission(User.objects.get(username=self.username), 'r') + download_url = reverse("chrislinkfile-resource", kwargs={"pk": link_file.id}) + 'SERVICES_PACS.chrislink' @@ -1241,8 +1225,6 @@ def test_fileBrowserlinkfile_resource_success_shared_feed(self): content = [c for c in response.streaming_content][0].decode('utf-8') self.assertEqual(content, 'SERVICES/PACS') - self.storage_manager.delete_obj(link_path) - def test_fileBrowserlinkfile_resource_failure_unauthorized_shared_feed_unauthenticated(self): other_user = User.objects.get(username=self.other_username) plugin = self.plugin @@ -1254,7 +1236,7 @@ def test_fileBrowserlinkfile_resource_failure_unauthorized_shared_feed_unauthent plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) # create link file in the output folder link_path = f'{pl_inst.output_folder.path}/SERVICES_PACS.chrislink' diff --git a/chris_backend/pacsfiles/models.py b/chris_backend/pacsfiles/models.py index 1a557794..75f104f6 100755 --- a/chris_backend/pacsfiles/models.py +++ b/chris_backend/pacsfiles/models.py @@ -29,7 +29,10 @@ def __str__(self): @receiver(post_delete, sender=PACS) def auto_delete_pacs_folder_with_pacs(sender, instance, **kwargs): - instance.folder.delete() + try: + instance.folder.delete() + except Exception: + pass class PACSSeries(models.Model): @@ -62,7 +65,10 @@ def __str__(self): @receiver(post_delete, sender=PACSSeries) def auto_delete_series_folder_with_series(sender, instance, **kwargs): - instance.folder.delete() + try: + instance.folder.delete() + except Exception: + pass class PACSSeriesFilter(FilterSet): diff --git a/chris_backend/pacsfiles/tests/test_views.py b/chris_backend/pacsfiles/tests/test_views.py index 6cf8d65f..b4c70be5 100755 --- a/chris_backend/pacsfiles/tests/test_views.py +++ b/chris_backend/pacsfiles/tests/test_views.py @@ -6,7 +6,7 @@ from django.test import TestCase, tag from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Group from django.urls import reverse from rest_framework import status @@ -37,7 +37,9 @@ def setUp(self): self.username = 'test' self.password = 'testpass' - User.objects.create_user(username=self.username, password=self.password) + pacs_grp, _ = Group.objects.get_or_create(name='pacs_users') + user = User.objects.create_user(username=self.username, password=self.password) + user.groups.set([pacs_grp]) # create a PACS file in the DB "already registered" to the server) self.storage_manager = connect_storage(settings) diff --git a/chris_backend/pipelines/models.py b/chris_backend/pipelines/models.py index cecf4f30..77d16501 100755 --- a/chris_backend/pipelines/models.py +++ b/chris_backend/pipelines/models.py @@ -263,7 +263,10 @@ def __str__(self): @receiver(post_delete, sender=PipelineSourceFileMeta) def auto_delete_source_file_with_meta(sender, instance, **kwargs): - instance.source_file.delete() + try: + instance.source_file.delete() + except Exception: + pass class PluginPiping(models.Model): diff --git a/chris_backend/plugininstances/models.py b/chris_backend/plugininstances/models.py index 10d7f2e3..031153a8 100755 --- a/chris_backend/plugininstances/models.py +++ b/chris_backend/plugininstances/models.py @@ -191,8 +191,10 @@ def get_output_path(self): @receiver(post_delete, sender=PluginInstance) def auto_delete_output_folder_with_plugin_instance(sender, instance, **kwargs): - if instance.output_folder: + try: instance.output_folder.delete() + except Exception: + pass class PluginInstanceFilter(FilterSet): diff --git a/chris_backend/plugininstances/serializers.py b/chris_backend/plugininstances/serializers.py index 5a742046..d9a4b80f 100755 --- a/chris_backend/plugininstances/serializers.py +++ b/chris_backend/plugininstances/serializers.py @@ -1,25 +1,19 @@ -import logging import pathlib from django.core.exceptions import ObjectDoesNotExist -from django.conf import settings from rest_framework import serializers from rest_framework.reverse import reverse from collectionjson.fields import ItemLinkField -from core.storage import connect_storage +from core.models import ChrisFolder, ChrisFile, ChrisLinkFile from plugins.models import TYPES, Plugin -from feeds.models import Feed from .models import PluginInstance, PluginInstanceSplit from .models import FloatParameter, IntParameter, BoolParameter from .models import PathParameter, UnextpathParameter, StrParameter -logger = logging.getLogger(__name__) - - class PluginInstanceSerializer(serializers.HyperlinkedModelSerializer): compute_resource_name = serializers.CharField(max_length=100, required=False, source='compute_resource.name') @@ -313,57 +307,52 @@ class Meta: def validate_paths(user, string): """ - Custom function to check whether a user is allowed to access the provided object - storage paths. + Custom function to check whether a user is allowed to access the provided paths. """ - storage_manager = connect_storage(settings) - path_list = [s.strip() for s in string.split(',')] + path_list = [s.strip().strip('/') for s in string.split(',')] for path in path_list: path_parts = pathlib.Path(path).parts - if len(path_parts) == 0: - # trying to access the root of the storage + if len(path_parts) < 2: + # trying to access a top-level folder or an unknown folder raise serializers.ValidationError( - ["You do not have permission to access this path."]) + [f"This field may not reference a top-level folder path '{path}'."]) - if len(path_parts) == 1 and path_parts[0] not in ('SERVICES', 'PIPELINES'): - # trying to access the home folder or an unknown folder within the root folder + if path_parts[0] not in ('home', 'SERVICES', 'PIPELINES'): raise serializers.ValidationError( - ["You do not have permission to access this path."]) + [f"This field may not reference an invalid path '{path}'."]) - if path_parts[0] == 'home' and path_parts[1] != user.username: - if len(path_parts) <= 3: - # trying to access another user's root or personal space - raise serializers.ValidationError( - ["You do not have permission to access this path."]) - try: - # file paths should be of the form home//feeds/feed_/.. - str_l = path_parts[3].split('_') - if len(str_l) != 2: - raise ValueError() - if str_l[0] != 'feed': - raise ValueError() - feed_id = str_l[-1] - feed = Feed.objects.get(pk=feed_id) - except (ValueError, Feed.DoesNotExist): - raise serializers.ValidationError( - ["This field may not be an invalid path."]) + if len(path_parts) == 2 and path_parts[0] == 'home': + raise serializers.ValidationError( + [f"This field may not reference a home folder path '{path}'."]) - if not (user == feed.owner or feed.has_user_permission(user)): - raise serializers.ValidationError( - ["You do not have permission to access this path."]) - else: - # check whether path exists in swift + if len(path_parts) == 3 and path_parts[0] == 'home' and path_parts[2] == 'feeds': + raise serializers.ValidationError( + [f"This field may not reference a home's feeds folder path '{path}'."]) + + try: + obj = ChrisFolder.objects.get(path=path) + except ChrisFolder.DoesNotExist: # path is not a folder try: - path_exists = storage_manager.path_exists(path) - except Exception as e: - logger.error('Swift storage error, detail: %s' % str(e)) - raise serializers.ValidationError( - ["Could not validate this path."]) - if not path_exists: - raise serializers.ValidationError( - ["This field may not be an invalid path."]) + obj = ChrisFile.objects.get(fname=path) + except ChrisFile.DoesNotExist: # path is not a file + try: + obj = ChrisLinkFile.objects.get(fname=path) + + if obj.path in ('PUBLIC', 'SHARED'): + raise serializers.ValidationError( + [f"This field may not reference an invalid path '{path}'."]) + + except ChrisLinkFile.DoesNotExist: # path is not a link file + raise serializers.ValidationError( + [f"This field may not reference an invalid path '{path}'."]) + + if not (obj.owner == user or user.username == 'chris' or obj.public + or obj.has_user_permission(user)): + raise serializers.ValidationError( + [f"User does not have permission to access path '{path}'."]) + return ','.join(path_list) @@ -390,8 +379,7 @@ def __init__(self, *args, **kwargs): def validate_value(self, value): """ Overriden to check that the user making the request is allowed to access - the provided object storage paths (value should be a string of paths separated - by commas). + the provided paths (value should be a string of paths separated by commas). """ return validate_paths(self.user, value) @@ -419,8 +407,7 @@ def __init__(self, *args, **kwargs): def validate_value(self, value): """ Overriden to check that the user making the request is allowed to access - the provided object storage paths (value should be a string of paths separated - by commas). + the provided paths (value should be a string of paths separated by commas). """ return validate_paths(self.user, value) diff --git a/chris_backend/plugininstances/tests/test_models.py b/chris_backend/plugininstances/tests/test_models.py index 0305e9b8..26cfa9f2 100755 --- a/chris_backend/plugininstances/tests/test_models.py +++ b/chris_backend/plugininstances/tests/test_models.py @@ -26,7 +26,7 @@ def setUp(self): # avoid cluttered console output (for instance logging all the http requests) logging.disable(logging.WARNING) - # create superuser chris (owner of root folders) + # superuser chris (owner of root folders) self.chris_username = 'chris' self.chris_password = CHRIS_SUPERUSER_PASSWORD diff --git a/chris_backend/plugininstances/tests/test_serializers.py b/chris_backend/plugininstances/tests/test_serializers.py index c0008998..c294f05a 100755 --- a/chris_backend/plugininstances/tests/test_serializers.py +++ b/chris_backend/plugininstances/tests/test_serializers.py @@ -9,6 +9,7 @@ from plugins.models import PluginMeta, Plugin, PluginParameter, ComputeResource from plugininstances.models import PluginInstance +from core.models import ChrisFolder from core.storage.helpers import mock_storage from plugininstances.serializers import PluginInstanceSerializer from plugininstances.serializers import (PathParameterSerializer, @@ -322,9 +323,8 @@ def test_validate_value_fail_path_does_not_exist(self): """ user = User.objects.get(username=self.username) path_parm_serializer = PathParameterSerializer(user=user) - with mock_storage('plugininstances.serializers.settings'): - with self.assertRaises(serializers.ValidationError): - path_parm_serializer.validate_value(self.username) + with self.assertRaises(serializers.ValidationError): + path_parm_serializer.validate_value(self.username) def test_validate_value_success(self): """ @@ -337,24 +337,18 @@ def test_validate_value_success(self): plugin = self.plugin pl_inst = PluginInstance.objects.create( plugin=plugin, owner=user1, compute_resource=plugin.compute_resources.all()[0]) - pl_inst.feed.owner.set([user1, user]) + pl_inst.feed.grant_user_permission(user) + + ChrisFolder.objects.get_or_create(path=f'home/{self.username}/uploads', + owner=user) + path_parm_serializer = PathParameterSerializer(user=user) - value = "home/{}, home/{}/feeds/feed_{} ".format(self.username, + value = "home/{}/uploads, home/{}/feeds/feed_{} ".format(self.username, self.other_username, pl_inst.feed.id) - with mock_storage('plugininstances.serializers.settings') as storage_manager: - storage_manager.upload_obj( - f'home/{self.username}/uploads/dummy_data.txt', - b'dummy data' - ) - storage_manager.upload_obj( - f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}/' - f'{pl_inst.plugin.meta.name}_{pl_inst.id}/data/dummy_data.txt', - b'dummy data' - ) - returned_value = path_parm_serializer.validate_value(value) - self.assertEqual(returned_value, "home/{},home/{}/feeds/feed_{}".format( - self.username, self.other_username, pl_inst.feed.id)) + returned_value = path_parm_serializer.validate_value(value) + self.assertEqual(returned_value, "home/{}/uploads,home/{}/feeds/feed_{}".format( + self.username, self.other_username, pl_inst.feed.id)) class UnextpathParameterSerializerTests(SerializerTests): @@ -423,9 +417,8 @@ def test_validate_value_fail_path_does_not_exist(self): """ user = User.objects.get(username=self.username) path_parm_serializer = UnextpathParameterSerializer(user=user) - with mock_storage('plugininstances.serializers.settings'): - with self.assertRaises(serializers.ValidationError): - path_parm_serializer.validate_value(self.username) + with self.assertRaises(serializers.ValidationError): + path_parm_serializer.validate_value(self.username) def test_validate_value_success(self): """ @@ -438,22 +431,15 @@ def test_validate_value_success(self): plugin = self.plugin pl_inst = PluginInstance.objects.create( plugin=plugin, owner=user1, compute_resource=plugin.compute_resources.all()[0]) - pl_inst.feed.owner.set([user1, user]) + pl_inst.feed.grant_user_permission(user) + + ChrisFolder.objects.get_or_create(path=f'home/{self.username}/uploads', owner=user) + path_parm_serializer = UnextpathParameterSerializer(user=user) - value = "home/{}, home/{}/feeds/feed_{} ".format(self.username, + value = "home/{}/uploads, home/{}/feeds/feed_{} ".format(self.username, self.other_username, pl_inst.feed.id) - with mock_storage('plugininstances.serializers.settings') as storage_manager: - storage_manager.upload_obj( - f'home/{self.username}/uploads/dummy_data.txt', - b'dummy data' - ) - storage_manager.upload_obj( - f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}/' - f'{pl_inst.plugin.meta.name}_{pl_inst.id}/data/dummy_data.txt', - b'dummy data' - ) - - returned_value = path_parm_serializer.validate_value(value) - self.assertEqual(returned_value, "home/{},home/{}/feeds/feed_{}".format( + + returned_value = path_parm_serializer.validate_value(value) + self.assertEqual(returned_value, "home/{}/uploads,home/{}/feeds/feed_{}".format( self.username, self.other_username, pl_inst.feed.id)) diff --git a/chris_backend/userfiles/tests/test_serializers.py b/chris_backend/userfiles/tests/test_serializers.py index ed6b4b92..bceaf4b3 100755 --- a/chris_backend/userfiles/tests/test_serializers.py +++ b/chris_backend/userfiles/tests/test_serializers.py @@ -30,9 +30,10 @@ def setUp(self): self.username = 'test' self.password = 'testpass' - # create user - User.objects.create_user(username=self.username, - password=self.password) + # create user and its home folder + user = User.objects.create_user(username=self.username, + password=self.password) + ChrisFolder.objects.get_or_create(path=f'home/{self.username}', owner=user) def tearDown(self): # re-enable logging @@ -89,18 +90,16 @@ def test_validate_upload_path_failure_does_not_start_with_home_username(self): must start with the 'home//' string. """ userfiles_serializer = UserFileSerializer() - user = mock.Mock(spec=User) - user.username = 'cube' request = mock.Mock() - request.user = user + request.user = User.objects.get(username=self.username) with mock.patch.dict(userfiles_serializer.context, {'request': request}, clear=True): with self.assertRaises(serializers.ValidationError): - userfiles_serializer.validate_upload_path('foo/file1.txt') + userfiles_serializer.validate_upload_path('random/file1.txt') with self.assertRaises(serializers.ValidationError): - userfiles_serializer.validate_upload_path('home/cube_file1.txt') + userfiles_serializer.validate_upload_path(f'home/{self.username}_file1.txt') with self.assertRaises(serializers.ValidationError): - userfiles_serializer.validate_upload_path('cube/uploads_file1.txt') + userfiles_serializer.validate_upload_path(f'{self.username}/uploads_file1.txt') @tag('integration') def test_validate_upload_path_success(self): @@ -108,12 +107,11 @@ def test_validate_upload_path_success(self): Test whether overriden validate_upload_path method validates submitted path. """ userfiles_serializer = UserFileSerializer() - user = mock.Mock(spec=User) - user.username = 'cube' request = mock.Mock() - request.user = user + request.user = User.objects.get(username=self.username) + with mock.patch.dict(userfiles_serializer.context, {'request': request}, clear=True): - upload_path = 'home/cube/uploads/file1.txt' + upload_path = f'home/{self.username}/uploads/file1.txt' self.assertEqual(userfiles_serializer.validate_upload_path(upload_path), upload_path) diff --git a/chris_backend/users/permissions.py b/chris_backend/users/permissions.py index 5dcedd11..0ab8c4cf 100755 --- a/chris_backend/users/permissions.py +++ b/chris_backend/users/permissions.py @@ -1,3 +1,4 @@ + from rest_framework import permissions @@ -20,15 +21,14 @@ def has_object_permission(self, request, view, obj): class IsAdminOrReadOnly(permissions.BasePermission): """ - Custom permission to only allow admin users to modify/edit it. Read only is allowed + Custom permission to only allow admin users to modify/edit. Read only is allowed to normal users. """ - def has_object_permission(self, request, view, obj): + def has_permission(self, request, view): # Read permissions are allowed to normal users. if request.method in permissions.SAFE_METHODS: - #if obj.user_set.filter(username=request.user.username).exists(): - return True + return True # Raed/Write permissions are allowed to the admin users return request.user.is_staff diff --git a/chris_backend/users/tests/test_models.py b/chris_backend/users/tests/test_models.py new file mode 100755 index 00000000..4ad8ba1a --- /dev/null +++ b/chris_backend/users/tests/test_models.py @@ -0,0 +1,134 @@ + +import logging +from unittest import mock + +from django.test import TestCase, tag +from django.conf import settings + +from core.models import ChrisFolder +from core.storage.helpers import mock_storage +from userfiles.models import UserFile +from users.models import UserProxy + + +COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD + + +class ModelTests(TestCase): + + def setUp(self): + # avoid cluttered console output (for instance logging all the http requests) + logging.disable(logging.WARNING) + + # superuser chris (owner of root folders) + self.chris_username = 'chris' + self.chris_password = CHRIS_SUPERUSER_PASSWORD + + self.username = 'foo' + self.password = 'foo-pass' + self.email = 'foo@gmail.com' + + def tearDown(self): + # re-enable logging + logging.disable(logging.NOTSET) + + +class UserProxyModelTests(ModelTests): + + def test_save_assigns_predefined_groups_first_time_user_is_saved(self): + """ + Test whether overriden save method assigns predefined groups to the user the + first time the user is saved. + """ + with mock_storage('users.models.settings') as storage_manager: + # create user + user = UserProxy.objects.create_user(username=self.username, + password=self.password) + + user_grp_names = [g.name for g in user.groups.all()] + self.assertEqual(len(user_grp_names), 2) + self.assertIn('all_users', user_grp_names) + self.assertIn('pacs_users', user_grp_names) + + user.save() + + user_grp_names = [g.name for g in user.groups.all()] + self.assertEqual(len(user_grp_names), 2) + self.assertIn('all_users', user_grp_names) + self.assertIn('pacs_users', user_grp_names) + + def test_save_creates_predefined_folders_under_home_first_time_user_is_saved(self): + """ + Test whether overriden save method creates predefined folders under the + user's home directory the first time the user is saved. + """ + with mock_storage('users.models.settings') as storage_manager: + # create user + user = UserProxy.objects.create_user(username=self.username, + password=self.password) + + home_folder = ChrisFolder.objects.get(path=f'home/{self.username}') + subfolder_paths = [folder.path for folder in home_folder.children.all()] + self.assertEqual(len(subfolder_paths), 2) + self.assertIn(f'home/{self.username}/feeds', subfolder_paths) + self.assertIn(f'home/{self.username}/uploads', subfolder_paths) + + user.save() + + home_folder.refresh_from_db() + subfolder_paths = [folder.path for folder in home_folder.children.all()] + self.assertEqual(len(subfolder_paths), 2) + self.assertIn(f'home/{self.username}/feeds', subfolder_paths) + self.assertIn(f'home/{self.username}/uploads', subfolder_paths) + + def test_save_creates_predefined_link_files_under_home_first_time_user_is_saved(self): + """ + Test whether overriden save method creates predefined link files under the + user's home directory the first time the user is saved. + """ + with mock_storage('users.models.settings') as storage_manager: + # create user + user = UserProxy.objects.create_user(username=self.username, + password=self.password) + + home_folder = ChrisFolder.objects.get(path=f'home/{self.username}') + lf_paths = [lf.fname.name for lf in home_folder.chris_link_files.all()] + lf_pointed_paths = [lf.path for lf in home_folder.chris_link_files.all()] + self.assertEqual(len(lf_paths), 2) + self.assertIn(f'home/{self.username}/public.chrislink', lf_paths) + self.assertIn('PUBLIC', lf_pointed_paths) + self.assertIn(f'home/{self.username}/shared.chrislink', lf_paths) + self.assertIn('SHARED', lf_pointed_paths) + + user.save() + + home_folder.refresh_from_db() + lf_paths = [lf.fname.name for lf in home_folder.chris_link_files.all()] + lf_pointed_paths = [lf.path for lf in home_folder.chris_link_files.all()] + self.assertEqual(len(lf_paths), 2) + self.assertIn(f'home/{self.username}/public.chrislink', lf_paths) + self.assertIn('PUBLIC', lf_pointed_paths) + self.assertIn(f'home/{self.username}/shared.chrislink', lf_paths) + self.assertIn('SHARED', lf_pointed_paths) + + def test_save_creates_welcome_file_under_home_uploads_first_time_user_is_saved(self): + """ + Test whether overriden save method creates a welcome.txt under the + user's uploads directory the first time the user is saved. + """ + with mock_storage('users.models.settings') as storage_manager: + # create user + user = UserProxy.objects.create_user(username=self.username, + password=self.password) + + welcome_file_path = f'home/{self.username}/uploads/welcome.txt' + welcome_file = UserFile.objects.get(owner=user) + self.assertEqual(welcome_file.fname.name, welcome_file_path) + self.assertTrue(storage_manager.obj_exists(welcome_file_path)) + + user.save() + + welcome_file = UserFile.objects.get(owner=user) + self.assertEqual(welcome_file.fname.name, welcome_file_path) + self.assertTrue(storage_manager.obj_exists(welcome_file_path)) diff --git a/chris_backend/users/tests/test_serializers.py b/chris_backend/users/tests/test_serializers.py index 917bf1ab..fd14b080 100755 --- a/chris_backend/users/tests/test_serializers.py +++ b/chris_backend/users/tests/test_serializers.py @@ -42,13 +42,12 @@ class UserSerializerTests(SerializerTests): def test_create(self): """ - Test whether overriden create method takes care of the password hashing and - creates a welcome file for the user in its personal storage space. + Test whether overriden create method takes care of the password hashing. """ user_serializer = UserSerializer() validated_data = {'username': self.username, 'password': self.password, 'email': self.email} - with mock_storage('users.serializers.settings') as storage_manager: + with mock_storage('users.models.settings') as storage_manager: user = user_serializer.create(validated_data) self.assertEqual(user.username, self.username) @@ -56,25 +55,16 @@ def test_create(self): self.assertNotEqual(user.password, self.password) self.assertTrue(user.check_password(self.password)) - welcome_file_path = f'home/{self.username}/uploads/welcome.txt' - welcome_file = UserFile.objects.get(owner=user) - self.assertEqual(welcome_file.fname.name, welcome_file_path) - self.assertTrue(storage_manager.obj_exists(welcome_file_path)) - def test_validate_username(self): """ Test whether overriden validate_username method raises a - serializers.ValidationError when the username contains forward slashes or is - the 'chris' special user. + serializers.ValidationError when the username contains forward slashes. """ user_serializer = UserSerializer() with self.assertRaises(serializers.ValidationError): user_serializer.validate_username('user/') - with self.assertRaises(serializers.ValidationError): - user_serializer.validate_username('chris') - username = user_serializer.validate_username(self.username) self.assertEqual(username, self.username) diff --git a/chris_backend/users/tests/test_views.py b/chris_backend/users/tests/test_views.py index 1d4073d9..5ab1d4c6 100755 --- a/chris_backend/users/tests/test_views.py +++ b/chris_backend/users/tests/test_views.py @@ -30,9 +30,9 @@ def setUp(self): self.chris_password = CHRIS_SUPERUSER_PASSWORD self.content_type = 'application/vnd.collection+json' - self.username = 'cube' - self.password = 'cubepass' - self.email = 'dev@babymri.org' + self.username = 'fooo' + self.password = 'fooopass' + self.email = 'foo@gmail.com' def tearDown(self): # re-enable logging @@ -53,7 +53,7 @@ def setUp(self): {"name": "email", "value": self.email}]}}) def test_user_create_success(self): - with mock_storage('users.serializers.settings') as storage_manager: + with mock_storage('users.models.settings') as storage_manager: response = self.client.post(self.create_url, data=self.post, content_type=self.content_type) self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -208,7 +208,7 @@ def test_group_create_failure_access_denied(self): self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_group_list_success(self): - self.client.login(username=self.chris_username, password=self.chris_password) + self.client.login(username=self.username, password=self.password) response = self.client.get(self.create_read_url) self.assertContains(response, "G1") self.assertContains(response, "G2") @@ -217,11 +217,6 @@ def test_group_list_failure_unauthenticated(self): response = self.client.get(self.create_read_url) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - def test_group_list_failure_access_denied(self): - self.client.login(username=self.username, password=self.password) - response = self.client.get(self.create_read_url) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - class GroupDetailViewTests(ViewTests): """ @@ -252,7 +247,7 @@ def test_group_delete_success(self): self.client.login(username=self.chris_username, password=self.chris_password) response = self.client.delete(self.read_delete_url) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(Group.objects.count(), 0) + self.assertEqual(Group.objects.count(), 2) def test_group_delete_failure_unauthenticated(self): response = self.client.delete(self.read_delete_url) @@ -304,7 +299,7 @@ def test_group_user_create_failure_access_denied(self): self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_group_user_list_success(self): - self.client.login(username=self.chris_username, password=self.chris_password) + self.client.login(username=self.username, password=self.password) response = self.client.get(self.create_read_url) self.assertContains(response, self.username) @@ -314,11 +309,6 @@ def test_group_user_list_failure_unauthenticated(self): response = self.client.get(self.create_read_url) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - def test_group_user_list_failure_access_denied(self): - self.client.login(username=self.username, password=self.password) - response = self.client.get(self.create_read_url) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - class GroupUserDetailViewTests(ViewTests): """ @@ -339,7 +329,7 @@ def setUp(self): self.read_delete_url = reverse("user_groups-detail", kwargs={"pk": group_user.id}) def test_group_user_detail_success(self): - self.client.login(username=self.chris_username, password=self.chris_password) + self.client.login(username=self.username, password=self.password) response = self.client.get(self.read_delete_url) self.assertContains(response, 'G4') self.assertContains(response, self.username) @@ -348,11 +338,6 @@ def test_group_user_detail_failure_unauthenticated(self): response = self.client.get(self.read_delete_url) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - def test_group_user_detail_failure_access_denied(self): - self.client.login(username=self.username, password=self.password) - response = self.client.get(self.read_delete_url) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_group_user_delete_success(self): self.client.login(username=self.chris_username, password=self.chris_password) response = self.client.delete(self.read_delete_url) diff --git a/chris_backend/users/views.py b/chris_backend/users/views.py index 86326707..0ab26bd7 100755 --- a/chris_backend/users/views.py +++ b/chris_backend/users/views.py @@ -32,7 +32,7 @@ class UserGroupList(generics.ListAPIView): http_method_names = ['get'] queryset = User.objects.all() serializer_class = GroupSerializer - permission_classes = (permissions.IsAuthenticated, ) + permission_classes = (permissions.IsAuthenticated, IsUserOrChrisOrReadOnly) def list(self, request, *args, **kwargs): """ From 14483ea76bf28f9b2fe4cf985f83f568bdc88c91 Mon Sep 17 00:00:00 2001 From: Jorge Date: Tue, 18 Jun 2024 20:01:22 -0400 Subject: [PATCH 06/11] Fix permissions bug --- chris_backend/core/models.py | 16 +- chris_backend/filebrowser/permissions.py | 11 +- chris_backend/filebrowser/serializers.py | 2 +- .../filebrowser/tests/test_services.py | 1 - chris_backend/filebrowser/tests/test_views.py | 206 +++++------------- chris_backend/users/tests/test_views.py | 6 +- 6 files changed, 78 insertions(+), 164 deletions(-) diff --git a/chris_backend/core/models.py b/chris_backend/core/models.py index f450dd11..d3a9e272 100755 --- a/chris_backend/core/models.py +++ b/chris_backend/core/models.py @@ -111,10 +111,12 @@ def save(self, *args, **kwargs): def get_descendants(self): """ Custom method to return all the folders that are a descendant of this - folder. + folder (including itself). """ - path = self.path.rstrip('/') + '/' - return list(ChrisFolder.objects.filter(path__startswith=path)) + path = str(self.path) + if path.endswith('/'): + return list(ChrisFolder.objects.filter(path__startswith=path)) + return [self] + list(ChrisFolder.objects.filter(path__startswith=path + '/')) def has_group_permission(self, group, permission=''): """ @@ -273,9 +275,13 @@ def _update_public_access(self, public_tf): Internal method to update public access to the folder and all its descendant folders, link files and files. """ - path = self.path.rstrip('/') + '/' + path = str(self.path) + + if path.endswith('/'): + folders = list(ChrisFolder.objects.filter(path__startswith=path)) + else: + folders = [self] + list(ChrisFolder.objects.filter(path__startswith=path + '/')) - folders = list(ChrisFolder.objects.filter(path__startswith=path)) for folder in folders: folder.public = public_tf ChrisFolder.objects.bulk_update(folders, ['public']) diff --git a/chris_backend/filebrowser/permissions.py b/chris_backend/filebrowser/permissions.py index 5cbc2234..12baa68e 100755 --- a/chris_backend/filebrowser/permissions.py +++ b/chris_backend/filebrowser/permissions.py @@ -4,7 +4,7 @@ class IsOwnerOrChrisOrCanWriteOrCanReadOnlyOrPublicReadOnly(permissions.BasePermission): """ - Custom permission to only allow owners of an object or superuser 'chris' or users + Custom permission to only allow owners of an object, superuser 'chris' and users with write permission to modify/edit it. Read-only access is allowed to other users that have been granted read permission or to any user if the object is public. """ @@ -15,11 +15,12 @@ def has_object_permission(self, request, view, obj): if obj.owner == user or user.username == 'chris': return True - if request.method in permissions.SAFE_METHODS and (obj.public or - obj.has_user_permission(user)): - return True + if request.method in permissions.SAFE_METHODS: + if obj.public: + return True + return user.is_authenticated and obj.has_user_permission(user) - return obj.has_user_permission(user, 'w') + return user.is_authenticated and obj.has_user_permission(user, 'w') class IsOwnerOrChrisOrHasAnyPermissionReadOnly(permissions.BasePermission): diff --git a/chris_backend/filebrowser/serializers.py b/chris_backend/filebrowser/serializers.py index fd825cb2..98517ab5 100755 --- a/chris_backend/filebrowser/serializers.py +++ b/chris_backend/filebrowser/serializers.py @@ -71,7 +71,7 @@ def validate_path(self, path): already exists. """ # remove leading and trailing slashes - path = path.strip(' ').strip('/') + path = path.strip().strip('/') if not path.startswith('home/'): raise serializers.ValidationError(["Invalid path. Path must start with " diff --git a/chris_backend/filebrowser/tests/test_services.py b/chris_backend/filebrowser/tests/test_services.py index be062826..f09c46eb 100755 --- a/chris_backend/filebrowser/tests/test_services.py +++ b/chris_backend/filebrowser/tests/test_services.py @@ -8,7 +8,6 @@ from core.models import ChrisFolder from users.models import UserProxy -from userfiles.models import UserFile from filebrowser import services diff --git a/chris_backend/filebrowser/tests/test_views.py b/chris_backend/filebrowser/tests/test_views.py index c42f333b..e789c63f 100755 --- a/chris_backend/filebrowser/tests/test_views.py +++ b/chris_backend/filebrowser/tests/test_views.py @@ -143,39 +143,6 @@ def test_filebrowserfolder_list_query_search_PIPELINES_folder_succes_unauthentic response = self.client.get(read_url) self.assertContains(response, 'PIPELINES') - def test_filebrowserfolder_list_query_search_feeds_folder_success_shared_feed(self): - other_user = User.objects.get(username=self.other_username) - plugin = self.plugin - - # create a feed by creating a "fs" plugin instance - pl_inst = PluginInstance.objects.create(plugin=plugin, owner=other_user, - title='test', - compute_resource=plugin.compute_resources.all()[0]) - pl_inst.feed.name = 'shared_feed' - pl_inst.feed.save() - pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) - - self.client.login(username=self.username, password=self.password) - read_url = reverse('chrisfolder-list-query-search') + f'?path=home/{self.other_username}/feeds' - response = self.client.get(read_url) - self.assertContains(response, f'home/{self.other_username}/feeds') - - def test_filebrowserfolder_list_query_search_feeds_folder_failure_shared_feed_unauthenticated(self): - other_user = User.objects.get(username=self.other_username) - plugin = self.plugin - - # create a feed by creating a "fs" plugin instance - pl_inst = PluginInstance.objects.create(plugin=plugin, owner=other_user, - title='test', - compute_resource=plugin.compute_resources.all()[0]) - pl_inst.feed.name = 'shared_feed' - pl_inst.feed.save() - pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) - - read_url = reverse('chrisfolder-list-query-search') + f'?path=home/{self.other_username}/feeds' - response = self.client.get(read_url) - self.assertFalse(response.data['results']) - def test_filebrowserfolder_list_query_search_feed_folder_success_shared_feed(self): other_user = User.objects.get(username=self.other_username) plugin = self.plugin @@ -284,11 +251,11 @@ def test_filebrowserfolder_home_folder_success(self): response = self.client.get(read_url) self.assertContains(response, 'home') - def test_filebrowserfolder_home_folder_success_unauthenticated(self): + def test_filebrowserfolder_home_folder_failure_unauthenticated(self): folder = ChrisFolder.objects.get(path='home') read_url = reverse("chrisfolder-detail", kwargs={"pk": folder.id}) response = self.client.get(read_url) - self.assertContains(response, 'home') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_filebrowserfolder_user_home_folder_success(self): folder = ChrisFolder.objects.get(path=f'home/{self.username}') @@ -297,11 +264,11 @@ def test_filebrowserfolder_user_home_folder_success(self): response = self.client.get(read_url) self.assertContains(response, f'home/{self.username}') - def test_filebrowserfolder_user_home_folder_failure_not_found_unauthenticated(self): + def test_filebrowserfolder_user_home_folder_failure_not_unauthenticated(self): folder = ChrisFolder.objects.get(path=f'home/{self.username}') read_url = reverse("chrisfolder-detail", kwargs={"pk": folder.id}) response = self.client.get(read_url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_filebrowserfolder_SERVICES_folder_success(self): folder = ChrisFolder.objects.get(path='SERVICES') @@ -310,11 +277,11 @@ def test_filebrowserfolder_SERVICES_folder_success(self): response = self.client.get(read_url) self.assertContains(response, 'SERVICES') - def test_filebrowserfolder_SERVICES_folder_failure_not_found_unauthenticated(self): + def test_filebrowserfolder_SERVICES_folder_failure_unauthenticated(self): folder = ChrisFolder.objects.get(path='SERVICES') read_url = reverse("chrisfolder-detail", kwargs={"pk": folder.id}) response = self.client.get(read_url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_filebrowserfolder_PIPELINES_folder_success(self): folder = ChrisFolder.objects.get(path='PIPELINES') @@ -329,43 +296,6 @@ def test_filebrowserfolder_PIPELINES_folder_succes_unauthenticated(self): response = self.client.get(read_url) self.assertContains(response, 'PIPELINES') - def test_filebrowserfolder_feeds_folder_success_shared_feed(self): - other_user = User.objects.get(username=self.other_username) - plugin = self.plugin - - # create a feed by creating a "fs" plugin instance - pl_inst = PluginInstance.objects.create(plugin=plugin, owner=other_user, - title='test', - compute_resource= - plugin.compute_resources.all()[0]) - pl_inst.feed.name = 'shared_feed' - pl_inst.feed.save() - pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) - - self.client.login(username=self.username, password=self.password) - folder = ChrisFolder.objects.get(path=f'home/{self.other_username}/feeds') - read_url = reverse("chrisfolder-detail", kwargs={"pk": folder.id}) - response = self.client.get(read_url) - self.assertContains(response,f'home/{self.other_username}/feeds') - - def test_filebrowserfolder_feeds_folder_failure_not_found_shared_feed_unauthenticated(self): - other_user = User.objects.get(username=self.other_username) - plugin = self.plugin - - # create a feed by creating a "fs" plugin instance - pl_inst = PluginInstance.objects.create(plugin=plugin, owner=other_user, - title='test', - compute_resource= - plugin.compute_resources.all()[0]) - pl_inst.feed.name = 'shared_feed' - pl_inst.feed.save() - pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) - - folder = ChrisFolder.objects.get(path=f'home/{self.other_username}/feeds') - read_url = reverse("chrisfolder-detail", kwargs={"pk": folder.id}) - response = self.client.get(read_url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_filebrowserfolder_feed_folder_success_shared_feed(self): other_user = User.objects.get(username=self.other_username) plugin = self.plugin @@ -417,9 +347,10 @@ def test_filebrowserfolder_feed_folder_success_public_feed(self): compute_resource= plugin.compute_resources.all()[0]) pl_inst.feed.name = 'public_feed' - pl_inst.feed.public = True pl_inst.feed.save() + pl_inst.feed.grant_public_access() + self.client.login(username=self.username, password=self.password) folder = ChrisFolder.objects.get( path=f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}') @@ -439,9 +370,11 @@ def test_filebrowserfolder_feed_folder_success_public_feed_unauthenticated(self) compute_resource= plugin.compute_resources.all()[0]) pl_inst.feed.name = 'public_feed' - pl_inst.feed.public = True pl_inst.feed.save() + # make feed public + pl_inst.feed.grant_public_access() + folder = ChrisFolder.objects.get( path=f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}') read_url = reverse("chrisfolder-detail", kwargs={"pk": folder.id}) @@ -490,13 +423,15 @@ def test_filebrowserfolderchild_list_root_folder_success(self): self.assertContains(response, 'home') self.assertContains(response, 'SERVICES') self.assertContains(response, 'PIPELINES') - self.assertEqual(len(response.data['results']), 3) + self.assertContains(response, 'PUBLIC') + self.assertContains(response, 'SHARED') + self.assertEqual(len(response.data['results']), 5) def test_filebrowserfolderchild_list_root_folder_success_unauthenticated(self): folder = ChrisFolder.objects.get(path='') read_url = reverse("chrisfolder-child-list", kwargs={"pk": folder.id}) response = self.client.get(read_url) - self.assertContains(response, 'home') + self.assertContains(response, 'PUBLIC') self.assertContains(response, 'PIPELINES') self.assertEqual(len(response.data['results']), 2) @@ -521,11 +456,11 @@ def test_filebrowserfolderchild_list_user_home_folder_success(self): response = self.client.get(read_url) self.assertContains(response, f'home/{self.username}') - def test_filebrowserfolderchild_list_user_home_folder_failure_not_found_unauthenticated(self): + def test_filebrowserfolderchild_list_user_home_folder_failure_unauthenticated(self): folder = ChrisFolder.objects.get(path=f'home/{self.username}') read_url = reverse("chrisfolder-child-list", kwargs={"pk": folder.id}) response = self.client.get(read_url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_filebrowserfolderchild_list_SERVICES_folder_success(self): folder = ChrisFolder.objects.get(path='SERVICES') @@ -534,18 +469,18 @@ def test_filebrowserfolderchild_list_SERVICES_folder_success(self): response = self.client.get(read_url) self.assertContains(response, 'SERVICES/PACS') - def test_filebrowserfolderchild_list_SERVICES_folder_failure_not_found_unauthenticated(self): + def test_filebrowserfolderchild_list_SERVICES_folder_failure_unauthenticated(self): folder = ChrisFolder.objects.get(path='SERVICES') read_url = reverse("chrisfolder-child-list", kwargs={"pk": folder.id}) response = self.client.get(read_url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_filebrowserfolderchild_list_PIPELINES_folder_success(self): folder = ChrisFolder.objects.get(path='PIPELINES') read_url = reverse("chrisfolder-child-list", kwargs={"pk": folder.id}) self.client.login(username=self.username, password=self.password) response = self.client.get(read_url) - self.assertContains(response, f'PIPELINES/{self.username}') + self.assertEqual(response.status_code, status.HTTP_200_OK) def test_filebrowserfolderchild_list_PIPELINES_folder_succes_unauthenticated(self): folder = ChrisFolder.objects.get(path='PIPELINES') @@ -553,7 +488,7 @@ def test_filebrowserfolderchild_list_PIPELINES_folder_succes_unauthenticated(sel response = self.client.get(read_url) self.assertEqual(response.status_code, status.HTTP_200_OK) - def test_filebrowserfolderchild_list_feeds_folder_success_shared_feed(self): + def test_filebrowserfolderchild_list_feeds_folder_failure_shared_feed(self): other_user = User.objects.get(username=self.other_username) plugin = self.plugin @@ -567,24 +502,6 @@ def test_filebrowserfolderchild_list_feeds_folder_success_shared_feed(self): pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) self.client.login(username=self.username, password=self.password) - folder = ChrisFolder.objects.get(path=f'home/{self.other_username}/feeds') - read_url = reverse("chrisfolder-child-list", kwargs={"pk": folder.id}) - response = self.client.get(read_url) - self.assertContains(response, f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}') - - def test_filebrowserfolderchild_list_feeds_folder_failure_shared_feed_unauthenticated_access_denied(self): - other_user = User.objects.get(username=self.other_username) - plugin = self.plugin - - # create a feed by creating a "fs" plugin instance - pl_inst = PluginInstance.objects.create(plugin=plugin, owner=other_user, - title='test', - compute_resource= - plugin.compute_resources.all()[0]) - pl_inst.feed.name = 'shared_feed' - pl_inst.feed.save() - pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) - folder = ChrisFolder.objects.get(path=f'home/{self.other_username}/feeds') read_url = reverse("chrisfolder-child-list", kwargs={"pk": folder.id}) response = self.client.get(read_url) @@ -612,7 +529,7 @@ def test_filebrowserfolderchild_list_feed_folder_success_shared_feed(self): self.assertContains(response, f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}/') - def test_filebrowserfolderchild_list_feed_folder_failure_shared_feed_unauthenticated_access_denied(self): + def test_filebrowserfolderchild_list_feed_folder_failure_shared_feed_unauthenticated(self): other_user = User.objects.get(username=self.other_username) plugin = self.plugin @@ -630,7 +547,7 @@ def test_filebrowserfolderchild_list_feed_folder_failure_shared_feed_unauthentic read_url = reverse("chrisfolder-child-list", kwargs={"pk": folder.id}) response = self.client.get(read_url) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_filebrowserfolderchild_list_feed_folder_success_public_feed(self): other_user = User.objects.get(username=self.other_username) @@ -641,9 +558,11 @@ def test_filebrowserfolderchild_list_feed_folder_success_public_feed(self): title='test', compute_resource=plugin.compute_resources.all()[0]) pl_inst.feed.name = 'public_feed' - pl_inst.feed.public = True pl_inst.feed.save() + # make feed public + pl_inst.feed.grant_public_access() + self.client.login(username=self.username, password=self.password) folder = ChrisFolder.objects.get( path=f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}') @@ -663,9 +582,11 @@ def test_filebrowserfolderchild_list_feed_folder_success_public_feed_unauthentic compute_resource= plugin.compute_resources.all()[0]) pl_inst.feed.name = 'public_feed' - pl_inst.feed.public = True pl_inst.feed.save() + # make feed public + pl_inst.feed.grant_public_access() + folder = ChrisFolder.objects.get( path=f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}') read_url = reverse("chrisfolder-child-list", kwargs={"pk": folder.id}) @@ -744,8 +665,6 @@ def test_filebrowserfolderfile_list_success_public_feed_unauthenticated(self): title='test', compute_resource= self.plugin.compute_resources.all()[0]) - pl_inst.feed.public = True - pl_inst.feed.save() # create file self.storage_manager = connect_storage(settings) @@ -759,6 +678,9 @@ def test_filebrowserfolderfile_list_success_public_feed_unauthenticated(self): userfile.fname.name = file_path userfile.save() + # make feed public + pl_inst.feed.grant_public_access() + read_url = reverse("chrisfolder-file-list", kwargs={"pk": pl_inst.output_folder.id}) @@ -770,17 +692,13 @@ def test_filebrowserfolderfile_list_success_public_feed_unauthenticated(self): def test_filebrowserfolderfile_list_failure_unauthenticated(self): response = self.client.get(self.read_url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_filebrowserfolderfile_list_failure_not_found(self): self.client.login(username=self.username, password=self.password) response = self.client.get(reverse("chrisfolder-file-list", kwargs={"pk": 111111111})) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_filebrowserfolderfile_list_failure_not_found_unauthenticated(self): - response = self.client.get(reverse("chrisfolder-file-list", kwargs={"pk": 111111111})) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_filebrowserfolderfile_list_file_folder_success(self): folder_path = os.path.dirname(self.upload_path) folder = ChrisFolder.objects.get(path=folder_path) @@ -789,13 +707,6 @@ def test_filebrowserfolderfile_list_file_folder_success(self): response = self.client.get(read_url) self.assertContains(response, self.upload_path) - def test_filebrowserfolderfile_list_file_folder_failure_not_found_unauthenticated(self): - folder_path = os.path.dirname(self.upload_path) - folder = ChrisFolder.objects.get(path=folder_path) - read_url = reverse("chrisfolder-file-list", kwargs={"pk": folder.id}) - response = self.client.get(read_url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_fileBrowserfile_list_success_shared_feed(self): other_user = User.objects.get(username=self.other_username) plugin = self.plugin @@ -807,7 +718,6 @@ def test_fileBrowserfile_list_success_shared_feed(self): plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) file_path = f'{pl_inst.output_folder.path}/file3.txt' with io.StringIO("test file") as file3: @@ -818,6 +728,9 @@ def test_fileBrowserfile_list_success_shared_feed(self): userfile.fname.name = file_path userfile.save() + # share feed + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) + read_url = reverse("chrisfolder-file-list", kwargs={"pk": pl_inst.output_folder.id}) self.client.login(username=self.username, password=self.password) @@ -826,7 +739,7 @@ def test_fileBrowserfile_list_success_shared_feed(self): self.storage_manager.delete_obj(file_path) - def test_fileBrowserfile_list_failure_not_found_shared_feed_unauthenticated(self): + def test_fileBrowserfile_list_failure_shared_feed_unauthenticated(self): other_user = User.objects.get(username=self.other_username) plugin = self.plugin @@ -837,7 +750,6 @@ def test_fileBrowserfile_list_failure_not_found_shared_feed_unauthenticated(self plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) file_path = f'{pl_inst.output_folder.path}/file3.txt' with io.StringIO("test file") as file3: @@ -848,11 +760,14 @@ def test_fileBrowserfile_list_failure_not_found_shared_feed_unauthenticated(self userfile.fname.name = file_path userfile.save() + # share feed + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) + read_url = reverse("chrisfolder-file-list", kwargs={"pk": pl_inst.output_folder.id}) response = self.client.get(read_url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.storage_manager.delete_obj(file_path) @@ -910,8 +825,6 @@ def test_filebrowserfolderlinkfile_list_success_public_feed_unauthenticated(self title='test', compute_resource= self.plugin.compute_resources.all()[0]) - pl_inst.feed.public = True - pl_inst.feed.save() # create link file link_path = f'{pl_inst.output_folder.path}/SERVICES_PACS.chrislink' @@ -920,6 +833,9 @@ def test_filebrowserfolderlinkfile_list_success_public_feed_unauthenticated(self parent_folder=pl_inst.output_folder) link_file.save(name='SERVICES_PACS') + # make feed public + pl_inst.feed.grant_public_access() + read_url = reverse("chrisfolder-linkfile-list", kwargs={"pk": pl_inst.output_folder.id}) response = self.client.get(read_url) @@ -930,7 +846,7 @@ def test_filebrowserfolderlinkfile_list_success_public_feed_unauthenticated(self def test_filebrowserfolderlinkfile_list_failure_unauthenticated(self): response = self.client.get(self.read_url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_filebrowserfolderlinkfile_list_failure_not_found(self): self.client.login(username=self.username, password=self.password) @@ -938,11 +854,6 @@ def test_filebrowserfolderlinkfile_list_failure_not_found(self): kwargs={"pk": 111111111})) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_filebrowserfolderlinkfile_list_failure_not_found_unauthenticated(self): - response = self.client.get(reverse("chrisfolder-linkfile-list", - kwargs={"pk": 111111111})) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_filebrowserfolderlinkfile_list_file_folder_success(self): folder_path = os.path.dirname(self.link_path) folder = ChrisFolder.objects.get(path=folder_path) @@ -951,13 +862,6 @@ def test_filebrowserfolderlinkfile_list_file_folder_success(self): response = self.client.get(read_url) self.assertContains(response, self.link_path) - def test_filebrowserfolderlinkfile_list_file_folder_failure_not_found_unauthenticated(self): - folder_path = os.path.dirname(self.link_path) - folder = ChrisFolder.objects.get(path=folder_path) - read_url = reverse("chrisfolder-linkfile-list", kwargs={"pk": folder.id}) - response = self.client.get(read_url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_fileBrowserlinkfile_list_success_shared_feed(self): other_user = User.objects.get(username=self.other_username) plugin = self.plugin @@ -969,7 +873,6 @@ def test_fileBrowserlinkfile_list_success_shared_feed(self): plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) # create link file in the output folder link_path = f'{pl_inst.output_folder.path}/SERVICES_PACS.chrislink' @@ -978,6 +881,9 @@ def test_fileBrowserlinkfile_list_success_shared_feed(self): parent_folder=pl_inst.output_folder) link_file.save(name='SERVICES_PACS') + # share feed + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) + read_url = reverse("chrisfolder-linkfile-list", kwargs={"pk": pl_inst.output_folder.id}) self.client.login(username=self.username, password=self.password) response = self.client.get(read_url) @@ -985,7 +891,7 @@ def test_fileBrowserlinkfile_list_success_shared_feed(self): self.storage_manager.delete_obj(link_path) - def test_fileBrowserlinkfile_list_failure_not_found_shared_feed_unauthenticated(self): + def test_fileBrowserlinkfile_list_failure_shared_feed_unauthenticated(self): other_user = User.objects.get(username=self.other_username) plugin = self.plugin @@ -996,7 +902,6 @@ def test_fileBrowserlinkfile_list_failure_not_found_shared_feed_unauthenticated( plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) # create link file in the output folder link_path = f'{pl_inst.output_folder.path}/SERVICES_PACS.chrislink' @@ -1005,9 +910,12 @@ def test_fileBrowserlinkfile_list_failure_not_found_shared_feed_unauthenticated( parent_folder=pl_inst.output_folder) link_file.save(name='SERVICES_PACS') + # share feed + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) + read_url = reverse("chrisfolder-linkfile-list", kwargs={"pk": pl_inst.output_folder.id}) response = self.client.get(read_url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.storage_manager.delete_obj(link_path) @@ -1072,8 +980,7 @@ def test_fileBrowserlinkfile_detail_failure_unauthenticated(self): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_fileBrowserlinkfile_detail_success_public_feed_unauthenticated(self): - self.pl_inst.feed.public = True - self.pl_inst.feed.save() + self.pl_inst.feed.grant_public_access() response = self.client.get(self.read_url) self.assertContains(response, self.link_path) @@ -1088,7 +995,6 @@ def test_fileBrowserlinkfile_detail_success_shared_feed(self): plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) # create link file in the output folder link_path = f'{pl_inst.output_folder.path}/SERVICES_PACS.chrislink' @@ -1097,6 +1003,10 @@ def test_fileBrowserlinkfile_detail_success_shared_feed(self): parent_folder=pl_inst.output_folder) link_file.save(name='SERVICES_PACS') + # share feed + user = User.objects.get(username=self.username) + pl_inst.feed.grant_user_permission(user) + read_url = reverse("chrislinkfile-detail", kwargs={"pk": link_file.id}) self.client.login(username=self.username, password=self.password) response = self.client.get(read_url) diff --git a/chris_backend/users/tests/test_views.py b/chris_backend/users/tests/test_views.py index 5ab1d4c6..e75089d7 100755 --- a/chris_backend/users/tests/test_views.py +++ b/chris_backend/users/tests/test_views.py @@ -76,10 +76,8 @@ def test_integration_user_create_success(self): welcome_file = UserFile.objects.get(owner=user) self.assertEqual(welcome_file.fname.name, welcome_file_path) - # delete welcome file - storage_manager = connect_storage(settings) - - storage_manager.delete_obj(welcome_file_path) + # delete user and it's home tree + user.delete() def test_user_create_failure_already_exists(self): User.objects.create_user(username=self.username, From 24e50e196f9ad7efb6c4ff3af9599a2048004ec3 Mon Sep 17 00:00:00 2001 From: Jorge Date: Fri, 21 Jun 2024 19:13:16 -0400 Subject: [PATCH 07/11] Add filebrowser's services module tests --- .../filebrowser/tests/test_services.py | 405 ++++++++++++++++-- chris_backend/filebrowser/tests/test_views.py | 125 ++++-- .../plugininstances/tests/test_views.py | 2 +- 3 files changed, 476 insertions(+), 56 deletions(-) diff --git a/chris_backend/filebrowser/tests/test_services.py b/chris_backend/filebrowser/tests/test_services.py index f09c46eb..b2e545ff 100755 --- a/chris_backend/filebrowser/tests/test_services.py +++ b/chris_backend/filebrowser/tests/test_services.py @@ -18,29 +18,32 @@ class ServiceTests(TestCase): """ Test top-level functions in the services module """ + + # superuser chris (owner of root and top-level folders) + chris_username = 'chris' + chris_password = CHRIS_SUPERUSER_PASSWORD - def setUp(self): + # normal users + username = 'foo' + password = 'foopass' + other_username = 'boo' + other_password = 'boopass' + + @classmethod + def setUpClass(cls): + # avoid cluttered console output (for instance logging all the http requests) logging.disable(logging.WARNING) - # superuser chris (owner of root folders) - self.chris_username = 'chris' - self.chris_password = CHRIS_SUPERUSER_PASSWORD - - # create users - self.username = 'foo' - self.password = 'foopass' - self.another_username = 'boo' - self.another_password = 'boopass' - # create users with their home folders setup - UserProxy.objects.create_user(username=self.username, password=self.password) - UserProxy.objects.create_user(username=self.another_username, - password=self.another_password) + UserProxy.objects.create_user(username=cls.username, password=cls.password) + UserProxy.objects.create_user(username=cls.other_username, + password=cls.other_password) - def tearDown(self): - User.objects.get(username=self.username).delete() - User.objects.get(username=self.another_username).delete() + @classmethod + def tearDownClass(cls): + User.objects.get(username=cls.username).delete() + User.objects.get(username=cls.other_username).delete() # re-enable logging logging.disable(logging.NOTSET) @@ -56,10 +59,10 @@ def test_get_folder_queryset_folder_does_not_exist(self): qs = services.get_folder_queryset(pk_dict, user) self.assertEqual(qs.count(), 0) - def test_get_folder_queryset_from_user_for_chris_user(self): + def test_get_folder_queryset_from_user_success_for_chris_user(self): """ Test whether the services.get_folder_queryset function - allows the chris user to see any existing folder. + allows the chris superuser to see any existing folder. """ chris_user = User.objects.get(username=self.chris_username) path = f'home/{self.username}/uploads' @@ -68,42 +71,95 @@ def test_get_folder_queryset_from_user_for_chris_user(self): self.assertEqual(qs.count(), 1) self.assertEqual(qs.first().path, pk_dict['path']) - def test_get_folder_queryset_from_user_prevent_another_user(self): + def test_get_folder_queryset_from_user_failure_other_user(self): """ Test whether the services.get_folder_queryset function doesn't allow a user to see other user's existing private folders. """ - another_user = User.objects.get(username=self.another_username) + other_user = User.objects.get(username=self.other_username) path = f'home/{self.username}/uploads' pk_dict = {'path': path} - qs = services.get_folder_queryset(pk_dict, another_user) + qs = services.get_folder_queryset(pk_dict, other_user) self.assertEqual(qs.count(), 0) - def test_get_folder_queryset_top_level_folders(self): + def test_get_folder_queryset_from_user_public_success_other_user(self): """ Test whether the services.get_folder_queryset function - returns the appropriate queryset for top-level folders for any user. + allows a user to see other user's existing public folders. """ - chris_user = User.objects.get(username=self.chris_username) - ChrisFolder.objects.get_or_create(path='PIPELINES', owner=chris_user) - ChrisFolder.objects.get_or_create(path='SERVICES', owner=chris_user) + other_user = User.objects.get(username=self.other_username) + path = f'home/{self.username}/uploads' + folder = ChrisFolder.objects.get(path=path) + folder.grant_public_access() + pk_dict = {'path': path} + qs = services.get_folder_queryset(pk_dict, other_user) + self.assertEqual(qs.count(), 1) + self.assertEqual(qs.first().path, pk_dict['path']) + folder.remove_public_access() + def test_get_folder_queryset_from_user_shared_success_other_user(self): + """ + Test whether the services.get_folder_queryset function + allows a user to see other user's existing shared folders. + """ + other_user = User.objects.get(username=self.other_username) + path = f'home/{self.username}/uploads' + folder = ChrisFolder.objects.get(path=path) + folder.grant_user_permission(other_user, 'r') + pk_dict = {'path': path} + qs = services.get_folder_queryset(pk_dict, other_user) + self.assertEqual(qs.count(), 1) + self.assertEqual(qs.first().path, pk_dict['path']) + folder.remove_user_permission(other_user, 'r') + + def test_get_folder_queryset_from_user_shared_group_success_other_user(self): + """ + Test whether the services.get_folder_queryset function + allows a user to see other user's existing shared folders with its group. + """ + other_user = User.objects.get(username=self.other_username) + other_user_group = other_user.groups.first() + path = f'home/{self.username}/uploads' + folder = ChrisFolder.objects.get(path=path) + folder.grant_group_permission(other_user_group, 'r') + pk_dict = {'path': path} + qs = services.get_folder_queryset(pk_dict, other_user) + self.assertEqual(qs.count(), 1) + self.assertEqual(qs.first().path, pk_dict['path']) + folder.remove_group_permission(other_user_group, 'r') + + def test_get_folder_queryset_top_level_folders(self): + """ + Test whether the services.get_folder_queryset function returns the + appropriate queryset for top-level folders for any authenticated user. + """ user = User.objects.get(username=self.username) - another_user = User.objects.get(username=self.another_username) + other_user = User.objects.get(username=self.other_username) - for path in ('', 'home', 'PIPELINES', 'SERVICES'): + for path in ('', 'home', 'PIPELINES', 'SERVICES', 'PUBLIC', 'SHARED'): pk_dict = {'path': path} qs = services.get_folder_queryset(pk_dict, user) self.assertEqual(qs.count(), 1) self.assertEqual(qs.first().path, pk_dict['path']) - qs = services.get_folder_queryset(pk_dict, another_user) + qs = services.get_folder_queryset(pk_dict, other_user) + self.assertEqual(qs.count(), 1) + self.assertEqual(qs.first().path, pk_dict['path']) + + def test_get_folder_queryset_top_level_folders_unauthenticated(self): + """ + Test whether the services.get_folder_queryset function returns the + appropriate queryset for top-level folders for unauthenticated users. + """ + for path in ('', 'PIPELINES', 'PUBLIC'): + pk_dict = {'path': path} + qs = services.get_folder_queryset(pk_dict) self.assertEqual(qs.count(), 1) self.assertEqual(qs.first().path, pk_dict['path']) def test_get_folder_queryset_user_space(self): """ Test whether the services.get_folder_queryset function - allows the chris user to see any existing folder. + allows users to see any existing folder owned by them. """ user = User.objects.get(username=self.username) path = f'home/{self.username}' @@ -111,3 +167,290 @@ def test_get_folder_queryset_user_space(self): qs = services.get_folder_queryset(pk_dict, user) self.assertEqual(qs.count(), 1) self.assertEqual(qs.first().path, pk_dict['path']) + + def test_get_folder_children_queryset_from_user_success_for_chris_user(self): + """ + Test whether the services.get_folder_children_queryset function + allows the chris superuser to see all the child folders of any existing folder. + """ + chris_user = User.objects.get(username=self.chris_username) + path = f'home/{self.username}' + folder = ChrisFolder.objects.get(path=path) + qs = services.get_folder_children_queryset(folder, chris_user) + self.assertEqual(qs.count(), 2) + self.assertIn(f'{path}/uploads', [f.path for f in qs.all()]) + self.assertIn(f'{path}/feeds', [f.path for f in qs.all()]) + + def test_get_folder_children_queryset_from_user_failure_other_user(self): + """ + Test whether the services.get_folder_children_queryset function + doesn't allow a user to see the private child folders of other user's + existing private folders. + """ + other_user = User.objects.get(username=self.other_username) + path = f'home/{self.username}' + folder = ChrisFolder.objects.get(path=path) + qs = services.get_folder_children_queryset(folder, other_user) + self.assertEqual(qs.count(), 0) + + def test_get_folder_children_queryset_from_user_public_success_other_user(self): + """ + Test whether the services.get_folder_children_queryset function + allows a user to see the child folders of other user's existing public folders. + """ + other_user = User.objects.get(username=self.other_username) + path = f'home/{self.username}' + folder = ChrisFolder.objects.get(path=path) + folder.grant_public_access() + qs = services.get_folder_children_queryset(folder, other_user) + self.assertEqual(qs.count(), 2) + self.assertIn(f'{path}/uploads', [f.path for f in qs.all()]) + self.assertIn(f'{path}/feeds', [f.path for f in qs.all()]) + folder.remove_public_access() + + def test_get_folder_children_queryset_from_user_shared_success_other_user(self): + """ + Test whether the services.get_folder_children_queryset function + allows a user to see the child folders of other user's existing shared folders. + """ + other_user = User.objects.get(username=self.other_username) + path = f'home/{self.username}' + folder = ChrisFolder.objects.get(path=path) + folder.grant_user_permission(other_user, 'r') + qs = services.get_folder_children_queryset(folder, other_user) + self.assertEqual(qs.count(), 2) + self.assertIn(f'{path}/uploads', [f.path for f in qs.all()]) + self.assertIn(f'{path}/feeds', [f.path for f in qs.all()]) + folder.remove_user_permission(other_user, 'r') + + def test_get_folder_children_queryset_from_user_shared_group_success_other_user(self): + """ + Test whether the services.get_folder_children_queryset function + allows a user to see the child folders of other user's existing folders + shared with its group. + """ + other_user = User.objects.get(username=self.other_username) + other_user_group = other_user.groups.first() + path = f'home/{self.username}' + folder = ChrisFolder.objects.get(path=path) + folder.grant_group_permission(other_user_group, 'r') + qs = services.get_folder_children_queryset(folder, other_user) + self.assertEqual(qs.count(), 2) + paths = [f.path for f in qs.all()] + self.assertIn(f'{path}/uploads', paths) + self.assertIn(f'{path}/feeds', paths) + folder.remove_group_permission(other_user_group, 'r') + + def test_get_folder_children_queryset_top_level_folders(self): + """ + Test whether the services.get_folder_children_queryset function returns the + appropriate queryset for top-level folders for any authenticated user. + """ + user = User.objects.get(username=self.username) + + folder = ChrisFolder.objects.get(path='') + qs = services.get_folder_children_queryset(folder, user) + self.assertEqual(qs.count(), 5) + paths = [f.path for f in qs.all()] + + for path in ('home', 'PIPELINES', 'SERVICES', 'PUBLIC', 'SHARED'): + self.assertIn(path, paths) + + def test_get_folder_children_queryset_top_level_folders_unauthenticated(self): + """ + Test whether the services.get_folder_children_queryset function returns the + appropriate queryset for top-level folders for unauthenticated users. + """ + folder = ChrisFolder.objects.get(path='') + qs = services.get_folder_children_queryset(folder) + self.assertEqual(qs.count(), 2) + paths = [f.path for f in qs.all()] + + for path in ('PIPELINES', 'PUBLIC'): + self.assertIn(path, paths) + + def test_get_folder_children_queryset_user_space(self): + """ + Test whether the services.get_folder_children_queryset function + allows users to see any existing folder owned by them. + """ + user = User.objects.get(username=self.username) + path = f'home/{self.username}' + folder = ChrisFolder.objects.get(path=path) + qs = services.get_folder_children_queryset(folder, user) + self.assertEqual(qs.count(), 2) + paths = [f.path for f in qs.all()] + self.assertIn(f'{path}/uploads', paths) + self.assertIn(f'{path}/feeds', paths) + + + def test_get_folder_files_queryset_from_user_success_for_chris_user(self): + """ + Test whether the services.get_folder_files_queryset function + allows the chris superuser to see all the child files of any existing folder. + """ + chris_user = User.objects.get(username=self.chris_username) + path = f'home/{self.username}/uploads' + folder = ChrisFolder.objects.get(path=path) + qs = services.get_folder_files_queryset(folder, chris_user) + self.assertEqual(qs.count(), 1) + self.assertEqual(f'{path}/welcome.txt', qs.first().fname.name) + + def test_get_folder_files_queryset_from_user_failure_other_user(self): + """ + Test whether the services.get_folder_files_queryset function + doesn't allow a user to see the private child files of other user's + existing private folders. + """ + other_user = User.objects.get(username=self.other_username) + path = f'home/{self.username}/uploads' + folder = ChrisFolder.objects.get(path=path) + qs = services.get_folder_files_queryset(folder, other_user) + self.assertEqual(qs.count(), 0) + + def test_get_folder_files_queryset_from_user_public_success_other_user(self): + """ + Test whether the services.get_folder_files_queryset function + allows a user to see the child files of other user's existing public folders. + """ + other_user = User.objects.get(username=self.other_username) + path = f'home/{self.username}/uploads' + folder = ChrisFolder.objects.get(path=path) + folder.grant_public_access() + qs = services.get_folder_files_queryset(folder, other_user) + self.assertEqual(qs.count(), 1) + self.assertEqual(f'{path}/welcome.txt', qs.first().fname.name) + folder.remove_public_access() + + def test_get_folder_files_queryset_from_user_shared_success_other_user(self): + """ + Test whether the services.get_folder_files_queryset function + allows a user to see the child files of other user's existing shared folders. + """ + other_user = User.objects.get(username=self.other_username) + path = f'home/{self.username}/uploads' + folder = ChrisFolder.objects.get(path=path) + folder.grant_user_permission(other_user, 'r') + qs = services.get_folder_files_queryset(folder, other_user) + self.assertEqual(qs.count(), 1) + self.assertEqual(f'{path}/welcome.txt', qs.first().fname.name) + folder.remove_user_permission(other_user, 'r') + + def test_get_folder_files_queryset_from_user_shared_group_success_other_user(self): + """ + Test whether the services.get_folder_files_queryset function + allows a user to see the child files of other user's existing folders + shared with its group. + """ + other_user = User.objects.get(username=self.other_username) + other_user_group = other_user.groups.first() + path = f'home/{self.username}/uploads' + folder = ChrisFolder.objects.get(path=path) + folder.grant_group_permission(other_user_group, 'r') + qs = services.get_folder_files_queryset(folder, other_user) + self.assertEqual(qs.count(), 1) + self.assertEqual(f'{path}/welcome.txt', qs.first().fname.name) + folder.remove_group_permission(other_user_group, 'r') + + def test_get_folder_files_queryset_user_space(self): + """ + Test whether the services.get_folder_files_queryset function + allows users to see any existing folder's files owned by them. + """ + user = User.objects.get(username=self.username) + path = f'home/{self.username}/uploads' + folder = ChrisFolder.objects.get(path=path) + qs = services.get_folder_files_queryset(folder, user) + self.assertEqual(qs.count(), 1) + self.assertEqual(f'{path}/welcome.txt', qs.first().fname.name) + + + def test_get_folder_link_files_queryset_from_user_success_for_chris_user(self): + """ + Test whether the services.get_folder_link_files_queryset function + allows the chris superuser to see all the child link files of any existing folder. + """ + chris_user = User.objects.get(username=self.chris_username) + path = f'home/{self.username}' + folder = ChrisFolder.objects.get(path=path) + qs = services.get_folder_link_files_queryset(folder, chris_user) + self.assertEqual(qs.count(), 2) + paths = [lf.fname.name for lf in qs.all()] + self.assertIn(f'{path}/public.chrislink', paths) + self.assertIn(f'{path}/shared.chrislink', paths) + + def test_get_folder_link_files_queryset_from_user_failure_other_user(self): + """ + Test whether the services.get_folder_link_files_queryset function + doesn't allow a user to see the private child link files of other user's + existing private folders. + """ + other_user = User.objects.get(username=self.other_username) + path = f'home/{self.username}' + folder = ChrisFolder.objects.get(path=path) + qs = services.get_folder_link_files_queryset(folder, other_user) + self.assertEqual(qs.count(), 0) + + def test_get_folder_link_files_queryset_from_user_public_success_other_user(self): + """ + Test whether the services.get_folder_link_files_queryset function + allows a user to see the child link files of other user's existing public folders. + """ + other_user = User.objects.get(username=self.other_username) + path = f'home/{self.username}' + folder = ChrisFolder.objects.get(path=path) + folder.grant_public_access() + qs = services.get_folder_link_files_queryset(folder, other_user) + self.assertEqual(qs.count(), 2) + paths = [lf.fname.name for lf in qs.all()] + self.assertIn(f'{path}/public.chrislink', paths) + self.assertIn(f'{path}/shared.chrislink', paths) + folder.remove_public_access() + + def test_get_folder_link_files_queryset_from_user_shared_success_other_user(self): + """ + Test whether the services.get_folder_link_files_queryset function + allows a user to see the child link files of other user's existing shared folders. + """ + other_user = User.objects.get(username=self.other_username) + path = f'home/{self.username}' + folder = ChrisFolder.objects.get(path=path) + folder.grant_user_permission(other_user, 'r') + qs = services.get_folder_link_files_queryset(folder, other_user) + self.assertEqual(qs.count(), 2) + paths = [lf.fname.name for lf in qs.all()] + self.assertIn(f'{path}/public.chrislink', paths) + self.assertIn(f'{path}/shared.chrislink', paths) + folder.remove_user_permission(other_user, 'r') + + def test_get_folder_link_files_queryset_from_user_shared_group_success_other_user(self): + """ + Test whether the services.get_folder_link_files_queryset function + allows a user to see the child link files of other user's existing folders + shared with its group. + """ + other_user = User.objects.get(username=self.other_username) + other_user_group = other_user.groups.first() + path = f'home/{self.username}' + folder = ChrisFolder.objects.get(path=path) + folder.grant_group_permission(other_user_group, 'r') + qs = services.get_folder_link_files_queryset(folder, other_user) + self.assertEqual(qs.count(), 2) + paths = [lf.fname.name for lf in qs.all()] + self.assertIn(f'{path}/public.chrislink', paths) + self.assertIn(f'{path}/shared.chrislink', paths) + folder.remove_group_permission(other_user_group, 'r') + + def test_get_folder_link_files_queryset_user_space(self): + """ + Test whether the services.get_folder_link_files_queryset function + allows users to see any existing folder's link files owned by them. + """ + user = User.objects.get(username=self.username) + path = f'home/{self.username}' + folder = ChrisFolder.objects.get(path=path) + qs = services.get_folder_link_files_queryset(folder, user) + self.assertEqual(qs.count(), 2) + paths = [lf.fname.name for lf in qs.all()] + self.assertIn(f'{path}/public.chrislink', paths) + self.assertIn(f'{path}/shared.chrislink', paths) diff --git a/chris_backend/filebrowser/tests/test_views.py b/chris_backend/filebrowser/tests/test_views.py index e789c63f..cadff7c8 100755 --- a/chris_backend/filebrowser/tests/test_views.py +++ b/chris_backend/filebrowser/tests/test_views.py @@ -2,6 +2,7 @@ import logging import io import os +import json from unittest import mock from django.test import TestCase, tag @@ -28,30 +29,33 @@ class FileBrowserViewTests(TestCase): Generic filebrowser view tests' setup and tearDown. """ - def setUp(self): - # avoid cluttered console output (for instance logging all the http requests) - logging.disable(logging.WARNING) + content_type = 'application/vnd.collection+json' - self.content_type = 'application/vnd.collection+json' + # superuser chris (owner of root and top-level folders) + chris_username = 'chris' + chris_password = CHRIS_SUPERUSER_PASSWORD - # superuser chris (owner of root folders) - self.chris_username = 'chris' - self.chris_password = CHRIS_SUPERUSER_PASSWORD + # normal users + username = 'foo' + password = 'foopass' + other_username = 'boo' + other_password = 'boopass' - # normal users - self.username = 'foo' - self.password = 'foopass' - self.other_username = 'boo' - self.other_password = 'boopass' + @classmethod + def setUpClass(cls): + + # avoid cluttered console output (for instance logging all the http requests) + logging.disable(logging.WARNING) - # create users - UserProxy.objects.create_user(username=self.username, password=self.password) - UserProxy.objects.create_user(username=self.other_username, - password=self.other_password) + # create users with their home folders setup + UserProxy.objects.create_user(username=cls.username, password=cls.password) + UserProxy.objects.create_user(username=cls.other_username, + password=cls.other_password) - def tearDown(self): - User.objects.get(username=self.username).delete() - User.objects.get(username=self.other_username).delete() + @classmethod + def tearDownClass(cls): + User.objects.get(username=cls.username).delete() + User.objects.get(username=cls.other_username).delete() # re-enable logging logging.disable(logging.NOTSET) @@ -64,17 +68,33 @@ class FileBrowserFolderListViewTests(FileBrowserViewTests): def setUp(self): super(FileBrowserFolderListViewTests, self).setUp() - self.read_url = reverse('chrisfolder-list') + self.create_read_url = reverse('chrisfolder-list') + self.post = json.dumps( + {"template": + {"data": [{"name": "path", + "value": f"home/{self.username}/uploads/folder1/folder2"}]}}) + + def test_filebrowserfolder_create_success(self): + self.client.login(username=self.username, password=self.password) + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.data["path"], f"home/{self.username}/uploads/folder1/folder2") + + def test_filebrowserfolder_create_failure_unauthenticated(self): + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_filebrowserfolder_list_success(self): self.client.login(username=self.username, password=self.password) - response = self.client.get(self.read_url) + response = self.client.get(self.create_read_url) self.assertContains(response, 'path') #import pdb; pdb.set_trace() self.assertEqual(response.data['results'][0]['path'],'') def test_filebrowserfolder_list_success_unauthenticated(self): - response = self.client.get(self.read_url) + response = self.client.get(self.create_read_url) self.assertContains(response, 'path') self.assertEqual(response.data['results'][0]['path'],'') @@ -244,11 +264,68 @@ def setUp(self): self.plugin = plugin + self.folder = ChrisFolder.objects.get(path=f'home/{self.username}') + self.read_update_delete_url = reverse("chrisfolder-detail", + kwargs={"pk": self.folder.id}) + + self.put = json.dumps({ + "template": {"data": [{"name": "public", "value": True}]}}) + + def test_filebrowserfolder_detail_success(self): + self.client.login(username=self.username, password=self.password) + response = self.client.get(self.read_update_delete_url) + self.assertContains(response, f'home/{self.username}') + + def test_filebrowserfolder_detail_failure_unauthenticated(self): + response = self.client.get(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfolder_update_success(self): + self.client.login(username=self.username, password=self.password) + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["public"],True) + self.folder.remove_public_access() + + def test_filebrowserfolder_update_failure_unauthenticated(self): + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfolder_update_failure_user_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_filebrowserfolder_delete_success(self): + # create a folder + owner = User.objects.get(username=self.username) + folder, _ = ChrisFolder.objects.get_or_create( + path=f'home/{self.username}/uploads/test', owner=owner) + + read_update_delete_url = reverse("chrisfolder-detail", + kwargs={"pk": folder.id}) + self.client.login(username=self.username, password=self.password) + response = self.client.delete(read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_filebrowserfolder_delete_failure_unauthenticated(self): + response = self.client.delete(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfolder_delete_failure_user_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.delete(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_filebrowserfolder_home_folder_success(self): folder = ChrisFolder.objects.get(path='home') - read_url = reverse("chrisfolder-detail", kwargs={"pk": folder.id}) + read_update_delete_url = reverse("chrisfolder-detail", + kwargs={"pk": folder.id}) self.client.login(username=self.username, password=self.password) - response = self.client.get(read_url) + response = self.client.get(read_update_delete_url) self.assertContains(response, 'home') def test_filebrowserfolder_home_folder_failure_unauthenticated(self): diff --git a/chris_backend/plugininstances/tests/test_views.py b/chris_backend/plugininstances/tests/test_views.py index d86733bb..99daeeef 100755 --- a/chris_backend/plugininstances/tests/test_views.py +++ b/chris_backend/plugininstances/tests/test_views.py @@ -37,7 +37,7 @@ def setUp(self): # avoid cluttered console output (for instance logging all the http requests) logging.disable(logging.WARNING) - # create superuser chris (owner of root folders) + # superuser chris (owner of root and top-level folders) self.chris_username = 'chris' self.chris_password = CHRIS_SUPERUSER_PASSWORD From c39b0d8a7c14ea1d430806b5de7aaffdf94f285f Mon Sep 17 00:00:00 2001 From: Jorge Date: Thu, 27 Jun 2024 17:55:46 -0400 Subject: [PATCH 08/11] Add permission-related tests to feeds --- chris_backend/core/api.py | 12 +- chris_backend/feeds/serializers.py | 27 +- chris_backend/feeds/tests/test_views.py | 424 +++- chris_backend/feeds/views.py | 4 +- chris_backend/filebrowser/serializers.py | 77 +- .../filebrowser/tests/test_services.py | 8 +- chris_backend/filebrowser/tests/test_views.py | 1873 +++++++++++++++-- chris_backend/filebrowser/views.py | 33 +- 8 files changed, 2196 insertions(+), 262 deletions(-) diff --git a/chris_backend/core/api.py b/chris_backend/core/api.py index 22132cc4..5d54fc54 100755 --- a/chris_backend/core/api.py +++ b/chris_backend/core/api.py @@ -83,27 +83,27 @@ path('v1//grouppermissions/', feed_views.FeedGroupPermissionList.as_view(), - name='feed-group-permission-list'), + name='feedgrouppermission-list'), path('v1//grouppermissions/search/', feed_views.FeedGroupPermissionListQuerySearch.as_view(), - name='feed-group-permission-list-query-search'), + name='feedgrouppermission-list-query-search'), path('v1/grouppermissions//', feed_views.FeedGroupPermissionDetail.as_view(), - name='feed-group-permission-detail'), + name='feedgrouppermission-detail'), path('v1//userpermissions/', feed_views.FeedUserPermissionList.as_view(), - name='feed-user-permission-list'), + name='feeduserpermission-list'), path('v1//userpermissions/search/', feed_views.FeedUserPermissionListQuerySearch.as_view(), - name='feed-user-permission-list-query-search'), + name='feeduserpermission-list-query-search'), path('v1/userpermissions//', feed_views.FeedUserPermissionDetail.as_view(), - name='feed-user-permission-detail'), + name='feeduserpermission-detail'), path('v1//comments/', feed_views.CommentList.as_view(), name='comment-list'), diff --git a/chris_backend/feeds/serializers.py b/chris_backend/feeds/serializers.py index d7ed42cf..9fcc4740 100755 --- a/chris_backend/feeds/serializers.py +++ b/chris_backend/feeds/serializers.py @@ -102,6 +102,10 @@ class FeedSerializer(serializers.HyperlinkedModelSerializer): folder = serializers.HyperlinkedRelatedField(view_name='chrisfolder-detail', read_only=True) note = serializers.HyperlinkedRelatedField(view_name='note-detail', read_only=True) + group_permissions = serializers.HyperlinkedIdentityField( + view_name='feedgrouppermission-list') + user_permissions = serializers.HyperlinkedIdentityField( + view_name='feeduserpermission-list') tags = serializers.HyperlinkedIdentityField(view_name='feed-tag-list') taggings = serializers.HyperlinkedIdentityField(view_name='feed-tagging-list') comments = serializers.HyperlinkedIdentityField(view_name='comment-list') @@ -115,7 +119,8 @@ class Meta: 'owner_username', 'folder_path', 'created_jobs', 'waiting_jobs', 'scheduled_jobs', 'started_jobs', 'registering_jobs', 'finished_jobs', 'errored_jobs', 'cancelled_jobs', 'folder', 'note', - 'tags', 'taggings', 'comments', 'plugin_instances', 'owner') + 'group_permissions', 'user_permissions', 'tags', 'taggings', + 'comments', 'plugin_instances', 'owner') def update(self, instance, validated_data): """ @@ -231,11 +236,13 @@ class FeedGroupPermissionSerializer(serializers.HyperlinkedModelSerializer): feed_name = serializers.ReadOnlyField(source='feed.name') group_id = serializers.ReadOnlyField(source='group.id') group_name = serializers.ReadOnlyField(source='group.name') + feed = serializers.HyperlinkedRelatedField(view_name='feed-detail', read_only=True) + group = serializers.HyperlinkedRelatedField(view_name='group-detail', read_only=True) class Meta: model = FeedGroupPermission fields = ('url', 'id', 'feed_id', 'feed_name', 'group_id', 'group_name', - 'feed', 'group', 'name') + 'feed', 'group', 'grp_name') def create(self, validated_data): """ @@ -260,13 +267,13 @@ def create(self, validated_data): def validate_grp_name(self, grp_name): """ - Custom method to check whether the provided group name exists in the DB. + Overriden to check whether the provided group name exists in the DB. """ try: group = Group.objects.get(name=grp_name) except Group.DoesNotExist: - raise serializers.ValidationError( - {'grp_name': [f"Couldn't find any group with name '{grp_name}'."]}) + raise serializers.ValidationError([f"Couldn't find any group with name " + f"'{grp_name}'."]) return group class FeedUserPermissionSerializer(serializers.HyperlinkedModelSerializer): @@ -275,10 +282,12 @@ class FeedUserPermissionSerializer(serializers.HyperlinkedModelSerializer): feed_name = serializers.ReadOnlyField(source='feed.name') user_id = serializers.ReadOnlyField(source='user.id') user_username = serializers.ReadOnlyField(source='user.username') + feed = serializers.HyperlinkedRelatedField(view_name='feed-detail', read_only=True) + user = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True) class Meta: model = FeedUserPermission - fields = ('url', 'id', 'feed_id', 'feed_name', 'user_id', 'user_username', + fields = ('url', 'id', 'feed_id', 'feed_name', 'user_id','user_username', 'feed', 'user', 'username') def create(self, validated_data): @@ -304,13 +313,13 @@ def create(self, validated_data): def validate_username(self, username): """ - Custom method to check whether the provided username exists in the DB. + Overriden to check whether the provided username exists in the DB. """ try: user = User.objects.get(username=username) except User.DoesNotExist: - raise serializers.ValidationError( - {'username': [f"Couldn't find any user with username '{username}'."]}) + raise serializers.ValidationError([f"Couldn't find any user with username " + f"'{username}'."]) return user diff --git a/chris_backend/feeds/tests/test_views.py b/chris_backend/feeds/tests/test_views.py index ce9e83db..32dc12db 100755 --- a/chris_backend/feeds/tests/test_views.py +++ b/chris_backend/feeds/tests/test_views.py @@ -4,13 +4,14 @@ from django.test import TestCase from django.urls import reverse -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Group from django.conf import settings from rest_framework import status from plugins.models import PluginMeta, Plugin, ComputeResource from plugininstances.models import PluginInstance -from feeds.models import Note, Tag, Tagging, Feed, Comment +from feeds.models import (Note, Tag, Tagging, Feed, FeedGroupPermission, + FeedUserPermission, Comment) COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL @@ -30,9 +31,9 @@ def setUp(self): self.content_type='application/vnd.collection+json' self.username = 'foo' - self.password = 'bar' - self.other_username = 'boo' - self.other_password = 'far' + self.password = 'foopass' + self.other_username = 'booo' + self.other_password = 'booopass' self.plugin_name = "pacspull" self.plugin_type = "fs" @@ -46,10 +47,16 @@ def setUp(self): # create basic models # create users - User.objects.create_user(username=self.other_username, - password=self.other_password) + other_user = User.objects.create_user(username=self.other_username, + password=self.other_password) user = User.objects.create_user(username=self.username, password=self.password) + + # assign predefined group + all_grp = Group.objects.get(name='all_users') + + other_user.groups.set([all_grp]) + user.groups.set([all_grp]) # create two plugins of different types @@ -252,6 +259,7 @@ def test_feed_update_success(self): self.assertContains(response, "Updated") feed = Feed.objects.get(name="Updated") self.assertTrue(feed.public) + feed.remove_public_access() def test_feed_update_failure_unauthenticated(self): response = self.client.put(self.read_update_delete_url, data=self.put, @@ -280,6 +288,408 @@ def test_feed_delete_failure_access_denied(self): self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) +class FeedGroupPermissionListViewTests(ViewTests): + """ + Test the 'feedgrouppermission-list' view. + """ + + def setUp(self): + super(FeedGroupPermissionListViewTests, self).setUp() + + self.grp_name = 'all_users' + + feed = Feed.objects.get(name=self.feedname) + + self.create_read_url = reverse('feedgrouppermission-list', + kwargs={"pk": feed.id}) + self.post = json.dumps( + {"template": + {"data": [{"name": "grp_name", "value": self.grp_name}]}}) + + def test_feed_group_permission_create_success(self): + self.client.login(username=self.username, password=self.password) + + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + feed = Feed.objects.get(name=self.feedname) + + self.assertIn(self.grp_name, [g.name for g in feed.shared_groups.all()]) + self.assertIn(self.grp_name, [g.name for g in feed.folder.shared_groups.all()]) + + grp = Group.objects.get(name=self.grp_name) + feed.remove_group_permission(grp) + feed.folder.remove_shared_link() + + def ttest_feed_group_permission_create_failure_unauthenticated(self): + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_feed_group_permission_create_failure_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_feed_group_permission_shared_create_failure_access_denied(self): + other_user = User.objects.get(username=self.other_username) + feed = Feed.objects.get(name=self.feedname) + feed.grant_user_permission(other_user) + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + feed.remove_user_permission(other_user) + + def test_feed_group_permission_list_success(self): + grp = Group.objects.get(name=self.grp_name) + feed = Feed.objects.get(name=self.feedname) + feed.grant_group_permission(grp) + + self.client.login(username=self.username, password=self.password) + response = self.client.get(self.create_read_url) + self.assertContains(response, self.grp_name) + + feed.remove_group_permission(grp) + + def test_feed_group_permission_list_failure_unauthenticated(self): + response = self.client.get(self.create_read_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_feed_group_permission_list_failure_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.create_read_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_feed_group_permission_shared_user_list_success(self): + other_user = User.objects.get(username=self.other_username) + feed = Feed.objects.get(name=self.feedname) + feed.grant_user_permission(other_user) + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.create_read_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + feed.remove_user_permission(other_user) + + +class FeedGroupPermissionListQuerySearchViewTests(ViewTests): + """ + Test the 'feedgrouppermission-list-query-search' view. + """ + + def setUp(self): + super(FeedGroupPermissionListQuerySearchViewTests, self).setUp() + + self.grp_name = 'all_users' + + feed = Feed.objects.get(name=self.feedname) + + self.read_url = reverse('feedgrouppermission-list-query-search', + kwargs={"pk": feed.id}) + + grp = Group.objects.get(name=self.grp_name) + feed.grant_group_permission(grp) + + def tearDown(self): + grp = Group.objects.get(name=self.grp_name) + feed = Feed.objects.get(name=self.feedname) + feed.remove_group_permission(grp) + + super(FeedGroupPermissionListQuerySearchViewTests, self).tearDown() + + def test_feed_group_permission_list_query_search_success(self): + read_url = f'{self.read_url}?group_name={self.grp_name}' + + self.client.login(username=self.username, password=self.password) + response = self.client.get(read_url) + self.assertContains(response, self.grp_name) + + def test_feed_group_permission_list_query_search_success_shared(self): + read_url = f'{self.read_url}?group_name={self.grp_name}' + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(read_url) + self.assertContains(response, self.grp_name) + + def test_feed_group_permission_list_query_search_failure_unauthenticated(self): + read_url = f'{self.read_url}?group_name={self.grp_name}' + + response = self.client.get(read_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_feed_group_permission_list_query_search_failure_other_user(self): + grp = Group.objects.get(name=self.grp_name) + feed = Feed.objects.get(name=self.feedname) + feed.remove_group_permission(grp) + + read_url = f'{self.read_url}?group_name={self.grp_name}' + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(read_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertFalse(response.data['results']) + + +class FeedGroupPermissionDetailViewTests(ViewTests): + """ + Test the feedgrouppermission-detail view. + """ + + def setUp(self): + super(FeedGroupPermissionDetailViewTests, self).setUp() + + self.grp_name = 'all_users' + + feed = Feed.objects.get(name=self.feedname) + + grp = Group.objects.get(name=self.grp_name) + feed.grant_group_permission(grp) + + gp = FeedGroupPermission.objects.get(group=grp, feed=feed) + + self.read_delete_url = reverse("feedgrouppermission-detail", + kwargs={"pk": gp.id}) + + def tearDown(self): + grp = Group.objects.get(name=self.grp_name) + feed = Feed.objects.get(name=self.feedname) + feed.remove_group_permission(grp) + #feed.folder.remove_shared_link() + + super(FeedGroupPermissionDetailViewTests, self).tearDown() + + def test_feed_group_permission_detail_success(self): + self.client.login(username=self.username, password=self.password) + response = self.client.get(self.read_delete_url) + self.assertContains(response, 'all_users') + self.assertContains(response, self.feedname) + + def test_feed_group_permission_detail_shared_success(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.read_delete_url) + self.assertContains(response, 'all_users') + self.assertContains(response, self.feedname) + + def test_feed_group_permission_detail_failure_unauthenticated(self): + response = self.client.get(self.read_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_feed_group_permission_delete_success(self): + feed = Feed.objects.get(name=self.feedname) + grp = Group.objects.get(name='pacs_users') + + # create a group permission + feed.grant_group_permission(grp) + gp = FeedGroupPermission.objects.get(group=grp, feed=feed) + + read_update_delete_url = reverse("feedgrouppermission-detail", + kwargs={"pk": gp.id}) + self.client.login(username=self.username, password=self.password) + response = self.client.delete(read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_feed_group_permission_delete_failure_unauthenticated(self): + response = self.client.delete(self.read_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_feed_group_permission_delete_failure_user_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.delete(self.read_delete_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +class FeedUserPermissionListViewTests(ViewTests): + """ + Test the 'feeduserpermission-list' view. + """ + + def setUp(self): + super(FeedUserPermissionListViewTests, self).setUp() + + feed = Feed.objects.get(name=self.feedname) + + self.create_read_url = reverse('feeduserpermission-list', + kwargs={"pk": feed.id}) + self.post = json.dumps( + {"template": + {"data": [{"name": "username", "value": self.other_username}]}}) + + def test_feed_user_permission_create_success(self): + self.client.login(username=self.username, password=self.password) + + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + feed = Feed.objects.get(name=self.feedname) + + self.assertIn(self.other_username, [u.username for u in feed.shared_users.all()]) + self.assertIn(self.other_username, [u.username for u in + feed.folder.shared_users.all()]) + + other_user = User.objects.get(username=self.other_username) + feed.remove_user_permission(other_user) + feed.folder.remove_shared_link() + + def test_feed_user_permission_create_failure_unauthenticated(self): + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_feed_user_permission_create_failure_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_feed_user_permission_shared_create_failure_access_denied(self): + other_user = User.objects.get(username=self.other_username) + feed = Feed.objects.get(name=self.feedname) + feed.grant_user_permission(other_user) + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + feed.remove_user_permission(other_user) + + def test_feed_user_permission_list_success(self): + other_user = User.objects.get(username=self.other_username) + feed = Feed.objects.get(name=self.feedname) + feed.grant_user_permission(other_user) + + self.client.login(username=self.username, password=self.password) + response = self.client.get(self.create_read_url) + self.assertContains(response, self.other_username) + + feed.remove_user_permission(other_user) + + def test_feed_user_permission_list_failure_unauthenticated(self): + response = self.client.get(self.create_read_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_feed_user_permission_list_failure_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.create_read_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_feed_user_permission_shared_user_list_success(self): + other_user = User.objects.get(username=self.other_username) + feed = Feed.objects.get(name=self.feedname) + feed.grant_user_permission(other_user) + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.create_read_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + feed.remove_user_permission(other_user) + + +class FeedUserPermissionListQuerySearchViewTests(ViewTests): + """ + Test the 'feeduserpermission-list-query-search' view. + """ + + def setUp(self): + super(FeedUserPermissionListQuerySearchViewTests, self).setUp() + + feed = Feed.objects.get(name=self.feedname) + + self.read_url = reverse('feeduserpermission-list-query-search', + kwargs={"pk": feed.id}) + + other_user = User.objects.get(username=self.other_username) + feed.grant_user_permission(other_user) + + def tearDown(self): + other_user = User.objects.get(username=self.other_username) + feed = Feed.objects.get(name=self.feedname) + feed.remove_user_permission(other_user) + + super(FeedUserPermissionListQuerySearchViewTests, self).tearDown() + + def test_feed_user_permission_list_query_search_success(self): + read_url = f'{self.read_url}?username={self.other_username}' + + self.client.login(username=self.username, password=self.password) + response = self.client.get(read_url) + self.assertContains(response, self.other_username) + + def test_feed_user_permission_list_query_search_success_shared(self): + read_url = f'{self.read_url}?username={self.other_username}' + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(read_url) + self.assertContains(response, self.other_username) + + def test_feed_user_permission_list_query_search_failure_unauthenticated(self): + read_url = f'{self.read_url}?username={self.other_username}' + + response = self.client.get(read_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + +class FeedUserPermissionDetailViewTests(ViewTests): + """ + Test the feeduserpermission-detail view. + """ + + def setUp(self): + super(FeedUserPermissionDetailViewTests, self).setUp() + other_user = User.objects.get(username=self.other_username) + + feed = Feed.objects.get(name=self.feedname) + feed.grant_user_permission(other_user) + + up = FeedUserPermission.objects.get(user=other_user, feed=feed) + + self.read_delete_url = reverse("feeduserpermission-detail", + kwargs={"pk": up.id}) + + def tearDown(self): + other_user = User.objects.get(username=self.other_username) + feed = Feed.objects.get(name=self.feedname) + feed.remove_user_permission(other_user) + + super(FeedUserPermissionDetailViewTests, self).tearDown() + + def test_feed_user_permission_detail_success(self): + self.client.login(username=self.username, password=self.password) + response = self.client.get(self.read_delete_url) + self.assertContains(response, self.other_username) + self.assertContains(response, self.feedname) + + def test_feed_user_permission_detail_shared_success(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.read_delete_url) + self.assertContains(response, self.other_username) + self.assertContains(response, self.feedname) + + def test_feed_user_permission_detail_failure_unauthenticated(self): + response = self.client.get(self.read_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_feed_user_permission_delete_success(self): + self.client.login(username=self.username, password=self.password) + response = self.client.delete(self.read_delete_url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_feed_user_permission_delete_failure_unauthenticated(self): + response = self.client.delete(self.read_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_feed_user_permission_delete_failure_user_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.delete(self.read_delete_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + class CommentListViewTests(ViewTests): """ Test the comment-list view. diff --git a/chris_backend/feeds/views.py b/chris_backend/feeds/views.py index 7ff0b0bb..9f60feaa 100755 --- a/chris_backend/feeds/views.py +++ b/chris_backend/feeds/views.py @@ -438,9 +438,9 @@ def perform_create(self, serializer): """ Overriden to provide a group and feed before first saving to the DB. """ - group = serializer.validated_data.pop('name') + group = serializer.validated_data.pop('grp_name') feed = self.get_object() - serializer.save(user=group, feed=feed) + serializer.save(group=group, feed=feed) def list(self, request, *args, **kwargs): """ diff --git a/chris_backend/filebrowser/serializers.py b/chris_backend/filebrowser/serializers.py index 98517ab5..827fbdbf 100755 --- a/chris_backend/filebrowser/serializers.py +++ b/chris_backend/filebrowser/serializers.py @@ -118,11 +118,20 @@ def validate_public(self, public): def validate(self, data): """ Overriden to validate that required fields are in data when creating or - updating a folder. + updating a folder. Also to verify that the user's home or feeds folder is not + being moved. """ if self.instance: # on update if 'public' not in data: raise serializers.ValidationError({'public': ['This field is required.']}) + + username = self.context['request'].user.username + + if 'path' in data and username != 'chris' and self.instance.path in ( + f'home/{username}', f'home/{username}/feeds'): + raise serializers.ValidationError( + {'non_field_errors': + [f"Moving folder '{self.instance.path}' is not allowed."]}) else: if 'path' not in data: # on create raise serializers.ValidationError({'path': ['This field is required.']}) @@ -132,7 +141,7 @@ def validate(self, data): class FileBrowserFolderGroupPermissionSerializer(serializers.HyperlinkedModelSerializer): grp_name = serializers.CharField(write_only=True, required=False) folder_id = serializers.ReadOnlyField(source='folder.id') - folder_name = serializers.ReadOnlyField(source='folder.name') + folder_path = serializers.ReadOnlyField(source='folder.path') group_id = serializers.ReadOnlyField(source='group.id') group_name = serializers.ReadOnlyField(source='group.name') folder = serializers.HyperlinkedRelatedField(view_name='chrisfolder-detail', @@ -141,7 +150,7 @@ class FileBrowserFolderGroupPermissionSerializer(serializers.HyperlinkedModelSer class Meta: model = FolderGroupPermission - fields = ('url', 'id', 'permission', 'folder_id', 'folder_name', 'group_id', + fields = ('url', 'id', 'permission', 'folder_id', 'folder_path', 'group_id', 'group_name', 'folder', 'group', 'grp_name') def create(self, validated_data): @@ -168,13 +177,13 @@ def create(self, validated_data): def validate_grp_name(self, grp_name): """ - Custom method to check whether the provided group name exists in the DB. + Overriden to check whether the provided group name exists in the DB. """ try: group = Group.objects.get(name=grp_name) except Group.DoesNotExist: - raise serializers.ValidationError( - {'grp_name': [f"Couldn't find any group with name '{grp_name}'."]}) + raise serializers.ValidationError([f"Couldn't find any group with name " + f"'{grp_name}'."]) return group def validate(self, data): @@ -197,7 +206,7 @@ class FileBrowserFolderUserPermissionSerializer(serializers.HyperlinkedModelSeri username = serializers.CharField(write_only=True, min_length=4, max_length=32, required=False) folder_id = serializers.ReadOnlyField(source='folder.id') - folder_name = serializers.ReadOnlyField(source='folder.name') + folder_path = serializers.ReadOnlyField(source='folder.path') user_id = serializers.ReadOnlyField(source='user.id') user_username = serializers.ReadOnlyField(source='user.username') folder = serializers.HyperlinkedRelatedField(view_name='chrisfolder-detail', @@ -206,7 +215,7 @@ class FileBrowserFolderUserPermissionSerializer(serializers.HyperlinkedModelSeri class Meta: model = FolderUserPermission - fields = ('url', 'id', 'permission', 'folder_id', 'folder_name', 'user_id', + fields = ('url', 'id', 'permission', 'folder_id', 'folder_path', 'user_id', 'user_username', 'folder', 'user', 'username') def create(self, validated_data): @@ -233,13 +242,13 @@ def create(self, validated_data): def validate_username(self, username): """ - Custom method to check whether the provided username exists in the DB. + Overriden to check whether the provided username exists in the DB. """ try: user = User.objects.get(username=username) except User.DoesNotExist: - raise serializers.ValidationError( - {'username': [f"Couldn't find any user with username '{username}'."]}) + raise serializers.ValidationError([f"Couldn't find any user with username " + f"'{username}'."]) return user def validate(self, data): @@ -380,7 +389,7 @@ def validate(self, data): class FileBrowserFileGroupPermissionSerializer(serializers.HyperlinkedModelSerializer): grp_name = serializers.CharField(write_only=True, required=False) file_id = serializers.ReadOnlyField(source='file.id') - file_fname = serializers.ReadOnlyField(source='file.fname') + file_fname = serializers.ReadOnlyField(source='file.fname.name') group_id = serializers.ReadOnlyField(source='group.id') group_name = serializers.ReadOnlyField(source='group.name') file = serializers.HyperlinkedRelatedField(view_name='chrisfile-detail', @@ -416,13 +425,13 @@ def create(self, validated_data): def validate_grp_name(self, grp_name): """ - Custom method to check whether the provided group name exists in the DB. + Overriden to check whether the provided group name exists in the DB. """ try: group = Group.objects.get(name=grp_name) except Group.DoesNotExist: - raise serializers.ValidationError( - {'grp_name': [f"Couldn't find any group with name '{grp_name}'."]}) + raise serializers.ValidationError([f"Couldn't find any group with name " + f"'{grp_name}'."]) return group def validate(self, data): @@ -445,7 +454,7 @@ class FileBrowserFileUserPermissionSerializer(serializers.HyperlinkedModelSerial username = serializers.CharField(write_only=True, min_length=4, max_length=32, required=False) file_id = serializers.ReadOnlyField(source='file.id') - file_fname = serializers.ReadOnlyField(source='file.fname') + file_fname = serializers.ReadOnlyField(source='file.fname.name') user_id = serializers.ReadOnlyField(source='user.id') user_username = serializers.ReadOnlyField(source='user.username') file = serializers.HyperlinkedRelatedField(view_name='chrisfile-detail', @@ -481,13 +490,13 @@ def create(self, validated_data): def validate_username(self, username): """ - Custom method to check whether the provided username exists in the DB. + Overriden to check whether the provided username exists in the DB. """ try: user = User.objects.get(username=username) except User.DoesNotExist: - raise serializers.ValidationError( - {'username': [f"Couldn't find any user with username '{username}'."]}) + raise serializers.ValidationError([f"Couldn't find any user with username " + f"'{username}'."]) return user def validate(self, data): @@ -655,20 +664,32 @@ def validate_public(self, public): def validate(self, data): """ Overriden to validate that at least one of two fields are in data when - updating a file. + updating a link file. Also to verify that the user's home's system-predefined + link files are not being moved. """ if self.instance: # on update if 'public' not in data and 'new_link_file_path' not in data: raise serializers.ValidationError( {'non_field_errors': ["At least one of the fields 'public' " "or 'new_link_file_path' must be provided."]}) + + username = self.context['request'].user.username + fname = self.instance.fname.name + + if ('new_link_file_path' in data and username != 'chris' and fname in ( + f'home/{username}/public.chrislink', + f'home/{username}/shared.chrislink')): + raise serializers.ValidationError( + {'non_field_errors': + [f"Moving link file '{fname}' is not allowed."]}) + return data class FileBrowserLinkFileGroupPermissionSerializer(serializers.HyperlinkedModelSerializer): grp_name = serializers.CharField(write_only=True, required=False) link_file_id = serializers.ReadOnlyField(source='link_file.id') - link_file_fname = serializers.ReadOnlyField(source='link_file.fname') + link_file_fname = serializers.ReadOnlyField(source='link_file.fname.name') group_id = serializers.ReadOnlyField(source='group.id') group_name = serializers.ReadOnlyField(source='group.name') link_file = serializers.HyperlinkedRelatedField(view_name='chrislinkfile-detail', @@ -704,13 +725,13 @@ def create(self, validated_data): def validate_grp_name(self, grp_name): """ - Custom method to check whether the provided group name exists in the DB. + Overriden to check whether the provided group name exists in the DB. """ try: group = Group.objects.get(name=grp_name) except Group.DoesNotExist: - raise serializers.ValidationError( - {'grp_name': [f"Couldn't find any group with name '{grp_name}'."]}) + raise serializers.ValidationError([f"Couldn't find any group with name " + f"'{grp_name}'."]) return group def validate(self, data): @@ -733,7 +754,7 @@ class FileBrowserLinkFileUserPermissionSerializer(serializers.HyperlinkedModelSe username = serializers.CharField(write_only=True, min_length=4, max_length=32, required=False) link_file_id = serializers.ReadOnlyField(source='link_file.id') - link_file_fname = serializers.ReadOnlyField(source='link_file.fname') + link_file_fname = serializers.ReadOnlyField(source='link_file.fname.name') user_id = serializers.ReadOnlyField(source='user.id') user_username = serializers.ReadOnlyField(source='user.username') link_file = serializers.HyperlinkedRelatedField(view_name='chrislinkfile-detail', @@ -769,13 +790,13 @@ def create(self, validated_data): def validate_username(self, username): """ - Custom method to check whether the provided username exists in the DB. + Overriden to check whether the provided username exists in the DB. """ try: user = User.objects.get(username=username) except User.DoesNotExist: - raise serializers.ValidationError( - {'username': [f"Couldn't find any user with username '{username}'."]}) + raise serializers.ValidationError([f"Couldn't find any user with username " + f"'{username}'."]) return user def validate(self, data): diff --git a/chris_backend/filebrowser/tests/test_services.py b/chris_backend/filebrowser/tests/test_services.py index b2e545ff..9c9eb93f 100755 --- a/chris_backend/filebrowser/tests/test_services.py +++ b/chris_backend/filebrowser/tests/test_services.py @@ -24,10 +24,10 @@ class ServiceTests(TestCase): chris_password = CHRIS_SUPERUSER_PASSWORD # normal users - username = 'foo' - password = 'foopass' - other_username = 'boo' - other_password = 'boopass' + username = 'fee' + password = 'feepass' + other_username = 'bee' + other_password = 'beepass' @classmethod def setUpClass(cls): diff --git a/chris_backend/filebrowser/tests/test_views.py b/chris_backend/filebrowser/tests/test_views.py index cadff7c8..f5d2173b 100755 --- a/chris_backend/filebrowser/tests/test_views.py +++ b/chris_backend/filebrowser/tests/test_views.py @@ -7,12 +7,13 @@ from django.test import TestCase, tag from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Group from django.urls import reverse from rest_framework import status - -from core.models import ChrisFolder, ChrisLinkFile +from core.models import (ChrisFolder, ChrisFile, ChrisLinkFile, FolderGroupPermission, + FolderUserPermission, FileGroupPermission, FileUserPermission, + LinkFileGroupPermission, LinkFileUserPermission) from core.storage import connect_storage from users.models import UserProxy from userfiles.models import UserFile @@ -38,8 +39,8 @@ class FileBrowserViewTests(TestCase): # normal users username = 'foo' password = 'foopass' - other_username = 'boo' - other_password = 'boopass' + other_username = 'booo' + other_password = 'booopass' @classmethod def setUpClass(cls): @@ -207,14 +208,19 @@ def test_filebrowserfolder_list_query_search_feed_folder_success_public_feed(sel title='test', compute_resource=plugin.compute_resources.all()[0]) pl_inst.feed.name = 'public_feed' - pl_inst.feed.public = True pl_inst.feed.save() + # make feed public + pl_inst.feed.grant_public_access() + self.client.login(username=self.username, password=self.password) read_url = reverse('chrisfolder-list-query-search') + f'?path=home/{self.other_username}/feeds/feed_{pl_inst.feed.id}' response = self.client.get(read_url) self.assertContains(response, f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}') + self.assertTrue(response.data['results']) + + pl_inst.feed.remove_public_access() def test_filebrowserfolder_list_query_search_feed_folder_success_public_feed_unauthenticated(self): other_user = User.objects.get(username=self.other_username) @@ -225,13 +231,18 @@ def test_filebrowserfolder_list_query_search_feed_folder_success_public_feed_una title='test', compute_resource=plugin.compute_resources.all()[0]) pl_inst.feed.name = 'public_feed' - pl_inst.feed.public = True pl_inst.feed.save() + # make feed public + pl_inst.feed.grant_public_access() + read_url = reverse('chrisfolder-list-query-search') + f'?path=home/{self.other_username}/feeds/feed_{pl_inst.feed.id}' response = self.client.get(read_url) self.assertContains(response, f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}') + self.assertTrue(response.data['results']) + + pl_inst.feed.remove_public_access() def test_filebrowserfolder_list_query_search_failure(self): self.client.login(username=self.username, password=self.password) @@ -264,9 +275,9 @@ def setUp(self): self.plugin = plugin - self.folder = ChrisFolder.objects.get(path=f'home/{self.username}') + folder = ChrisFolder.objects.get(path=f'home/{self.username}') self.read_update_delete_url = reverse("chrisfolder-detail", - kwargs={"pk": self.folder.id}) + kwargs={"pk": folder.id}) self.put = json.dumps({ "template": {"data": [{"name": "public", "value": True}]}}) @@ -286,7 +297,10 @@ def test_filebrowserfolder_update_success(self): content_type=self.content_type) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["public"],True) - self.folder.remove_public_access() + + folder = ChrisFolder.objects.get(path=f'home/{self.username}') + folder.remove_public_link() + folder.remove_public_access() def test_filebrowserfolder_update_failure_unauthenticated(self): response = self.client.put(self.read_update_delete_url, data=self.put, @@ -311,6 +325,26 @@ def test_filebrowserfolder_delete_success(self): response = self.client.delete(read_update_delete_url) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + def test_filebrowserfolder_delete_failure_home_folder(self): + folder = ChrisFolder.objects.get(path=f'home/{self.username}') + + read_update_delete_url = reverse("chrisfolder-detail", + kwargs={"pk": folder.id}) + + self.client.login(username=self.username, password=self.password) + response = self.client.delete(read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_filebrowserfolder_delete_failure_feeds_folder(self): + folder = ChrisFolder.objects.get(path=f'home/{self.username}/feeds') + + read_update_delete_url = reverse("chrisfolder-detail", + kwargs={"pk": folder.id}) + + self.client.login(username=self.username, password=self.password) + response = self.client.delete(read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_filebrowserfolder_delete_failure_unauthenticated(self): response = self.client.delete(self.read_update_delete_url) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) @@ -437,6 +471,8 @@ def test_filebrowserfolder_feed_folder_success_public_feed(self): self.assertContains(response, f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}') + pl_inst.feed.remove_public_access() + def test_filebrowserfolder_feed_folder_success_public_feed_unauthenticated(self): other_user = User.objects.get(username=self.other_username) plugin = self.plugin @@ -459,6 +495,7 @@ def test_filebrowserfolder_feed_folder_success_public_feed_unauthenticated(self) response = self.client.get(read_url) self.assertContains(response, f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}') + pl_inst.feed.remove_public_access() def test_filebrowserfolder_failure_not_found(self): self.client.login(username=self.username, password=self.password) @@ -648,6 +685,7 @@ def test_filebrowserfolderchild_list_feed_folder_success_public_feed(self): response = self.client.get(read_url) self.assertContains(response, f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}/') + pl_inst.feed.remove_public_access() def test_filebrowserfolderchild_list_feed_folder_success_public_feed_unauthenticated(self): other_user = User.objects.get(username=self.other_username) @@ -671,6 +709,7 @@ def test_filebrowserfolderchild_list_feed_folder_success_public_feed_unauthentic response = self.client.get(read_url) self.assertContains(response, f'home/{self.other_username}/feeds/feed_{pl_inst.feed.id}/') + pl_inst.feed.remove_public_access() def test_filebrowserfolderchild_list_failure_not_found(self): self.client.login(username=self.username, password=self.password) @@ -684,250 +723,1671 @@ def test_filebrowserfolderchild_list_failure_not_found_unauthenticated(self): self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) -class FileBrowserFolderFileListViewTests(FileBrowserViewTests): +class FileBrowserFolderGroupPermissionListViewTests(FileBrowserViewTests): """ - Test the 'chrisfolder-file-list' view. + Test the 'foldergrouppermission-list' view. """ def setUp(self): - super(FileBrowserFolderFileListViewTests, self).setUp() + super(FileBrowserFolderGroupPermissionListViewTests, self).setUp() - # create compute resource - (compute_resource, tf) = ComputeResource.objects.get_or_create( - name="host", compute_url=COMPUTE_RESOURCE_URL) + user = User.objects.get(username=self.username) + self.grp_name = 'all_users' - # create 'fs' plugin - (pl_meta, tf) = PluginMeta.objects.get_or_create(name='pacspull', type='fs') - (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1') - plugin.compute_resources.set([compute_resource]) - plugin.save() + # create folder + self.path = f'home/{self.username}/test1' + folder = ChrisFolder.objects.create(path=self.path, owner=user) - self.plugin = plugin + self.create_read_url = reverse('foldergrouppermission-list', + kwargs={"pk": folder.id}) + self.post = json.dumps( + {"template": + {"data": [{"name": "grp_name", "value": self.grp_name}, + {"name": "permission", "value": "r"}]}}) - # create a file in the DB "already uploaded" to the server) - self.storage_manager = connect_storage(settings) - # upload file to storage - self.upload_path = f'home/{self.username}/uploads/file2.txt' - with io.StringIO("test file") as file1: - self.storage_manager.upload_obj(self.upload_path, file1.read(), - content_type='text/plain') + def tearDown(self): + # delete folder tree + folder = ChrisFolder.objects.get(path=self.path) + folder.delete() + + super(FileBrowserFolderGroupPermissionListViewTests, self).tearDown() + + def test_filebrowserfoldergrouppermission_create_success(self): user = User.objects.get(username=self.username) - folder_path = os.path.dirname(self.upload_path) - (file_parent_folder, _) = ChrisFolder.objects.get_or_create(path=folder_path, - owner=user) - userfile = UserFile(owner=user, parent_folder=file_parent_folder) - userfile.fname.name = self.upload_path - userfile.save() + # create inner folder + inner_folder = ChrisFolder.objects.create(path=f'{self.path}/inner', owner=user) - self.read_url = reverse("chrisfolder-file-list", kwargs={"pk": file_parent_folder.id}) + # create a file in the inner folder + self.storage_manager = connect_storage(settings) - def tearDown(self): - # delete file from storage - self.storage_manager.delete_obj(self.upload_path) + upload_path = f'{self.path}/inner/file7.txt' + with io.StringIO("test file") as file7: + self.storage_manager.upload_obj(upload_path, file7.read(), + content_type='text/plain') + f = UserFile(owner=user, parent_folder=inner_folder) + f.fname.name = upload_path + f.save() - super(FileBrowserFolderFileListViewTests, self).tearDown() + # create link file in the inner folder + lf = ChrisLinkFile(path='SERVICES/PACS', owner=user, + parent_folder=inner_folder) + lf.save(name='SERVICES_PACS') - def test_filebrowserfolderfile_list_success(self): self.client.login(username=self.username, password=self.password) - response = self.client.get(self.read_url) - self.assertContains(response, 'file_resource') - self.assertContains(response, self.upload_path) - def test_filebrowserfolderfile_list_success_public_feed_unauthenticated(self): - user = User.objects.get(username=self.username) + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) - # create a feed by creating a "fs" plugin instance - pl_inst = PluginInstance.objects.create(plugin=self.plugin, owner=user, - title='test', - compute_resource= - self.plugin.compute_resources.all()[0]) + folder = ChrisFolder.objects.get(path=self.path) - # create file - self.storage_manager = connect_storage(settings) + self.assertIn(self.grp_name, [g.name for g in folder.shared_groups.all()]) + self.assertIn(self.grp_name, [g.name for g in inner_folder.shared_groups.all()]) + self.assertIn(self.grp_name, [g.name for g in f.shared_groups.all()]) + self.assertIn(self.grp_name, [g.name for g in lf.shared_groups.all()]) - file_path = f'{pl_inst.output_folder.path}/file3.txt' - with io.StringIO("test file") as file3: - self.storage_manager.upload_obj(file_path, file3.read(), - content_type='text/plain') + folder.remove_shared_link() - userfile = UserFile(owner=user, parent_folder=pl_inst.output_folder) - userfile.fname.name = file_path - userfile.save() + def test_filebrowserfoldergrouppermission_create_failure_unauthenticated(self): + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - # make feed public - pl_inst.feed.grant_public_access() + def test_filebrowserfoldergrouppermission_create_failure_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - read_url = reverse("chrisfolder-file-list", - kwargs={"pk": pl_inst.output_folder.id}) + def test_filebrowserfoldergrouppermission_shared_create_failure_access_denied(self): + user = User.objects.get(username=self.other_username) + folder = ChrisFolder.objects.get(path=self.path) + folder.grant_user_permission(user, 'w') - response = self.client.get(read_url) - self.assertContains(response, 'file_resource') - self.assertContains(response, file_path) + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.storage_manager.delete_obj(file_path) + def test_filebrowserfoldergrouppermission_list_success(self): + grp = Group.objects.get(name=self.grp_name) + folder = ChrisFolder.objects.get(path=self.path) + folder.grant_group_permission(grp, 'r') - def test_filebrowserfolderfile_list_failure_unauthenticated(self): - response = self.client.get(self.read_url) + self.client.login(username=self.username, password=self.password) + response = self.client.get(self.create_read_url) + self.assertContains(response, self.grp_name) + + def test_filebrowserfoldergrouppermission_list_failure_unauthenticated(self): + response = self.client.get(self.create_read_url) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - def test_filebrowserfolderfile_list_failure_not_found(self): - self.client.login(username=self.username, password=self.password) - response = self.client.get(reverse("chrisfolder-file-list", kwargs={"pk": 111111111})) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + def test_filebrowserfoldergrouppermission_list_failure_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.create_read_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_filebrowserfolderfile_list_file_folder_success(self): - folder_path = os.path.dirname(self.upload_path) - folder = ChrisFolder.objects.get(path=folder_path) - read_url = reverse("chrisfolder-file-list", kwargs={"pk": folder.id}) - self.client.login(username=self.username, password=self.password) - response = self.client.get(read_url) - self.assertContains(response, self.upload_path) + def test_filebrowserfoldergrouppermission_shared_user_list_success(self): + user = User.objects.get(username=self.other_username) + folder = ChrisFolder.objects.get(path=self.path) + folder.grant_user_permission(user, 'w') - def test_fileBrowserfile_list_success_shared_feed(self): - other_user = User.objects.get(username=self.other_username) - plugin = self.plugin + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.create_read_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) - # create a feed by creating a "fs" plugin instance - pl_inst = PluginInstance.objects.create(plugin=plugin, owner=other_user, - title='test', - compute_resource= - plugin.compute_resources.all()[0]) - pl_inst.feed.name = 'shared_feed' - pl_inst.feed.save() - file_path = f'{pl_inst.output_folder.path}/file3.txt' - with io.StringIO("test file") as file3: - self.storage_manager.upload_obj(file_path, file3.read(), - content_type='text/plain') +class FileBrowserFolderGroupPermissionListQuerySearchViewTests(FileBrowserViewTests): + """ + Test the 'foldergrouppermission-list-query-search' view. + """ - userfile = UserFile(owner=other_user, parent_folder=pl_inst.output_folder) - userfile.fname.name = file_path - userfile.save() + def setUp(self): + super(FileBrowserFolderGroupPermissionListQuerySearchViewTests, self).setUp() - # share feed - pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) + user = User.objects.get(username=self.username) + self.grp_name = 'all_users' - read_url = reverse("chrisfolder-file-list", - kwargs={"pk": pl_inst.output_folder.id}) - self.client.login(username=self.username, password=self.password) - response = self.client.get(read_url) - self.assertContains(response, file_path) + # create folder + self.path = f'home/{self.username}/test2' + folder = ChrisFolder.objects.create(path=self.path, owner=user) - self.storage_manager.delete_obj(file_path) + self.read_url = reverse('foldergrouppermission-list-query-search', + kwargs={"pk": folder.id}) - def test_fileBrowserfile_list_failure_shared_feed_unauthenticated(self): - other_user = User.objects.get(username=self.other_username) - plugin = self.plugin + grp = Group.objects.get(name=self.grp_name) + folder.grant_group_permission(grp, 'r') - # create a feed by creating a "fs" plugin instance - pl_inst = PluginInstance.objects.create(plugin=plugin, owner=other_user, - title='test', - compute_resource= - plugin.compute_resources.all()[0]) - pl_inst.feed.name = 'shared_feed' - pl_inst.feed.save() + def tearDown(self): + # delete folder tree + folder = ChrisFolder.objects.get(path=self.path) + folder.delete() - file_path = f'{pl_inst.output_folder.path}/file3.txt' - with io.StringIO("test file") as file3: - self.storage_manager.upload_obj(file_path, file3.read(), - content_type='text/plain') + super(FileBrowserFolderGroupPermissionListQuerySearchViewTests, self).tearDown() - userfile = UserFile(owner=other_user, parent_folder=pl_inst.output_folder) - userfile.fname.name = file_path - userfile.save() + def test_filebrowserfoldergrouppermission_list_query_search_success(self): + read_url = f'{self.read_url}?group_name={self.grp_name}' - # share feed - pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) + self.client.login(username=self.username, password=self.password) + response = self.client.get(read_url) + self.assertContains(response, self.grp_name) - read_url = reverse("chrisfolder-file-list", - kwargs={"pk": pl_inst.output_folder.id}) + def test_filebrowserfoldergrouppermission_list_query_search_success_shared(self): + read_url = f'{self.read_url}?group_name={self.grp_name}' + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(read_url) + self.assertContains(response, self.grp_name) + + def test_filebrowserfoldergrouppermission_list_query_search_failure_unauthenticated(self): + read_url = f'{self.read_url}?group_name={self.grp_name}' response = self.client.get(read_url) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.storage_manager.delete_obj(file_path) + def test_filebrowserfoldergrouppermission_list_query_search_failure_other_user(self): + grp = Group.objects.get(name=self.grp_name) + folder = ChrisFolder.objects.get(path=self.path) + folder.remove_group_permission(grp, 'r') + read_url = f'{self.read_url}?group_name={self.grp_name}' -class FileBrowserFolderLinkFileListViewTests(FileBrowserViewTests): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(read_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertFalse(response.data['results']) + + +class FileBrowserFolderGroupPermissionDetailViewTests(FileBrowserViewTests): """ - Test the 'chrisfolder-linkfile-list' view. + Test the foldergrouppermission-detail view. """ def setUp(self): - super(FileBrowserFolderLinkFileListViewTests, self).setUp() + super(FileBrowserFolderGroupPermissionDetailViewTests, self).setUp() - self.storage_manager = connect_storage(settings) + user = User.objects.get(username=self.username) + self.grp_name = 'all_users' - # create compute resource - (compute_resource, tf) = ComputeResource.objects.get_or_create( - name="host", compute_url=COMPUTE_RESOURCE_URL) + # create folder + self.path = f'home/{self.username}/test3' + folder = ChrisFolder.objects.create(path=self.path, owner=user) - # create 'fs' plugin - (pl_meta, tf) = PluginMeta.objects.get_or_create(name='pacspull', type='fs') - (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1') - plugin.compute_resources.set([compute_resource]) - plugin.save() + grp = Group.objects.get(name=self.grp_name) + folder.grant_group_permission(grp, 'r') - self.plugin = plugin + gp = FolderGroupPermission.objects.get(group=grp, folder=folder) - # create link file - self.link_path = f'home/{self.username}/feeds/feed_1/out/SERVICES_PACS.chrislink' - user = User.objects.get(username=self.username) + self.read_update_delete_url = reverse("foldergrouppermission-detail", + kwargs={"pk": gp.id}) + + self.put = json.dumps({ + "template": {"data": [{"name": "permission", "value": "w"}]}}) - folder_path = os.path.dirname(self.link_path) - (parent_folder, _) = ChrisFolder.objects.get_or_create(path=folder_path, - owner=user) - link_file = ChrisLinkFile(path='SERVICES/PACS', owner=user, - parent_folder=parent_folder) - link_file.save(name='SERVICES_PACS') - self.read_url = reverse("chrisfolder-linkfile-list", - kwargs={"pk": parent_folder.id}) def tearDown(self): - self.storage_manager.delete_obj(self.link_path) - super(FileBrowserFolderLinkFileListViewTests, self).tearDown() + # delete folder tree + folder = ChrisFolder.objects.get(path=self.path) + folder.delete() - def test_filebrowserfolderlinkfile_list_success(self): + super(FileBrowserFolderGroupPermissionDetailViewTests, self).tearDown() + + def test_filebrowserfoldergrouppermission_detail_success(self): self.client.login(username=self.username, password=self.password) - response = self.client.get(self.read_url) - self.assertContains(response, 'file_resource') - self.assertContains(response, self.link_path) + response = self.client.get(self.read_update_delete_url) + self.assertContains(response, 'all_users') + self.assertContains(response, self.path) - def test_filebrowserfolderlinkfile_list_success_public_feed_unauthenticated(self): + def test_filebrowserfoldergrouppermission_detail_shared_success(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.read_update_delete_url) + self.assertContains(response, 'all_users') + self.assertContains(response, self.path) + + def test_filebrowserfoldergrouppermission_detail_failure_unauthenticated(self): + response = self.client.get(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfoldergrouppermission_update_success(self): user = User.objects.get(username=self.username) - # create a feed by creating a "fs" plugin instance - pl_inst = PluginInstance.objects.create(plugin=self.plugin, owner=user, - title='test', - compute_resource= - self.plugin.compute_resources.all()[0]) + # create inner folder + inner_folder = ChrisFolder.objects.create(path=f'{self.path}/inner', owner=user) - # create link file - link_path = f'{pl_inst.output_folder.path}/SERVICES_PACS.chrislink' + # create a file in the inner folder + self.storage_manager = connect_storage(settings) - link_file = ChrisLinkFile(path='SERVICES/PACS', owner=user, - parent_folder=pl_inst.output_folder) - link_file.save(name='SERVICES_PACS') + upload_path = f'{self.path}/inner/file8.txt' + with io.StringIO("test file") as file8: + self.storage_manager.upload_obj(upload_path, file8.read(), + content_type='text/plain') + f = UserFile(owner=user, parent_folder=inner_folder) + f.fname.name = upload_path + f.save() - # make feed public - pl_inst.feed.grant_public_access() + # create link file in the inner folder + lf = ChrisLinkFile(path='SERVICES/PACS', owner=user, + parent_folder=inner_folder) + lf.save(name='SERVICES_PACS') - read_url = reverse("chrisfolder-linkfile-list", - kwargs={"pk": pl_inst.output_folder.id}) - response = self.client.get(read_url) - self.assertContains(response, 'file_resource') - self.assertContains(response, link_path) + self.client.login(username=self.username, password=self.password) + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["permission"], 'w') - self.storage_manager.delete_obj(link_path) + self.assertEqual(FileGroupPermission.objects.get(file=f).permission, 'w') + self.assertEqual(LinkFileGroupPermission.objects.get(link_file=lf).permission, + 'w') - def test_filebrowserfolderlinkfile_list_failure_unauthenticated(self): - response = self.client.get(self.read_url) + def test_filebrowserfoldergrouppermission_update_failure_unauthenticated(self): + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - def test_filebrowserfolderlinkfile_list_failure_not_found(self): - self.client.login(username=self.username, password=self.password) - response = self.client.get(reverse("chrisfolder-linkfile-list", + def test_filebrowserfoldergrouppermission_update_failure_user_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_filebrowserfoldergrouppermission_delete_success(self): + folder = ChrisFolder.objects.get(path=self.path) + grp = Group.objects.get(name='pacs_users') + + # create a group permission + folder.grant_group_permission(grp, 'r') + gp = FolderGroupPermission.objects.get(group=grp, folder=folder) + + read_update_delete_url = reverse("foldergrouppermission-detail", + kwargs={"pk": gp.id}) + self.client.login(username=self.username, password=self.password) + response = self.client.delete(read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_filebrowserfoldergrouppermission_delete_failure_unauthenticated(self): + response = self.client.delete(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfoldergrouppermission_delete_failure_user_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.delete(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +class FileBrowserFolderUserPermissionListViewTests(FileBrowserViewTests): + """ + Test the 'folderuserpermission-list' view. + """ + + def setUp(self): + super(FileBrowserFolderUserPermissionListViewTests, self).setUp() + + user = User.objects.get(username=self.username) + + # create folder + self.path = f'home/{self.username}/test4' + folder = ChrisFolder.objects.create(path=self.path, owner=user) + + self.create_read_url = reverse('folderuserpermission-list', + kwargs={"pk": folder.id}) + self.post = json.dumps( + {"template": + {"data": [{"name": "username", "value": self.other_username}, + {"name": "permission", "value": "r"}]}}) + + def tearDown(self): + # delete folder tree + folder = ChrisFolder.objects.get(path=self.path) + folder.delete() + + super(FileBrowserFolderUserPermissionListViewTests, self).tearDown() + + def test_filebrowserfolderuserpermission_create_success(self): + user = User.objects.get(username=self.username) + + # create inner folder + inner_folder = ChrisFolder.objects.create(path=f'{self.path}/inner', owner=user) + + # create a file in the inner folder + self.storage_manager = connect_storage(settings) + + upload_path = f'{self.path}/inner/file7.txt' + with io.StringIO("test file") as file7: + self.storage_manager.upload_obj(upload_path, file7.read(), + content_type='text/plain') + f = UserFile(owner=user, parent_folder=inner_folder) + f.fname.name = upload_path + f.save() + + # create link file in the inner folder + lf = ChrisLinkFile(path='SERVICES/PACS', owner=user, + parent_folder=inner_folder) + lf.save(name='SERVICES_PACS') + + self.client.login(username=self.username, password=self.password) + + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + folder = ChrisFolder.objects.get(path=self.path) + + self.assertIn(self.other_username, [u.username for u in folder.shared_users.all()]) + self.assertIn(self.other_username, [u.username for u in inner_folder.shared_users.all()]) + self.assertIn(self.other_username, [u.username for u in f.shared_users.all()]) + self.assertIn(self.other_username, [u.username for u in lf.shared_users.all()]) + + folder.remove_shared_link() + + def test_filebrowserfolderuserpermission_create_failure_unauthenticated(self): + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfolderuserpermission_create_failure_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_filebrowserfolderuserpermission_shared_create_failure_access_denied(self): + user = User.objects.get(username=self.other_username) + folder = ChrisFolder.objects.get(path=self.path) + folder.grant_user_permission(user, 'w') + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_filebrowserfolderuserpermission_list_success(self): + user = User.objects.get(username=self.other_username) + folder = ChrisFolder.objects.get(path=self.path) + folder.grant_user_permission(user, 'r') + + self.client.login(username=self.username, password=self.password) + response = self.client.get(self.create_read_url) + self.assertContains(response, self.other_username) + + def test_filebrowserfolderuserpermission_list_failure_unauthenticated(self): + response = self.client.get(self.create_read_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfolderuserpermission_list_failure_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.create_read_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_filebrowserfolderuserpermission_shared_user_list_success(self): + user = User.objects.get(username=self.other_username) + folder = ChrisFolder.objects.get(path=self.path) + folder.grant_user_permission(user, 'w') + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.create_read_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class FileBrowserFolderUserPermissionListQuerySearchViewTests(FileBrowserViewTests): + """ + Test the 'folderuserpermission-list-query-search' view. + """ + + def setUp(self): + super(FileBrowserFolderUserPermissionListQuerySearchViewTests, self).setUp() + + user = User.objects.get(username=self.username) + + # create folder + self.path = f'home/{self.username}/test5' + folder = ChrisFolder.objects.create(path=self.path, owner=user) + + self.read_url = reverse('folderuserpermission-list-query-search', + kwargs={"pk": folder.id}) + + other_user = User.objects.get(username=self.other_username) + folder.grant_user_permission(other_user, 'r') + + def tearDown(self): + # delete folder tree + folder = ChrisFolder.objects.get(path=self.path) + folder.delete() + + super(FileBrowserFolderUserPermissionListQuerySearchViewTests, self).tearDown() + + def test_filebrowserfolderuserpermission_list_query_search_success(self): + read_url = f'{self.read_url}?username={self.other_username}' + + self.client.login(username=self.username, password=self.password) + response = self.client.get(read_url) + self.assertContains(response, self.other_username) + + def test_filebrowserfolderuserpermission_list_query_search_success_shared(self): + read_url = f'{self.read_url}?username={self.other_username}' + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(read_url) + self.assertContains(response, self.other_username) + + def test_filebrowserfolderuserpermission_list_query_search_failure_unauthenticated(self): + read_url = f'{self.read_url}?username={self.other_username}' + + response = self.client.get(read_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + +class FileBrowserFolderUserPermissionDetailViewTests(FileBrowserViewTests): + """ + Test the folderuserpermission-detail view. + """ + + def setUp(self): + super(FileBrowserFolderUserPermissionDetailViewTests, self).setUp() + + user = User.objects.get(username=self.username) + + # create folder + self.path = f'home/{self.username}/test6' + folder = ChrisFolder.objects.create(path=self.path, owner=user) + + other_user = User.objects.get(username=self.other_username) + folder.grant_user_permission(other_user, 'r') + + up = FolderUserPermission.objects.get(user=other_user, folder=folder) + + self.read_update_delete_url = reverse("folderuserpermission-detail", + kwargs={"pk": up.id}) + + self.put = json.dumps({ + "template": {"data": [{"name": "permission", "value": "w"}]}}) + + + def tearDown(self): + # delete folder tree + folder = ChrisFolder.objects.get(path=self.path) + folder.delete() + + super(FileBrowserFolderUserPermissionDetailViewTests, self).tearDown() + + def test_filebrowserfolderuserpermission_detail_success(self): + self.client.login(username=self.username, password=self.password) + response = self.client.get(self.read_update_delete_url) + self.assertContains(response, self.other_username) + self.assertContains(response, self.path) + + def test_filebrowserfolderuserpermission_detail_shared_success(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.read_update_delete_url) + self.assertContains(response, self.other_username) + self.assertContains(response, self.path) + + def test_filebrowserfolderuserpermission_detail_failure_unauthenticated(self): + response = self.client.get(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfolderuserpermission_update_success(self): + user = User.objects.get(username=self.username) + + # create inner folder + inner_folder = ChrisFolder.objects.create(path=f'{self.path}/inner', owner=user) + + # create a file in the inner folder + self.storage_manager = connect_storage(settings) + + upload_path = f'{self.path}/inner/file8.txt' + with io.StringIO("test file") as file8: + self.storage_manager.upload_obj(upload_path, file8.read(), + content_type='text/plain') + f = UserFile(owner=user, parent_folder=inner_folder) + f.fname.name = upload_path + f.save() + + # create link file in the inner folder + lf = ChrisLinkFile(path='SERVICES/PACS', owner=user, + parent_folder=inner_folder) + lf.save(name='SERVICES_PACS') + + self.client.login(username=self.username, password=self.password) + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["permission"], 'w') + + self.assertEqual(FileUserPermission.objects.get(file=f).permission, 'w') + self.assertEqual(LinkFileUserPermission.objects.get(link_file=lf).permission, + 'w') + + def test_filebrowserfolderuserpermission_update_failure_unauthenticated(self): + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfolderuserpermission_update_failure_user_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_filebrowserfolderuserpermission_delete_success(self): + self.client.login(username=self.username, password=self.password) + response = self.client.delete(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_filebrowserfolderuserpermission_delete_failure_unauthenticated(self): + response = self.client.delete(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfolderuserpermission_delete_failure_user_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.delete(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +class FileBrowserFolderFileListViewTests(FileBrowserViewTests): + """ + Test the 'chrisfolder-file-list' view. + """ + + def setUp(self): + super(FileBrowserFolderFileListViewTests, self).setUp() + + # create compute resource + (compute_resource, tf) = ComputeResource.objects.get_or_create( + name="host", compute_url=COMPUTE_RESOURCE_URL) + + # create 'fs' plugin + (pl_meta, tf) = PluginMeta.objects.get_or_create(name='pacspull', type='fs') + (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1') + plugin.compute_resources.set([compute_resource]) + plugin.save() + + self.plugin = plugin + + # create a file in the DB "already uploaded" to the server) + self.storage_manager = connect_storage(settings) + # upload file to storage + self.upload_path = f'home/{self.username}/uploads/file2.txt' + with io.StringIO("test file") as file1: + self.storage_manager.upload_obj(self.upload_path, file1.read(), + content_type='text/plain') + user = User.objects.get(username=self.username) + + folder_path = os.path.dirname(self.upload_path) + (file_parent_folder, _) = ChrisFolder.objects.get_or_create(path=folder_path, + owner=user) + self.file = UserFile(owner=user, parent_folder=file_parent_folder) + self.file.fname.name = self.upload_path + self.file.save() + + self.read_url = reverse("chrisfolder-file-list", + kwargs={"pk": file_parent_folder.id}) + + def tearDown(self): + # delete file from storage + self.file.delete() + + super(FileBrowserFolderFileListViewTests, self).tearDown() + + def test_filebrowserfolderfile_list_success(self): + self.client.login(username=self.username, password=self.password) + response = self.client.get(self.read_url) + self.assertContains(response, 'file_resource') + self.assertContains(response, self.upload_path) + + def test_filebrowserfolderfile_list_success_public_feed_unauthenticated(self): + user = User.objects.get(username=self.username) + + # create a feed by creating a "fs" plugin instance + pl_inst = PluginInstance.objects.create(plugin=self.plugin, owner=user, + title='test', + compute_resource= + self.plugin.compute_resources.all()[0]) + + # create file + self.storage_manager = connect_storage(settings) + + file_path = f'{pl_inst.output_folder.path}/file3.txt' + with io.StringIO("test file") as file3: + self.storage_manager.upload_obj(file_path, file3.read(), + content_type='text/plain') + + userfile = UserFile(owner=user, parent_folder=pl_inst.output_folder) + userfile.fname.name = file_path + userfile.save() + + # make feed public + pl_inst.feed.grant_public_access() + + read_url = reverse("chrisfolder-file-list", + kwargs={"pk": pl_inst.output_folder.id}) + + response = self.client.get(read_url) + self.assertContains(response, 'file_resource') + self.assertContains(response, file_path) + + pl_inst.feed.remove_public_access() + userfile.delete() + + def test_filebrowserfolderfile_list_failure_unauthenticated(self): + response = self.client.get(self.read_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfolderfile_list_failure_not_found(self): + self.client.login(username=self.username, password=self.password) + response = self.client.get(reverse("chrisfolder-file-list", kwargs={"pk": 111111111})) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_filebrowserfolderfile_list_file_folder_success(self): + folder_path = os.path.dirname(self.upload_path) + folder = ChrisFolder.objects.get(path=folder_path) + read_url = reverse("chrisfolder-file-list", kwargs={"pk": folder.id}) + self.client.login(username=self.username, password=self.password) + response = self.client.get(read_url) + self.assertContains(response, self.upload_path) + + def test_fileBrowserfile_list_success_shared_feed(self): + other_user = User.objects.get(username=self.other_username) + plugin = self.plugin + + # create a feed by creating a "fs" plugin instance + pl_inst = PluginInstance.objects.create(plugin=plugin, owner=other_user, + title='test', + compute_resource= + plugin.compute_resources.all()[0]) + pl_inst.feed.name = 'shared_feed' + pl_inst.feed.save() + + file_path = f'{pl_inst.output_folder.path}/file3.txt' + with io.StringIO("test file") as file3: + self.storage_manager.upload_obj(file_path, file3.read(), + content_type='text/plain') + + userfile = UserFile(owner=other_user, parent_folder=pl_inst.output_folder) + userfile.fname.name = file_path + userfile.save() + + # share feed + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) + + read_url = reverse("chrisfolder-file-list", + kwargs={"pk": pl_inst.output_folder.id}) + self.client.login(username=self.username, password=self.password) + response = self.client.get(read_url) + self.assertContains(response, file_path) + + userfile.delete() + + def test_fileBrowserfile_list_failure_shared_feed_unauthenticated(self): + other_user = User.objects.get(username=self.other_username) + plugin = self.plugin + + # create a feed by creating a "fs" plugin instance + pl_inst = PluginInstance.objects.create(plugin=plugin, owner=other_user, + title='test', + compute_resource= + plugin.compute_resources.all()[0]) + pl_inst.feed.name = 'shared_feed' + pl_inst.feed.save() + + file_path = f'{pl_inst.output_folder.path}/file3.txt' + with io.StringIO("test file") as file3: + self.storage_manager.upload_obj(file_path, file3.read(), + content_type='text/plain') + + userfile = UserFile(owner=other_user, parent_folder=pl_inst.output_folder) + userfile.fname.name = file_path + userfile.save() + + # share feed + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) + + read_url = reverse("chrisfolder-file-list", + kwargs={"pk": pl_inst.output_folder.id}) + + response = self.client.get(read_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + userfile.delete() + + +class FileBrowserFileDetailViewTests(FileBrowserViewTests): + """ + Test the chrisfile-detail view. + """ + + def setUp(self): + super(FileBrowserFileDetailViewTests, self).setUp() + + # create compute resource + (compute_resource, tf) = ComputeResource.objects.get_or_create( + name="host", compute_url=COMPUTE_RESOURCE_URL) + + # create 'fs' plugin + (pl_meta, tf) = PluginMeta.objects.get_or_create(name='pacspull', type='fs') + (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1') + plugin.compute_resources.set([compute_resource]) + plugin.save() + + self.plugin = plugin + + # create a file in the DB "already uploaded" to the server) + self.storage_manager = connect_storage(settings) + # upload file to storage + self.upload_path = f'home/{self.username}/uploads/file2.txt' + with io.StringIO("test file") as file1: + self.storage_manager.upload_obj(self.upload_path, file1.read(), + content_type='text/plain') + user = User.objects.get(username=self.username) + + folder_path = os.path.dirname(self.upload_path) + (file_parent_folder, _) = ChrisFolder.objects.get_or_create(path=folder_path, + owner=user) + self.file = UserFile(owner=user, parent_folder=file_parent_folder) + self.file.fname.name = self.upload_path + self.file.save() + + self.read_update_delete_url = reverse("chrisfile-detail", + kwargs={"pk": self.file.id}) + + self.put = json.dumps({ + "template": {"data": [{"name": "public", "value": True}]}}) + + def tearDown(self): + self.file.delete() + super(FileBrowserFileDetailViewTests, self).tearDown() + + def test_fileBrowserfile_detail_success(self): + self.client.login(username=self.username, password=self.password) + response = self.client.get(self.read_update_delete_url) + self.assertContains(response, self.upload_path) + + def test_fileBrowserfile_detail_success_user_chris(self): + self.client.login(username=self.chris_username, password=self.chris_password) + response = self.client.get(self.read_update_delete_url) + self.assertContains(response, self.upload_path) + + def test_fileBrowserfile_detail_failure_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_fileBrowserfile_detail_failure_unauthenticated(self): + response = self.client.get(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_fileBrowserfile_detail_success_shared_feed(self): + other_user = User.objects.get(username=self.other_username) + plugin = self.plugin + + # create a feed by creating a "fs" plugin instance + pl_inst = PluginInstance.objects.create(plugin=plugin, owner=other_user, + title='test', + compute_resource= + plugin.compute_resources.all()[0]) + pl_inst.feed.name = 'shared_feed' + pl_inst.feed.save() + + # create file in the output folder + self.storage_manager = connect_storage(settings) + + file_path = f'{pl_inst.output_folder.path}/file4.txt' + with io.StringIO("test file") as file4: + self.storage_manager.upload_obj(file_path, file4.read(), + content_type='text/plain') + + userfile = UserFile(owner=other_user, parent_folder=pl_inst.output_folder) + userfile.fname.name = file_path + userfile.save() + + # share feed + user = User.objects.get(username=self.username) + pl_inst.feed.grant_user_permission(user) + + read_update_delete_url = reverse("chrisfile-detail", kwargs={"pk": userfile.id}) + self.client.login(username=self.username, password=self.password) + response = self.client.get(read_update_delete_url) + self.assertContains(response, file_path) + + userfile.delete() + + def test_fileBrowserfile_detail_failure_unauthorized_shared_feed_unauthenticated(self): + other_user = User.objects.get(username=self.other_username) + plugin = self.plugin + + # create a feed by creating a "fs" plugin instance + pl_inst = PluginInstance.objects.create(plugin=plugin, owner=other_user, + title='test', + compute_resource= + plugin.compute_resources.all()[0]) + pl_inst.feed.name = 'shared_feed' + pl_inst.feed.save() + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) + + # create file in the output folder + self.storage_manager = connect_storage(settings) + + file_path = f'{pl_inst.output_folder.path}/file5.txt' + with io.StringIO("test file") as file5: + self.storage_manager.upload_obj(file_path, file5.read(), + content_type='text/plain') + + userfile = UserFile(owner=other_user, parent_folder=pl_inst.output_folder) + userfile.fname.name = file_path + userfile.save() + + read_update_delete_url = reverse("chrisfile-detail", kwargs={"pk": userfile.id}) + response = self.client.get(read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + userfile.delete() + + def test_filebrowserfile_update_success(self): + self.client.login(username=self.username, password=self.password) + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["public"],True) + self.file.remove_public_link() + self.file.remove_public_access() + + def test_filebrowserfile_update_failure_unauthenticated(self): + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfile_update_failure_user_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_filebrowserfile_delete_success(self): + # create a file + upload_path = f'home/{self.username}/uploads/mytestfile.txt' + + with io.StringIO("test file") as f: + self.storage_manager.upload_obj(upload_path, f.read(), + content_type='text/plain') + user = User.objects.get(username=self.username) + + folder_path = os.path.dirname(upload_path) + (file_parent_folder, _) = ChrisFolder.objects.get_or_create(path=folder_path, + owner=user) + f = UserFile(owner=user, parent_folder=file_parent_folder) + f.fname.name = upload_path + f.save() + + read_update_delete_url = reverse("chrisfile-detail", + kwargs={"pk": f.id}) + + self.client.login(username=self.username, password=self.password) + response = self.client.delete(read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_filebrowserfile_delete_failure_unauthenticated(self): + response = self.client.delete(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfile_delete_failure_user_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.delete(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +class FileBrowserFileResourceViewTests(FileBrowserViewTests): + """ + Test the chrisfile-resource view. + """ + + def setUp(self): + super(FileBrowserFileResourceViewTests, self).setUp() + + self.storage_manager = connect_storage(settings) + + # create compute resource + (compute_resource, tf) = ComputeResource.objects.get_or_create( + name="host", compute_url=COMPUTE_RESOURCE_URL) + + # create 'fs' plugin + (pl_meta, tf) = PluginMeta.objects.get_or_create(name='pacspull', type='fs') + (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1') + plugin.compute_resources.set([compute_resource]) + plugin.save() + + self.plugin = plugin + user = User.objects.get(username=self.username) + + # create a feed by creating a "fs" plugin instance + self.pl_inst = PluginInstance.objects.create(plugin=plugin, owner=user, + title='test', + compute_resource= + plugin.compute_resources.all()[0]) + + # create a file in the DB "already uploaded" to the server) + self.storage_manager = connect_storage(settings) + + # upload file to storage + self.upload_path = f'home/{self.username}/uploads/file2.txt' + with io.StringIO("test file") as file1: + self.storage_manager.upload_obj(self.upload_path, file1.read(), + content_type='text/plain') + user = User.objects.get(username=self.username) + + folder_path = os.path.dirname(self.upload_path) + (file_parent_folder, _) = ChrisFolder.objects.get_or_create(path=folder_path, + owner=user) + self.file = UserFile(owner=user, parent_folder=file_parent_folder) + self.file.fname.name = self.upload_path + self.file.save() + + self.download_url = (reverse("chrisfile-resource", + kwargs={"pk": self.file.id}) + 'file2.txt') + + def tearDown(self): + self.file.delete() + super(FileBrowserFileResourceViewTests, self).tearDown() + + def test_fileBrowserfile_resource_success(self): + self.client.login(username=self.username, password=self.password) + response = self.client.get(self.download_url) + self.assertEqual(response.status_code, 200) + content = [c for c in response.streaming_content][0].decode('utf-8') + self.assertEqual(content, "test file") + + def test_fileBrowserfile_resource_success_user_chris(self): + self.client.login(username=self.chris_username, password=self.chris_password) + response = self.client.get(self.download_url) + self.assertEqual(response.status_code, 200) + content = [c for c in response.streaming_content][0].decode('utf-8') + self.assertEqual(content, "test file") + + def test_fileBrowserfile_resource_failure_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.download_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_fileBrowserfile_resource_failure_unauthenticated(self): + response = self.client.get(self.download_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_fileBrowserfile_resource_success_shared_file(self): + other_user = User.objects.get(username=self.other_username) + + # create a file in the uploads folder + path = f'home/{self.other_username}/uploads' + uploads_folder = ChrisFolder.objects.get(path=path) + + self.storage_manager = connect_storage(settings) + + file_path = f'{path}/file6.txt' + with io.StringIO("test file") as file6: + self.storage_manager.upload_obj(file_path, file6.read(), + content_type='text/plain') + + userfile = UserFile(owner=other_user, parent_folder=uploads_folder) + userfile.fname.name = file_path + userfile.save() + + userfile.grant_user_permission(User.objects.get(username=self.username), 'r') + + download_url = (reverse("chrisfile-resource", + kwargs={"pk": userfile.id}) + 'file6.txt') + + self.client.login(username=self.username, password=self.password) + response = self.client.get(download_url) + self.assertEqual(response.status_code, 200) + content = [c for c in response.streaming_content][0].decode('utf-8') + self.assertEqual(content, "test file") + + userfile.delete() + + def test_fileBrowserfile_resource_failure_unauthorized_shared_feed_unauthenticated(self): + other_user = User.objects.get(username=self.other_username) + plugin = self.plugin + + # create a feed by creating a "fs" plugin instance + pl_inst = PluginInstance.objects.create(plugin=plugin, owner=other_user, + title='test', + compute_resource= + plugin.compute_resources.all()[0]) + pl_inst.feed.name = 'shared_feed' + pl_inst.feed.save() + + # create a file in the output folder + self.storage_manager = connect_storage(settings) + + file_path = f'{pl_inst.output_folder}/file6.txt' + with io.StringIO("test file") as file6: + self.storage_manager.upload_obj(file_path, file6.read(), + content_type='text/plain') + + userfile = UserFile(owner=other_user, parent_folder=pl_inst.output_folder) + userfile.fname.name = file_path + userfile.save() + + # share feed + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) + + download_url = reverse("chrisfile-resource", kwargs={"pk": userfile.id}) + + response = self.client.get(download_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + userfile.delete() + + + +class FileBrowserFileGroupPermissionListViewTests(FileBrowserViewTests): + """ + Test the 'filegrouppermission-list' view. + """ + + def setUp(self): + super(FileBrowserFileGroupPermissionListViewTests, self).setUp() + + user = User.objects.get(username=self.username) + self.grp_name = 'all_users' + + # create file + self.path = f'home/{self.username}/test7/file9.txt' + folder = ChrisFolder.objects.create(path=f'home/{self.username}/test7', owner=user) + + self.storage_manager = connect_storage(settings) + + with io.StringIO("test file") as file9: + self.storage_manager.upload_obj(self.path, file9.read(), + content_type='text/plain') + f = UserFile(owner=user, parent_folder=folder) + f.fname.name = self.path + f.save() + + self.create_read_url = reverse('filegrouppermission-list', + kwargs={"pk": f.id}) + self.post = json.dumps( + {"template": + {"data": [{"name": "grp_name", "value": self.grp_name}, + {"name": "permission", "value": "r"}]}}) + + def tearDown(self): + # delete file + f = ChrisFile.objects.get(fname=self.path) + f.delete() + + super(FileBrowserFileGroupPermissionListViewTests, self).tearDown() + + def test_filebrowserfilegrouppermission_create_success(self): + + self.client.login(username=self.username, password=self.password) + + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + f = ChrisFile.objects.get(fname=self.path) + self.assertIn(self.grp_name, [g.name for g in f.shared_groups.all()]) + + f.remove_shared_link() + + def test_filebrowserfilegrouppermission_create_failure_unauthenticated(self): + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfilegrouppermission_create_failure_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_filebrowserfilegrouppermission_shared_create_failure_access_denied(self): + user = User.objects.get(username=self.other_username) + + f = ChrisFile.objects.get(fname=self.path) + f.grant_user_permission(user, 'w') + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_filebrowserfilegrouppermission_list_success(self): + grp = Group.objects.get(name=self.grp_name) + + f = ChrisFile.objects.get(fname=self.path) + f.grant_group_permission(grp, 'r') + + self.client.login(username=self.username, password=self.password) + response = self.client.get(self.create_read_url) + self.assertContains(response, self.grp_name) + + def test_filebrowserfilegrouppermission_list_failure_unauthenticated(self): + response = self.client.get(self.create_read_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfilegrouppermission_list_failure_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.create_read_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_filebrowserfilegrouppermission_shared_user_list_success(self): + user = User.objects.get(username=self.other_username) + + f = ChrisFile.objects.get(fname=self.path) + f.grant_user_permission(user, 'w') + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.create_read_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class FileBrowserFileGroupPermissionListQuerySearchViewTests(FileBrowserViewTests): + """ + Test the 'filegrouppermission-list-query-search' view. + """ + + def setUp(self): + super(FileBrowserFileGroupPermissionListQuerySearchViewTests, self).setUp() + + user = User.objects.get(username=self.username) + self.grp_name = 'all_users' + + # create file + self.path = f'home/{self.username}/test8/file10.txt' + folder = ChrisFolder.objects.create(path=f'home/{self.username}/test8', + owner=user) + + self.storage_manager = connect_storage(settings) + + with io.StringIO("test file") as file10: + self.storage_manager.upload_obj(self.path, file10.read(), + content_type='text/plain') + f = UserFile(owner=user, parent_folder=folder) + f.fname.name = self.path + f.save() + + self.read_url = reverse('filegrouppermission-list-query-search', + kwargs={"pk": f.id}) + + grp = Group.objects.get(name=self.grp_name) + f.grant_group_permission(grp, 'r') + + def tearDown(self): + # delete file + f = ChrisFile.objects.get(fname=self.path) + f.delete() + + super(FileBrowserFileGroupPermissionListQuerySearchViewTests, self).tearDown() + + def test_filebrowserfilegrouppermission_list_query_search_success(self): + read_url = f'{self.read_url}?group_name={self.grp_name}' + + self.client.login(username=self.username, password=self.password) + response = self.client.get(read_url) + self.assertContains(response, self.grp_name) + + def test_filebrowserfilegrouppermission_list_query_search_success_shared(self): + read_url = f'{self.read_url}?group_name={self.grp_name}' + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(read_url) + self.assertContains(response, self.grp_name) + + def test_filebrowserfilegrouppermission_list_query_search_failure_unauthenticated(self): + read_url = f'{self.read_url}?group_name={self.grp_name}' + + response = self.client.get(read_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfilegrouppermission_list_query_search_failure_other_user(self): + grp = Group.objects.get(name=self.grp_name) + f = ChrisFile.objects.get(fname=self.path) + f.remove_group_permission(grp, 'r') + + read_url = f'{self.read_url}?group_name={self.grp_name}' + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(read_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertFalse(response.data['results']) + + +class FileBrowserFileGroupPermissionDetailViewTests(FileBrowserViewTests): + """ + Test the filegrouppermission-detail view. + """ + + def setUp(self): + super(FileBrowserFileGroupPermissionDetailViewTests, self).setUp() + + user = User.objects.get(username=self.username) + self.grp_name = 'all_users' + + # create file + self.path = f'home/{self.username}/test9/file11.txt' + folder = ChrisFolder.objects.create(path=f'home/{self.username}/test9', + owner=user) + + self.storage_manager = connect_storage(settings) + + with io.StringIO("test file") as file11: + self.storage_manager.upload_obj(self.path, file11.read(), + content_type='text/plain') + f = UserFile(owner=user, parent_folder=folder) + f.fname.name = self.path + f.save() + + grp = Group.objects.get(name=self.grp_name) + f.grant_group_permission(grp, 'r') + + gp = FileGroupPermission.objects.get(group=grp, file=f) + + self.read_update_delete_url = reverse("filegrouppermission-detail", + kwargs={"pk": gp.id}) + + self.put = json.dumps({ + "template": {"data": [{"name": "permission", "value": "w"}]}}) + + + def tearDown(self): + # delete file + f = ChrisFile.objects.get(fname=self.path) + f.delete() + + super(FileBrowserFileGroupPermissionDetailViewTests, self).tearDown() + + def test_filebrowserfilegrouppermission_detail_success(self): + self.client.login(username=self.username, password=self.password) + response = self.client.get(self.read_update_delete_url) + self.assertContains(response, 'all_users') + self.assertContains(response, self.path) + + def test_filebrowserfilegrouppermission_detail_shared_success(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.read_update_delete_url) + self.assertContains(response, 'all_users') + self.assertContains(response, self.path) + + def test_filebrowserfilegrouppermission_detail_failure_unauthenticated(self): + response = self.client.get(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfilegrouppermission_update_success(self): + self.client.login(username=self.username, password=self.password) + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["permission"], 'w') + + def test_filebrowserfilegrouppermission_update_failure_unauthenticated(self): + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfilegrouppermission_update_failure_user_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_filebrowserfilegrouppermission_delete_success(self): + f = ChrisFile.objects.get(fname=self.path) + grp = Group.objects.get(name='pacs_users') + + # create a group permission + f.grant_group_permission(grp, 'r') + gp = FileGroupPermission.objects.get(group=grp, file=f) + + read_update_delete_url = reverse("filegrouppermission-detail", + kwargs={"pk": gp.id}) + self.client.login(username=self.username, password=self.password) + response = self.client.delete(read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_filebrowserfilegrouppermission_delete_failure_unauthenticated(self): + response = self.client.delete(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfilegrouppermission_delete_failure_user_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.delete(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +class FileBrowserFileUserPermissionListViewTests(FileBrowserViewTests): + """ + Test the 'fileuserpermission-list' view. + """ + + def setUp(self): + super(FileBrowserFileUserPermissionListViewTests, self).setUp() + + user = User.objects.get(username=self.username) + + # create file + self.path = f'home/{self.username}/test9/file11.txt' + folder = ChrisFolder.objects.create(path=f'home/{self.username}/test9', + owner=user) + + self.storage_manager = connect_storage(settings) + + with io.StringIO("test file") as file11: + self.storage_manager.upload_obj(self.path, file11.read(), + content_type='text/plain') + f = UserFile(owner=user, parent_folder=folder) + f.fname.name = self.path + f.save() + + self.create_read_url = reverse('fileuserpermission-list', + kwargs={"pk": f.id}) + self.post = json.dumps( + {"template": + {"data": [{"name": "username", "value": self.other_username}, + {"name": "permission", "value": "r"}]}}) + + def tearDown(self): + # delete file + f = ChrisFile.objects.get(fname=self.path) + f.delete() + + super(FileBrowserFileUserPermissionListViewTests, self).tearDown() + + def test_filebrowserfileuserpermission_create_success(self): + user = User.objects.get(username=self.username) + + self.client.login(username=self.username, password=self.password) + + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + f = ChrisFile.objects.get(fname=self.path) + self.assertIn(self.other_username, [u.username for u in f.shared_users.all()]) + + f.remove_shared_link() + + def test_filebrowserfileuserpermission_create_failure_unauthenticated(self): + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfileuserpermission_create_failure_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_filebrowserfileuserpermission_shared_create_failure_access_denied(self): + user = User.objects.get(username=self.other_username) + f = ChrisFile.objects.get(fname=self.path) + f.grant_user_permission(user, 'w') + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.post(self.create_read_url, data=self.post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_filebrowserfileuserpermission_list_success(self): + user = User.objects.get(username=self.other_username) + f = ChrisFile.objects.get(fname=self.path) + f.grant_user_permission(user, 'r') + + self.client.login(username=self.username, password=self.password) + response = self.client.get(self.create_read_url) + self.assertContains(response, self.other_username) + + def test_filebrowserfileuserpermission_list_failure_unauthenticated(self): + response = self.client.get(self.create_read_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfileuserpermission_list_failure_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.create_read_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_filebrowserfileuserpermission_shared_user_list_success(self): + user = User.objects.get(username=self.other_username) + f = ChrisFile.objects.get(fname=self.path) + f.grant_user_permission(user, 'w') + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.create_read_url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class FileBrowserFileUserPermissionListQuerySearchViewTests(FileBrowserViewTests): + """ + Test the 'fileuserpermission-list-query-search' view. + """ + + def setUp(self): + super(FileBrowserFileUserPermissionListQuerySearchViewTests, self).setUp() + + user = User.objects.get(username=self.username) + + # create file + self.path = f'home/{self.username}/test10/file12.txt' + folder = ChrisFolder.objects.create(path=f'home/{self.username}/test10', + owner=user) + + self.storage_manager = connect_storage(settings) + + with io.StringIO("test file") as file12: + self.storage_manager.upload_obj(self.path, file12.read(), + content_type='text/plain') + f = UserFile(owner=user, parent_folder=folder) + f.fname.name = self.path + f.save() + + self.read_url = reverse('fileuserpermission-list-query-search', + kwargs={"pk": f.id}) + + other_user = User.objects.get(username=self.other_username) + f.grant_user_permission(other_user, 'r') + + def tearDown(self): + # delete file + f = ChrisFile.objects.get(fname=self.path) + f.delete() + + super(FileBrowserFileUserPermissionListQuerySearchViewTests, self).tearDown() + + def test_filebrowserfileuserpermission_list_query_search_success(self): + read_url = f'{self.read_url}?username={self.other_username}' + + self.client.login(username=self.username, password=self.password) + response = self.client.get(read_url) + self.assertContains(response, self.other_username) + + def test_filebrowserfileuserpermission_list_query_search_success_shared(self): + read_url = f'{self.read_url}?username={self.other_username}' + + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(read_url) + self.assertContains(response, self.other_username) + + def test_filebrowserfileuserpermission_list_query_search_failure_unauthenticated(self): + read_url = f'{self.read_url}?username={self.other_username}' + + response = self.client.get(read_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + +class FileBrowserFileUserPermissionDetailViewTests(FileBrowserViewTests): + """ + Test the fileuserpermission-detail view. + """ + + def setUp(self): + super(FileBrowserFileUserPermissionDetailViewTests, self).setUp() + + user = User.objects.get(username=self.username) + + # create file + self.path = f'home/{self.username}/test10/file12.txt' + folder = ChrisFolder.objects.create(path=f'home/{self.username}/test10', + owner=user) + + self.storage_manager = connect_storage(settings) + + with io.StringIO("test file") as file12: + self.storage_manager.upload_obj(self.path, file12.read(), + content_type='text/plain') + f = UserFile(owner=user, parent_folder=folder) + f.fname.name = self.path + f.save() + + other_user = User.objects.get(username=self.other_username) + f.grant_user_permission(other_user, 'r') + + up = FileUserPermission.objects.get(user=other_user, file=f) + + self.read_update_delete_url = reverse("fileuserpermission-detail", + kwargs={"pk": up.id}) + + self.put = json.dumps({ + "template": {"data": [{"name": "permission", "value": "w"}]}}) + + + def tearDown(self): + # delete file + f = ChrisFile.objects.get(fname=self.path) + f.delete() + + super(FileBrowserFileUserPermissionDetailViewTests, self).tearDown() + + def test_filebrowserfileuserpermission_detail_success(self): + self.client.login(username=self.username, password=self.password) + response = self.client.get(self.read_update_delete_url) + self.assertContains(response, self.other_username) + self.assertContains(response, self.path) + + def test_filebrowserfileuserpermission_detail_shared_success(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.get(self.read_update_delete_url) + self.assertContains(response, self.other_username) + self.assertContains(response, self.path) + + def test_filebrowserfileuserpermission_detail_failure_unauthenticated(self): + response = self.client.get(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfileuserpermission_update_success(self): + user = User.objects.get(username=self.username) + + self.client.login(username=self.username, password=self.password) + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["permission"], 'w') + + def test_filebrowserfileuserpermission_update_failure_unauthenticated(self): + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfileuserpermission_update_failure_user_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_filebrowserfileuserpermission_delete_success(self): + self.client.login(username=self.username, password=self.password) + response = self.client.delete(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_filebrowserfileuserpermission_delete_failure_unauthenticated(self): + response = self.client.delete(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfileuserpermission_delete_failure_user_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.delete(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +class FileBrowserFolderLinkFileListViewTests(FileBrowserViewTests): + """ + Test the 'chrisfolder-linkfile-list' view. + """ + + def setUp(self): + super(FileBrowserFolderLinkFileListViewTests, self).setUp() + + self.storage_manager = connect_storage(settings) + + # create compute resource + (compute_resource, tf) = ComputeResource.objects.get_or_create( + name="host", compute_url=COMPUTE_RESOURCE_URL) + + # create 'fs' plugin + (pl_meta, tf) = PluginMeta.objects.get_or_create(name='pacspull', type='fs') + (plugin, tf) = Plugin.objects.get_or_create(meta=pl_meta, version='0.1') + plugin.compute_resources.set([compute_resource]) + plugin.save() + + self.plugin = plugin + + # create link file + self.link_path = f'home/{self.username}/feeds/feed_1/out/SERVICES_PACS.chrislink' + user = User.objects.get(username=self.username) + + folder_path = os.path.dirname(self.link_path) + (parent_folder, _) = ChrisFolder.objects.get_or_create(path=folder_path, + owner=user) + self.link_file = ChrisLinkFile(path='SERVICES/PACS', owner=user, + parent_folder=parent_folder) + self.link_file.save(name='SERVICES_PACS') + self.read_url = reverse("chrisfolder-linkfile-list", + kwargs={"pk": parent_folder.id}) + + def tearDown(self): + self.link_file.delete() + super(FileBrowserFolderLinkFileListViewTests, self).tearDown() + + def test_filebrowserfolderlinkfile_list_success(self): + self.client.login(username=self.username, password=self.password) + response = self.client.get(self.read_url) + self.assertContains(response, 'file_resource') + self.assertContains(response, self.link_path) + + def test_filebrowserfolderlinkfile_list_success_public_feed_unauthenticated(self): + user = User.objects.get(username=self.username) + + # create a feed by creating a "fs" plugin instance + pl_inst = PluginInstance.objects.create(plugin=self.plugin, owner=user, + title='test', + compute_resource= + self.plugin.compute_resources.all()[0]) + + # create link file + link_path = f'{pl_inst.output_folder.path}/SERVICES_PACS.chrislink' + + link_file = ChrisLinkFile(path='SERVICES/PACS', owner=user, + parent_folder=pl_inst.output_folder) + link_file.save(name='SERVICES_PACS') + + # make feed public + pl_inst.feed.grant_public_access() + + read_url = reverse("chrisfolder-linkfile-list", + kwargs={"pk": pl_inst.output_folder.id}) + response = self.client.get(read_url) + self.assertContains(response, 'file_resource') + self.assertContains(response, link_path) + + pl_inst.feed.remove_public_access() + link_file.delete() + + def test_filebrowserfolderlinkfile_list_failure_unauthenticated(self): + response = self.client.get(self.read_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserfolderlinkfile_list_failure_not_found(self): + self.client.login(username=self.username, password=self.password) + response = self.client.get(reverse("chrisfolder-linkfile-list", kwargs={"pk": 111111111})) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) @@ -966,7 +2426,7 @@ def test_fileBrowserlinkfile_list_success_shared_feed(self): response = self.client.get(read_url) self.assertContains(response, link_path) - self.storage_manager.delete_obj(link_path) + link_file.delete() def test_fileBrowserlinkfile_list_failure_shared_feed_unauthenticated(self): other_user = User.objects.get(username=self.other_username) @@ -981,8 +2441,6 @@ def test_fileBrowserlinkfile_list_failure_shared_feed_unauthenticated(self): pl_inst.feed.save() # create link file in the output folder - link_path = f'{pl_inst.output_folder.path}/SERVICES_PACS.chrislink' - link_file = ChrisLinkFile(path='SERVICES/PACS', owner=other_user, parent_folder=pl_inst.output_folder) link_file.save(name='SERVICES_PACS') @@ -994,7 +2452,7 @@ def test_fileBrowserlinkfile_list_failure_shared_feed_unauthenticated(self): response = self.client.get(read_url) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.storage_manager.delete_obj(link_path) + link_file.delete() class FileBrowserLinkFileDetailViewTests(FileBrowserViewTests): """ @@ -1028,14 +2486,14 @@ def setUp(self): # create link file self.link_path = f'{self.pl_inst.output_folder.path}/SERVICES_PACS.chrislink' - link_file = ChrisLinkFile(path='SERVICES/PACS', owner=user, + self.link_file = ChrisLinkFile(path='SERVICES/PACS', owner=user, parent_folder=self.pl_inst.output_folder) - link_file.save(name='SERVICES_PACS') + self.link_file.save(name='SERVICES_PACS') self.read_url = reverse("chrislinkfile-detail", - kwargs={"pk": link_file.id}) + kwargs={"pk": self.link_file.id}) def tearDown(self): - self.storage_manager.delete_obj(self.link_path) + self.link_file.delete() super(FileBrowserLinkFileDetailViewTests, self).tearDown() def test_fileBrowserlinkfile_detail_success(self): @@ -1060,6 +2518,7 @@ def test_fileBrowserlinkfile_detail_success_public_feed_unauthenticated(self): self.pl_inst.feed.grant_public_access() response = self.client.get(self.read_url) self.assertContains(response, self.link_path) + self.pl_inst.feed.remove_public_access() def test_fileBrowserlinkfile_detail_success_shared_feed(self): other_user = User.objects.get(username=self.other_username) @@ -1089,7 +2548,7 @@ def test_fileBrowserlinkfile_detail_success_shared_feed(self): response = self.client.get(read_url) self.assertContains(response, link_path) - self.storage_manager.delete_obj(link_path) + link_file.delete() def test_fileBrowserlinkfile_detail_failure_unauthorized_shared_feed_unauthenticated(self): other_user = User.objects.get(username=self.other_username) @@ -1102,20 +2561,21 @@ def test_fileBrowserlinkfile_detail_failure_unauthorized_shared_feed_unauthentic plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) # create link file in the output folder - link_path = f'{pl_inst.output_folder.path}/SERVICES_PACS.chrislink' link_file = ChrisLinkFile(path='SERVICES/PACS', owner=other_user, parent_folder=pl_inst.output_folder) link_file.save(name='SERVICES_PACS') + # share feed + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) + read_url = reverse("chrislinkfile-detail", kwargs={"pk": link_file.id}) response = self.client.get(read_url) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.storage_manager.delete_obj(link_path) + link_file.delete() class FileBrowserLinkFileResourceViewTests(FileBrowserViewTests): @@ -1148,17 +2608,16 @@ def setUp(self): plugin.compute_resources.all()[0]) # create link file - self.link_path = f'{self.pl_inst.output_folder.path}/SERVICES_PACS.chrislink' - - link_file = ChrisLinkFile(path='SERVICES/PACS', owner=user, + self.link_file = ChrisLinkFile(path='SERVICES/PACS', owner=user, parent_folder=self.pl_inst.output_folder) - link_file.save(name='SERVICES_PACS') + self.link_file.save(name='SERVICES_PACS') - self.download_url = reverse("chrislinkfile-resource", - kwargs={"pk": link_file.id}) + 'SERVICES_PACS.chrislink' + self.download_url = (reverse("chrislinkfile-resource", + kwargs={"pk": self.link_file.id}) + + 'SERVICES_PACS.chrislink') def tearDown(self): - self.storage_manager.delete_obj(self.link_path) + self.link_file.delete() super(FileBrowserLinkFileResourceViewTests, self).tearDown() def test_fileBrowserlinkfile_resource_success(self): @@ -1189,6 +2648,7 @@ def test_fileBrowserlinkfile_resource_success_public_feed_unauthenticated(self): self.assertEqual(response.status_code, 200) content = [c for c in response.streaming_content][0].decode('utf-8') self.assertEqual(content, 'SERVICES/PACS') + self.pl_inst.feed.remove_public_access() def test_fileBrowserlinkfile_resource_success_shared_link_file(self): other_user = User.objects.get(username=self.other_username) @@ -1212,6 +2672,8 @@ def test_fileBrowserlinkfile_resource_success_shared_link_file(self): content = [c for c in response.streaming_content][0].decode('utf-8') self.assertEqual(content, 'SERVICES/PACS') + link_file.delete() + def test_fileBrowserlinkfile_resource_failure_unauthorized_shared_feed_unauthenticated(self): other_user = User.objects.get(username=self.other_username) plugin = self.plugin @@ -1223,19 +2685,20 @@ def test_fileBrowserlinkfile_resource_failure_unauthorized_shared_feed_unauthent plugin.compute_resources.all()[0]) pl_inst.feed.name = 'shared_feed' pl_inst.feed.save() - pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) - # create link file in the output folder - link_path = f'{pl_inst.output_folder.path}/SERVICES_PACS.chrislink' + # create link file in the output folder link_file = ChrisLinkFile(path='SERVICES/PACS', owner=other_user, parent_folder=pl_inst.output_folder) link_file.save(name='SERVICES_PACS') + # share feed + pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) + download_url = reverse("chrislinkfile-resource", kwargs={"pk": link_file.id}) + 'SERVICES_PACS.chrislink' response = self.client.get(download_url) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.storage_manager.delete_obj(link_path) + link_file.delete() diff --git a/chris_backend/filebrowser/views.py b/chris_backend/filebrowser/views.py index ba4e4c57..672aaaa4 100755 --- a/chris_backend/filebrowser/views.py +++ b/chris_backend/filebrowser/views.py @@ -3,7 +3,7 @@ from django.http import Http404, FileResponse from django.shortcuts import get_object_or_404 -from rest_framework import generics, permissions +from rest_framework import generics, permissions, serializers from rest_framework.reverse import reverse from rest_framework.authentication import BasicAuthentication, SessionAuthentication @@ -140,6 +140,21 @@ def update(self, request, *args, **kwargs): request.data.pop('path', None) # change path is not implemented yet return super(FileBrowserFolderDetail, self).update(request, *args, **kwargs) + def destroy(self, request, *args, **kwargs): + """ + Overriden to verify that the user's home or feeds folder is not being deleted. + """ + username = request.user.username + folder = self.get_object() + + if username != 'chris' and folder.path in ( + f'home/{username}', f'home/{username}/feeds'): + raise serializers.ValidationError( + {'non_field_errors': + [f"Deleting folder '{folder.path}' is not allowed."]}) + + return super(FileBrowserFolderDetail, self).destroy(request, *args, **kwargs) + class FileBrowserFolderChildList(generics.ListAPIView): """ @@ -757,6 +772,22 @@ def update(self, request, *args, **kwargs): request.data['fname'] = chris_link_file.fname.file # fname required in the serializer return super(FileBrowserLinkFileDetail, self).update(request, *args, **kwargs) + def destroy(self, request, *args, **kwargs): + """ + Overriden to verify that the user's home's system-predefined link files are not + being deleted. + """ + username = request.user.username + lf = self.get_object() + + if username != 'chris' and lf.fname.name in ( + f'home/{username}/public.chrislink', f'home/{username}/shared.chrislink'): + raise serializers.ValidationError( + {'non_field_errors': + [f"Deleting link file '{lf.fname.name}' is not allowed."]}) + + return super(FileBrowserLinkFileDetail, self).destroy(request, *args, **kwargs) + class FileBrowserLinkFileResource(generics.GenericAPIView): """ From afabf33660dc19ece65c5c551904bb654064be2a Mon Sep 17 00:00:00 2001 From: Jorge Date: Fri, 28 Jun 2024 14:53:30 -0400 Subject: [PATCH 09/11] Add additional serializer validation that prevents deleting a user's home folder --- chris_backend/filebrowser/serializers.py | 36 +++++----- chris_backend/filebrowser/views.py | 20 +++--- chris_backend/userfiles/serializers.py | 66 +++++++++++++------ .../userfiles/tests/test_serializers.py | 35 ++++++++-- chris_backend/userfiles/views.py | 2 +- 5 files changed, 110 insertions(+), 49 deletions(-) diff --git a/chris_backend/filebrowser/serializers.py b/chris_backend/filebrowser/serializers.py index 827fbdbf..9e51e215 100755 --- a/chris_backend/filebrowser/serializers.py +++ b/chris_backend/filebrowser/serializers.py @@ -111,7 +111,7 @@ def validate_public(self, public): if not (self.instance.owner == user or user.username == 'chris'): raise serializers.ValidationError( - ["Public status of a feed can only be changed by its owner or" + ["Public status of a folder can only be changed by its owner or" "superuser 'chris'."]) return public @@ -127,11 +127,15 @@ def validate(self, data): username = self.context['request'].user.username - if 'path' in data and username != 'chris' and self.instance.path in ( - f'home/{username}', f'home/{username}/feeds'): - raise serializers.ValidationError( - {'non_field_errors': - [f"Moving folder '{self.instance.path}' is not allowed."]}) + if 'path' in data and username != 'chris': + inst_path_parts = self.instance.path.split('/') + + if len(inst_path_parts) > 1 and inst_path_parts[0] == 'home' and ( + len(inst_path_parts) == 2 or (len(inst_path_parts) == 3 and + inst_path_parts[2] == 'feeds')): + raise serializers.ValidationError( + {'non_field_errors': + [f"Moving folder '{self.instance.path}' is not allowed."]}) else: if 'path' not in data: # on create raise serializers.ValidationError({'path': ['This field is required.']}) @@ -369,7 +373,7 @@ def validate_public(self, public): if not (self.instance.owner == user or user.username == 'chris'): raise serializers.ValidationError( - ["Public status of a feed can only be changed by its owner or" + ["Public status of a file can only be changed by its owner or" "superuser 'chris'."]) return public @@ -657,7 +661,7 @@ def validate_public(self, public): if not (self.instance.owner == user or user.username == 'chris'): raise serializers.ValidationError( - ["Public status of a feed can only be changed by its owner or" + ["Public status of a link file can only be changed by its owner or" "superuser 'chris'."]) return public @@ -674,15 +678,17 @@ def validate(self, data): "or 'new_link_file_path' must be provided."]}) username = self.context['request'].user.username - fname = self.instance.fname.name - if ('new_link_file_path' in data and username != 'chris' and fname in ( - f'home/{username}/public.chrislink', - f'home/{username}/shared.chrislink')): - raise serializers.ValidationError( - {'non_field_errors': - [f"Moving link file '{fname}' is not allowed."]}) + if 'new_link_file_path' in data and username != 'chris': + fname = self.instance.fname.name + fname_parts = fname.split('/') + + if len(fname_parts) == 3 and fname_parts[0] == 'home' and ( + fname_parts[2] in ('public.chrislink', 'shared.chrislink')): + raise serializers.ValidationError( + {'non_field_errors': + [f"Moving link file '{fname}' is not allowed."]}) return data diff --git a/chris_backend/filebrowser/views.py b/chris_backend/filebrowser/views.py index 672aaaa4..ac23cd97 100755 --- a/chris_backend/filebrowser/views.py +++ b/chris_backend/filebrowser/views.py @@ -146,12 +146,14 @@ def destroy(self, request, *args, **kwargs): """ username = request.user.username folder = self.get_object() + path_parts = folder.path.split('/') - if username != 'chris' and folder.path in ( - f'home/{username}', f'home/{username}/feeds'): - raise serializers.ValidationError( - {'non_field_errors': - [f"Deleting folder '{folder.path}' is not allowed."]}) + if username != 'chris' and len(path_parts) > 1 and path_parts[0] == 'home': + if len(path_parts) == 2 or (len(path_parts) == 3 and path_parts[2] == 'feeds'): + + raise serializers.ValidationError( + {'non_field_errors': + [f"Deleting folder '{folder.path}' is not allowed."]}) return super(FileBrowserFolderDetail, self).destroy(request, *args, **kwargs) @@ -465,7 +467,7 @@ def update(self, request, *args, **kwargs): Overriden to include the current fname in the request. """ chris_file = self.get_object() - request.data['fname'] = chris_file .fname.file # fname required in the serializer + request.data['fname'] = chris_file.fname.file # fname required in the serializer return super(FileBrowserFileDetail, self).update(request, *args, **kwargs) @@ -779,9 +781,11 @@ def destroy(self, request, *args, **kwargs): """ username = request.user.username lf = self.get_object() + fname_parts = lf.fname.name.split('/') + + if username != 'chris' and len(fname_parts) == 3 and fname_parts[0] == 'home' and ( + fname_parts[2] in ('public.chrislink', 'shared.chrislink')): - if username != 'chris' and lf.fname.name in ( - f'home/{username}/public.chrislink', f'home/{username}/shared.chrislink'): raise serializers.ValidationError( {'non_field_errors': [f"Deleting link file '{lf.fname.name}' is not allowed."]}) diff --git a/chris_backend/userfiles/serializers.py b/chris_backend/userfiles/serializers.py index 8d3b3078..71cbdca9 100755 --- a/chris_backend/userfiles/serializers.py +++ b/chris_backend/userfiles/serializers.py @@ -15,17 +15,22 @@ class UserFileSerializer(serializers.HyperlinkedModelSerializer): fname = serializers.FileField(use_url=False) fsize = serializers.ReadOnlyField(source='fname.size') - upload_path = serializers.CharField(max_length=1024, write_only=True) + upload_path = serializers.CharField(max_length=1024, write_only=True, required=False) owner_username = serializers.ReadOnlyField(source='owner.username') file_resource = ItemLinkField('get_file_link') parent_folder = serializers.HyperlinkedRelatedField(view_name='chrisfolder-detail', read_only=True) + group_permissions = serializers.HyperlinkedIdentityField( + view_name='filegrouppermission-list') + user_permissions = serializers.HyperlinkedIdentityField( + view_name='fileuserpermission-list') owner = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True) class Meta: model = UserFile fields = ('url', 'id', 'creation_date', 'upload_path', 'fname', 'fsize', 'public', - 'owner_username', 'file_resource', 'parent_folder', 'owner') + 'owner_username', 'file_resource', 'parent_folder', 'group_permissions', + 'user_permissions','owner') def create(self, validated_data): """ @@ -50,24 +55,30 @@ def update(self, instance, validated_data): Overriden to set the file's saving path and parent folder and delete the old path from storage. """ - # user file will be stored at: SWIFT_CONTAINER_NAME/ - # where must start with home/ - upload_path = validated_data.pop('upload_path') - old_storage_path = instance.fname.name + if 'public' in validated_data: + instance.public = validated_data['public'] - storage_manager = connect_storage(settings) - if storage_manager.obj_exists(upload_path): - storage_manager.delete_obj(upload_path) + upload_path = validated_data.pop('upload_path', None) - storage_manager.copy_obj(old_storage_path, upload_path) - storage_manager.delete_obj(old_storage_path) + if upload_path: + # user file will be stored at: SWIFT_CONTAINER_NAME/ + # where must start with home/ + old_storage_path = instance.fname.name + + storage_manager = connect_storage(settings) + if storage_manager.obj_exists(upload_path): + storage_manager.delete_obj(upload_path) + + storage_manager.copy_obj(old_storage_path, upload_path) + storage_manager.delete_obj(old_storage_path) + + folder_path = os.path.dirname(upload_path) + owner = instance.owner + (parent_folder, _) = ChrisFolder.objects.get_or_create(path=folder_path, + owner=owner) + instance.parent_folder = parent_folder + instance.fname.name = upload_path - folder_path = os.path.dirname(upload_path) - owner = instance.owner - (parent_folder, _) = ChrisFolder.objects.get_or_create(path=folder_path, - owner=owner) - instance.parent_folder = parent_folder - instance.fname.name = upload_path instance.save() return instance @@ -82,8 +93,7 @@ def validate_upload_path(self, upload_path): Overriden to check whether the provided path is under a home/'s subdirectory for which the user has write permission. """ - # remove leading and trailing slashes - upload_path = upload_path.strip(' ').strip('/') + upload_path = upload_path.strip().strip('/') if upload_path.endswith('.chrislink'): raise serializers.ValidationError(["Invalid path. Uploading ChRIS link " @@ -104,7 +114,23 @@ def validate_upload_path(self, upload_path): if not (folder.owner == user or folder.public or folder.has_user_permission(user, 'w')): - raise serializers.ValidationError([f"Invalid path. User do not have write " + raise serializers.ValidationError([f"Invalid path. User does not have write " f"permission under the folder " f"'{folder_path}'."]) return upload_path + + def validate(self, data): + """ + Overriden to validate that at least one of two fields are in data when + updating a file. + """ + if self.instance: # on update + if 'public' not in data and 'upload_path' not in data: + raise serializers.ValidationError( + {'non_field_errors': ["At least one of the fields 'public' " + "or 'upload_path' must be provided."]}) + else: # on create + if 'upload_path' not in data: + raise serializers.ValidationError( + {'upload_path': ["This field is required."]}) + return data diff --git a/chris_backend/userfiles/tests/test_serializers.py b/chris_backend/userfiles/tests/test_serializers.py index bceaf4b3..97103ada 100755 --- a/chris_backend/userfiles/tests/test_serializers.py +++ b/chris_backend/userfiles/tests/test_serializers.py @@ -84,10 +84,10 @@ def test_update(self): connect_storage_mock.assert_called_with(settings) storage_manager_mock.delete_obj.assert_called_with(upload_path) - def test_validate_upload_path_failure_does_not_start_with_home_username(self): + def test_validate_upload_path_failure_uploading_link_file(self): """ Test whether overriden validate_upload_path method validates submitted path - must start with the 'home//' string. + does not end with the .chrislink string. """ userfiles_serializer = UserFileSerializer() request = mock.Mock() @@ -95,11 +95,36 @@ def test_validate_upload_path_failure_does_not_start_with_home_username(self): with mock.patch.dict(userfiles_serializer.context, {'request': request}, clear=True): with self.assertRaises(serializers.ValidationError): - userfiles_serializer.validate_upload_path('random/file1.txt') + upload_path = f'home/{self.username}/uploads/mylink.chrislink' + userfiles_serializer.validate_upload_path(upload_path) + + def test_validate_upload_path_failure_does_not_start_with_home(self): + """ + Test whether overriden validate_upload_path method validates submitted path + must start with the 'home/' string. + """ + userfiles_serializer = UserFileSerializer() + request = mock.Mock() + request.user = User.objects.get(username=self.username) + with mock.patch.dict(userfiles_serializer.context, + {'request': request}, clear=True): + with self.assertRaises(serializers.ValidationError): + userfiles_serializer.validate_upload_path('home') with self.assertRaises(serializers.ValidationError): - userfiles_serializer.validate_upload_path(f'home/{self.username}_file1.txt') + userfiles_serializer.validate_upload_path('random/file1.txt') + + def test_validate_upload_path_failure_does_not_have_write_permission(self): + """ + Test whether overriden validate_upload_path method validates submitted path + must start with the 'home//' string. + """ + userfiles_serializer = UserFileSerializer() + request = mock.Mock() + request.user = User.objects.get(username=self.username) + with mock.patch.dict(userfiles_serializer.context, + {'request': request}, clear=True): with self.assertRaises(serializers.ValidationError): - userfiles_serializer.validate_upload_path(f'{self.username}/uploads_file1.txt') + userfiles_serializer.validate_upload_path('SERVICES/PACS/random/file1.txt') @tag('integration') def test_validate_upload_path_success(self): diff --git a/chris_backend/userfiles/views.py b/chris_backend/userfiles/views.py index 292ec62f..ed292565 100755 --- a/chris_backend/userfiles/views.py +++ b/chris_backend/userfiles/views.py @@ -93,7 +93,7 @@ def retrieve(self, request, *args, **kwargs): Overriden to append a collection+json template. """ response = super(UserFileDetail, self).retrieve(request, *args, **kwargs) - template_data = {"upload_path": ""} + template_data = {"upload_path": "", "public": ""} return services.append_collection_template(response, template_data) def update(self, request, *args, **kwargs): From 58d233e975cfc40fe00cde596abf15da584b5ccb Mon Sep 17 00:00:00 2001 From: Jorge Date: Sat, 6 Jul 2024 19:18:57 -0400 Subject: [PATCH 10/11] Add a filter API for the users of a group --- chris_backend/core/api.py | 4 ++++ chris_backend/users/models.py | 8 ++++++++ chris_backend/users/views.py | 38 +++++++++++++++++++++++++++++------ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/chris_backend/core/api.py b/chris_backend/core/api.py index 5d54fc54..d8f2a90b 100755 --- a/chris_backend/core/api.py +++ b/chris_backend/core/api.py @@ -47,6 +47,10 @@ user_views.GroupUserList.as_view(), name='group-user-list'), + path('v1/groups//users/search/', + user_views.GroupUserListQuerySearch.as_view(), + name='group-user-list-query-search'), + path('v1/groups/users//', user_views.GroupUserDetail.as_view(), name='user_groups-detail'), diff --git a/chris_backend/users/models.py b/chris_backend/users/models.py index 2f05d0e2..4dbc0808 100755 --- a/chris_backend/users/models.py +++ b/chris_backend/users/models.py @@ -25,6 +25,14 @@ class Meta: fields = ['id', 'name', 'name_icontains'] +class GroupUserFilter(FilterSet): + username = django_filters.CharFilter(field_name='user__username', lookup_expr='exact') + + class Meta: + model = User.groups.through + fields = ['id', 'username'] + + class UserProxy(User): class Meta: diff --git a/chris_backend/users/views.py b/chris_backend/users/views.py index 0ab26bd7..513a46a1 100755 --- a/chris_backend/users/views.py +++ b/chris_backend/users/views.py @@ -1,12 +1,13 @@ from django.contrib.auth.models import User, Group +from django.shortcuts import get_object_or_404 from rest_framework import generics, permissions, serializers from rest_framework.reverse import reverse from rest_framework.response import Response from collectionjson import services -from .models import GroupFilter +from .models import GroupFilter, GroupUserFilter from .serializers import UserSerializer, GroupSerializer, GroupUserSerializer from .permissions import IsUserOrChrisOrReadOnly, IsAdminOrReadOnly @@ -137,7 +138,7 @@ class GroupDetail(generics.RetrieveDestroyAPIView): class GroupUserList(generics.ListCreateAPIView): """ - A view for a group-specific collection of users. + A view for a group-specific collection of group users. """ http_method_names = ['get', 'post'] queryset = Group.objects.all() @@ -158,27 +159,52 @@ def perform_create(self, serializer): def list(self, request, *args, **kwargs): """ - Overriden to return a list of the users for the queried group. - Document-level link relations and a collection+json template are also added - to the response. + Overriden to return a list of the group users for the queried group. + A query list, document-level link relations and a collection+json template + are also added to the response. """ queryset = self.get_users_queryset() response = services.get_list_response(self, queryset) group = self.get_object() + + query_list = [reverse('group-user-list-query-search', + request=request, kwargs={"pk": group.id})] + response = services.append_collection_querylist(response, query_list) + links = {'group': reverse('group-detail', request=request, kwargs={"pk": group.id})} response = services.append_collection_links(response, links) + template_data = {"username": ""} return services.append_collection_template(response, template_data) def get_users_queryset(self): """ - Custom method to get the actual users queryset for the group. + Custom method to get the actual group users queryset for the group. """ group = self.get_object() return User.groups.through.objects.filter(group=group) +class GroupUserListQuerySearch(generics.ListAPIView): + """ + A view for the collection of group users resulting from a query + search. + """ + http_method_names = ['get'] + serializer_class = GroupUserSerializer + permission_classes = (permissions.IsAuthenticated, IsAdminOrReadOnly) + filterset_class = GroupUserFilter + + def get_queryset(self): + """ + Overriden to return a custom queryset that is comprised by the group-specific + group users. + """ + group = get_object_or_404(Group, pk=self.kwargs['pk']) + return User.groups.through.objects.filter(group=group) + + class GroupUserDetail(generics.RetrieveDestroyAPIView): """ A view for a group-user relationship that can be used by ChRIS admins to delete From 99cc0d0ed9be35d0467cff18b59808d7f9db81c4 Mon Sep 17 00:00:00 2001 From: Jorge Date: Thu, 11 Jul 2024 14:14:35 -0400 Subject: [PATCH 11/11] Fix allowed http methods in pacs files API --- chris_backend/pacsfiles/views.py | 2 +- chris_backend/userfiles/views.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/chris_backend/pacsfiles/views.py b/chris_backend/pacsfiles/views.py index 663e16ff..d01e5204 100755 --- a/chris_backend/pacsfiles/views.py +++ b/chris_backend/pacsfiles/views.py @@ -71,7 +71,7 @@ class PACSFileList(generics.ListAPIView): """ A view for the collection of PACS files. """ - http_method_names = ['get', 'post'] + http_method_names = ['get'] queryset = PACSFile.get_base_queryset() serializer_class = PACSFileSerializer permission_classes = (permissions.IsAuthenticated, IsChrisOrIsPACSUserReadOnly) diff --git a/chris_backend/userfiles/views.py b/chris_backend/userfiles/views.py index ed292565..7a5f363a 100755 --- a/chris_backend/userfiles/views.py +++ b/chris_backend/userfiles/views.py @@ -42,8 +42,7 @@ def perform_create(self, serializer): def list(self, request, *args, **kwargs): """ - Overriden to append document-level link relations, a query list and a - collection+json template to the response. + Overriden to append a query list and a collection+json template to the response. """ response = super(UserFileList, self).list(request, *args, **kwargs)