From 4de4aa1d61c712e0b594f28f6873b56a2f5e5764 Mon Sep 17 00:00:00 2001 From: Nikita Manovich Date: Wed, 31 Oct 2018 18:35:29 +0300 Subject: [PATCH 01/10] Draft version of permissions per objects. --- CHANGELOG.md | 3 +- cvat/apps/authentication/apps.py | 13 ++- cvat/apps/authentication/settings/__init__.py | 10 ++ .../apps/authentication/settings/auth_ldap.py | 28 +++-- .../authentication/settings/auth_simple.py | 17 ++- .../authentication/settings/authentication.py | 99 +++++++++-------- cvat/apps/authentication/signals.py | 62 +---------- cvat/apps/authentication/views.py | 3 +- cvat/apps/dashboard/views.py | 6 +- cvat/apps/engine/annotation.py | 2 +- .../migrations/0013_auto_20181031_1554.py | 103 ++++++++++++++++++ cvat/apps/engine/models.py | 39 +++++-- .../engine/static/engine/js/annotationUI.js | 3 +- cvat/apps/engine/task.py | 7 -- cvat/apps/engine/urls.py | 2 +- cvat/apps/engine/views.py | 56 +++++----- cvat/apps/tf_annotation/views.py | 14 +-- cvat/requirements/base.txt | 3 +- cvat/settings/base.py | 3 +- 19 files changed, 298 insertions(+), 175 deletions(-) create mode 100644 cvat/apps/engine/migrations/0013_auto_20181031_1554.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f3bac4bcfc4a..b62cfc183a1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,13 +20,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Polyshape editing method has been improved. You can redraw part of shape instead of points cloning. - Unified shortcut (Esc) for close any mode instead of different shortcuts (Alt+N, Alt+G, Alt+M etc.). - Dump file contains information about data source (e.g. video name, archive name, ...) +- Update requests library due to https://nvd.nist.gov/vuln/detail/CVE-2018-18074 +- Per task/job permissions to create/access/change/delete tasks and annotations ### Fixed - Performance bottleneck has been fixed during you create new objects (draw, copy, merge etc). - Label UI elements aren't updated after changelabel. - Attribute annotation mode can use invalid shape position after resize or move shapes. - ## [0.2.0] - 2018-09-28 ### Added - New annotation shapes: polygons, polylines, points diff --git a/cvat/apps/authentication/apps.py b/cvat/apps/authentication/apps.py index c0e41e420d6a..c1122a0d7d44 100644 --- a/cvat/apps/authentication/apps.py +++ b/cvat/apps/authentication/apps.py @@ -5,19 +5,20 @@ from django.apps import AppConfig from django.db.models.signals import post_migrate, post_save -from .settings.authentication import DJANGO_AUTH_TYPE class AuthenticationConfig(AppConfig): name = 'cvat.apps.authentication' def ready(self): - from . import signals from django.contrib.auth.models import User + from . import signals + from .settings.authentication import DJANGO_AUTH_TYPE, create_user post_migrate.connect(signals.create_groups) if DJANGO_AUTH_TYPE == 'SIMPLE': - post_save.connect(signals.create_user, sender=User, dispatch_uid="create_user") - - import django_auth_ldap.backend - django_auth_ldap.backend.populate_user.connect(signals.update_ldap_groups) + pass + # post_save.connect(create_user, sender=User, dispatch_uid="create_user") + elif DJANGO_AUTH_TYPE == 'LDAP': + import django_auth_ldap.backend + django_auth_ldap.backend.populate_user.connect(create_user) diff --git a/cvat/apps/authentication/settings/__init__.py b/cvat/apps/authentication/settings/__init__.py index d8e62e54b356..cf2e798249ce 100644 --- a/cvat/apps/authentication/settings/__init__.py +++ b/cvat/apps/authentication/settings/__init__.py @@ -3,3 +3,13 @@ # # SPDX-License-Identifier: MIT +from enum import Enum + +class AUTH_GROUP(Enum): + ADMINS = 'admins' + USERS = 'users' + ANNOTATORS = 'annotators' + OBSERVERS = 'observers' + + def __str__(self): + return self.value diff --git a/cvat/apps/authentication/settings/auth_ldap.py b/cvat/apps/authentication/settings/auth_ldap.py index 01021060e106..21abc2cbb48e 100644 --- a/cvat/apps/authentication/settings/auth_ldap.py +++ b/cvat/apps/authentication/settings/auth_ldap.py @@ -6,6 +6,7 @@ from django.conf import settings import ldap from django_auth_ldap.config import LDAPSearch, NestedActiveDirectoryGroupType +from . import AUTH_GROUP # Baseline configuration. settings.AUTH_LDAP_SERVER_URI = "" @@ -44,13 +45,24 @@ # superuser. settings.AUTHENTICATION_BACKENDS.append('django_auth_ldap.backend.LDAPBackend') -AUTH_LDAP_ADMIN_GROUPS = [ - "cn=cvat_admins,ou=Groups,dc=example,dc=com" -] +AUTH_LDAP_GROUPS = { + AUTH_GROUP.ADMINS: "cn=cvat_admins,ou=Groups,dc=example,dc=com", + AUTH_GROUP.ANNOTATORS: "cn=cvat_annotators,ou=Groups,dc=example,dc=com", + AUTH_GROUP.USERS: "cn=cvat_users,ou=Groups,dc=example,dc=com", + AUTH_GROUP.OBSERVERS: "cn=cvat_observers,ou=Groups,dc=example,dc=com" +} + +def create_user(sender, user=None, ldap_user=None, **kwargs): + from django.contrib.auth.models import Group + user_groups = [] + for group in AUTH_GROUP: + db_group = Group.objects.get(name=group) + + for ldap_group in AUTH_LDAP_GROUPS[group]: + if ldap_group.lower() in ldap_user.group_dns: + user_groups.append(db_group) -AUTH_LDAP_DATA_ANNOTATORS_GROUPS = [ -] + user.groups.set(user_groups) + user.is_staff = user.is_superuser = (AUTH_GROUP.ADMINS in user_groups) + user.save() -AUTH_LDAP_DEVELOPER_GROUPS = [ - "cn=cvat_users,ou=Groups,dc=example,dc=com" -] diff --git a/cvat/apps/authentication/settings/auth_simple.py b/cvat/apps/authentication/settings/auth_simple.py index 41a18c98f12f..40fadd7a5883 100644 --- a/cvat/apps/authentication/settings/auth_simple.py +++ b/cvat/apps/authentication/settings/auth_simple.py @@ -2,7 +2,20 @@ # Copyright (C) 2018 Intel Corporation # # SPDX-License-Identifier: MIT +from . import AUTH_GROUP -# Specify groups that new users will have -AUTH_SIMPLE_DEFAULT_GROUPS = [] +# Specify groups that new users will have by default +AUTH_SIMPLE_DEFAULT_GROUPS = [AUTH_GROUP.OBSERVERS] + +def create_user(sender, user, created, **kwargs): + from django.contrib.auth.models import Group + + if user.is_superuser and user.is_staff: + db_group = Group.objects.get(name=AUTH_GROUP.ADMINS) + user.groups.add(db_group) + + if created: + for group in AUTH_SIMPLE_DEFAULT_GROUPS: + db_group = Group.objects.get(name=group) + user.groups.add(db_group) diff --git a/cvat/apps/authentication/settings/authentication.py b/cvat/apps/authentication/settings/authentication.py index 3948d0eeab7d..8b8b1e369e48 100644 --- a/cvat/apps/authentication/settings/authentication.py +++ b/cvat/apps/authentication/settings/authentication.py @@ -1,58 +1,69 @@ - # Copyright (C) 2018 Intel Corporation # # SPDX-License-Identifier: MIT -from django.conf import settings import os +from django.conf import settings +import rules +from . import AUTH_GROUP settings.LOGIN_URL = 'login' settings.LOGIN_REDIRECT_URL = '/' settings.AUTHENTICATION_BACKENDS = [ - 'django.contrib.auth.backends.ModelBackend', + 'rules.permissions.ObjectPermissionBackend', + 'django.contrib.auth.backends.ModelBackend' ] -AUTH_LDAP_DEVELOPER_GROUPS = [] -AUTH_LDAP_DATA_ANNOTATORS_GROUPS = [] -AUTH_LDAP_ADMIN_GROUPS = [] - DJANGO_AUTH_TYPE = 'LDAP' if os.environ.get('DJANGO_AUTH_TYPE', '') == 'LDAP' else 'SIMPLE' +if DJANGO_AUTH_TYPE == 'SIMPLE': + from .auth_simple import create_user +elif DJANGO_AUTH_TYPE == 'LDAP': + from .auth_ldap import create_user + + +has_admins_group = rules.is_group_member(AUTH_GROUP.ADMINS.value) +has_users_group = rules.is_group_member(AUTH_GROUP.USERS.value) +has_annotators_group = rules.is_group_member(AUTH_GROUP.ANNOTATORS.value) +has_observers_group = rules.is_group_member(AUTH_GROUP.OBSERVERS.value) + +@rules.predicate +def is_task_owner(db_user, db_task): + return db_task.owner == db_user + +@rules.predicate +def is_task_assignee(db_user, db_task): + return db_task.assignee == db_user + +@rules.predicate +def is_task_annotator(db_user, db_task): + db_segments = list(db_task.segment_set.prefetch_related('job_set__assignee').all()) + db_jobs = [db_job for db_segment in db_segments + for db_job in db_segment.job_set.all()] + return db_user in [db_job.assignee for db_job in db_jobs] + [db_task.assignee] + +@rules.predicate +def is_job_owner(db_user, db_job): + return db_user == db_job.segment.task.owner + +@rules.predicate +def is_job_assignee(db_user, db_job): + return db_user == db_job.assignee + +@rules.predicate +def is_job_annotator(db_user, db_job): + return db_user in [db_job.assignee, db_job.segment.task.assignee] + + +# Auth rules +rules.add_perm('engine.task.create', has_admins_group | has_users_group) +rules.add_perm('engine.task.access', has_admins_group | is_task_owner | + has_observers_group | is_task_annotator) +rules.add_perm('engine.task.change', has_admins_group | is_task_owner | + is_task_assignee) +rules.add_perm('engine.task.delete', has_admins_group | is_task_owner) -if DJANGO_AUTH_TYPE == 'LDAP': - from .auth_ldap import * -else: - from .auth_simple import * - -# Definition of CVAT groups with permissions for task and annotation objects -# Annotator - can modify annotation for task, but cannot add, change and delete tasks -# Developer - can create tasks and modify (delete) owned tasks and any actions with annotation -# Admin - can any actions for task and annotation, can login to admin area and manage groups and users -cvat_groups_definition = { - 'user': { - 'description': '', - 'permissions': { - 'task': ['view', 'add', 'change', 'delete'], - 'annotation': ['view', 'change'], - }, - 'ldap_groups': AUTH_LDAP_DEVELOPER_GROUPS, - }, - - 'annotator': { - 'description': '', - 'permissions': { - 'task': ['view'], - 'annotation': ['view', 'change'], - }, - 'ldap_groups': AUTH_LDAP_DATA_ANNOTATORS_GROUPS, - }, - - 'admin': { - 'description': '', - 'permissions': { - 'task': ['view', 'add', 'change', 'delete'], - 'annotation': ['view', 'change'], - }, - 'ldap_groups': AUTH_LDAP_ADMIN_GROUPS, - }, -} +rules.add_perm('engine.job.access', has_admins_group | is_job_owner | + has_observers_group | is_job_annotator) +rules.add_perm('engine.job.change', has_admins_group | is_job_owner | + is_job_annotator) diff --git a/cvat/apps/authentication/signals.py b/cvat/apps/authentication/signals.py index 2dfc89dc6cdb..eebf70039c20 100644 --- a/cvat/apps/authentication/signals.py +++ b/cvat/apps/authentication/signals.py @@ -1,62 +1,12 @@ - # Copyright (C) 2018 Intel Corporation # # SPDX-License-Identifier: MIT -from django.db import models - -from django.conf import settings -from .settings import authentication -from django.contrib.auth.models import User, Group - -def setup_group_permissions(group): - from cvat.apps.engine.models import Task - from django.contrib.auth.models import Permission - from django.contrib.contenttypes.models import ContentType - - def append_permissions_for_model(model): - content_type = ContentType.objects.get_for_model(model) - for perm_target, actions in authentication.cvat_groups_definition[group.name]['permissions'].items(): - for action in actions: - codename = '{}_{}'.format(action, perm_target) - try: - perm = Permission.objects.get(codename=codename, content_type=content_type) - group_permissions.append(perm) - except: - pass - group_permissions = [] - append_permissions_for_model(Task) - - group.permissions.set(group_permissions) - group.save() +from django.contrib.auth.models import Group +from .settings import AUTH_GROUP +from .settings.authentication import create_user def create_groups(sender, **kwargs): - for cvat_role, _ in authentication.cvat_groups_definition.items(): - Group.objects.get_or_create(name=cvat_role) - -def update_ldap_groups(sender, user=None, ldap_user=None, **kwargs): - user_groups = [] - for cvat_role, role_settings in authentication.cvat_groups_definition.items(): - group_instance, _ = Group.objects.get_or_create(name=cvat_role) - setup_group_permissions(group_instance) - - for ldap_group in role_settings['ldap_groups']: - if ldap_group.lower() in ldap_user.group_dns: - user_groups.append(group_instance) - - user.save() - user.groups.set(user_groups) - user.is_staff = user.is_superuser = user.groups.filter(name='admin').exists() - -def create_user(sender, instance, created, **kwargs): - if instance.is_superuser and instance.is_staff: - admin_group, _ = Group.objects.get_or_create(name='admin') - admin_group.user_set.add(instance) - - if created: - for cvat_role, _ in authentication.cvat_groups_definition.items(): - group_instance, _ = Group.objects.get_or_create(name=cvat_role) - setup_group_permissions(group_instance) - - if cvat_role in authentication.AUTH_SIMPLE_DEFAULT_GROUPS: - instance.groups.add(group_instance) + for group in AUTH_GROUP: + db_group, _ = Group.objects.get_or_create(name=group) + db_group.save() diff --git a/cvat/apps/authentication/views.py b/cvat/apps/authentication/views.py index 2964cf4b82a7..777fcd617acf 100644 --- a/cvat/apps/authentication/views.py +++ b/cvat/apps/authentication/views.py @@ -6,6 +6,7 @@ from django.shortcuts import render from django.contrib.auth.views import LoginView from django.http import HttpResponseRedirect +from django.conf import settings from . import forms from django.contrib.auth import login, authenticate @@ -20,7 +21,7 @@ def register_user(request): raw_password = form.cleaned_data.get('password1') user = authenticate(username=username, password=raw_password) login(request, user) - return redirect('/') + return redirect(settings.LOGIN_REDIRECT_URL) else: form = forms.NewUserForm() return render(request, 'register.html', {'form': form}) diff --git a/cvat/apps/dashboard/views.py b/cvat/apps/dashboard/views.py index ea695ce0cb63..0341cc4dc9e0 100644 --- a/cvat/apps/dashboard/views.py +++ b/cvat/apps/dashboard/views.py @@ -7,7 +7,6 @@ from django.shortcuts import redirect from django.shortcuts import render from django.conf import settings -from django.contrib.auth.decorators import permission_required from cvat.apps.authentication.decorators import login_required from cvat.apps.engine.models import Task as TaskModel, Job as JobModel @@ -40,7 +39,6 @@ def ScanNode(directory): return result @login_required -@permission_required('engine.add_task', raise_exception=True) def JsTreeView(request): node_id = None if 'id' in request.GET: @@ -57,7 +55,6 @@ def JsTreeView(request): @login_required -@permission_required('engine.view_task', raise_exception=True) def DashboardView(request): query_name = request.GET['search'] if 'search' in request.GET else None query_job = int(request.GET['jid']) if 'jid' in request.GET and request.GET['jid'].isdigit() else None @@ -70,6 +67,9 @@ def DashboardView(request): if query_name is not None: task_list = list(filter(lambda x: query_name.lower() in x.name.lower(), task_list)) + task_list = list(filter(lambda task: request.user.has_perm( + 'engine.task.access', task), task_list)) + return render(request, 'dashboard/dashboard.html', { 'data': task_list, 'max_upload_size': settings.LOCAL_LOAD_MAX_FILES_SIZE, diff --git a/cvat/apps/engine/annotation.py b/cvat/apps/engine/annotation.py index 4cb402eb9b83..6a4266f1dead 100644 --- a/cvat/apps/engine/annotation.py +++ b/cvat/apps/engine/annotation.py @@ -38,7 +38,7 @@ def dump(tid, data_format, scheme, host): def check(tid): """ - Check that potentialy long operation 'dump' is completed. + Check that potentially long operation 'dump' is completed. Return the status as json/dictionary object. """ queue = django_rq.get_queue('default') diff --git a/cvat/apps/engine/migrations/0013_auto_20181031_1554.py b/cvat/apps/engine/migrations/0013_auto_20181031_1554.py new file mode 100644 index 000000000000..578340d4f799 --- /dev/null +++ b/cvat/apps/engine/migrations/0013_auto_20181031_1554.py @@ -0,0 +1,103 @@ +# Generated by Django 2.0.9 on 2018-10-31 12:54 + +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), + ('engine', '0012_auto_20181025_1618'), + ] + + operations = [ + migrations.AlterModelOptions( + name='attributespec', + options={'default_permissions': ()}, + ), + migrations.AlterModelOptions( + name='job', + options={'default_permissions': ()}, + ), + migrations.AlterModelOptions( + name='label', + options={'default_permissions': ()}, + ), + migrations.AlterModelOptions( + name='labeledboxattributeval', + options={'default_permissions': ()}, + ), + migrations.AlterModelOptions( + name='labeledpointsattributeval', + options={'default_permissions': ()}, + ), + migrations.AlterModelOptions( + name='labeledpolygonattributeval', + options={'default_permissions': ()}, + ), + migrations.AlterModelOptions( + name='labeledpolylineattributeval', + options={'default_permissions': ()}, + ), + migrations.AlterModelOptions( + name='objectpathattributeval', + options={'default_permissions': ()}, + ), + migrations.AlterModelOptions( + name='segment', + options={'default_permissions': ()}, + ), + migrations.AlterModelOptions( + name='task', + options={'default_permissions': ()}, + ), + migrations.AlterModelOptions( + name='trackedbox', + options={'default_permissions': ()}, + ), + migrations.AlterModelOptions( + name='trackedboxattributeval', + options={'default_permissions': ()}, + ), + migrations.AlterModelOptions( + name='trackedpoints', + options={'default_permissions': ()}, + ), + migrations.AlterModelOptions( + name='trackedpointsattributeval', + options={'default_permissions': ()}, + ), + migrations.AlterModelOptions( + name='trackedpolygon', + options={'default_permissions': ()}, + ), + migrations.AlterModelOptions( + name='trackedpolygonattributeval', + options={'default_permissions': ()}, + ), + migrations.AlterModelOptions( + name='trackedpolyline', + options={'default_permissions': ()}, + ), + migrations.AlterModelOptions( + name='trackedpolylineattributeval', + options={'default_permissions': ()}, + ), + migrations.RenameField( + model_name='job', + old_name='annotator', + new_name='assignee', + ), + migrations.AddField( + model_name='task', + name='assignee', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assignees', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='task', + name='owner', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owners', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 1aaa5198a37a..b20096f8a140 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -39,7 +39,8 @@ class Task(models.Model): size = models.PositiveIntegerField() path = models.CharField(max_length=256) mode = models.CharField(max_length=32) - owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) + owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL, related_name="owners") + assignee = models.ForeignKey(User, null=True, on_delete=models.SET_NULL, related_name="assignees") bug_tracker = models.CharField(max_length=2000, default="") created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now_add=True) @@ -51,11 +52,7 @@ class Task(models.Model): # Extend default permission model class Meta: - permissions = ( - ("view_task", "Can see available tasks"), - ("view_annotation", "Can see annotation for the task"), - ("change_annotation", "Can modify annotation for the task"), - ) + default_permissions = () def get_upload_dirname(self): return os.path.join(self.path, ".upload") @@ -91,11 +88,16 @@ class Segment(models.Model): start_frame = models.IntegerField() stop_frame = models.IntegerField() + class Meta: + default_permissions = () + class Job(models.Model): segment = models.ForeignKey(Segment, on_delete=models.CASCADE) - annotator = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) + assignee = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) status = models.CharField(max_length=32, default=StatusChoice.ANNOTATION) - # TODO: add sub-issue number for the task + + class Meta: + default_permissions = () class Label(models.Model): task = models.ForeignKey(Task, on_delete=models.CASCADE) @@ -104,6 +106,10 @@ class Label(models.Model): def __str__(self): return self.name + class Meta: + default_permissions = () + + def parse_attribute(text): match = re.match(r'^([~@])(\w+)=(\w+):(.+)?$', text) prefix = match.group(1) @@ -120,6 +126,9 @@ class AttributeSpec(models.Model): label = models.ForeignKey(Label, on_delete=models.CASCADE) text = models.CharField(max_length=1024) + class Meta: + default_permissions = () + def get_attribute(self): return parse_attribute(self.text) @@ -143,17 +152,20 @@ def get_values(self): attr = self.get_attribute() return attr['values'] - def __str__(self): return self.get_attribute()['name'] + class AttributeVal(models.Model): # TODO: add a validator here to be sure that it corresponds to self.label id = models.BigAutoField(primary_key=True) spec = models.ForeignKey(AttributeSpec, on_delete=models.CASCADE) value = SafeCharField(max_length=64) + class Meta: abstract = True + default_permissions = () + class Annotation(models.Model): job = models.ForeignKey(Job, on_delete=models.CASCADE) @@ -161,14 +173,17 @@ class Annotation(models.Model): frame = models.PositiveIntegerField() group_id = models.PositiveIntegerField(default=0) client_id = models.BigIntegerField(default=-1) + class Meta: abstract = True class Shape(models.Model): occluded = models.BooleanField(default=False) z_order = models.IntegerField(default=0) + class Meta: abstract = True + default_permissions = () class BoundingBox(Shape): id = models.BigAutoField(primary_key=True) @@ -176,14 +191,19 @@ class BoundingBox(Shape): ytl = models.FloatField() xbr = models.FloatField() ybr = models.FloatField() + class Meta: abstract = True + default_permissions = () + default_permissions = () class PolyShape(Shape): id = models.BigAutoField(primary_key=True) points = models.TextField() + class Meta: abstract = True + default_permissions = () class LabeledBox(Annotation, BoundingBox): pass @@ -222,6 +242,7 @@ class TrackedObject(models.Model): outside = models.BooleanField(default=False) class Meta: abstract = True + default_permissions = () class TrackedBox(TrackedObject, BoundingBox): pass diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js index 85035a7b51bc..1cbd9079c0a6 100644 --- a/cvat/apps/engine/static/engine/js/annotationUI.js +++ b/cvat/apps/engine/static/engine/js/annotationUI.js @@ -518,9 +518,8 @@ function setupMenu(job, shapeCollectionModel, annotationParser, aamModel, player $('#statTaskStatus').prop("value", job.status).on('change', (e) => { $.ajax({ type: 'POST', - url: 'save/job/status', + url: 'save/status/job/' + window.cvat.job.id, data: JSON.stringify({ - jid: window.cvat.job.id, status: e.target.value }), contentType: "application/json; charset=utf-8", diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index 626ae47b4f72..7f6102f614fe 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -249,13 +249,6 @@ def get_job(jid): return response -def is_task_owner(user, tid): - try: - return user == models.Task.objects.get(pk=tid).owner or \ - user.groups.filter(name='admin').exists() - except: - return False - @transaction.atomic def rq_handler(job, exc_type, exc_value, traceback): tid = job.id.split('/')[1] diff --git a/cvat/apps/engine/urls.py b/cvat/apps/engine/urls.py index b12e3017233d..99aa48a97528 100644 --- a/cvat/apps/engine/urls.py +++ b/cvat/apps/engine/urls.py @@ -23,5 +23,5 @@ path('get/annotation/job/', views.get_annotation), path('get/username', views.get_username), path('save/exception/', views.catch_client_exception), - path('save/job/status', views.save_job_status), + path('save/status/job/', views.save_job_status), ] diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 0b76de50d98a..f85a1861eb4a 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -10,7 +10,7 @@ from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse from django.shortcuts import redirect, render from django.conf import settings -from django.contrib.auth.decorators import permission_required +from rules.contrib.views import permission_required, objectgetter from django.views.decorators.gzip import gzip_page from sendfile import sendfile @@ -24,7 +24,8 @@ ############################# High Level server API @login_required -@permission_required('engine.view_task', raise_exception=True) +@permission_required(perm=['engine.job.access'], + fn=objectgetter(models.Job, 'jid'), raise_exception=True) def catch_client_exception(request, jid): data = json.loads(request.body.decode('utf-8')) for event in data['exceptions']: @@ -44,7 +45,7 @@ def dispatch_request(request): return redirect('/dashboard/') @login_required -@permission_required('engine.add_task', raise_exception=True) +@permission_required(perm=['engine.task.create'], raise_exception=True) def create_task(request): """Create a new annotation task""" @@ -103,10 +104,10 @@ def create_task(request): return JsonResponse({'tid': db_task.id}) @login_required -@permission_required('engine.view_task', raise_exception=True) +@permission_required(perm=['engine.task.access'], + fn=objectgetter(models.Task, 'tid'), raise_exception=True) def check_task(request, tid): """Check the status of a task""" - try: slogger.glob.info("check task #{}".format(tid)) response = task.check(tid) @@ -117,7 +118,8 @@ def check_task(request, tid): return JsonResponse(response) @login_required -@permission_required('engine.view_task', raise_exception=True) +@permission_required(perm=['engine.task.access'], + fn=objectgetter(models.Task, 'tid'), raise_exception=True) def get_frame(request, tid, frame): """Stream corresponding from for the task""" @@ -131,14 +133,12 @@ def get_frame(request, tid, frame): return HttpResponseBadRequest(str(e)) @login_required -@permission_required('engine.delete_task', raise_exception=True) +@permission_required(perm=['engine.task.delete'], + fn=objectgetter(models.Task, 'tid'), raise_exception=True) def delete_task(request, tid): """Delete the task""" try: slogger.glob.info("delete task #{}".format(tid)) - if not task.is_task_owner(request.user, tid): - return HttpResponseBadRequest("You don't have permissions to delete the task.") - task.delete(tid) except Exception as e: slogger.glob.error("cannot delete task #{}".format(tid), exc_info=True) @@ -147,14 +147,12 @@ def delete_task(request, tid): return HttpResponse() @login_required -@permission_required('engine.change_task', raise_exception=True) +@permission_required(perm=['engine.task.change'], + fn=objectgetter(models.Task, 'tid'), raise_exception=True) def update_task(request, tid): """Update labels for the task""" try: slogger.task[tid].info("update task request") - if not task.is_task_owner(request.user, tid): - return HttpResponseBadRequest("You don't have permissions to change the task.") - labels = request.POST['labels'] task.update(tid, labels) except Exception as e: @@ -164,7 +162,8 @@ def update_task(request, tid): return HttpResponse() @login_required -@permission_required(perm='engine.view_task', raise_exception=True) +@permission_required(perm=['engine.task.access'], + fn=objectgetter(models.Task, 'tid'), raise_exception=True) def get_task(request, tid): try: slogger.task[tid].info("get task request") @@ -176,7 +175,8 @@ def get_task(request, tid): return JsonResponse(response, safe=False) @login_required -@permission_required(perm=['engine.view_task', 'engine.view_annotation'], raise_exception=True) +@permission_required(perm=['engine.job.access'], + fn=objectgetter(models.Job, 'jid'), raise_exception=True) def get_job(request, jid): try: slogger.job[jid].info("get job #{} request".format(jid)) @@ -188,7 +188,8 @@ def get_job(request, jid): return JsonResponse(response, safe=False) @login_required -@permission_required(perm=['engine.view_task', 'engine.view_annotation'], raise_exception=True) +@permission_required(perm=['engine.task.access'], + fn=objectgetter(models.Task, 'tid'), raise_exception=True) def dump_annotation(request, tid): try: slogger.task[tid].info("dump annotation request") @@ -201,7 +202,8 @@ def dump_annotation(request, tid): @login_required @gzip_page -@permission_required(perm=['engine.view_task', 'engine.view_annotation'], raise_exception=True) +@permission_required(perm=['engine.task.access'], + fn=objectgetter(models.Task, 'tid'), raise_exception=True) def check_annotation(request, tid): try: slogger.task[tid].info("check annotation") @@ -215,7 +217,8 @@ def check_annotation(request, tid): @login_required @gzip_page -@permission_required(perm=['engine.view_task', 'engine.view_annotation'], raise_exception=True) +@permission_required(perm=['engine.task.access'], + fn=objectgetter(models.Task, 'tid'), raise_exception=True) def download_annotation(request, tid): try: slogger.task[tid].info("get dumped annotation") @@ -231,7 +234,8 @@ def download_annotation(request, tid): @login_required @gzip_page -@permission_required(perm=['engine.view_task', 'engine.view_annotation'], raise_exception=True) +@permission_required(perm=['engine.job.access'], + fn=objectgetter(models.Job, 'jid'), raise_exception=True) def get_annotation(request, jid): try: slogger.job[jid].info("get annotation for {} job".format(jid)) @@ -243,7 +247,8 @@ def get_annotation(request, jid): return JsonResponse(response, safe=False) @login_required -@permission_required(perm=['engine.view_task', 'engine.change_annotation'], raise_exception=True) +@permission_required(perm=['engine.job.change'], + fn=objectgetter(models.Job, 'jid'), raise_exception=True) def save_annotation_for_job(request, jid): try: slogger.job[jid].info("save annotation for {} job".format(jid)) @@ -263,7 +268,8 @@ def save_annotation_for_job(request, jid): return HttpResponse() @login_required -@permission_required(perm=['engine.view_task', 'engine.change_annotation'], raise_exception=True) +@permission_required(perm=['engine.task.change'], + fn=objectgetter(models.Task, 'tid'), raise_exception=True) def save_annotation_for_task(request, tid): try: slogger.task[tid].info("save annotation request") @@ -276,11 +282,11 @@ def save_annotation_for_task(request, tid): return HttpResponse() @login_required -@permission_required(perm=['engine.view_task', 'engine.change_task'], raise_exception=True) -def save_job_status(request): +@permission_required(perm=['engine.job.change'], + fn=objectgetter(models.Job, 'jid'), raise_exception=True) +def save_job_status(request, jid): try: data = json.loads(request.body.decode('utf-8')) - jid = data['jid'] status = data['status'] slogger.job[jid].info("changing job status request") task.save_job_status(jid, status, request.user.username) diff --git a/cvat/apps/tf_annotation/views.py b/cvat/apps/tf_annotation/views.py index c58891d4fe9f..71ef3747ab4d 100644 --- a/cvat/apps/tf_annotation/views.py +++ b/cvat/apps/tf_annotation/views.py @@ -6,7 +6,7 @@ from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest, QueryDict from django.core.exceptions import ObjectDoesNotExist from django.shortcuts import render -from django.contrib.auth.decorators import permission_required +from rules.contrib.views import permission_required, objectgetter from cvat.apps.authentication.decorators import login_required from cvat.apps.engine.models import Task as TaskModel from cvat.apps.engine import annotation, task @@ -286,14 +286,12 @@ def get_meta_info(request): @login_required -@permission_required(perm=['engine.view_task', 'engine.change_annotation'], raise_exception=True) +@permission_required(perm=['engine.task.change'], + fn=objectgetter(TaskModel, 'tid'), raise_exception=True) def create(request, tid): slogger.glob.info('tf annotation create request for task {}'.format(tid)) try: db_task = TaskModel.objects.get(pk=tid) - if not task.is_task_owner(request.user, tid): - raise Exception('Not enought of permissions for tf annotation') - queue = django_rq.get_queue('low') job = queue.fetch_job('tf_annotation.create/{}'.format(tid)) if job is not None and (job.is_started or job.is_queued): @@ -347,7 +345,8 @@ def create(request, tid): return HttpResponse() @login_required -@permission_required(perm='engine.view_task', raise_exception=True) +@permission_required(perm=['engine.task.access'], + fn=objectgetter(TaskModel, 'tid'), raise_exception=True) def check(request, tid): try: queue = django_rq.get_queue('low') @@ -376,7 +375,8 @@ def check(request, tid): @login_required -@permission_required(perm='engine.view_task', raise_exception=True) +@permission_required(perm=['engine.task.change'], + fn=objectgetter(TaskModel, 'tid'), raise_exception=True) def cancel(request, tid): try: queue = django_rq.get_queue('low') diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index ad8cbb714a34..e423c5c3db73 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -15,7 +15,7 @@ pytz==2018.3 pyunpack==0.1.2 rcssmin==1.0.6 redis==2.10.6 -requests==2.18.4 +requests==2.20.0 rjsmin==1.0.12 rq==0.10.0 scipy==1.0.1 @@ -24,3 +24,4 @@ django-sendfile==0.3.11 dj-pagination==2.3.2 python-logstash==0.4.6 django-revproxy==0.9.15 +rules==2.0 diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 6cef92eca632..9972cc8b1206 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -54,7 +54,8 @@ 'cacheops', 'sendfile', 'dj_pagination', - 'revproxy' + 'revproxy', + 'rules' ] if 'yes' == os.environ.get('TF_ANNOTATION', 'no'): From 55bac03013ff9009c535825239730ecfa623c7c5 Mon Sep 17 00:00:00 2001 From: Nikita Manovich Date: Thu, 1 Nov 2018 14:15:52 +0300 Subject: [PATCH 02/10] Refactoring is in progress --- cvat/apps/authentication/__init__.py | 10 ++ cvat/apps/authentication/apps.py | 14 +-- cvat/apps/authentication/auth.py | 91 +++++++++++++++++++ .../auth_simple.py => auth_basic.py} | 9 +- .../{settings => }/auth_ldap.py | 18 ++-- cvat/apps/authentication/decorators.py | 3 +- cvat/apps/authentication/settings/__init__.py | 15 --- .../authentication/settings/authentication.py | 69 -------------- cvat/apps/authentication/signals.py | 12 --- cvat/apps/authentication/urls.py | 4 +- cvat/settings/base.py | 2 + 11 files changed, 122 insertions(+), 125 deletions(-) create mode 100644 cvat/apps/authentication/auth.py rename cvat/apps/authentication/{settings/auth_simple.py => auth_basic.py} (68%) rename cvat/apps/authentication/{settings => }/auth_ldap.py (76%) delete mode 100644 cvat/apps/authentication/settings/__init__.py delete mode 100644 cvat/apps/authentication/settings/authentication.py delete mode 100644 cvat/apps/authentication/signals.py diff --git a/cvat/apps/authentication/__init__.py b/cvat/apps/authentication/__init__.py index a7b92720ff0c..4921b1df39ac 100644 --- a/cvat/apps/authentication/__init__.py +++ b/cvat/apps/authentication/__init__.py @@ -5,3 +5,13 @@ default_app_config = 'cvat.apps.authentication.apps.AuthenticationConfig' +from enum import Enum + +class AUTH_ROLE(Enum): + ADMIN = 'admin' + USER = 'user' + ANNOTATOR = 'annotator' + OBSERVER = 'observer' + + def __str__(self): + return self.value diff --git a/cvat/apps/authentication/apps.py b/cvat/apps/authentication/apps.py index c1122a0d7d44..c6f9e549461b 100644 --- a/cvat/apps/authentication/apps.py +++ b/cvat/apps/authentication/apps.py @@ -4,21 +4,11 @@ # SPDX-License-Identifier: MIT from django.apps import AppConfig -from django.db.models.signals import post_migrate, post_save class AuthenticationConfig(AppConfig): name = 'cvat.apps.authentication' def ready(self): - from django.contrib.auth.models import User - from . import signals - from .settings.authentication import DJANGO_AUTH_TYPE, create_user + from .auth import register_signals - post_migrate.connect(signals.create_groups) - - if DJANGO_AUTH_TYPE == 'SIMPLE': - pass - # post_save.connect(create_user, sender=User, dispatch_uid="create_user") - elif DJANGO_AUTH_TYPE == 'LDAP': - import django_auth_ldap.backend - django_auth_ldap.backend.populate_user.connect(create_user) + register_signals() diff --git a/cvat/apps/authentication/auth.py b/cvat/apps/authentication/auth.py new file mode 100644 index 000000000000..b71c121a5aa1 --- /dev/null +++ b/cvat/apps/authentication/auth.py @@ -0,0 +1,91 @@ +# Copyright (C) 2018 Intel Corporation +# +# SPDX-License-Identifier: MIT + +import os +from django.conf import settings +import rules +from . import AUTH_ROLE + +settings.LOGIN_URL = 'login' +settings.LOGIN_REDIRECT_URL = '/' + +settings.AUTHENTICATION_BACKENDS.extend([ + 'rules.permissions.ObjectPermissionBackend', + 'django.contrib.auth.backends.ModelBackend' +]) + +def register_signals(): + from django.db.models.signals import post_migrate, post_save + from django.contrib.auth.models import User, Group + + def create_groups(sender, **kwargs): + for role in AUTH_ROLE: + db_group, _ = Group.objects.get_or_create(name=role) + db_group.save() + + post_migrate.connect(create_groups) + + if settings.DJANGO_AUTH_TYPE == 'BASIC': + from .auth_basic import create_user + + post_save.connect(create_user, sender=User, dispatch_uid="create_user") + elif settings.DJANGO_AUTH_TYPE == 'LDAP': + import django_auth_ldap.backend + from .auth_ldap import create_user + + django_auth_ldap.backend.populate_user.connect(create_user) + +# AUTH PREDICATES +has_admin_role = rules.is_group_member(str(AUTH_ROLE.ADMIN)) +has_user_role = rules.is_group_member(str(AUTH_ROLE.USER)) +has_annotator_role = rules.is_group_member(str(AUTH_ROLE.ANNOTATOR)) +has_observer_role = rules.is_group_member(str(AUTH_ROLE.OBSERVER)) + +@rules.predicate +def is_task_owner(db_user, db_task): + # If owner is None (null) the task can be accessed/changed/deleted + # by any user (publically available tasks) + return db_task.owner == db_user or db_task.owner is None + +@rules.predicate +def is_task_assignee(db_user, db_task): + # If assignee is None (null) the task can be accessed/changeed by any user. + return db_task.assignee == db_user or db_task.assignee is None + +@rules.predicate +def is_job_owner(db_user, db_job): + return is_task_owner(db_user, db_job.segment.task) + +@rules.predicate +def is_job_assignee(db_user, db_job): + # If assignee is None (null) the job can be accessed/changed by any user. + return db_user == db_job.assignee or db_job.assignee is None + +@rules.predicate +def is_task_annotator(db_user, db_task): + from functools import reduce + + db_segments = list(db_task.segment_set.prefetch_related('job_set__assignee').all()) + # If job.assignee is None it doesn't mean that everybody can access the task. + return is_task_assignee(db_user, db_task) or \ + reduce((lambda x, y: x | y), [db_user == db_job.assignee + for db_segment in db_segments for db_job in db_segment.job_set.all()]) + +@rules.predicate +def is_job_annotator(db_user, db_job): + db_task = db_job.segment.task + return is_job_assignee(db_user, db_job) or is_task_assignee(db_user, db_task) + +# AUTH PERMISSIONS RULES +rules.add_perm('engine.task.create', has_admin_role | has_user_role) +rules.add_perm('engine.task.access', has_admin_role | is_task_owner | + has_observer_role | is_task_annotator) +rules.add_perm('engine.task.change', has_admin_role | is_task_owner | + is_task_assignee) +rules.add_perm('engine.task.delete', has_admin_role | is_task_owner) + +rules.add_perm('engine.job.access', has_admin_role | is_job_owner | + has_observer_role | is_job_annotator) +rules.add_perm('engine.job.change', has_admin_role | is_job_owner | + is_job_annotator) diff --git a/cvat/apps/authentication/settings/auth_simple.py b/cvat/apps/authentication/auth_basic.py similarity index 68% rename from cvat/apps/authentication/settings/auth_simple.py rename to cvat/apps/authentication/auth_basic.py index 40fadd7a5883..4b2fa019ebab 100644 --- a/cvat/apps/authentication/settings/auth_simple.py +++ b/cvat/apps/authentication/auth_basic.py @@ -1,21 +1,20 @@ - # Copyright (C) 2018 Intel Corporation # # SPDX-License-Identifier: MIT -from . import AUTH_GROUP +from . import AUTH_ROLE # Specify groups that new users will have by default -AUTH_SIMPLE_DEFAULT_GROUPS = [AUTH_GROUP.OBSERVERS] +AUTH_DEFAULT_GROUPS = [AUTH_ROLE.OBSERVER] def create_user(sender, user, created, **kwargs): from django.contrib.auth.models import Group if user.is_superuser and user.is_staff: - db_group = Group.objects.get(name=AUTH_GROUP.ADMINS) + db_group = Group.objects.get(name=AUTH_ROLE.ADMIN) user.groups.add(db_group) if created: - for group in AUTH_SIMPLE_DEFAULT_GROUPS: + for group in AUTH_DEFAULT_GROUPS: db_group = Group.objects.get(name=group) user.groups.add(db_group) diff --git a/cvat/apps/authentication/settings/auth_ldap.py b/cvat/apps/authentication/auth_ldap.py similarity index 76% rename from cvat/apps/authentication/settings/auth_ldap.py rename to cvat/apps/authentication/auth_ldap.py index 21abc2cbb48e..3b0464cbe225 100644 --- a/cvat/apps/authentication/settings/auth_ldap.py +++ b/cvat/apps/authentication/auth_ldap.py @@ -6,7 +6,7 @@ from django.conf import settings import ldap from django_auth_ldap.config import LDAPSearch, NestedActiveDirectoryGroupType -from . import AUTH_GROUP +from . import AUTH_ROLE # Baseline configuration. settings.AUTH_LDAP_SERVER_URI = "" @@ -46,23 +46,23 @@ settings.AUTHENTICATION_BACKENDS.append('django_auth_ldap.backend.LDAPBackend') AUTH_LDAP_GROUPS = { - AUTH_GROUP.ADMINS: "cn=cvat_admins,ou=Groups,dc=example,dc=com", - AUTH_GROUP.ANNOTATORS: "cn=cvat_annotators,ou=Groups,dc=example,dc=com", - AUTH_GROUP.USERS: "cn=cvat_users,ou=Groups,dc=example,dc=com", - AUTH_GROUP.OBSERVERS: "cn=cvat_observers,ou=Groups,dc=example,dc=com" + AUTH_ROLE.ADMIN: "cn=cvat_admins,ou=Groups,dc=example,dc=com", + AUTH_ROLE.ANNOTATOR: "cn=cvat_annotators,ou=Groups,dc=example,dc=com", + AUTH_ROLE.USER: "cn=cvat_users,ou=Groups,dc=example,dc=com", + AUTH_ROLE.OBSERVER: "cn=cvat_observers,ou=Groups,dc=example,dc=com" } def create_user(sender, user=None, ldap_user=None, **kwargs): from django.contrib.auth.models import Group user_groups = [] - for group in AUTH_GROUP: - db_group = Group.objects.get(name=group) + for role in AUTH_ROLE: + db_group = Group.objects.get(name=role) - for ldap_group in AUTH_LDAP_GROUPS[group]: + for ldap_group in AUTH_LDAP_GROUPS[role]: if ldap_group.lower() in ldap_user.group_dns: user_groups.append(db_group) user.groups.set(user_groups) - user.is_staff = user.is_superuser = (AUTH_GROUP.ADMINS in user_groups) + user.is_staff = user.is_superuser = (AUTH_ROLE.ADMIN in user_groups) user.save() diff --git a/cvat/apps/authentication/decorators.py b/cvat/apps/authentication/decorators.py index 3ef9d2631ec6..00d7612caf5f 100644 --- a/cvat/apps/authentication/decorators.py +++ b/cvat/apps/authentication/decorators.py @@ -12,7 +12,8 @@ from functools import wraps from django.conf import settings -def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None, redirect_methods=['GET']): +def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, + login_url=None, redirect_methods=['GET']): def decorator(view_func): @wraps(view_func) def _wrapped_view(request, *args, **kwargs): diff --git a/cvat/apps/authentication/settings/__init__.py b/cvat/apps/authentication/settings/__init__.py deleted file mode 100644 index cf2e798249ce..000000000000 --- a/cvat/apps/authentication/settings/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ - -# Copyright (C) 2018 Intel Corporation -# -# SPDX-License-Identifier: MIT - -from enum import Enum - -class AUTH_GROUP(Enum): - ADMINS = 'admins' - USERS = 'users' - ANNOTATORS = 'annotators' - OBSERVERS = 'observers' - - def __str__(self): - return self.value diff --git a/cvat/apps/authentication/settings/authentication.py b/cvat/apps/authentication/settings/authentication.py deleted file mode 100644 index 8b8b1e369e48..000000000000 --- a/cvat/apps/authentication/settings/authentication.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (C) 2018 Intel Corporation -# -# SPDX-License-Identifier: MIT - -import os -from django.conf import settings -import rules -from . import AUTH_GROUP - -settings.LOGIN_URL = 'login' -settings.LOGIN_REDIRECT_URL = '/' - -settings.AUTHENTICATION_BACKENDS = [ - 'rules.permissions.ObjectPermissionBackend', - 'django.contrib.auth.backends.ModelBackend' -] - -DJANGO_AUTH_TYPE = 'LDAP' if os.environ.get('DJANGO_AUTH_TYPE', '') == 'LDAP' else 'SIMPLE' -if DJANGO_AUTH_TYPE == 'SIMPLE': - from .auth_simple import create_user -elif DJANGO_AUTH_TYPE == 'LDAP': - from .auth_ldap import create_user - - -has_admins_group = rules.is_group_member(AUTH_GROUP.ADMINS.value) -has_users_group = rules.is_group_member(AUTH_GROUP.USERS.value) -has_annotators_group = rules.is_group_member(AUTH_GROUP.ANNOTATORS.value) -has_observers_group = rules.is_group_member(AUTH_GROUP.OBSERVERS.value) - -@rules.predicate -def is_task_owner(db_user, db_task): - return db_task.owner == db_user - -@rules.predicate -def is_task_assignee(db_user, db_task): - return db_task.assignee == db_user - -@rules.predicate -def is_task_annotator(db_user, db_task): - db_segments = list(db_task.segment_set.prefetch_related('job_set__assignee').all()) - db_jobs = [db_job for db_segment in db_segments - for db_job in db_segment.job_set.all()] - return db_user in [db_job.assignee for db_job in db_jobs] + [db_task.assignee] - -@rules.predicate -def is_job_owner(db_user, db_job): - return db_user == db_job.segment.task.owner - -@rules.predicate -def is_job_assignee(db_user, db_job): - return db_user == db_job.assignee - -@rules.predicate -def is_job_annotator(db_user, db_job): - return db_user in [db_job.assignee, db_job.segment.task.assignee] - - -# Auth rules -rules.add_perm('engine.task.create', has_admins_group | has_users_group) -rules.add_perm('engine.task.access', has_admins_group | is_task_owner | - has_observers_group | is_task_annotator) -rules.add_perm('engine.task.change', has_admins_group | is_task_owner | - is_task_assignee) -rules.add_perm('engine.task.delete', has_admins_group | is_task_owner) - -rules.add_perm('engine.job.access', has_admins_group | is_job_owner | - has_observers_group | is_job_annotator) -rules.add_perm('engine.job.change', has_admins_group | is_job_owner | - is_job_annotator) diff --git a/cvat/apps/authentication/signals.py b/cvat/apps/authentication/signals.py deleted file mode 100644 index eebf70039c20..000000000000 --- a/cvat/apps/authentication/signals.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (C) 2018 Intel Corporation -# -# SPDX-License-Identifier: MIT - -from django.contrib.auth.models import Group -from .settings import AUTH_GROUP -from .settings.authentication import create_user - -def create_groups(sender, **kwargs): - for group in AUTH_GROUP: - db_group, _ = Group.objects.get_or_create(name=group) - db_group.save() diff --git a/cvat/apps/authentication/urls.py b/cvat/apps/authentication/urls.py index d12271719054..ad51c1a60ed0 100644 --- a/cvat/apps/authentication/urls.py +++ b/cvat/apps/authentication/urls.py @@ -9,9 +9,9 @@ from django.contrib.auth import views as auth_views from . import forms from . import views -from .settings.authentication import DJANGO_AUTH_TYPE +from django.conf import settings -login_page = 'login{}.html'.format('_ldap' if DJANGO_AUTH_TYPE == 'LDAP' else '') +login_page = 'login{}.html'.format('_ldap' if settings.DJANGO_AUTH_TYPE == 'LDAP' else '') urlpatterns = [ path('login', auth_views.LoginView.as_view(form_class=forms.AuthForm, template_name=login_page), name='login'), diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 9972cc8b1206..a3478980abbb 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -64,6 +64,8 @@ if os.getenv('DJANGO_LOG_VIEWER_HOST'): INSTALLED_APPS += ['cvat.apps.log_viewer'] +DJANGO_AUTH_TYPE = os.environ.get('DJANGO_AUTH_TYPE', 'BASIC') + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', From e6e289cc93d625e8da4c9f4345c51bdca45a82d0 Mon Sep 17 00:00:00 2001 From: Nikita Manovich Date: Thu, 1 Nov 2018 15:47:27 +0300 Subject: [PATCH 03/10] Fix a couple of typos. --- cvat/apps/authentication/auth.py | 4 ++-- cvat/apps/authentication/auth_basic.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cvat/apps/authentication/auth.py b/cvat/apps/authentication/auth.py index b71c121a5aa1..2eceac923848 100644 --- a/cvat/apps/authentication/auth.py +++ b/cvat/apps/authentication/auth.py @@ -24,12 +24,12 @@ def create_groups(sender, **kwargs): db_group, _ = Group.objects.get_or_create(name=role) db_group.save() - post_migrate.connect(create_groups) + post_migrate.connect(create_groups, weak=False) if settings.DJANGO_AUTH_TYPE == 'BASIC': from .auth_basic import create_user - post_save.connect(create_user, sender=User, dispatch_uid="create_user") + post_save.connect(create_user, sender=User) elif settings.DJANGO_AUTH_TYPE == 'LDAP': import django_auth_ldap.backend from .auth_ldap import create_user diff --git a/cvat/apps/authentication/auth_basic.py b/cvat/apps/authentication/auth_basic.py index 4b2fa019ebab..654a6c9e6c49 100644 --- a/cvat/apps/authentication/auth_basic.py +++ b/cvat/apps/authentication/auth_basic.py @@ -7,14 +7,14 @@ # Specify groups that new users will have by default AUTH_DEFAULT_GROUPS = [AUTH_ROLE.OBSERVER] -def create_user(sender, user, created, **kwargs): +def create_user(sender, instance, created, **kwargs): from django.contrib.auth.models import Group - if user.is_superuser and user.is_staff: + if instance.is_superuser and instance.is_staff: db_group = Group.objects.get(name=AUTH_ROLE.ADMIN) - user.groups.add(db_group) + instance.groups.add(db_group) if created: for group in AUTH_DEFAULT_GROUPS: db_group = Group.objects.get(name=group) - user.groups.add(db_group) + instance.groups.add(db_group) From 14e5fd72bb125d0ea229b2a3c9b695887c8429c9 Mon Sep 17 00:00:00 2001 From: Nikita Manovich Date: Thu, 1 Nov 2018 17:31:23 +0300 Subject: [PATCH 04/10] Improve admin panel. --- cvat/apps/engine/admin.py | 28 ++++++++++++--- .../migrations/0014_auto_20181101_1725.py | 35 +++++++++++++++++++ cvat/apps/engine/models.py | 10 +++--- 3 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 cvat/apps/engine/migrations/0014_auto_20181101_1725.py diff --git a/cvat/apps/engine/admin.py b/cvat/apps/engine/admin.py index a0969f10f845..9cc6599c71b4 100644 --- a/cvat/apps/engine/admin.py +++ b/cvat/apps/engine/admin.py @@ -4,17 +4,27 @@ # SPDX-License-Identifier: MIT from django.contrib import admin -from .models import Task, Segment, Label, AttributeSpec +from .models import Task, Segment, Job, Label, AttributeSpec + +class JobInline(admin.TabularInline): + model = Job + can_delete = False + + # Don't show extra lines to add an object + def has_add_permission(self, request, object=None): + return False class SegmentInline(admin.TabularInline): model = Segment + show_change_link = True readonly_fields = ('start_frame', 'stop_frame') can_delete = False - # Don't show on admin index page + # Don't show extra lines to add an object def has_add_permission(self, request, object=None): return False + class AttributeSpecInline(admin.TabularInline): model = AttributeSpec extra = 0 @@ -35,14 +45,23 @@ def has_module_permission(self, request): AttributeSpecInline ] +class SegmentAdmin(admin.ModelAdmin): + # Don't show on admin index page + def has_module_permission(self, request): + return False + + inlines = [ + JobInline + ] class TaskAdmin(admin.ModelAdmin): date_hierarchy = 'updated_date' readonly_fields = ('size', 'path', 'created_date', 'updated_date', 'overlap', 'flipped') - list_display = ('name', 'mode', 'owner', 'created_date', 'updated_date') + list_display = ('name', 'mode', 'owner', 'assignee', 'created_date', 'updated_date') search_fields = ('name', 'mode', 'owner__username', 'owner__first_name', - 'owner__last_name', 'owner__email') + 'owner__last_name', 'owner__email', 'assignee__username', 'assignee__first_name', + 'assignee__last_name') inlines = [ SegmentInline, LabelInline @@ -54,4 +73,5 @@ def has_add_permission(self, request): admin.site.register(Task, TaskAdmin) +admin.site.register(Segment, SegmentAdmin) admin.site.register(Label, LabelAdmin) diff --git a/cvat/apps/engine/migrations/0014_auto_20181101_1725.py b/cvat/apps/engine/migrations/0014_auto_20181101_1725.py new file mode 100644 index 000000000000..5b24f40cb92c --- /dev/null +++ b/cvat/apps/engine/migrations/0014_auto_20181101_1725.py @@ -0,0 +1,35 @@ +# Generated by Django 2.0.9 on 2018-11-01 14:25 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('engine', '0013_auto_20181031_1554'), + ] + + operations = [ + migrations.AlterField( + model_name='job', + name='assignee', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='task', + name='assignee', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assignees', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='task', + name='bug_tracker', + field=models.CharField(blank=True, default='', max_length=2000), + ), + migrations.AlterField( + model_name='task', + name='owner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owners', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index b20096f8a140..b3ab7f09e972 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -39,9 +39,11 @@ class Task(models.Model): size = models.PositiveIntegerField() path = models.CharField(max_length=256) mode = models.CharField(max_length=32) - owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL, related_name="owners") - assignee = models.ForeignKey(User, null=True, on_delete=models.SET_NULL, related_name="assignees") - bug_tracker = models.CharField(max_length=2000, default="") + owner = models.ForeignKey(User, null=True, blank=True, + on_delete=models.SET_NULL, related_name="owners") + assignee = models.ForeignKey(User, null=True, blank=True, + on_delete=models.SET_NULL, related_name="assignees") + bug_tracker = models.CharField(max_length=2000, blank=True, default="") created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now_add=True) overlap = models.PositiveIntegerField(default=0) @@ -93,7 +95,7 @@ class Meta: class Job(models.Model): segment = models.ForeignKey(Segment, on_delete=models.CASCADE) - assignee = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) + assignee = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL) status = models.CharField(max_length=32, default=StatusChoice.ANNOTATION) class Meta: From 78201e939e8a8f220f75a20eac061e81c7a4ce1a Mon Sep 17 00:00:00 2001 From: Nikita Manovich Date: Fri, 2 Nov 2018 15:42:46 +0300 Subject: [PATCH 05/10] Refactored settings for auth --- cvat/apps/authentication/auth.py | 8 ---- cvat/apps/authentication/auth_basic.py | 10 +---- cvat/apps/authentication/auth_ldap.py | 51 +++----------------------- cvat/apps/authentication/decorators.py | 5 +-- cvat/apps/authentication/urls.py | 6 +-- cvat/apps/authentication/views.py | 8 ++-- cvat/settings/base.py | 13 ++++++- 7 files changed, 26 insertions(+), 75 deletions(-) diff --git a/cvat/apps/authentication/auth.py b/cvat/apps/authentication/auth.py index 2eceac923848..2ce36bef746d 100644 --- a/cvat/apps/authentication/auth.py +++ b/cvat/apps/authentication/auth.py @@ -7,14 +7,6 @@ import rules from . import AUTH_ROLE -settings.LOGIN_URL = 'login' -settings.LOGIN_REDIRECT_URL = '/' - -settings.AUTHENTICATION_BACKENDS.extend([ - 'rules.permissions.ObjectPermissionBackend', - 'django.contrib.auth.backends.ModelBackend' -]) - def register_signals(): from django.db.models.signals import post_migrate, post_save from django.contrib.auth.models import User, Group diff --git a/cvat/apps/authentication/auth_basic.py b/cvat/apps/authentication/auth_basic.py index 654a6c9e6c49..936628e7fe8c 100644 --- a/cvat/apps/authentication/auth_basic.py +++ b/cvat/apps/authentication/auth_basic.py @@ -2,10 +2,7 @@ # # SPDX-License-Identifier: MIT from . import AUTH_ROLE - - -# Specify groups that new users will have by default -AUTH_DEFAULT_GROUPS = [AUTH_ROLE.OBSERVER] +from django.conf import settings def create_user(sender, instance, created, **kwargs): from django.contrib.auth.models import Group @@ -13,8 +10,3 @@ def create_user(sender, instance, created, **kwargs): if instance.is_superuser and instance.is_staff: db_group = Group.objects.get(name=AUTH_ROLE.ADMIN) instance.groups.add(db_group) - - if created: - for group in AUTH_DEFAULT_GROUPS: - db_group = Group.objects.get(name=group) - instance.groups.add(db_group) diff --git a/cvat/apps/authentication/auth_ldap.py b/cvat/apps/authentication/auth_ldap.py index 3b0464cbe225..821a99ba716f 100644 --- a/cvat/apps/authentication/auth_ldap.py +++ b/cvat/apps/authentication/auth_ldap.py @@ -4,52 +4,13 @@ # SPDX-License-Identifier: MIT from django.conf import settings -import ldap -from django_auth_ldap.config import LDAPSearch, NestedActiveDirectoryGroupType from . import AUTH_ROLE -# Baseline configuration. -settings.AUTH_LDAP_SERVER_URI = "" - -# Credentials for LDAP server -settings.AUTH_LDAP_BIND_DN = "" -settings.AUTH_LDAP_BIND_PASSWORD = "" - -# Set up basic user search -settings.AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=example,dc=com", - ldap.SCOPE_SUBTREE, "(sAMAccountName=%(user)s)") - -# Set up the basic group parameters. -settings.AUTH_LDAP_GROUP_SEARCH = LDAPSearch("dc=example,dc=com", - ldap.SCOPE_SUBTREE, "(objectClass=group)") -settings.AUTH_LDAP_GROUP_TYPE = NestedActiveDirectoryGroupType() - -# # Simple group restrictions -settings.AUTH_LDAP_REQUIRE_GROUP = "cn=cvat,ou=Groups,dc=example,dc=com" - -# Populate the Django user from the LDAP directory. -settings.AUTH_LDAP_USER_ATTR_MAP = { - "first_name": "givenName", - "last_name": "sn", - "email": "mail", -} - -settings.AUTH_LDAP_ALWAYS_UPDATE_USER = True - -# Cache group memberships for an hour to minimize LDAP traffic -settings.AUTH_LDAP_CACHE_GROUPS = True -settings.AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600 -settings.AUTH_LDAP_AUTHORIZE_ALL_USERS = True - -# Keep ModelBackend around for per-user permissions and maybe a local -# superuser. -settings.AUTHENTICATION_BACKENDS.append('django_auth_ldap.backend.LDAPBackend') - AUTH_LDAP_GROUPS = { - AUTH_ROLE.ADMIN: "cn=cvat_admins,ou=Groups,dc=example,dc=com", - AUTH_ROLE.ANNOTATOR: "cn=cvat_annotators,ou=Groups,dc=example,dc=com", - AUTH_ROLE.USER: "cn=cvat_users,ou=Groups,dc=example,dc=com", - AUTH_ROLE.OBSERVER: "cn=cvat_observers,ou=Groups,dc=example,dc=com" + AUTH_ROLE.ADMIN: settings.AUTH_LDAP_ADMIN_GROUPS, + AUTH_ROLE.ANNOTATOR: settings.AUTH_LDAP_ANNOTATOR_GROUPS, + AUTH_ROLE.USER: settings.AUTH_LDAP_USER_GROUPS, + AUTH_ROLE.OBSERVER: settings.AUTH_LDAP_OBSERVER_GROUPS } def create_user(sender, user=None, ldap_user=None, **kwargs): @@ -61,8 +22,8 @@ def create_user(sender, user=None, ldap_user=None, **kwargs): for ldap_group in AUTH_LDAP_GROUPS[role]: if ldap_group.lower() in ldap_user.group_dns: user_groups.append(db_group) + if role == AUTH_ROLE.ADMIN: + user.is_staff = user.is_superuser = True user.groups.set(user_groups) - user.is_staff = user.is_superuser = (AUTH_ROLE.ADMIN in user_groups) user.save() - diff --git a/cvat/apps/authentication/decorators.py b/cvat/apps/authentication/decorators.py index 00d7612caf5f..dc0b107fee90 100644 --- a/cvat/apps/authentication/decorators.py +++ b/cvat/apps/authentication/decorators.py @@ -3,13 +3,12 @@ # # SPDX-License-Identifier: MIT +from functools import wraps +from urllib.parse import urlparse from django.contrib.auth import REDIRECT_FIELD_NAME from django.shortcuts import resolve_url, reverse from django.http import JsonResponse -from urllib.parse import urlparse from django.contrib.auth.views import redirect_to_login - -from functools import wraps from django.conf import settings def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, diff --git a/cvat/apps/authentication/urls.py b/cvat/apps/authentication/urls.py index ad51c1a60ed0..ff7f918844c1 100644 --- a/cvat/apps/authentication/urls.py +++ b/cvat/apps/authentication/urls.py @@ -4,12 +4,12 @@ # SPDX-License-Identifier: MIT from django.urls import path -import os - from django.contrib.auth import views as auth_views +from django.conf import settings + from . import forms from . import views -from django.conf import settings + login_page = 'login{}.html'.format('_ldap' if settings.DJANGO_AUTH_TYPE == 'LDAP' else '') diff --git a/cvat/apps/authentication/views.py b/cvat/apps/authentication/views.py index 777fcd617acf..c8effb07c3de 100644 --- a/cvat/apps/authentication/views.py +++ b/cvat/apps/authentication/views.py @@ -3,14 +3,12 @@ # # SPDX-License-Identifier: MIT -from django.shortcuts import render -from django.contrib.auth.views import LoginView -from django.http import HttpResponseRedirect +from django.shortcuts import render, redirect from django.conf import settings +from django.contrib.auth import login, authenticate + from . import forms -from django.contrib.auth import login, authenticate -from django.shortcuts import render, redirect def register_user(request): if request.method == 'POST': diff --git a/cvat/settings/base.py b/cvat/settings/base.py index a3478980abbb..65d68fdac812 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -64,8 +64,6 @@ if os.getenv('DJANGO_LOG_VIEWER_HOST'): INSTALLED_APPS += ['cvat.apps.log_viewer'] -DJANGO_AUTH_TYPE = os.environ.get('DJANGO_AUTH_TYPE', 'BASIC') - MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -103,6 +101,17 @@ WSGI_APPLICATION = 'cvat.wsgi.application' +# Django Auth +DJANGO_AUTH_TYPE = 'BASIC' +LOGIN_URL = 'login' +LOGIN_REDIRECT_URL = '/' + +AUTHENTICATION_BACKENDS = [ + 'rules.permissions.ObjectPermissionBackend', + 'django.contrib.auth.backends.ModelBackend' +] + + # Django-RQ # https://github.com/rq/django-rq From c442f3906a0db11db8c884a19ed64f73ba9c37e0 Mon Sep 17 00:00:00 2001 From: Nikita Manovich Date: Fri, 2 Nov 2018 17:22:48 +0300 Subject: [PATCH 06/10] Unify login page for ldap and basic auth. --- cvat/apps/authentication/templates/login.html | 4 ++- .../authentication/templates/login_ldap.html | 27 ------------------- cvat/apps/authentication/templates/note.html | 7 ----- cvat/apps/authentication/urls.py | 13 +++++---- cvat/settings/base.py | 1 + 5 files changed, 12 insertions(+), 40 deletions(-) delete mode 100644 cvat/apps/authentication/templates/login_ldap.html delete mode 100644 cvat/apps/authentication/templates/note.html diff --git a/cvat/apps/authentication/templates/login.html b/cvat/apps/authentication/templates/login.html index 128a8ccb849f..855031f73222 100644 --- a/cvat/apps/authentication/templates/login.html +++ b/cvat/apps/authentication/templates/login.html @@ -23,5 +23,7 @@

