Commit ab53069d authored by Stevan Radakovic's avatar Stevan Radakovic
Browse files

Auth refactoring.

This includes new auth backend, model changes, database migration
and code changes to support the new backend.
parent 7c3c02c6
......@@ -107,3 +107,20 @@ class MultinodeProtocolTimeoutError(LAVAError):
"MultinodeProtocolTimeoutError: Multinode wait/sync call " "has timed out."
)
error_type = "MultinodeTimeout"
class LAVAServerError(Exception):
""" Subclass for all exceptions on LAVA server side """
error_help = ""
error_type = ""
class ObjectNotPersisted(LAVAServerError):
error_help = "ObjectNotPersisted: Object is not persisted."
error_type = "ObjectNotPersisted"
class PermissionNameError(LAVAServerError):
error_help = "PermissionNameError: Unexisting permission codename."
error_type = "Unexisting permission codename."
......@@ -23,7 +23,6 @@ import tap
from lava_scheduler_app.models import Device, DeviceType, TestJob, Worker
from lava_results_app.models import TestSuite, TestCase
from lava_scheduler_app.views import filter_device_types
from lava_scheduler_app.logutils import read_logs
from linaro_django_xmlrpc.models import AuthToken
......@@ -120,7 +119,6 @@ class LavaObtainAuthToken(ObtainAuthToken):
class TestJobSerializer(serializers.ModelSerializer):
health = serializers.CharField(source="get_health_display")
state = serializers.CharField(source="get_state_display")
visibility = serializers.CharField(source="get_visibility_display")
submitter = serializers.CharField(source="submitter.username")
class Meta:
......@@ -128,7 +126,6 @@ class TestJobSerializer(serializers.ModelSerializer):
fields = (
"id",
"submitter",
"visibility",
"viewing_groups",
"description",
"health_check",
......@@ -181,7 +178,6 @@ class TestJobViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = TestJobSerializer
filter_fields = (
"submitter",
"visibility",
"viewing_groups",
"description",
"health_check",
......@@ -352,7 +348,6 @@ class DeviceTypeSerializer(serializers.ModelSerializer):
"disable_health_check",
"health_denominator",
"display",
"owners_only",
)
......@@ -373,13 +368,11 @@ class DeviceTypeViewSet(viewsets.ReadOnlyModelViewSet):
"disable_health_check",
"health_denominator",
"display",
"owners_only",
)
filter_class = filters.DeviceTypeFilter
def get_queryset(self):
visible = filter_device_types(self.request.user)
return DeviceType.objects.filter(name__in=visible)
return DeviceType.objects.visible_by_user(self.request.user)
class DeviceSerializer(serializers.ModelSerializer):
......@@ -436,8 +429,7 @@ class DeviceViewSet(viewsets.ReadOnlyModelViewSet):
filter_class = filters.DeviceFilter
def get_queryset(self):
visible = filter_device_types(self.request.user)
query = Device.objects.filter(device_type__in=visible)
query = Device.objects.visible_by_user(self.request.user)
if not self.request.query_params.get("all", False):
query = query.exclude(health=Device.HEALTH_RETIRED)
return query
......
......@@ -203,7 +203,6 @@ class DeviceTypeFilter(filters.FilterSet):
"disable_health_check": ["exact", "in"],
"health_denominator": ["exact", "in"],
"display": ["exact", "in"],
"owners_only": ["exact", "in"],
"core_count": ["exact", "in"],
}
......@@ -283,7 +282,6 @@ class TestJobFilter(filters.FilterSet):
)
health = ChoiceFilter(choices=TestJob.HEALTH_CHOICES)
state = ChoiceFilter(choices=TestJob.STATE_CHOICES)
visibility = ChoiceFilter(choices=TestJob.VISIBLE_CHOICES)
class Meta:
model = TestJob
......@@ -329,5 +327,4 @@ class TestJobFilter(filters.FilterSet):
"endswith",
"isnull",
],
"visibility": ["exact", "in"],
}
......@@ -96,9 +96,6 @@ class TestRestApi:
self.invisible_device_type1 = DeviceType.objects.create(
name="invisible_device_type1", display=False
)
self.private_device_type1 = DeviceType.objects.create(
name="private_device_type1", owners_only=True
)
# create devices
self.public_device1 = Device.objects.create(
......@@ -106,13 +103,6 @@ class TestRestApi:
device_type=self.public_device_type1,
worker_host=self.worker1,
)
self.private_device1 = Device.objects.create(
hostname="private01",
user=self.admin,
is_public=False,
device_type=self.private_device_type1,
worker_host=self.worker1,
)
self.retired_device1 = Device.objects.create(
hostname="retired01",
device_type=self.public_device_type1,
......@@ -124,18 +114,12 @@ class TestRestApi:
self.public_testjob1 = TestJob.objects.create(
definition=yaml.safe_dump(EXAMPLE_JOB),
submitter=self.user,
user=self.user,
requested_device_type=self.public_device_type1,
is_public=True,
visibility=TestJob.VISIBLE_PUBLIC,
)
self.private_testjob1 = TestJob.objects.create(
definition=yaml.safe_dump(EXAMPLE_JOB),
submitter=self.admin,
user=self.admin,
requested_device_type=self.public_device_type1,
is_public=False,
visibility=TestJob.VISIBLE_PERSONAL,
)
# create logs
......@@ -210,8 +194,7 @@ class TestRestApi:
data = self.hit(
self.userclient, reverse("api-root", args=[self.version]) + "jobs/"
)
# only public test jobs should be available without logging in
assert len(data["results"]) == 1 # nosec - unit test support
assert len(data["results"]) == 2 # nosec - unit test support
def test_testjobs_admin(self):
data = self.hit(
......@@ -361,14 +344,12 @@ ok 2 - bar
data = self.hit(
self.userclient, reverse("api-root", args=[self.version]) + "devicetypes/"
)
# only public device types should be available without logging in
assert len(data["results"]) == 1 # nosec - unit test support
assert len(data["results"]) == 2 # nosec - unit test support
def test_devices(self):
data = self.hit(
self.userclient, reverse("api-root", args=[self.version]) + "devices/"
)
# only public devices should be available without logging in
assert len(data["results"]) == 1 # nosec - unit test support
def test_devicetypes_admin(self):
......@@ -381,7 +362,7 @@ ok 2 - bar
data = self.hit(
self.adminclient, reverse("api-root", args=[self.version]) + "devices/"
)
assert len(data["results"]) == 2 # nosec - unit test support
assert len(data["results"]) == 1 # nosec - unit test support
def test_workers(self):
data = self.hit(
......
......@@ -1161,8 +1161,6 @@ class QueryCondition(models.Model):
"actual_device",
"requested_device_type",
"health_check",
"user",
"group",
"priority",
"description",
],
......
......@@ -56,25 +56,20 @@ class ModelFactory:
def make_device_type(self, name="qemu"):
return DeviceType.objects.get_or_create(name=name)[0]
def make_device(
self, device_type=None, hostname=None, tags=None, is_public=True, **kw
):
def make_device(self, device_type=None, hostname=None, tags=None, **kw):
if device_type is None:
device_type = self.make_device_type()
if hostname is None:
hostname = self.getUniqueString()
if tags and type(tags) != list:
tags = []
device = Device(
device_type=device_type, is_public=is_public, hostname=hostname, **kw
)
device = Device(device_type=device_type, hostname=hostname, **kw)
if tags:
device.tags = tags
logging.debug(
"making a device of type %s %s %s with tags '%s'"
"making a device of type %s %s with tags '%s'"
% (
device_type,
device.is_public,
device.hostname,
", ".join([x.name for x in device.tags.all()]),
)
......
......@@ -21,9 +21,11 @@
import os
import yaml
import logging
from django.db import DataError
from django.utils.translation import ungettext_lazy
from django.core.exceptions import PermissionDenied
from django.utils.translation import ungettext_lazy
from linaro_django_xmlrpc.models import AuthToken
......
......@@ -22,6 +22,7 @@ from django import forms
from django.core.exceptions import ValidationError
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import Permission
from django.db import transaction
from django.conf import settings
......@@ -30,9 +31,10 @@ from lava_scheduler_app.models import (
Alias,
BitWidth,
Core,
DefaultDeviceOwner,
Device,
DeviceType,
GroupDeviceTypePermission,
GroupDevicePermission,
JobFailureTag,
NotificationRecipient,
ProcessorFamily,
......@@ -47,6 +49,29 @@ from linaro_django_xmlrpc.models import AuthToken
# pylint: disable=no-self-use,function-redefined
class GroupObjectPermissionInline(admin.TabularInline):
extra = 0
def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
if db_field.name == "permission":
kwargs["queryset"] = Permission.objects.filter(
content_type__model=self.parent_model._meta.object_name.lower()
)
return super(GroupObjectPermissionInline, self).formfield_for_foreignkey(
db_field, request, **kwargs
)
class GroupDeviceTypePermissionInline(GroupObjectPermissionInline):
model = GroupDeviceTypePermission
extra = 0
class GroupDevicePermissionInline(GroupObjectPermissionInline):
model = GroupDevicePermission
extra = 0
class AliasAdmin(admin.ModelAdmin):
def get_readonly_fields(self, _, obj=None):
if obj: # editing an existing object
......@@ -77,16 +102,6 @@ class CoreAdmin(admin.ModelAdmin):
return self.readonly_fields
class DefaultOwnerInline(admin.StackedInline):
"""
Exposes the default owner override class
in the Django admin interface
"""
model = DefaultDeviceOwner
can_delete = False
def expire_user_action(
modeladmin, request, queryset
): # pylint: disable=unused-argument
......@@ -106,11 +121,7 @@ expire_user_action.short_description = "Expire user account"
class CustomUserAdmin(UserAdmin):
"""
Defines the override class for DefaultOwnerInline
"""
inlines = (DefaultOwnerInline,)
actions = [expire_user_action]
def has_delete_permission(self, request, obj=None):
......@@ -300,16 +311,7 @@ class DeviceAdmin(admin.ModelAdmin):
"Properties",
{"fields": ("hostname", "device_type", "worker_host", "device_version")},
),
(
"Device owner",
{
"fields": (
("user", "group"),
("physical_owner", "physical_group"),
"is_public",
)
},
),
("Device owner", {"fields": (("physical_owner", "physical_group"),)}),
(
"Status",
{
......@@ -337,7 +339,6 @@ class DeviceAdmin(admin.ModelAdmin):
"health",
"has_health_check",
"health_check_enabled",
"is_public",
"valid_device",
)
search_fields = ("hostname", "device_type__name")
......@@ -348,28 +349,28 @@ class DeviceAdmin(admin.ModelAdmin):
device_health_maintenance,
device_health_retired,
]
inlines = [GroupDevicePermissionInline]
class VisibilityForm(forms.ModelForm):
def clean_viewing_groups(self):
viewing_groups = self.cleaned_data["viewing_groups"]
visibility = self.cleaned_data["visibility"]
if len(viewing_groups) != 1 and visibility == TestJob.VISIBLE_GROUP:
raise ValidationError(
"Group visibility must have exactly one viewing group."
)
elif viewing_groups and visibility == TestJob.VISIBLE_PERSONAL:
raise ValidationError(
"Personal visibility cannot have any viewing groups assigned."
)
elif viewing_groups and visibility == TestJob.VISIBLE_PUBLIC:
is_public = self.cleaned_data["is_public"]
if viewing_groups and is_public:
raise ValidationError(
"Pulibc visibility cannot have any viewing groups assigned."
"Public test jobs cannot have any viewing groups assigned."
)
return self.cleaned_data["viewing_groups"]
class TestJobAdmin(admin.ModelAdmin):
def get_queryset(self, request):
return (
super(TestJobAdmin, self)
.get_queryset(request)
.select_related("requested_device_type", "actual_device", "submitter")
)
def requested_device_type_name(self, obj):
return "" if obj.requested_device_type is None else obj.requested_device_type
......@@ -384,19 +385,7 @@ class TestJobAdmin(admin.ModelAdmin):
actions = [cancel_action, fail_action]
list_filter = ("state", RequestedDeviceTypeFilter, ActualDeviceFilter)
fieldsets = (
(
"Owner",
{
"fields": (
"user",
"group",
"submitter",
"is_public",
"visibility",
"viewing_groups",
)
},
),
("Owner", {"fields": ("submitter", "viewing_groups")}),
("Request", {"fields": ("requested_device_type", "priority", "health_check")}),
(
"Advanced properties",
......@@ -492,7 +481,6 @@ class DeviceTypeAdmin(admin.ModelAdmin):
list_display = (
"name",
"display",
"owners_only",
"health_check_enabled",
"health_check_frequency",
"architecture_name",
......@@ -502,7 +490,7 @@ class DeviceTypeAdmin(admin.ModelAdmin):
"bit_count",
)
fieldsets = (
("Properties", {"fields": ("name", "description", "display", "owners_only")}),
("Properties", {"fields": ("name", "description", "display")}),
(
"Health checks",
{
......@@ -526,6 +514,7 @@ class DeviceTypeAdmin(admin.ModelAdmin):
),
)
ordering = ["name"]
inlines = [GroupDeviceTypePermissionInline]
def worker_health_active(ModelAdmin, request, queryset):
......
......@@ -114,11 +114,11 @@ class SchedulerAPI(ExposedAPI):
job IDs.
"""
self._authenticate()
if not self.user.has_perm("lava_scheduler_app.add_testjob"):
if not self.user.has_perm("lava_scheduler_app.submit_testjob"):
raise xmlrpc.client.Fault(
403,
"Permission denied. User %r does not have the "
"'lava_scheduler_app.add_testjob' permission. Contact "
"'lava_scheduler_app.submit_testjob' permission. Contact "
"the administrators." % self.user.username,
)
try:
......@@ -159,11 +159,11 @@ class SchedulerAPI(ExposedAPI):
token.
"""
self._authenticate()
if not self.user.has_perm("lava_scheduler_app.add_testjob"):
if not self.user.has_perm("lava_scheduler_app.submit_testjob"):
raise xmlrpc.client.Fault(
403,
"Permission denied. User %r does not have the "
"'lava_scheduler_app.add_testjob' permission. Contact "
"'lava_scheduler_app.submit_testjob' permission. Contact "
"the administrators." % self.user.username,
)
try:
......@@ -330,11 +330,9 @@ class SchedulerAPI(ExposedAPI):
[['panda01', 'panda', 'running', 'good', 164, False], ['qemu01', 'qemu', 'idle', 'unknwon', None, True]]
"""
devices_list = []
for dev in Device.objects.exclude(health=Device.HEALTH_RETIRED):
if not dev.is_visible_to(self.user):
continue
devices_list.append(dev)
devices_list = Device.objects.visible_by_user(self.user).exclude(
health=Device.HEALTH_RETIRED
)
return [
[
......@@ -372,16 +370,10 @@ class SchedulerAPI(ExposedAPI):
{'idle': 1, 'busy': 0, 'name': 'qemu', 'offline': 0}]
"""
device_type_names = []
all_device_types = []
keys = ["busy", "idle", "offline"]
for dev_type in DeviceType.objects.all():
if not dev_type.some_devices_visible_to(self.user):
continue
device_type_names.append(dev_type.name)
device_types = device_type_summary(device_type_names)
device_types = device_type_summary(self.user)
for dev_type in device_types:
device_type = {"name": dev_type["device_type"]}
......@@ -450,15 +442,10 @@ class SchedulerAPI(ExposedAPI):
404, "DeviceType '%s' was not found." % device_type
)
if not dt.some_devices_visible_to(self.user):
raise xmlrpc.client.Fault(
403,
"DeviceType '%s' not available to user '%s'."
% (device_type, self.user),
)
job_qs = (
TestJob.objects.filter(state=TestJob.STATE_FINISHED)
.filter(requested_device_type=dt)
.visible_by_user(self.user)
.order_by("-id")
)
if restrict_to_user:
......@@ -474,8 +461,6 @@ class SchedulerAPI(ExposedAPI):
"status": job.get_legacy_status_display(),
"device": hostname,
}
if not job.can_view(self.user):
job_dict["id"] = None
job_list.append(job_dict)
return job_list
......@@ -530,13 +515,14 @@ class SchedulerAPI(ExposedAPI):
except Device.DoesNotExist:
raise xmlrpc.client.Fault(404, "Device '%s' was not found." % device)
if not device_obj.is_visible_to(self.user):
if not device_obj.can_view(self.user):
raise xmlrpc.client.Fault(
403, "Device '%s' not available to user '%s'." % (device, self.user)
)
job_qs = (
TestJob.objects.filter(state=TestJob.STATE_FINISHED)
.filter(actual_device=device_obj)
.visible_by_user(self.user)
.order_by("-id")
)
if restrict_to_user:
......@@ -548,8 +534,6 @@ class SchedulerAPI(ExposedAPI):
"description": job.description,
"status": job.get_legacy_status_display(),
}
if not job.can_view(self.user):
job_dict["id"] = None
job_list.append(job_dict)
return job_list
......@@ -588,7 +572,9 @@ class SchedulerAPI(ExposedAPI):
{'ompa4-panda': ['panda', 'panda-es']}
"""
aliases = DeviceType.objects.filter(aliases__name__contains=alias)
aliases = DeviceType.objects.filter(
aliases__name__contains=alias
).visible_by_user(self.user)
return {alias: [device_type.name for device_type in aliases]}
def get_device_status(self, hostname):
......@@ -628,7 +614,7 @@ class SchedulerAPI(ExposedAPI):
raise xmlrpc.client.Fault(404, "Device '%s' was not found." % hostname)
device_dict = {}
if device.is_visible_to(self.user):
if device.can_view(self.user):
device_dict["hostname"] = device.hostname
device_dict["status"] = build_device_status_display(
device.state, device.health
......@@ -767,12 +753,10 @@ class SchedulerAPI(ExposedAPI):
pending_jobs_by_device = {}
jobs_res = TestJob.objects.filter(state=TestJob.STATE_SUBMITTED)
jobs_res = TestJob.objects.filter(
state=TestJob.STATE_SUBMITTED
).visible_by_user(self.user)
jobs_res = jobs_res.exclude(requested_device_type_id__isnull=True)
if not self.user or not self.user.is_superuser:
jobs_res = jobs_res.filter(is_public=True)
jobs_res = jobs_res.values_list("requested_device_type_id")
jobs_res = jobs_res.annotate(pending_jobs=(Count("id")))
......@@ -788,9 +772,7 @@ class SchedulerAPI(ExposedAPI):
else:
device_types = active_device_types()
if not self.user or not self.user.is_superuser:
device_types = device_types.filter(owners_only=False)
device_types = device_types.visible_by_user(self.user)
for device_type in device_types.values_list("name", flat=True):
if device_type not in pending_jobs_by_device:
pending_jobs_by_device[device_type] = 0
......@@ -819,12 +801,11 @@ class SchedulerAPI(ExposedAPI):
The elements available in XML-RPC structure include:
_state, submitter_id, is_pipeline, id, failure_comment,
multinode_definition, user_id, priority, _actual_device_cache,
multinode_definition, priority, _actual_device_cache,
original_definition, status, health_check, description,
start_time, target_group, visibility, pipeline_compatibility,
submit_time, is_public, _old_status, actual_device_id, definition,
sub_id, requested_device_type_id, end_time, group_id, absolute_url,
submitter_username
start_time, target_group, pipeline_compatibility, submit_time,
is_public, _old_status, actual_device_id, definition, sub_id,
requested_device_type_id, end_time, absolute_url, submitter_username
"""
self._authenticate()
if not job_id:
......@@ -942,21 +923,12 @@ class SchedulerAPI(ExposedAPI):
raise xmlrpc.client.Fault(
400, "Bad request: needs to be a list of integers or floats"
)
jobs = TestJob.objects.filter(
Q(id__in=job_id_list) | Q(sub_id__in=job_id_list)
).select_related("actual_device", "requested_device_type")
jobs = (
TestJob.objects.filter(Q(id__in=job_id_list) | Q(sub_id__in=job_id_list))
.visible_by_user(self.user)
.select_related("actual_device", "requested_device_type")
)
for job in jobs:
device_type = job.requested_device_type
if (
not job.can_view(self.user)
or not job.is_accessible_by(self.user)
and not self.user.is_superuser
):
continue
if device_type and device_type.owners_only:
# do the more expensive check second and only for a hidden device type