Permissions per tasks and jobs (#185)

main
Nikita Manovich 7 years ago committed by GitHub
parent d7e69982fd
commit 608253f1cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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

@ -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)

@ -4,20 +4,11 @@
# SPDX-License-Identifier: MIT
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 .auth import register_signals
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)
register_signals()

@ -0,0 +1,80 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
import os
from django.conf import settings
import rules
from . import AUTH_ROLE
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, weak=False)
if settings.DJANGO_AUTH_TYPE == 'BASIC':
from .auth_basic import 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
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
# 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):
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())
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
# 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 | 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 | has_observer_role |
is_job_owner | is_job_annotator)
rules.add_perm('engine.job.change', has_admin_role | is_job_owner |
is_job_annotator)

@ -0,0 +1,12 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
from . import AUTH_ROLE
from django.conf import settings
def create_user(sender, instance, created, **kwargs):
from django.contrib.auth.models import Group
if instance.is_superuser and instance.is_staff:
db_group = Group.objects.get(name=AUTH_ROLE.ADMIN)
instance.groups.add(db_group)

@ -0,0 +1,29 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
from django.conf import settings
from . import AUTH_ROLE
AUTH_LDAP_GROUPS = {
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):
from django.contrib.auth.models import Group
user_groups = []
for role in AUTH_ROLE:
db_group = Group.objects.get(name=role)
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.save()

@ -3,16 +3,16 @@
#
# 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, 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):

@ -1,5 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT

@ -1,56 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
from django.conf import settings
import ldap
from django_auth_ldap.config import LDAPSearch, NestedActiveDirectoryGroupType
# 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_ADMIN_GROUPS = [
"cn=cvat_admins,ou=Groups,dc=example,dc=com"
]
AUTH_LDAP_DATA_ANNOTATORS_GROUPS = [
]
AUTH_LDAP_DEVELOPER_GROUPS = [
"cn=cvat_users,ou=Groups,dc=example,dc=com"
]

@ -1,8 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
# Specify groups that new users will have
AUTH_SIMPLE_DEFAULT_GROUPS = []

@ -1,58 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
from django.conf import settings
import os
settings.LOGIN_URL = 'login'
settings.LOGIN_REDIRECT_URL = '/'
settings.AUTHENTICATION_BACKENDS = [
'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 == '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,
},
}

@ -1,62 +0,0 @@
# 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()
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)

@ -23,5 +23,7 @@
{% endblock %}
{% block note%}
<p>Have not registered yet? <a href="{% url 'register' %}">Register here</a>.</p>
{% autoescape off %}
{{ note }}
{% endautoescape %}
{% endblock %}

@ -1,27 +0,0 @@
<!--
Copyright (C) 2018 Intel Corporation
SPDX-License-Identifier: MIT
-->
{% extends "auth_base.html" %}
{% block title %}Login{% endblock %}
{% block content %}
<h1>Login</h1>
{% if form.errors %}
<small>Your username and password didn't match. Please try again.</small>
{% endif %}
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
{% for field in form %}
{{ field }}
{% endfor %}
<input type="hidden" name="next" value="{{ next }}" />
<button type="submit" class="btn btn-primary btn-block btn-large">Login</button>
</form>
{% endblock %}
{% block note %}
{% include "note.html" %}
{% endblock %}

@ -1,7 +0,0 @@
<!--
Copyright (C) 2018 Intel Corporation
SPDX-License-Identifier: MIT
-->
<p>
</p>

@ -4,17 +4,20 @@
# 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 .settings.authentication import DJANGO_AUTH_TYPE
login_page = 'login{}.html'.format('_ldap' if 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'),
]

@ -3,13 +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':
@ -20,7 +19,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})

@ -7,7 +7,6 @@ from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
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,

@ -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 @@ class LabelAdmin(admin.ModelAdmin):
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 @@ class TaskAdmin(admin.ModelAdmin):
admin.site.register(Task, TaskAdmin)
admin.site.register(Segment, SegmentAdmin)
admin.site.register(Label, LabelAdmin)

@ -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')

@ -0,0 +1,118 @@
# Generated by Django 2.0.9 on 2018-11-07 12:25
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('engine', '0012_auto_20181025_1618'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
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(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),
),
]

@ -39,8 +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)
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)
@ -51,11 +54,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 +90,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, blank=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 +108,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 +128,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 +154,20 @@ class AttributeSpec(models.Model):
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 +175,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 +193,18 @@ class BoundingBox(Shape):
ytl = models.FloatField()
xbr = models.FloatField()
ybr = models.FloatField()
class Meta:
abstract = True
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 +243,7 @@ class TrackedObject(models.Model):
outside = models.BooleanField(default=False)
class Meta:
abstract = True
default_permissions = ()
class TrackedBox(TrackedObject, BoundingBox):
pass

@ -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",

@ -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]

@ -23,5 +23,5 @@ urlpatterns = [
path('get/annotation/job/<int:jid>', views.get_annotation),
path('get/username', views.get_username),
path('save/exception/<int:jid>', views.catch_client_exception),
path('save/job/status', views.save_job_status),
path('save/status/job/<int:jid>', views.save_job_status),
]

@ -10,7 +10,7 @@ import traceback
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 @@ from cvat.apps.engine.models import StatusChoice
############################# 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)

@ -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
@ -285,14 +285,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):
@ -346,7 +344,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')
@ -375,7 +374,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')

@ -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
@ -15,12 +15,13 @@ 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
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

@ -54,7 +54,8 @@ INSTALLED_APPS = [
'cacheops',
'sendfile',
'dj_pagination',
'revproxy'
'revproxy',
'rules'
]
if 'yes' == os.environ.get('TF_ANNOTATION', 'no'):
@ -100,6 +101,18 @@ TEMPLATES = [
WSGI_APPLICATION = 'cvat.wsgi.application'
# Django Auth
DJANGO_AUTH_TYPE = 'BASIC'
LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = '/'
AUTH_LOGIN_NOTE = '<p>Have not registered yet? <a href="/auth/register">Register here</a>.</p>'
AUTHENTICATION_BACKENDS = [
'rules.permissions.ObjectPermissionBackend',
'django.contrib.auth.backends.ModelBackend'
]
# Django-RQ
# https://github.com/rq/django-rq

Loading…
Cancel
Save