Login

{% endblock %} {% block note%} -

Have not registered yet? Register here.

+ {% autoescape off %} + {{ note }} + {% endautoescape %} {% endblock %} \ No newline at end of file diff --git a/cvat/apps/authentication/templates/login_ldap.html b/cvat/apps/authentication/templates/login_ldap.html deleted file mode 100644 index 8cba181db5fd..000000000000 --- a/cvat/apps/authentication/templates/login_ldap.html +++ /dev/null @@ -1,27 +0,0 @@ - -{% extends "auth_base.html" %} - -{% block title %}Login{% endblock %} - -{% block content %} -

Login

- {% if form.errors %} - Your username and password didn't match. Please try again. - {% endif %} -
- {% csrf_token %} - {% for field in form %} - {{ field }} - {% endfor %} - - -
-{% endblock %} - -{% block note %} - {% include "note.html" %} -{% endblock %} diff --git a/cvat/apps/authentication/templates/note.html b/cvat/apps/authentication/templates/note.html deleted file mode 100644 index ba2fea92248b..000000000000 --- a/cvat/apps/authentication/templates/note.html +++ /dev/null @@ -1,7 +0,0 @@ - -

-

\ No newline at end of file diff --git a/cvat/apps/authentication/urls.py b/cvat/apps/authentication/urls.py index ff7f918844c1..e05d734035f9 100644 --- a/cvat/apps/authentication/urls.py +++ b/cvat/apps/authentication/urls.py @@ -10,11 +10,14 @@ from . import forms from . import views - -login_page = 'login{}.html'.format('_ldap' if settings.DJANGO_AUTH_TYPE == 'LDAP' else '') - urlpatterns = [ - path('login', auth_views.LoginView.as_view(form_class=forms.AuthForm, template_name=login_page), name='login'), + path('login', auth_views.LoginView.as_view(form_class=forms.AuthForm, + template_name='login.html', extra_context={'note': settings.AUTH_LOGIN_NOTE}), + name='login'), path('logout', auth_views.LogoutView.as_view(next_page='login'), name='logout'), - path('register', views.register_user, name='register'), ] + +if settings.DJANGO_AUTH_TYPE == 'BASIC': + urlpatterns += [ + path('register', views.register_user, name='register'), + ] diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 65d68fdac812..015f2c003fd6 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -105,6 +105,7 @@ DJANGO_AUTH_TYPE = 'BASIC' LOGIN_URL = 'login' LOGIN_REDIRECT_URL = '/' +AUTH_LOGIN_NOTE = '

Have not registered yet? Register here.

' AUTHENTICATION_BACKENDS = [ 'rules.permissions.ObjectPermissionBackend', From 47600ca682db6103232dbcaf2357350fc8bb0d62 Mon Sep 17 00:00:00 2001 From: Nikita Manovich Date: Fri, 2 Nov 2018 17:35:05 +0300 Subject: [PATCH 07/10] Fix a typo --- cvat/apps/engine/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index b3ab7f09e972..ebc813383637 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -197,7 +197,6 @@ class BoundingBox(Shape): class Meta: abstract = True default_permissions = () - default_permissions = () class PolyShape(Shape): id = models.BigAutoField(primary_key=True) From fdb693fd8917d5a3cc6744713858d815b78116e2 Mon Sep 17 00:00:00 2001 From: Nikita Manovich Date: Wed, 7 Nov 2018 13:41:42 +0300 Subject: [PATCH 08/10] Fix task/job access and change permissions. Now only task's owner and assignee can change a task. If a person has task access it has also job access. --- cvat/apps/authentication/auth.py | 41 +++++++++++++++----------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/cvat/apps/authentication/auth.py b/cvat/apps/authentication/auth.py index 2ce36bef746d..0da83216e8ed 100644 --- a/cvat/apps/authentication/auth.py +++ b/cvat/apps/authentication/auth.py @@ -37,47 +37,44 @@ def create_groups(sender, **kwargs): @rules.predicate def is_task_owner(db_user, db_task): # If owner is None (null) the task can be accessed/changed/deleted - # by any user (publically available tasks) - return db_task.owner == db_user or db_task.owner is None + # only by admin. At the moment each task has an owner. + return db_task.owner == db_user @rules.predicate def is_task_assignee(db_user, db_task): - # If assignee is None (null) the task can be accessed/changeed by any user. - return db_task.assignee == db_user or db_task.assignee is None - -@rules.predicate -def is_job_owner(db_user, db_job): - return is_task_owner(db_user, db_job.segment.task) - -@rules.predicate -def is_job_assignee(db_user, db_job): - # If assignee is None (null) the job can be accessed/changed by any user. - return db_user == db_job.assignee or db_job.assignee is None + return db_task.assignee == db_user @rules.predicate def is_task_annotator(db_user, db_task): from functools import reduce db_segments = list(db_task.segment_set.prefetch_related('job_set__assignee').all()) - # If job.assignee is None it doesn't mean that everybody can access the task. - return is_task_assignee(db_user, db_task) or \ - reduce((lambda x, y: x | y), [db_user == db_job.assignee - for db_segment in db_segments for db_job in db_segment.job_set.all()]) + return any([is_job_annotator(db_user, db_job) + for db_segment in db_segments for db_job in db_segment.job_set.all()]) + +@rules.predicate +def is_job_owner(db_user, db_job): + return is_task_owner(db_user, db_job.segment.task) @rules.predicate def is_job_annotator(db_user, db_job): db_task = db_job.segment.task - return is_job_assignee(db_user, db_job) or is_task_assignee(db_user, db_task) + # A job can be annotated by any user if the task's assignee is None. + has_rights = db_task.assignee is None or is_task_assignee(db_user, db_task) + if db_job.assignee is not None: + has_rights |= (db_user == db_job.assignee) + + return has_rights # AUTH PERMISSIONS RULES rules.add_perm('engine.task.create', has_admin_role | has_user_role) -rules.add_perm('engine.task.access', has_admin_role | is_task_owner | - has_observer_role | is_task_annotator) +rules.add_perm('engine.task.access', has_admin_role | has_observer_role | + is_task_owner | is_task_annotator) rules.add_perm('engine.task.change', has_admin_role | is_task_owner | is_task_assignee) rules.add_perm('engine.task.delete', has_admin_role | is_task_owner) -rules.add_perm('engine.job.access', has_admin_role | is_job_owner | - has_observer_role | is_job_annotator) +rules.add_perm('engine.job.access', has_admin_role | has_observer_role | + is_job_owner | is_job_annotator) rules.add_perm('engine.job.change', has_admin_role | is_job_owner | is_job_annotator) From b4925b83cf3841913fcd00e9665e6bb42a0e8a1d Mon Sep 17 00:00:00 2001 From: Nikita Manovich Date: Wed, 7 Nov 2018 13:51:41 +0300 Subject: [PATCH 09/10] Hide permissions section in admin panel for User and Group --- cvat/apps/authentication/admin.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/cvat/apps/authentication/admin.py b/cvat/apps/authentication/admin.py index af8dfc47525b..3267ca6871e1 100644 --- a/cvat/apps/authentication/admin.py +++ b/cvat/apps/authentication/admin.py @@ -4,6 +4,24 @@ # SPDX-License-Identifier: MIT from django.contrib import admin +from django.contrib.auth.models import Group, User +from django.contrib.auth.admin import GroupAdmin, UserAdmin +from django.utils.translation import ugettext_lazy as _ -# Register your models here. +class CustomUserAdmin(UserAdmin): + fieldsets = ( + (None, {'fields': ('username', 'password')}), + (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}), + (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', + 'groups',)}), + (_('Important dates'), {'fields': ('last_login', 'date_joined')}), + ) +class CustomGroupAdmin(GroupAdmin): + fieldsets = ((None, {'fields': ('name',)}),) + + +admin.site.unregister(User) +admin.site.unregister(Group) +admin.site.register(User, CustomUserAdmin) +admin.site.register(Group, CustomGroupAdmin) \ No newline at end of file From c4fcbbe9ad8162aed4f8869a50112bfcb1928bb6 Mon Sep 17 00:00:00 2001 From: Nikita Manovich Date: Wed, 7 Nov 2018 18:49:41 +0300 Subject: [PATCH 10/10] Updated Django and squashed migraitons. --- ...py => 0013_auth_no_default_permissions.py} | 21 +++++++++-- .../migrations/0014_auto_20181101_1725.py | 35 ------------------- cvat/requirements/base.txt | 4 +-- 3 files changed, 20 insertions(+), 40 deletions(-) rename cvat/apps/engine/migrations/{0013_auto_20181031_1554.py => 0013_auth_no_default_permissions.py} (78%) delete mode 100644 cvat/apps/engine/migrations/0014_auto_20181101_1725.py diff --git a/cvat/apps/engine/migrations/0013_auto_20181031_1554.py b/cvat/apps/engine/migrations/0013_auth_no_default_permissions.py similarity index 78% rename from cvat/apps/engine/migrations/0013_auto_20181031_1554.py rename to cvat/apps/engine/migrations/0013_auth_no_default_permissions.py index 578340d4f799..bc735269eed6 100644 --- a/cvat/apps/engine/migrations/0013_auto_20181031_1554.py +++ b/cvat/apps/engine/migrations/0013_auth_no_default_permissions.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.9 on 2018-10-31 12:54 +# Generated by Django 2.0.9 on 2018-11-07 12:25 from django.conf import settings from django.db import migrations, models @@ -8,8 +8,8 @@ class Migration(migrations.Migration): dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('engine', '0012_auto_20181025_1618'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -93,11 +93,26 @@ class Migration(migrations.Migration): migrations.AddField( model_name='task', name='assignee', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assignees', to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assignees', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( model_name='task', name='owner', field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owners', to=settings.AUTH_USER_MODEL), ), + migrations.AlterField( + model_name='job', + name='assignee', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='task', + name='bug_tracker', + field=models.CharField(blank=True, default='', max_length=2000), + ), + migrations.AlterField( + model_name='task', + name='owner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owners', to=settings.AUTH_USER_MODEL), + ), ] diff --git a/cvat/apps/engine/migrations/0014_auto_20181101_1725.py b/cvat/apps/engine/migrations/0014_auto_20181101_1725.py deleted file mode 100644 index 5b24f40cb92c..000000000000 --- a/cvat/apps/engine/migrations/0014_auto_20181101_1725.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 2.0.9 on 2018-11-01 14:25 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('engine', '0013_auto_20181031_1554'), - ] - - operations = [ - migrations.AlterField( - model_name='job', - name='assignee', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='task', - name='assignee', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assignees', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='task', - name='bug_tracker', - field=models.CharField(blank=True, default='', max_length=2000), - ), - migrations.AlterField( - model_name='task', - name='owner', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owners', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index e423c5c3db73..bf4e6c1e4a22 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -1,5 +1,5 @@ click==6.7 -Django==2.0.9 +Django==2.1.3 django-appconf==1.0.2 django-auth-ldap==1.4.0 django-cacheops==4.0.6 @@ -21,7 +21,7 @@ rq==0.10.0 scipy==1.0.1 sqlparse==0.2.4 django-sendfile==0.3.11 -dj-pagination==2.3.2 +dj-pagination==2.4.0 python-logstash==0.4.6 django-revproxy==0.9.15 rules==2.0