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. - 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.). - 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, ...) - 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 ### Fixed
- Performance bottleneck has been fixed during you create new objects (draw, copy, merge etc). - Performance bottleneck has been fixed during you create new objects (draw, copy, merge etc).
- Label UI elements aren't updated after changelabel. - Label UI elements aren't updated after changelabel.
- Attribute annotation mode can use invalid shape position after resize or move shapes. - Attribute annotation mode can use invalid shape position after resize or move shapes.
## [0.2.0] - 2018-09-28 ## [0.2.0] - 2018-09-28
### Added ### Added
- New annotation shapes: polygons, polylines, points - New annotation shapes: polygons, polylines, points

@ -5,3 +5,13 @@
default_app_config = 'cvat.apps.authentication.apps.AuthenticationConfig' 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 # SPDX-License-Identifier: MIT
from django.contrib import admin 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 # SPDX-License-Identifier: MIT
from django.apps import AppConfig 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): class AuthenticationConfig(AppConfig):
name = 'cvat.apps.authentication' name = 'cvat.apps.authentication'
def ready(self): def ready(self):
from . import signals from .auth import register_signals
from django.contrib.auth.models import User
post_migrate.connect(signals.create_groups) register_signals()
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)

@ -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 # SPDX-License-Identifier: MIT
from functools import wraps
from urllib.parse import urlparse
from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import REDIRECT_FIELD_NAME
from django.shortcuts import resolve_url, reverse from django.shortcuts import resolve_url, reverse
from django.http import JsonResponse from django.http import JsonResponse
from urllib.parse import urlparse
from django.contrib.auth.views import redirect_to_login from django.contrib.auth.views import redirect_to_login
from functools import wraps
from django.conf import settings 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): def decorator(view_func):
@wraps(view_func) @wraps(view_func)
def _wrapped_view(request, *args, **kwargs): 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 %} {% endblock %}
{% block note%} {% block note%}
<p>Have not registered yet? <a href="{% url 'register' %}">Register here</a>.</p> {% autoescape off %}
{{ note }}
{% endautoescape %}
{% endblock %} {% 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 # SPDX-License-Identifier: MIT
from django.urls import path from django.urls import path
import os
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.conf import settings
from . import forms from . import forms
from . import views from . import views
from .settings.authentication import DJANGO_AUTH_TYPE
login_page = 'login{}.html'.format('_ldap' if DJANGO_AUTH_TYPE == 'LDAP' else '')
urlpatterns = [ 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('logout', auth_views.LogoutView.as_view(next_page='login'), name='logout'),
]
if settings.DJANGO_AUTH_TYPE == 'BASIC':
urlpatterns += [
path('register', views.register_user, name='register'), path('register', views.register_user, name='register'),
] ]

@ -3,13 +3,12 @@
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from django.shortcuts import render from django.shortcuts import render, redirect
from django.contrib.auth.views import LoginView from django.conf import settings
from django.http import HttpResponseRedirect from django.contrib.auth import login, authenticate
from . import forms from . import forms
from django.contrib.auth import login, authenticate
from django.shortcuts import render, redirect
def register_user(request): def register_user(request):
if request.method == 'POST': if request.method == 'POST':
@ -20,7 +19,7 @@ def register_user(request):
raw_password = form.cleaned_data.get('password1') raw_password = form.cleaned_data.get('password1')
user = authenticate(username=username, password=raw_password) user = authenticate(username=username, password=raw_password)
login(request, user) login(request, user)
return redirect('/') return redirect(settings.LOGIN_REDIRECT_URL)
else: else:
form = forms.NewUserForm() form = forms.NewUserForm()
return render(request, 'register.html', {'form': form}) 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 redirect
from django.shortcuts import render from django.shortcuts import render
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import permission_required
from cvat.apps.authentication.decorators import login_required from cvat.apps.authentication.decorators import login_required
from cvat.apps.engine.models import Task as TaskModel, Job as JobModel from cvat.apps.engine.models import Task as TaskModel, Job as JobModel
@ -40,7 +39,6 @@ def ScanNode(directory):
return result return result
@login_required @login_required
@permission_required('engine.add_task', raise_exception=True)
def JsTreeView(request): def JsTreeView(request):
node_id = None node_id = None
if 'id' in request.GET: if 'id' in request.GET:
@ -57,7 +55,6 @@ def JsTreeView(request):
@login_required @login_required
@permission_required('engine.view_task', raise_exception=True)
def DashboardView(request): def DashboardView(request):
query_name = request.GET['search'] if 'search' in request.GET else None 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 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: 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 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', { return render(request, 'dashboard/dashboard.html', {
'data': task_list, 'data': task_list,
'max_upload_size': settings.LOCAL_LOAD_MAX_FILES_SIZE, 'max_upload_size': settings.LOCAL_LOAD_MAX_FILES_SIZE,

@ -4,17 +4,27 @@
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from django.contrib import admin 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): class SegmentInline(admin.TabularInline):
model = Segment model = Segment
show_change_link = True
readonly_fields = ('start_frame', 'stop_frame') readonly_fields = ('start_frame', 'stop_frame')
can_delete = False 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): def has_add_permission(self, request, object=None):
return False return False
class AttributeSpecInline(admin.TabularInline): class AttributeSpecInline(admin.TabularInline):
model = AttributeSpec model = AttributeSpec
extra = 0 extra = 0
@ -35,14 +45,23 @@ class LabelAdmin(admin.ModelAdmin):
AttributeSpecInline 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): class TaskAdmin(admin.ModelAdmin):
date_hierarchy = 'updated_date' date_hierarchy = 'updated_date'
readonly_fields = ('size', 'path', 'created_date', 'updated_date', readonly_fields = ('size', 'path', 'created_date', 'updated_date',
'overlap', 'flipped') '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', 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 = [ inlines = [
SegmentInline, SegmentInline,
LabelInline LabelInline
@ -54,4 +73,5 @@ class TaskAdmin(admin.ModelAdmin):
admin.site.register(Task, TaskAdmin) admin.site.register(Task, TaskAdmin)
admin.site.register(Segment, SegmentAdmin)
admin.site.register(Label, LabelAdmin) admin.site.register(Label, LabelAdmin)

@ -38,7 +38,7 @@ def dump(tid, data_format, scheme, host):
def check(tid): 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. Return the status as json/dictionary object.
""" """
queue = django_rq.get_queue('default') 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() size = models.PositiveIntegerField()
path = models.CharField(max_length=256) path = models.CharField(max_length=256)
mode = models.CharField(max_length=32) mode = models.CharField(max_length=32)
owner = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) owner = models.ForeignKey(User, null=True, blank=True,
bug_tracker = models.CharField(max_length=2000, default="") 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) created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now_add=True)
overlap = models.PositiveIntegerField(default=0) overlap = models.PositiveIntegerField(default=0)
@ -51,11 +54,7 @@ class Task(models.Model):
# Extend default permission model # Extend default permission model
class Meta: class Meta:
permissions = ( default_permissions = ()
("view_task", "Can see available tasks"),
("view_annotation", "Can see annotation for the task"),
("change_annotation", "Can modify annotation for the task"),
)
def get_upload_dirname(self): def get_upload_dirname(self):
return os.path.join(self.path, ".upload") return os.path.join(self.path, ".upload")
@ -91,11 +90,16 @@ class Segment(models.Model):
start_frame = models.IntegerField() start_frame = models.IntegerField()
stop_frame = models.IntegerField() stop_frame = models.IntegerField()
class Meta:
default_permissions = ()
class Job(models.Model): class Job(models.Model):
segment = models.ForeignKey(Segment, on_delete=models.CASCADE) 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) 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): class Label(models.Model):
task = models.ForeignKey(Task, on_delete=models.CASCADE) task = models.ForeignKey(Task, on_delete=models.CASCADE)
@ -104,6 +108,10 @@ class Label(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
class Meta:
default_permissions = ()
def parse_attribute(text): def parse_attribute(text):
match = re.match(r'^([~@])(\w+)=(\w+):(.+)?$', text) match = re.match(r'^([~@])(\w+)=(\w+):(.+)?$', text)
prefix = match.group(1) prefix = match.group(1)
@ -120,6 +128,9 @@ class AttributeSpec(models.Model):
label = models.ForeignKey(Label, on_delete=models.CASCADE) label = models.ForeignKey(Label, on_delete=models.CASCADE)
text = models.CharField(max_length=1024) text = models.CharField(max_length=1024)
class Meta:
default_permissions = ()
def get_attribute(self): def get_attribute(self):
return parse_attribute(self.text) return parse_attribute(self.text)
@ -143,17 +154,20 @@ class AttributeSpec(models.Model):
attr = self.get_attribute() attr = self.get_attribute()
return attr['values'] return attr['values']
def __str__(self): def __str__(self):
return self.get_attribute()['name'] return self.get_attribute()['name']
class AttributeVal(models.Model): class AttributeVal(models.Model):
# TODO: add a validator here to be sure that it corresponds to self.label # TODO: add a validator here to be sure that it corresponds to self.label
id = models.BigAutoField(primary_key=True) id = models.BigAutoField(primary_key=True)
spec = models.ForeignKey(AttributeSpec, on_delete=models.CASCADE) spec = models.ForeignKey(AttributeSpec, on_delete=models.CASCADE)
value = SafeCharField(max_length=64) value = SafeCharField(max_length=64)
class Meta: class Meta:
abstract = True abstract = True
default_permissions = ()
class Annotation(models.Model): class Annotation(models.Model):
job = models.ForeignKey(Job, on_delete=models.CASCADE) job = models.ForeignKey(Job, on_delete=models.CASCADE)
@ -161,14 +175,17 @@ class Annotation(models.Model):
frame = models.PositiveIntegerField() frame = models.PositiveIntegerField()
group_id = models.PositiveIntegerField(default=0) group_id = models.PositiveIntegerField(default=0)
client_id = models.BigIntegerField(default=-1) client_id = models.BigIntegerField(default=-1)
class Meta: class Meta:
abstract = True abstract = True
class Shape(models.Model): class Shape(models.Model):
occluded = models.BooleanField(default=False) occluded = models.BooleanField(default=False)
z_order = models.IntegerField(default=0) z_order = models.IntegerField(default=0)
class Meta: class Meta:
abstract = True abstract = True
default_permissions = ()
class BoundingBox(Shape): class BoundingBox(Shape):
id = models.BigAutoField(primary_key=True) id = models.BigAutoField(primary_key=True)
@ -176,14 +193,18 @@ class BoundingBox(Shape):
ytl = models.FloatField() ytl = models.FloatField()
xbr = models.FloatField() xbr = models.FloatField()
ybr = models.FloatField() ybr = models.FloatField()
class Meta: class Meta:
abstract = True abstract = True
default_permissions = ()
class PolyShape(Shape): class PolyShape(Shape):
id = models.BigAutoField(primary_key=True) id = models.BigAutoField(primary_key=True)
points = models.TextField() points = models.TextField()
class Meta: class Meta:
abstract = True abstract = True
default_permissions = ()
class LabeledBox(Annotation, BoundingBox): class LabeledBox(Annotation, BoundingBox):
pass pass
@ -222,6 +243,7 @@ class TrackedObject(models.Model):
outside = models.BooleanField(default=False) outside = models.BooleanField(default=False)
class Meta: class Meta:
abstract = True abstract = True
default_permissions = ()
class TrackedBox(TrackedObject, BoundingBox): class TrackedBox(TrackedObject, BoundingBox):
pass pass

@ -518,9 +518,8 @@ function setupMenu(job, shapeCollectionModel, annotationParser, aamModel, player
$('#statTaskStatus').prop("value", job.status).on('change', (e) => { $('#statTaskStatus').prop("value", job.status).on('change', (e) => {
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: 'save/job/status', url: 'save/status/job/' + window.cvat.job.id,
data: JSON.stringify({ data: JSON.stringify({
jid: window.cvat.job.id,
status: e.target.value status: e.target.value
}), }),
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",

@ -249,13 +249,6 @@ def get_job(jid):
return response 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 @transaction.atomic
def rq_handler(job, exc_type, exc_value, traceback): def rq_handler(job, exc_type, exc_value, traceback):
tid = job.id.split('/')[1] tid = job.id.split('/')[1]

@ -23,5 +23,5 @@ urlpatterns = [
path('get/annotation/job/<int:jid>', views.get_annotation), path('get/annotation/job/<int:jid>', views.get_annotation),
path('get/username', views.get_username), path('get/username', views.get_username),
path('save/exception/<int:jid>', views.catch_client_exception), 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.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.conf import settings 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 django.views.decorators.gzip import gzip_page
from sendfile import sendfile from sendfile import sendfile
@ -24,7 +24,8 @@ from cvat.apps.engine.models import StatusChoice
############################# High Level server API ############################# High Level server API
@login_required @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): def catch_client_exception(request, jid):
data = json.loads(request.body.decode('utf-8')) data = json.loads(request.body.decode('utf-8'))
for event in data['exceptions']: for event in data['exceptions']:
@ -44,7 +45,7 @@ def dispatch_request(request):
return redirect('/dashboard/') return redirect('/dashboard/')
@login_required @login_required
@permission_required('engine.add_task', raise_exception=True) @permission_required(perm=['engine.task.create'], raise_exception=True)
def create_task(request): def create_task(request):
"""Create a new annotation task""" """Create a new annotation task"""
@ -103,10 +104,10 @@ def create_task(request):
return JsonResponse({'tid': db_task.id}) return JsonResponse({'tid': db_task.id})
@login_required @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): def check_task(request, tid):
"""Check the status of a task""" """Check the status of a task"""
try: try:
slogger.glob.info("check task #{}".format(tid)) slogger.glob.info("check task #{}".format(tid))
response = task.check(tid) response = task.check(tid)
@ -117,7 +118,8 @@ def check_task(request, tid):
return JsonResponse(response) return JsonResponse(response)
@login_required @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): def get_frame(request, tid, frame):
"""Stream corresponding from for the task""" """Stream corresponding from for the task"""
@ -131,14 +133,12 @@ def get_frame(request, tid, frame):
return HttpResponseBadRequest(str(e)) return HttpResponseBadRequest(str(e))
@login_required @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): def delete_task(request, tid):
"""Delete the task""" """Delete the task"""
try: try:
slogger.glob.info("delete task #{}".format(tid)) 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) task.delete(tid)
except Exception as e: except Exception as e:
slogger.glob.error("cannot delete task #{}".format(tid), exc_info=True) slogger.glob.error("cannot delete task #{}".format(tid), exc_info=True)
@ -147,14 +147,12 @@ def delete_task(request, tid):
return HttpResponse() return HttpResponse()
@login_required @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): def update_task(request, tid):
"""Update labels for the task""" """Update labels for the task"""
try: try:
slogger.task[tid].info("update task request") 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'] labels = request.POST['labels']
task.update(tid, labels) task.update(tid, labels)
except Exception as e: except Exception as e:
@ -164,7 +162,8 @@ def update_task(request, tid):
return HttpResponse() return HttpResponse()
@login_required @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): def get_task(request, tid):
try: try:
slogger.task[tid].info("get task request") slogger.task[tid].info("get task request")
@ -176,7 +175,8 @@ def get_task(request, tid):
return JsonResponse(response, safe=False) return JsonResponse(response, safe=False)
@login_required @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): def get_job(request, jid):
try: try:
slogger.job[jid].info("get job #{} request".format(jid)) slogger.job[jid].info("get job #{} request".format(jid))
@ -188,7 +188,8 @@ def get_job(request, jid):
return JsonResponse(response, safe=False) return JsonResponse(response, safe=False)
@login_required @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): def dump_annotation(request, tid):
try: try:
slogger.task[tid].info("dump annotation request") slogger.task[tid].info("dump annotation request")
@ -201,7 +202,8 @@ def dump_annotation(request, tid):
@login_required @login_required
@gzip_page @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): def check_annotation(request, tid):
try: try:
slogger.task[tid].info("check annotation") slogger.task[tid].info("check annotation")
@ -215,7 +217,8 @@ def check_annotation(request, tid):
@login_required @login_required
@gzip_page @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): def download_annotation(request, tid):
try: try:
slogger.task[tid].info("get dumped annotation") slogger.task[tid].info("get dumped annotation")
@ -231,7 +234,8 @@ def download_annotation(request, tid):
@login_required @login_required
@gzip_page @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): def get_annotation(request, jid):
try: try:
slogger.job[jid].info("get annotation for {} job".format(jid)) slogger.job[jid].info("get annotation for {} job".format(jid))
@ -243,7 +247,8 @@ def get_annotation(request, jid):
return JsonResponse(response, safe=False) return JsonResponse(response, safe=False)
@login_required @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): def save_annotation_for_job(request, jid):
try: try:
slogger.job[jid].info("save annotation for {} job".format(jid)) slogger.job[jid].info("save annotation for {} job".format(jid))
@ -263,7 +268,8 @@ def save_annotation_for_job(request, jid):
return HttpResponse() return HttpResponse()
@login_required @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): def save_annotation_for_task(request, tid):
try: try:
slogger.task[tid].info("save annotation request") slogger.task[tid].info("save annotation request")
@ -276,11 +282,11 @@ def save_annotation_for_task(request, tid):
return HttpResponse() return HttpResponse()
@login_required @login_required
@permission_required(perm=['engine.view_task', 'engine.change_task'], raise_exception=True) @permission_required(perm=['engine.job.change'],
def save_job_status(request): fn=objectgetter(models.Job, 'jid'), raise_exception=True)
def save_job_status(request, jid):
try: try:
data = json.loads(request.body.decode('utf-8')) data = json.loads(request.body.decode('utf-8'))
jid = data['jid']
status = data['status'] status = data['status']
slogger.job[jid].info("changing job status request") slogger.job[jid].info("changing job status request")
task.save_job_status(jid, status, request.user.username) task.save_job_status(jid, status, request.user.username)

@ -6,7 +6,7 @@
from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest, QueryDict from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest, QueryDict
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import render 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.authentication.decorators import login_required
from cvat.apps.engine.models import Task as TaskModel from cvat.apps.engine.models import Task as TaskModel
from cvat.apps.engine import annotation, task from cvat.apps.engine import annotation, task
@ -285,14 +285,12 @@ def get_meta_info(request):
@login_required @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): def create(request, tid):
slogger.glob.info('tf annotation create request for task {}'.format(tid)) slogger.glob.info('tf annotation create request for task {}'.format(tid))
try: try:
db_task = TaskModel.objects.get(pk=tid) 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') queue = django_rq.get_queue('low')
job = queue.fetch_job('tf_annotation.create/{}'.format(tid)) job = queue.fetch_job('tf_annotation.create/{}'.format(tid))
if job is not None and (job.is_started or job.is_queued): if job is not None and (job.is_started or job.is_queued):
@ -346,7 +344,8 @@ def create(request, tid):
return HttpResponse() return HttpResponse()
@login_required @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): def check(request, tid):
try: try:
queue = django_rq.get_queue('low') queue = django_rq.get_queue('low')
@ -375,7 +374,8 @@ def check(request, tid):
@login_required @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): def cancel(request, tid):
try: try:
queue = django_rq.get_queue('low') queue = django_rq.get_queue('low')

@ -1,5 +1,5 @@
click==6.7 click==6.7
Django==2.0.9 Django==2.1.3
django-appconf==1.0.2 django-appconf==1.0.2
django-auth-ldap==1.4.0 django-auth-ldap==1.4.0
django-cacheops==4.0.6 django-cacheops==4.0.6
@ -15,12 +15,13 @@ pytz==2018.3
pyunpack==0.1.2 pyunpack==0.1.2
rcssmin==1.0.6 rcssmin==1.0.6
redis==2.10.6 redis==2.10.6
requests==2.18.4 requests==2.20.0
rjsmin==1.0.12 rjsmin==1.0.12
rq==0.10.0 rq==0.10.0
scipy==1.0.1 scipy==1.0.1
sqlparse==0.2.4 sqlparse==0.2.4
django-sendfile==0.3.11 django-sendfile==0.3.11
dj-pagination==2.3.2 dj-pagination==2.4.0
python-logstash==0.4.6 python-logstash==0.4.6
django-revproxy==0.9.15 django-revproxy==0.9.15
rules==2.0

@ -54,7 +54,8 @@ INSTALLED_APPS = [
'cacheops', 'cacheops',
'sendfile', 'sendfile',
'dj_pagination', 'dj_pagination',
'revproxy' 'revproxy',
'rules'
] ]
if 'yes' == os.environ.get('TF_ANNOTATION', 'no'): if 'yes' == os.environ.get('TF_ANNOTATION', 'no'):
@ -100,6 +101,18 @@ TEMPLATES = [
WSGI_APPLICATION = 'cvat.wsgi.application' 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 # Django-RQ
# https://github.com/rq/django-rq # https://github.com/rq/django-rq

Loading…
Cancel
Save