diff --git a/chris_backend/config/settings/local.py b/chris_backend/config/settings/local.py index 58e49308..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 = ['*'] @@ -189,7 +192,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..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 @@ -162,7 +166,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 88d2856a..d8f2a90b 100755 --- a/chris_backend/core/api.py +++ b/chris_backend/core/api.py @@ -31,6 +31,30 @@ 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/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'), + path('v1/downloadtokens/', core_views.FileDownloadTokenList.as_view(), @@ -61,6 +85,30 @@ path('v1/note/', feed_views.NoteDetail.as_view(), name='note-detail'), + path('v1//grouppermissions/', + feed_views.FeedGroupPermissionList.as_view(), + name='feedgrouppermission-list'), + + path('v1//grouppermissions/search/', + feed_views.FeedGroupPermissionListQuerySearch.as_view(), + name='feedgrouppermission-list-query-search'), + + path('v1/grouppermissions//', + feed_views.FeedGroupPermissionDetail.as_view(), + name='feedgrouppermission-detail'), + + path('v1//userpermissions/', + feed_views.FeedUserPermissionList.as_view(), + name='feeduserpermission-list'), + + path('v1//userpermissions/search/', + feed_views.FeedUserPermissionListQuerySearch.as_view(), + name='feeduserpermission-list-query-search'), + + path('v1/userpermissions//', + feed_views.FeedUserPermissionDetail.as_view(), + name='feeduserpermission-detail'), + path('v1//comments/', feed_views.CommentList.as_view(), name='comment-list'), @@ -358,26 +406,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='foldergrouppermission-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..41304661 100755 --- a/chris_backend/core/apps.py +++ b/chris_backend/core/apps.py @@ -1,5 +1,52 @@ + 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 django.conf import settings + 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', + 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') + + # 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/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..d3a9e272 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',) @@ -76,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: @@ -83,17 +103,198 @@ 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) def get_descendants(self): """ Custom method to return all the folders that are a descendant of this - folder. + folder (including itself). + """ + 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=''): + """ + Custom method to determine whether a group has been granted a permission + to access the folder. + """ + 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=''): + """ + Custom method to determine whether a user has been granted a permission + 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): + """ + 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. """ - path = self.path.rstrip('/') + '/' - return list(ChrisFolder.objects.filter(path__startswith=path)) + 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 + folders, link files and files. + """ + 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 + '/')) + + for folder in folders: + folder.public = public_tf + ChrisFolder.objects.bulk_update(folders, ['public']) + + 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(fname__startswith=path)) + for lf in link_files: + lf.public = public_tf + ChrisLinkFile.objects.bulk_update(link_files, ['public']) class ChrisFolderFilter(FilterSet): @@ -104,12 +305,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', 'user_id']) + + 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', 'user_id']) + + link_files = ChrisLinkFile.objects.filter(fname__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', + 'user_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 +484,164 @@ 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 a permission to + access the file. + """ + 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=''): + """ + Custom method to determine whether a user has been granted a permission to + access the file (perhaps through one of its groups). + """ + 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.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. + """ + 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.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. + """ + try: + perm = FileUserPermission.objects.get(file=self, user=user, + permission=permission) + except FileUserPermission.DoesNotExist: + pass + else: + perm.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() + + 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): """ @@ -125,6 +650,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 +662,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 +738,165 @@ 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 a permission to + access the link file. + """ + 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=''): + """ + Custom method to determine whether a user has been granted a permission to + access the link file (perhaps through one of its groups). + """ + 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.update_or_create(link_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. + """ + try: + perm = LinkFileGroupPermission.objects.get(link_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.update_or_create(link_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. + """ + try: + perm = LinkFileUserPermission.objects.get(link_file=self, user=user, + permission=permission) + except LinkFileUserPermission.DoesNotExist: + pass + else: + perm.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() + + 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): @@ -179,6 +909,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/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/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..71035011 100755 --- a/chris_backend/feeds/models.py +++ b/chris_backend/feeds/models.py @@ -1,6 +1,7 @@ from django.db import models from django.db.models.signals import post_delete +from django.contrib.auth.models import User, Group from django.dispatch import receiver import django_filters @@ -17,7 +18,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 +46,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,10 +53,86 @@ 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(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 (perhaps through one of its groups). + """ + 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 permission to access the feed and all its + folder's descendant folders, link files and files. + """ + 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. + """ + 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 permission to access the feed and all its + folder's descendant folders, link files and files. + """ + 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. + """ + try: + perm = FeedUserPermission.objects.get(feed=self, user=user) + except FeedUserPermission.DoesNotExist: + pass + else: + perm.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.folder.create_public_link() + 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_link() + self.folder.remove_public_access() + self.save() + @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): @@ -78,8 +152,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): """ @@ -110,6 +183,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..fb6d640d 100755 --- a/chris_backend/feeds/permissions.py +++ b/chris_backend/feeds/permissions.py @@ -2,83 +2,98 @@ 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 IsOwnerOrChrisOrFeedOwnerOrHasFeedPermissionReadOnlyOrPublicFeedReadOnly( + 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 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): - # 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 obj.owner == user or user.username == 'chris' or obj.feed.owner == user: return True - return False + + 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 e42d5da0..9fcc4740 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() @@ -104,21 +102,41 @@ 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') 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') + 'group_permissions', 'user_permissions', '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_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): """ @@ -129,23 +147,19 @@ def validate_name(self, name): ["This field may not contain forward slashes."]) return name - def validate_new_owner(self, username): + def validate_public(self, public): """ - Custom method to check whether a new feed owner is a system-registered user. + Overriden to check that only the owner or superuser chris can change a feed's + public status. """ - 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 + if self.instance: # validation on update + user = self.context['request'].user - def get_creator_username(self, obj): - """ - Overriden to get the username of the creator of the feed. - """ - return obj.get_creator().username + 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): """ @@ -216,6 +230,99 @@ 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') + 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', 'grp_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. 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'] + + 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}"]}) + + lf = feed.folder.create_shared_link() + lf.grant_group_permission(group, 'r') + return feed_perm + + def validate_grp_name(self, grp_name): + """ + 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([f"Couldn't find any group with name " + f"'{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') + 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', + '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. 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'] + + 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}"]}) + + lf = feed.folder.create_shared_link() + lf.grant_user_permission(user, 'r') + return feed_perm + + def validate_username(self, username): + """ + Overriden to check whether the provided username exists in the DB. + """ + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + raise serializers.ValidationError([f"Couldn't find any user with username " + f"'{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/tests/test_models.py b/chris_backend/feeds/tests/test_models.py index 83a708f9..2fc6a111 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" @@ -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 5f37d88e..9bf169ff 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' @@ -106,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 @@ -155,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 96072728..32dc12db 100755 --- a/chris_backend/feeds/tests/test_views.py +++ b/chris_backend/feeds/tests/test_views.py @@ -4,16 +4,18 @@ 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 +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD class ViewTests(TestCase): @@ -24,15 +26,14 @@ 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 = '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 @@ -205,9 +212,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 +236,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 +257,9 @@ 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) + feed.remove_public_access() def test_feed_update_failure_unauthenticated(self): response = self.client.put(self.read_update_delete_url, data=self.put, @@ -263,14 +272,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) @@ -287,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. @@ -365,11 +768,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 +840,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): - 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) + def test_tag_list_success_unauthenticated(self): 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 +869,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 +956,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 +996,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 +1063,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 +1123,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): - 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) + def test_tag_tagging_list_success_unauthenticated(self): 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/feeds/views.py b/chris_backend/feeds/views.py index 8dbe2e48..9f60feaa 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,17 @@ 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, + IsOwnerOrChrisOrFeedOwnerOrHasFeedPermissionReadOnlyOrPublicFeedReadOnly) class NoteDetail(generics.RetrieveUpdateAPIView): @@ -26,7 +27,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 +41,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 +77,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 +87,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 +105,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,46 +126,53 @@ 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): """ - 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() 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__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() serializer_class = TaggingSerializer - permission_classes = (permissions.IsAuthenticated, IsOwnerOrChris) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrHasPermissionOrPublicReadOnly) def perform_create(self, serializer): """ @@ -207,7 +206,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 +216,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 +236,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 +245,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__pk__in=group_ids)) + return Tagging.objects.filter(tag=tag).filter(lookup) class TaggingDetail(generics.RetrieveDestroyAPIView): @@ -261,7 +267,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 +281,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 + return Feed.objects.none() + + # 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.filter(owner=user) + 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__pk__in=group_ids) + return Feed.objects.filter(lookup) def list(self, request, *args, **kwargs): """ @@ -317,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: @@ -330,19 +343,25 @@ class FeedListQuerySearch(generics.ListAPIView): """ http_method_names = ['get'] serializer_class = FeedSerializer - permission_classes = (permissions.IsAuthenticated,) filterset_class = FeedFilter 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 not user.is_authenticated: + return Feed.objects.none() + + # 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.filter(owner=user) + 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__pk__in=group_ids) + return Feed.objects.filter(lookup) class FeedDetail(generics.RetrieveUpdateDestroyAPIView): @@ -352,35 +371,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 +424,196 @@ 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('grp_name') + feed = self.get_object() + serializer.save(group=group, feed=feed) + + def list(self, request, *args, **kwargs): + """ + Overriden to return a list of the group permissions for the queried feed. + 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) + + 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 FeedGroupPermission.objects.filter(feed=feed) + + +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) + + 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): + """ + 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. + 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) + + 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 FeedUserPermission.objects.filter(feed=feed) + + +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) + + 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): """ A view for the collection of comments. @@ -432,7 +621,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): """ @@ -444,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) @@ -476,7 +669,8 @@ class CommentListQuerySearch(generics.ListAPIView): """ http_method_names = ['get'] serializer_class = CommentSerializer - permission_classes = (IsAuthenticatedOrRelatedFeedPublicReadOnly,) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrHasPermissionOrPublicReadOnly) filterset_class = CommentFilter def get_queryset(self): @@ -495,7 +689,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): """ @@ -513,7 +709,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..12baa68e 100755 --- a/chris_backend/filebrowser/permissions.py +++ b/chris_backend/filebrowser/permissions.py @@ -1,58 +1,91 @@ from rest_framework import permissions -from feeds.models import Feed - -class IsOwnerOrChrisOrReadOnly(permissions.BasePermission): +class IsOwnerOrChrisOrCanWriteOrCanReadOnlyOrPublicReadOnly(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, 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. """ 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. + user = request.user + + if obj.owner == user or user.username == 'chris': + 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 user.is_authenticated and 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 - # Write permissions are only allowed to the owner and superuser 'chris'. - return (request.user == obj.owner) or (request.user.username == 'chris') + return (request.method in permissions.SAFE_METHODS and + 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)) + - if path_tokens[0] == 'SERVICES': # accessible to all authenticated users +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 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 b3e11d1a..9e51e215 100755 --- a/chris_backend/filebrowser/serializers.py +++ b/chris_backend/filebrowser/serializers.py @@ -1,15 +1,22 @@ import os +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 +from core.models import (ChrisFolder, ChrisFile, ChrisLinkFile, FolderGroupPermission, + FolderUserPermission, FileGroupPermission, FileUserPermission, + LinkFileGroupPermission, LinkFileUserPermission) 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( @@ -17,18 +24,22 @@ 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', 'parent', 'children', - 'files', 'link_files', 'owner') + fields = ('url', 'id', 'creation_date', 'path', 'public', 'parent', 'children', + 'files', 'link_files', 'group_permissions', 'user_permissions', 'owner') def create(self, validated_data): """ Overriden to set the parent folder. """ - path = validated_data.get('path') + path = validated_data['path'] parent_path = os.path.dirname(path) owner = validated_data['owner'] @@ -37,48 +48,282 @@ def create(self, validated_data): validated_data['parent'] = parent_folder return super(FileBrowserFolderSerializer, self).create(validated_data) - def validate_path(self, path): + def update(self, instance, validated_data): """ - Overriden to check whether the provided path is under home// but not - under home//feeds/. + 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. """ - # remove leading and trailing slashes - path = path.strip(' ').strip('/') - user = self.context['request'].user - prefix = f'home/{user.username}/' + if 'public' in validated_data: + if instance.public and not validated_data['public']: + instance.remove_public_link() + instance.remove_public_access() - 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]) + elif not instance.public and validated_data['public']: + instance.grant_public_access() + instance.create_public_link() + return super(FileBrowserFolderSerializer, self).update(instance, validated_data) - if not path.startswith(prefix): - error_msg = f"Invalid field value. Path must start with '{prefix}'." - raise serializers.ValidationError([error_msg]) + def validate_path(self, path): + """ + 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('/') + 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 folder can only be changed by its owner or" + "superuser 'chris'."]) + return public + + def validate(self, data): + """ + Overriden to validate that required fields are in data when creating or + 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': + 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.']}) + return data + + +class FileBrowserFolderGroupPermissionSerializer(serializers.HyperlinkedModelSerializer): + grp_name = serializers.CharField(write_only=True, required=False) + folder_id = serializers.ReadOnlyField(source='folder.id') + 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', + read_only=True) + group = serializers.HyperlinkedRelatedField(view_name='group-detail', read_only=True) + + class Meta: + model = FolderGroupPermission + fields = ('url', 'id', 'permission', 'folder_id', 'folder_path', '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. 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'] + + 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}"]}) + + lf = folder.create_shared_link() + lf.grant_group_permission(group, 'r') + return perm + + def validate_grp_name(self, grp_name): + """ + 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([f"Couldn't find any group with name " + f"'{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, + required=False) + folder_id = serializers.ReadOnlyField(source='folder.id') + 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', + read_only=True) + user = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True) + + class Meta: + model = FolderUserPermission + fields = ('url', 'id', 'permission', 'folder_id', 'folder_path', '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. 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'] + + 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}"]}) + + lf = folder.create_shared_link() + lf.grant_user_permission(user, 'r') + return perm + + def validate_username(self, username): + """ + Overriden to check whether the provided username exists in the DB. + """ + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + raise serializers.ValidationError([f"Couldn't find any user with username " + f"'{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 FileBrowserChrisFileSerializer(serializers.HyperlinkedModelSerializer): + +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') 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', + '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): """ @@ -86,8 +331,197 @@ 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 file 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, required=False) + file_id = serializers.ReadOnlyField(source='file.id') + 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', + read_only=True) + group = serializers.HyperlinkedRelatedField(view_name='group-detail', read_only=True) + + 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. 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'] + + 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}"]}) -class FileBrowserChrisLinkFileSerializer(serializers.HyperlinkedModelSerializer): + lf = f.create_shared_link() + lf.grant_group_permission(group, 'r') + return perm + + def validate_grp_name(self, grp_name): + """ + 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([f"Couldn't find any group with name " + f"'{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, + required=False) + file_id = serializers.ReadOnlyField(source='file.id') + 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', + read_only=True) + user = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True) + + 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. 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'] + + 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}"]}) + + lf = f.create_shared_link() + lf.grant_user_permission(user, 'r') + return perm + + def validate_username(self, username): + """ + Overriden to check whether the provided username exists in the DB. + """ + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + raise serializers.ValidationError([f"Couldn't find any user with username " + f"'{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, + required=False) fname = serializers.FileField(use_url=False, required=False) fsize = serializers.ReadOnlyField(source='fname.size') owner_username = serializers.ReadOnlyField(source='owner.username') @@ -96,13 +530,51 @@ class FileBrowserChrisLinkFileSerializer(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', - 'owner_username', 'file_resource', 'linked_folder', 'linked_file', - 'parent_folder', 'owner') + fields = ('url', 'id', 'creation_date', 'path', 'fname', 'fsize', 'public', + '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): """ @@ -146,3 +618,204 @@ def get_linked_file_link(self, obj): request = self.context['request'] 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 link file 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 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 + + 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 + + +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.name') + 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 + 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. 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'] + + 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}"]}) + + shared_lf = lf.create_shared_link() + shared_lf.grant_group_permission(group, 'r') + return perm + + def validate_grp_name(self, grp_name): + """ + 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([f"Couldn't find any group with name " + f"'{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, + required=False) + link_file_id = serializers.ReadOnlyField(source='link_file.id') + 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', + read_only=True) + user = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True) + + 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. 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'] + + 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}"]}) + + shared_lf = lf.create_shared_link() + shared_lf.grant_user_permission(user, 'r') + return perm + + def validate_username(self, username): + """ + Overriden to check whether the provided username exists in the DB. + """ + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + raise serializers.ValidationError([f"Couldn't find any user with username " + f"'{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/services.py b/chris_backend/filebrowser/services.py index 4a35c17e..b8fbb202 100755 --- a/chris_backend/filebrowser/services.py +++ b/chris_backend/filebrowser/services.py @@ -1,182 +1,73 @@ + 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 + if qs.exists(): + folder = qs.first() - path_tokens = path.split('/', 4) - if path_tokens[1] == username: - return qs - - 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 user.username == 'chris': + return folder.chris_files.all() - 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() + 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): +def get_folder_link_files_queryset(folder, user=None): """ - Convenience function to get the list of the immediate subfolders under a folder - for the unauthenticated user. + Convenience function to get the queryset of the immediate link files under a folder. """ - 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): - """ - Convenience function to get the set of creators of the feeds that have been shared - with the passed user (including public feeds). - """ - 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.get_creator() - 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/tests/test_services.py b/chris_backend/filebrowser/tests/test_services.py index 8928c493..9c9eb93f 100755 --- a/chris_backend/filebrowser/tests/test_services.py +++ b/chris_backend/filebrowser/tests/test_services.py @@ -4,114 +4,453 @@ 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 users.models import UserProxy from filebrowser import services +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD + + 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 = 'fee' + password = 'feepass' + other_username = 'bee' + other_password = 'beepass' + + @classmethod + def setUpClass(cls): + # avoid cluttered console output (for instance logging all the http requests) logging.disable(logging.WARNING) - # 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) - + # 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) - # 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) + @classmethod + def tearDownClass(cls): + User.objects.get(username=cls.username).delete() + User.objects.get(username=cls.other_username).delete() - # 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() - - def tearDown(self): # 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_success_for_chris_user(self): """ - Test whether the services.get_authenticated_user_folder_queryset function - allows the chris user to see any existing folder. + Test whether the services.get_folder_queryset function + allows the chris superuser 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_failure_other_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) + other_user = User.objects.get(username=self.other_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, other_user) self.assertEqual(qs.count(), 0) - def test_get_authenticated_user_folder_queryset_top_level_folders(self): + def test_get_folder_queryset_from_user_public_success_other_user(self): """ - Test whether the services.get_authenticated_user_folder_queryset function - returns the appropriate queryset for top-level folders for any user. + Test whether the services.get_folder_queryset function + 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_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_folder_queryset(pk_dict, other_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) + + 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_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 - allows the chris user to see any existing folder. + Test whether the services.get_folder_queryset function + allows users to see any existing folder owned by them. """ 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']) + 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 cb4bcfc9..f5d2173b 100755 --- a/chris_backend/filebrowser/tests/test_views.py +++ b/chris_backend/filebrowser/tests/test_views.py @@ -2,23 +2,27 @@ import logging import io import os +import json from unittest import mock 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 from plugins.models import PluginMeta, Plugin, ComputeResource from plugininstances.models import PluginInstance COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD class FileBrowserViewTests(TestCase): @@ -26,41 +30,34 @@ 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' - # 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) + # superuser chris (owner of root and top-level folders) + chris_username = 'chris' + chris_password = CHRIS_SUPERUSER_PASSWORD - self.content_type = 'application/vnd.collection+json' - self.username = 'foo' - self.password = 'foopass' - self.other_username = 'boo' - self.other_password = 'boopass' + # normal users + username = 'foo' + password = 'foopass' + other_username = 'booo' + other_password = 'booopass' - # 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) + @classmethod + def setUpClass(cls): - # 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) + # avoid cluttered console output (for instance logging all the http requests) + logging.disable(logging.WARNING) - # create a file in the DB "already uploaded" to the server) - upload_path = f'home/{self.username}/uploads/myfolder/file1.txt' + # 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) - 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() + @classmethod + def tearDownClass(cls): + User.objects.get(username=cls.username).delete() + User.objects.get(username=cls.other_username).delete() - def tearDown(self): # re-enable logging logging.disable(logging.NOTSET) @@ -72,17 +69,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'],'') @@ -151,39 +164,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.owner.add(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.owner.add(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 @@ -194,7 +174,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 +192,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}' @@ -228,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) @@ -246,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) @@ -285,18 +275,98 @@ def setUp(self): self.plugin = plugin + folder = ChrisFolder.objects.get(path=f'home/{self.username}') + self.read_update_delete_url = reverse("chrisfolder-detail", + kwargs={"pk": 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) + + 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, + 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_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) + + 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_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}') @@ -305,11 +375,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') @@ -318,11 +388,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') @@ -337,43 +407,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.owner.add(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.owner.add(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 @@ -385,7 +418,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 +439,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) @@ -425,9 +458,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}') @@ -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 @@ -447,9 +483,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}) @@ -457,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) @@ -498,13 +537,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) @@ -520,7 +561,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}') @@ -529,11 +570,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') @@ -542,18 +583,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') @@ -561,7 +602,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 @@ -572,31 +613,13 @@ 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') 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_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.owner.add(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 +632,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 +643,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(self): other_user = User.objects.get(username=self.other_username) plugin = self.plugin @@ -631,14 +654,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_401_UNAUTHORIZED) def test_filebrowserfolderchild_list_feed_folder_success_public_feed(self): other_user = User.objects.get(username=self.other_username) @@ -649,9 +672,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}') @@ -660,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,9 +697,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}) @@ -681,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) @@ -694,175 +723,1589 @@ 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]) - pl_inst.feed.public = True - pl_inst.feed.save() + 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) - read_url = reverse("chrisfolder-file-list", - kwargs={"pk": pl_inst.output_folder.id}) + 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) - response = self.client.get(read_url) - self.assertContains(response, 'file_resource') - self.assertContains(response, file_path) + 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') - self.storage_manager.delete_obj(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) - def test_filebrowserfolderfile_list_failure_unauthenticated(self): - response = self.client.get(self.read_url) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + 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_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) + response = self.client.get(self.create_read_url) + self.assertContains(response, self.grp_name) - 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_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_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_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_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_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() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) - 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(), +class FileBrowserFolderGroupPermissionListQuerySearchViewTests(FileBrowserViewTests): + """ + Test the 'foldergrouppermission-list-query-search' view. + """ + + def setUp(self): + super(FileBrowserFolderGroupPermissionListQuerySearchViewTests, self).setUp() + + user = User.objects.get(username=self.username) + self.grp_name = 'all_users' + + # create folder + self.path = f'home/{self.username}/test2' + folder = ChrisFolder.objects.create(path=self.path, owner=user) + + self.read_url = reverse('foldergrouppermission-list-query-search', + kwargs={"pk": folder.id}) + + grp = Group.objects.get(name=self.grp_name) + folder.grant_group_permission(grp, 'r') + + def tearDown(self): + # delete folder tree + folder = ChrisFolder.objects.get(path=self.path) + folder.delete() + + super(FileBrowserFolderGroupPermissionListQuerySearchViewTests, self).tearDown() + + def test_filebrowserfoldergrouppermission_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_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) + + 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}' + + 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 foldergrouppermission-detail view. + """ + + def setUp(self): + super(FileBrowserFolderGroupPermissionDetailViewTests, self).setUp() + + user = User.objects.get(username=self.username) + self.grp_name = 'all_users' + + # create folder + self.path = f'home/{self.username}/test3' + folder = ChrisFolder.objects.create(path=self.path, owner=user) + + grp = Group.objects.get(name=self.grp_name) + folder.grant_group_permission(grp, 'r') + + gp = FolderGroupPermission.objects.get(group=grp, folder=folder) + + self.read_update_delete_url = reverse("foldergrouppermission-detail", + kwargs={"pk": gp.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(FileBrowserFolderGroupPermissionDetailViewTests, self).tearDown() + + def test_filebrowserfoldergrouppermission_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_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 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(FileGroupPermission.objects.get(file=f).permission, 'w') + self.assertEqual(LinkFileGroupPermission.objects.get(link_file=lf).permission, + 'w') + + 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_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() + 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}' - 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) + 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.storage_manager.delete_obj(file_path) + self.read_url = reverse('fileuserpermission-list-query-search', + kwargs={"pk": f.id}) - def test_fileBrowserfile_list_failure_not_found_shared_feed_unauthenticated(self): other_user = User.objects.get(username=self.other_username) - plugin = self.plugin + f.grant_user_permission(other_user, '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() - pl_inst.feed.owner.add(User.objects.get(username=self.username)) + def tearDown(self): + # delete file + f = ChrisFile.objects.get(fname=self.path) + f.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(FileBrowserFileUserPermissionListQuerySearchViewTests, self).tearDown() - userfile = UserFile(owner=other_user, parent_folder=pl_inst.output_folder) - userfile.fname.name = file_path - userfile.save() + def test_filebrowserfileuserpermission_list_query_search_success(self): + read_url = f'{self.read_url}?username={self.other_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, 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.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + 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) - self.storage_manager.delete_obj(file_path) + 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): @@ -894,15 +2337,15 @@ def setUp(self): 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.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() - self.storage_manager.delete_obj(self.link_path) def test_filebrowserfolderlinkfile_list_success(self): self.client.login(username=self.username, password=self.password) @@ -918,8 +2361,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' @@ -928,17 +2369,21 @@ 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) self.assertContains(response, 'file_resource') self.assertContains(response, link_path) - self.storage_manager.delete_obj(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_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) @@ -946,11 +2391,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) @@ -959,13 +2399,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 @@ -977,7 +2410,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.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' @@ -986,14 +2418,17 @@ 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) self.assertContains(response, link_path) - self.storage_manager.delete_obj(link_path) + link_file.delete() - 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 @@ -1004,20 +2439,20 @@ 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)) # 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("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) + link_file.delete() class FileBrowserLinkFileDetailViewTests(FileBrowserViewTests): """ @@ -1051,15 +2486,15 @@ 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.link_file.delete() super(FileBrowserLinkFileDetailViewTests, self).tearDown() - self.storage_manager.delete_obj(self.link_path) def test_fileBrowserlinkfile_detail_success(self): self.client.login(username=self.username, password=self.password) @@ -1080,10 +2515,10 @@ 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) + self.pl_inst.feed.remove_public_access() def test_fileBrowserlinkfile_detail_success_shared_feed(self): other_user = User.objects.get(username=self.other_username) @@ -1096,7 +2531,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.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' @@ -1105,12 +2539,16 @@ 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) 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) @@ -1123,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.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' 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): @@ -1169,18 +2608,17 @@ 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.link_file.delete() super(FileBrowserLinkFileResourceViewTests, self).tearDown() - self.storage_manager.delete_obj(self.link_path) def test_fileBrowserlinkfile_resource_success(self): self.client.login(username=self.username, password=self.password) @@ -1205,33 +2643,26 @@ 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') + self.pl_inst.feed.remove_public_access() - 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,7 +2672,7 @@ 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) + link_file.delete() def test_fileBrowserlinkfile_resource_failure_unauthorized_shared_feed_unauthenticated(self): other_user = User.objects.get(username=self.other_username) @@ -1254,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.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 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 4d1587f0..ac23cd97 100755 --- a/chris_backend/filebrowser/views.py +++ b/chris_backend/filebrowser/views.py @@ -2,23 +2,40 @@ import logging from django.http import Http404, FileResponse -from rest_framework import generics, permissions +from django.shortcuts import get_object_or_404 +from rest_framework import generics, permissions, serializers 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 .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, - IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly) +from .serializers import (FileBrowserFolderSerializer, + FileBrowserFolderGroupPermissionSerializer, + FileBrowserFolderUserPermissionSerializer, + FileBrowserFileSerializer, + FileBrowserFileGroupPermissionSerializer, + FileBrowserFileUserPermissionSerializer, + FileBrowserLinkFileSerializer, + FileBrowserLinkFileGroupPermissionSerializer, + FileBrowserLinkFileUserPermissionSerializer) +from .services import (get_folder_queryset, + get_folder_children_queryset, + get_folder_files_queryset, + get_folder_link_files_queryset) +from .permissions import (IsOwnerOrChrisOrCanWriteOrCanReadOnlyOrPublicReadOnly, + IsOwnerOrChrisOrHasAnyPermissionReadOnly, + IsFolderOwnerOrChrisOrHasAnyFolderPermissionReadOnly, + IsFileOwnerOrChrisOrHasAnyFilePermissionReadOnly, + IsLinkFileOwnerOrChrisOrHasAnyLinkFilePermissionReadOnly) logger = logging.getLogger(__name__) @@ -56,10 +73,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 not qs.exists(): + raise Http404('Not found.') + return qs class FileBrowserFolderListQuerySearch(generics.ListAPIView): @@ -77,41 +104,59 @@ 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) + return get_folder_queryset(pk_dict, user) + return get_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, + IsOwnerOrChrisOrCanWriteOrCanReadOnlyOrPublicReadOnly) 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} + response = super(FileBrowserFolderDetail, self).retrieve(request, *args, **kwargs) + template_data = {"public": ""} + return services.append_collection_template(response, template_data) - if user.is_authenticated: - qs = get_authenticated_user_folder_queryset(pk_dict, user) - else: - qs = get_unauthenticated_user_folder_queryset(pk_dict) + 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) - if qs.count() == 0: - raise Http404('Not found.') + 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() + path_parts = folder.path.split('/') + + 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) - return super(FileBrowserFolderDetail, self).retrieve(request, *args, **kwargs) class FileBrowserFolderChildList(generics.ListAPIView): """ @@ -120,37 +165,253 @@ 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} + folder = self.get_object() if user.is_authenticated: - qs = get_authenticated_user_folder_queryset(pk_dict, user) + children_qs = get_folder_children_queryset(folder, user) else: - qs = get_unauthenticated_user_folder_queryset(pk_dict) + children_qs = get_folder_children_queryset(folder) - if qs.count() == 0: - raise Http404('Not found.') + 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) - queryset = self.get_children_queryset() - return services.get_list_response(self, queryset) - def get_children_queryset(self): +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, + IsOwnerOrChrisOrHasAnyPermissionReadOnly) + + def perform_create(self, serializer): """ - Custom method to get the actual queryset of the children. + Overriden to provide a group and folder before first saving to the DB. """ - user = self.request.user + group = serializer.validated_data.pop('grp_name') folder = self.get_object() - if user.is_authenticated: - children = get_authenticated_user_folder_children(folder, user) - else: - children = get_unauthenticated_user_folder_children(folder) - return self.filter_queryset(children) + 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. + 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": "", "permission": ""} + 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, + IsOwnerOrChrisOrHasAnyPermissionReadOnly) + 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 FolderGroupPermission.objects.filter(folder=folder) + + +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, + IsFolderOwnerOrChrisOrHasAnyFolderPermissionReadOnly) + + 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) + + 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 + 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): + """ + 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, + IsOwnerOrChrisOrHasAnyPermissionReadOnly) + + 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. + 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": "", "permission": ""} + 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, + IsOwnerOrChrisOrHasAnyPermissionReadOnly) + 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 FolderUserPermission.objects.filter(folder=folder) + + +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, + IsFolderOwnerOrChrisOrHasAnyFolderPermissionReadOnly) + + 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) + + 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 + 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): @@ -159,44 +420,55 @@ class FileBrowserFolderFileList(generics.ListAPIView): """ http_method_names = ['get'] queryset = ChrisFolder.objects.all() - serializer_class = FileBrowserChrisFileSerializer + 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} + folder = self.get_object() if user.is_authenticated: - qs = get_authenticated_user_folder_queryset(pk_dict, user) + files_qs = get_folder_files_queryset(folder, user) else: - qs = get_unauthenticated_user_folder_queryset(pk_dict) + files_qs = get_folder_files_queryset(folder) - if qs.count() == 0: - raise Http404('Not found.') + response = services.get_list_response(self, files_qs) - queryset = self.get_files_queryset() - response = services.get_list_response(self, queryset) - return response + links = {'folder': reverse('chrisfolder-detail', request=request, + kwargs={"pk": folder.id})} + return services.append_collection_links(response, links) - 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() - -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 = FileBrowserChrisFileSerializer - permission_classes = (IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly,) + serializer_class = FileBrowserFileSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + 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): @@ -206,7 +478,8 @@ class FileBrowserFileResource(generics.GenericAPIView): http_method_names = ['get'] queryset = ChrisFile.get_base_queryset() renderer_classes = (BinaryFileRenderer,) - permission_classes = (IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly,) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrCanWriteOrCanReadOnlyOrPublicReadOnly) authentication_classes = (TokenAuthSupportQueryString, BasicAuthentication, SessionAuthentication) @@ -218,51 +491,306 @@ 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, + IsOwnerOrChrisOrHasAnyPermissionReadOnly) + + def perform_create(self, serializer): + """ + Overriden to provide a group and file before first saving to the DB. + """ + group = serializer.validated_data.pop('grp_name') + f = self.get_object() + 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. + 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": "", "permission": ""} + 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, + IsOwnerOrChrisOrHasAnyPermissionReadOnly) + 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 FileGroupPermission.objects.filter(file=f) + + +class FileBrowserFileGroupPermissionDetail(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, + IsFileOwnerOrChrisOrHasAnyFilePermissionReadOnly) + + def retrieve(self, request, *args, **kwargs): + """ + Overriden to append a collection+json template. + """ + response = super(FileBrowserFileGroupPermissionDetail, + self).retrieve(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 + 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): + """ + 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, + IsOwnerOrChrisOrHasAnyPermissionReadOnly) + + 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. + 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": "" , "permission": ""} + 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, + IsOwnerOrChrisOrHasAnyPermissionReadOnly) + 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 FileUserPermission.objects.filter(file=f) + + +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, + IsFileOwnerOrChrisOrHasAnyFilePermissionReadOnly) + + 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) + + 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 + 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): """ 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 + 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} + folder = self.get_object() if user.is_authenticated: - qs = get_authenticated_user_folder_queryset(pk_dict, user) + link_files_qs = get_folder_link_files_queryset(folder, user) else: - qs = get_unauthenticated_user_folder_queryset(pk_dict) + link_files_qs = get_folder_link_files_queryset(folder) - if qs.count() == 0: - raise Http404('Not found.') + response = services.get_list_response(self, link_files_qs) - queryset = self.get_link_files_queryset() - response = services.get_list_response(self, queryset) - return response + links = {'folder': reverse('chrisfolder-detail', request=request, + kwargs={"pk": folder.id})} + return services.append_collection_links(response, links) - 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() - -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 = FileBrowserChrisLinkFileSerializer - permission_classes = (IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly,) + serializer_class = FileBrowserLinkFileSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + 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) + + 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() + 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')): + + 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): @@ -272,7 +800,8 @@ class FileBrowserLinkFileResource(generics.GenericAPIView): http_method_names = ['get'] queryset = ChrisLinkFile.objects.all() renderer_classes = (BinaryFileRenderer,) - permission_classes = (IsOwnerOrChrisOrRelatedFeedOwnerOrPublicReadOnly,) + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrChrisOrCanWriteOrCanReadOnlyOrPublicReadOnly) authentication_classes = (TokenAuthSupportQueryString, BasicAuthentication, SessionAuthentication) @@ -282,3 +811,229 @@ 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, + IsOwnerOrChrisOrHasAnyPermissionReadOnly) + + def perform_create(self, serializer): + """ + Overriden to provide a group and link file before first saving to the DB. + """ + group = serializer.validated_data.pop('grp_name') + lf = self.get_object() + 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. + 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": "", "permission": ""} + 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, + IsOwnerOrChrisOrHasAnyPermissionReadOnly) + 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 LinkFileGroupPermission.objects.filter(link_file=lf) + + +class FileBrowserLinkFileGroupPermissionDetail(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, + IsLinkFileOwnerOrChrisOrHasAnyLinkFilePermissionReadOnly) + + def retrieve(self, request, *args, **kwargs): + """ + Overriden to append a collection+json template. + """ + response = super(FileBrowserLinkFileGroupPermissionDetail, + self).retrieve(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 + 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): + """ + 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, + IsOwnerOrChrisOrHasAnyPermissionReadOnly) + + 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. + 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": "", "permission": ""} + 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, + IsOwnerOrChrisOrHasAnyPermissionReadOnly) + 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 LinkFileUserPermission.objects.filter(link_file=lf) + + +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, + IsLinkFileOwnerOrChrisOrHasAnyLinkFilePermissionReadOnly) + + 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) + + 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 + 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/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/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 a117ad17..f03ccf55 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 @@ -27,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') @@ -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/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..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 @@ -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,15 +31,15 @@ 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' 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/pacsfiles/views.py b/chris_backend/pacsfiles/views.py index 3525dcd2..d01e5204 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,17 +64,17 @@ 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): """ 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,) + 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/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/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/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/models.py b/chris_backend/plugininstances/models.py index 7983a598..031153a8 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) @@ -190,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/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..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') @@ -83,6 +77,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 +89,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 @@ -309,52 +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."]) - 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 + [f"This field may not reference a top-level folder path '{path}'."]) + + if path_parts[0] not in ('home', 'SERVICES', 'PIPELINES'): 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 - 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 user not in feed.owner.all(): - raise serializers.ValidationError( - ["You do not have permission to access this path."]) - else: - # check whether path exists in swift + [f"This field may not reference an invalid path '{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 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) @@ -381,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) @@ -410,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/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/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..26cfa9f2 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): @@ -25,11 +26,9 @@ 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 = '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..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, @@ -16,6 +17,7 @@ COMPUTE_RESOURCE_URL = settings.COMPUTE_RESOURCE_URL +CHRIS_SUPERUSER_PASSWORD = settings.CHRIS_SUPERUSER_PASSWORD class SerializerTests(TestCase): @@ -26,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.username = 'foo' self.password = 'foopassword' @@ -323,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): """ @@ -338,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): @@ -424,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): """ @@ -439,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/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..99daeeef 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): @@ -36,11 +37,9 @@ 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 = '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/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 1479668a..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(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', - 'owner_username', 'file_resource', 'parent_folder', 'owner') + fields = ('url', 'id', 'creation_date', 'upload_path', 'fname', 'fsize', 'public', + 'owner_username', 'file_resource', 'parent_folder', 'group_permissions', + 'user_permissions','owner') def create(self, validated_data): """ @@ -47,26 +52,33 @@ 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// - 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) - storage_manager.copy_obj(old_storage_path, upload_path) - storage_manager.delete_obj(old_storage_path) + upload_path = validated_data.pop('upload_path', None) + + 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 @@ -78,24 +90,47 @@ 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]) + upload_path = upload_path.strip().strip('/') 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 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 024b3531..97103ada 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,16 +25,15 @@ 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' - # 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 @@ -82,24 +84,47 @@ 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() + request.user = User.objects.get(username=self.username) + with mock.patch.dict(userfiles_serializer.context, + {'request': request}, clear=True): + with self.assertRaises(serializers.ValidationError): + 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() - 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('home') with self.assertRaises(serializers.ValidationError): - userfiles_serializer.validate_upload_path('home/cube_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('cube/uploads_file1.txt') + userfiles_serializer.validate_upload_path('SERVICES/PACS/random/file1.txt') @tag('integration') def test_validate_upload_path_success(self): @@ -107,12 +132,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/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/userfiles/views.py b/chris_backend/userfiles/views.py index aaba8ba9..7a5f363a 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): @@ -41,13 +42,14 @@ 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) + # 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 +61,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,14 +85,14 @@ 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): """ 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): @@ -97,7 +111,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..4dbc0808 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,77 @@ class GroupFilter(FilterSet): class Meta: model = Group 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: + 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/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/serializers.py b/chris_backend/users/serializers.py index d9acaf2f..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 -from core.storage import connect_storage -from userfiles.models import UserFile - - -logger = logging.getLogger(__name__) +from .models import UserProxy class UserSerializer(serializers.HyperlinkedModelSerializer): @@ -31,46 +22,22 @@ 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. """ username = validated_data.get('username') email = validated_data.get('email') password = validated_data.get('password') - user = User.objects.create_user(username, email, password) - - home_path = f'home/{username}' - uploads_path = f'{home_path}/uploads' - feeds_path = f'{home_path}/feeds' - (uploads_folder, _) = ChrisFolder.objects.get_or_create(path=uploads_path, - owner=user) - (feeds_folder, _) = ChrisFolder.objects.get_or_create(path=feeds_path, owner=user) - - 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): """ - 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/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 678be04e..fd14b080 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' @@ -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 e9f84bd7..e75089d7 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,14 +27,12 @@ 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' - 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) @@ -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, @@ -208,7 +206,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 +215,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 +245,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 +297,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 +307,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 +327,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 +336,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 97668c0e..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 @@ -32,7 +33,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): """ @@ -96,7 +97,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 +121,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 @@ -137,16 +138,16 @@ 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() serializer_class = GroupUserSerializer - permission_classes = (permissions.IsAdminUser, ) + permission_classes = (permissions.IsAuthenticated, IsAdminOrReadOnly) 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() @@ -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 @@ -187,4 +213,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) 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 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' 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: