...
 
Commits (11)
......@@ -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,7 +194,6 @@ 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
def test_testjobs_admin(self):
......@@ -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
......@@ -121,21 +123,16 @@ def anonymous_token(request, job):
token = querydict.get("token", default=None)
# safe to call with (None, None) - returns None
auth_user = AuthToken.get_user_for_secret(username=user, secret=token)
if not user and not job.is_public:
raise PermissionDenied()
if not auth_user:
raise PermissionDenied()
return auth_user
def check_request_auth(request, job):
if job.is_public:
return
if not request.user.is_authenticated:
# handle anonymous access
auth_user = anonymous_token(request, job)
if not auth_user or not job.can_view(auth_user):
raise PermissionDenied()
if not job.can_view(request.user):
# handle anonymous access
auth_user = anonymous_token(request, job)
if not auth_user or not job.can_view(auth_user):
raise PermissionDenied()
elif not job.can_view(request.user):
raise PermissionDenied()
......
......@@ -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):
......@@ -266,6 +277,13 @@ class DeviceAdmin(admin.ModelAdmin):
)
raw_id_fields = ["last_health_report_job"]
def get_queryset(self, request):
return (
super(DeviceAdmin, self)
.get_queryset(request)
.select_related("worker_host", "device_type")
)
def has_health_check(self, obj):
return bool(obj.get_health_check())
......@@ -300,16 +318,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 +346,6 @@ class DeviceAdmin(admin.ModelAdmin):
"health",
"has_health_check",
"health_check_enabled",
"is_public",
"valid_device",
)
search_fields = ("hostname", "device_type__name")
......@@ -348,28 +356,17 @@ class DeviceAdmin(admin.ModelAdmin):
device_health_maintenance,
device_health_retired,
]
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:
raise ValidationError(
"Pulibc visibility cannot have any viewing groups assigned."
)
return self.cleaned_data["viewing_groups"]
inlines = [GroupDevicePermissionInline]
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
......@@ -380,23 +377,10 @@ class TestJobAdmin(admin.ModelAdmin):
return settings.ALLOW_ADMIN_DELETE
requested_device_type_name.short_description = "Request device type"
form = VisibilityForm
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", "is_public")}),
("Request", {"fields": ("requested_device_type", "priority", "health_check")}),
(
"Advanced properties",
......@@ -431,6 +415,13 @@ disable_health_check_action.short_description = "disable health checks"
class DeviceTypeAdmin(admin.ModelAdmin):
def get_queryset(self, request):
return (
super(DeviceTypeAdmin, self)
.get_queryset(request)
.select_related("architecture", "bits", "processor")
)
def architecture_name(self, obj):
if obj.architecture:
return obj.architecture
......@@ -492,7 +483,6 @@ class DeviceTypeAdmin(admin.ModelAdmin):
list_display = (
"name",
"display",
"owners_only",
"health_check_enabled",
"health_check_frequency",
"architecture_name",
......@@ -502,7 +492,7 @@ class DeviceTypeAdmin(admin.ModelAdmin):
"bit_count",
)
fieldsets = (
("Properties", {"fields": ("name", "description", "display", "owners_only")}),
("Properties", {"fields": ("name", "description", "display")}),
(
"Health checks",
{
......@@ -526,6 +516,7 @@ class DeviceTypeAdmin(admin.ModelAdmin):
),
)
ordering = ["name"]
inlines = [GroupDeviceTypePermissionInline]
def worker_health_active(ModelAdmin, request, queryset):
......
This diff is collapsed.
......@@ -52,7 +52,7 @@ class SchedulerAliasesAPI(ExposedV2API):
"""
try:
dt = DeviceType.objects.get(name=device_type_name)
if not dt.some_devices_visible_to(self.user):
if not self.user.has_perm(DeviceType.VIEW_PERMISSION, dt):
raise xmlrpc.client.Fault(
404, "Device-type '%s' was not found." % device_type_name
)
......@@ -142,7 +142,7 @@ class SchedulerAliasesAPI(ExposedV2API):
raise xmlrpc.client.Fault(404, "Alias '%s' was not found." % name)
dt = alias.device_type
if dt is None or (dt.owners_only and dt.some_devices_visible_to(self.user)):
if dt is None or not self.user.has_perm(DeviceType.VIEW_PERMISSION, dt):
return {"name": alias.name, "device_type": ""}
return {"name": alias.name, "device_type": alias.device_type.name}
......@@ -43,20 +43,12 @@ class SchedulerDeviceTypesAPI(ExposedV2API):
available_types.sort()
return available_types
@check_perm("lava_scheduler_app.add_devicetype")
def add(
self,
name,
description,
display,
owners_only,
health_frequency,
health_denominator,
):
@check_perm("lava_scheduler_app.admin_devicetype")
def add(self, name, description, display, health_frequency, health_denominator):
"""
Name
----
`scheduler.device_types.add` (`name`, `description`, `display`, `owners_only`,
`scheduler.device_types.add` (`name`, `description`, `display`,
`health_frequency`, health_denominator`)
Description
......@@ -73,8 +65,6 @@ class SchedulerDeviceTypesAPI(ExposedV2API):
Device-type description
`display`: bool
Is the device-type displayed in the GUI?
`owners_only`: bool
Is the device-type only available to owners?
`health_frequency`: int
How often to run health checks
`health_denominator`: string ("hours" or "jobs")
......@@ -96,7 +86,6 @@ class SchedulerDeviceTypesAPI(ExposedV2API):
name=name,
description=description,
display=display,
owners_only=owners_only,
health_frequency=health_frequency,
health_denominator=health_denominator,
)
......@@ -134,7 +123,7 @@ class SchedulerDeviceTypesAPI(ExposedV2API):
"""
with contextlib.suppress(DeviceType.DoesNotExist):
dt = DeviceType.objects.get(name=name)
if dt.owners_only and not dt.some_devices_visible_to(self.user):
if not dt.can_view(self.user):
raise xmlrpc.client.Fault(404, "Device-type '%s' was not found." % name)
# Filename should not be a path or starting with a dot
......@@ -182,7 +171,7 @@ class SchedulerDeviceTypesAPI(ExposedV2API):
"""
with contextlib.suppress(DeviceType.DoesNotExist):
dt = DeviceType.objects.get(name=name)
if dt.owners_only and not dt.some_devices_visible_to(self.user):
if not dt.can_view(self.user):
raise xmlrpc.client.Fault(404, "Device-type '%s' was not found." % name)
# Filename should not be a path or starting with a dot
......@@ -203,7 +192,7 @@ class SchedulerDeviceTypesAPI(ExposedV2API):
400, "Unable to read device-type configuration: %s" % exc.strerror
)
@check_perm("lava_scheduler_app.change_devicetype")
@check_perm("lava_scheduler_app.admin_devicetype")
def set_health_check(self, name, config):
"""
Name
......@@ -248,7 +237,7 @@ class SchedulerDeviceTypesAPI(ExposedV2API):
400, "Unable to write health-check: %s" % exc.strerror
)
@check_perm("lava_scheduler_app.change_devicetype")
@check_perm("lava_scheduler_app.admin_devicetype")
def set_template(self, name, config):
"""
Name
......@@ -314,10 +303,12 @@ class SchedulerDeviceTypesAPI(ExposedV2API):
This function returns an XML-RPC array of device-types
"""
available_types = self._available_device_types()
device_types = [
dt
for dt in DeviceType.objects.all().order_by("name")
if not dt.owners_only or dt.some_devices_visible_to(self.user)
for dt in DeviceType.objects.all()
.visible_by_user(self.user)
.order_by("name")
]
ret = []
for dt in device_types:
......@@ -367,19 +358,17 @@ class SchedulerDeviceTypesAPI(ExposedV2API):
dt = DeviceType.objects.get(name=name)
except DeviceType.DoesNotExist:
raise xmlrpc.client.Fault(404, "Device-type '%s' was not found." % name)
if dt.owners_only and not dt.some_devices_visible_to(self.user):
if not dt.can_view(self.user):
raise xmlrpc.client.Fault(404, "Device-type '%s' was not found." % name)
aliases = [str(alias.name) for alias in dt.aliases.all()]
devices = [
str(d.hostname) for d in dt.device_set.all() if d.is_visible_to(self.user)
str(d.hostname) for d in dt.device_set.all() if d.can_view(self.user)
]
dt_dict = {
"name": dt.name,
"description": dt.description,
"display": dt.display,
"owners_only": dt.owners_only,
"health_disabled": dt.disable_health_check,
"aliases": aliases,
"devices": devices,
......@@ -387,13 +376,11 @@ class SchedulerDeviceTypesAPI(ExposedV2API):
return dt_dict
@check_perm("lava_scheduler_app.change_devicetype")
def update(
self,
name,
description,
display,
owners_only,
health_frequency,
health_denominator,
health_disabled,
......@@ -402,7 +389,7 @@ class SchedulerDeviceTypesAPI(ExposedV2API):
Name
----
`scheduler.device_types.update` (`name`, `description=None`,
`display=None`, `owners_only=None`,
`display=None`,
`health_frequency=None`,
`health_denominator=None`,
`health_disabled=None`)
......@@ -420,8 +407,6 @@ class SchedulerDeviceTypesAPI(ExposedV2API):
Device-type description
`display`: bool
Is the device-type displayed in the GUI?
`owners_only`: bool
Hide this device type for all users except owners of devices of this type.
`health_frequency`: int
How often to run health checks
`health_denominator`: string ("hours" or "jobs")
......@@ -437,6 +422,10 @@ class SchedulerDeviceTypesAPI(ExposedV2API):
dt = DeviceType.objects.get(name=name)
except DeviceType.DoesNotExist:
raise xmlrpc.client.Fault(404, "Device-type '%s' was not found." % name)
if not dt.can_admin(self.user):
raise xmlrpc.client.Fault(
403, "No 'admin' permissions for device-type '%s'." % name
)
if description is not None:
dt.description = description
......@@ -444,9 +433,6 @@ class SchedulerDeviceTypesAPI(ExposedV2API):
if display is not None:
dt.display = display
if owners_only is not None:
dt.owners_only = owners_only
if health_frequency is not None:
dt.health_frequency = health_frequency
......@@ -470,7 +456,6 @@ class SchedulerDeviceTypesAPI(ExposedV2API):
class SchedulerDeviceTypesAliasesAPI(ExposedV2API):
@check_perm("lava_scheduler_app.add_alias")
@check_perm("lava_scheduler_app.change_devicetype")
def add(self, name, alias):
"""
Name
......@@ -497,6 +482,10 @@ class SchedulerDeviceTypesAliasesAPI(ExposedV2API):
dt = DeviceType.objects.get(name=name)
except DeviceType.DoesNotExist:
raise xmlrpc.client.Fault(404, "Device-type '%s' was not found." % name)
if not dt.can_admin(self.user):
raise xmlrpc.client.Fault(
403, "No 'admin' permissions for device-type '%s'." % name
)
alias_obj, _ = Alias.objects.get_or_create(name=alias)
dt.aliases.add(alias_obj)
......@@ -524,13 +513,10 @@ class SchedulerDeviceTypesAliasesAPI(ExposedV2API):
dt = DeviceType.objects.get(name=name)
except DeviceType.DoesNotExist:
raise xmlrpc.client.Fault(404, "Device-type '%s' was not found." % name)
if dt.owners_only and not dt.some_devices_visible_to(self.user):
if not dt.can_view(self.user):
raise xmlrpc.client.Fault(404, "Device-type '%s' was not found." % name)
return [a.name for a in dt.aliases.all().order_by("name")]
@check_perm("lava_scheduler_app.change_devicetype")
def delete(self, name, alias):
"""
Name
......@@ -556,6 +542,10 @@ class SchedulerDeviceTypesAliasesAPI(ExposedV2API):
dt = DeviceType.objects.get(name=name)
except DeviceType.DoesNotExist:
raise xmlrpc.client.Fault(404, "Device-type '%s' was not found." % name)
if not dt.can_admin(self.user):
raise xmlrpc.client.Fault(
403, "No 'admin' permissions for device-type '%s'." % name
)
try:
alias_obj = Alias.objects.get(name=alias)
......
This diff is collapsed.
......@@ -195,7 +195,7 @@ class SchedulerJobsAPI(ExposedV2API):
ret = []
start = max(0, start)
limit = min(limit, 100)
jobs = TestJob.objects.all().select_related(
jobs = TestJob.objects.visible_by_user(self.user).select_related(
"requested_device_type", "submitter"
)
if state:
......@@ -298,8 +298,10 @@ class SchedulerJobsAPI(ExposedV2API):
ret = []
start = max(0, start)
limit = min(limit, 100)
jobs = TestJob.objects.filter(state=TestJob.STATE_SUBMITTED).select_related(
"requested_device_type", "submitter"
jobs = (
TestJob.objects.filter(state=TestJob.STATE_SUBMITTED)
.visible_by_user(self.user)
.select_related("requested_device_type", "submitter")
)
if device_types is not None:
jobs = jobs.filter(requested_device_type__name__in=device_types)
......@@ -408,7 +410,6 @@ class SchedulerJobsAPI(ExposedV2API):
"start_time": job.start_time,
"end_time": job.end_time,
"tags": [t.name for t in job.tags.all()],
"visibility": job.get_visibility_display(),
"failure_comment": job.failure_comment,
}
......
......@@ -127,7 +127,5 @@ class SchedulerTagsAPI(ExposedV2API):
except Tag.DoesNotExist:
raise xmlrpc.client.Fault(404, "Tag '%s' was not found." % name)
devices = [
d.hostname for d in tag.device_set.all() if d.is_visible_to(self.user)
]
devices = [d.hostname for d in tag.device_set.all() if d.can_view(self.user)]
return {"name": name, "description": tag.description, "devices": devices}
# -*- coding: utf-8 -*-
# Copyright (C) 2019 Linaro Limited
#
# Author: Stevan Radakovic <stevan.radakovic@linaro.org>
#
# This file is part of LAVA.
#
# LAVA is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License version 3
# as published by the Free Software Foundation
#
# LAVA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with LAVA. If not, see <http://www.gnu.org/licenses/>.
from itertools import chain
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
class PermissionAuth:
def __init__(self, user):
self.user = user
def has_perm(self, perm, obj):
"""
Checks if user has given permission for object.
Checks for unsupported models will return False.
:param perm: permission as string, must contain app_label
:param obj: Django model instance for which permission should be checked
"""
# Handle anonymous users.
if not self.user.is_authenticated:
# Anonymous users can only have view permission and only when
# the object does not have any view permission restrictions.
if perm == obj.VIEW_PERMISSION:
return not obj.has_any_permission_restrictions(obj.VIEW_PERMISSION)
else:
return False
# Handle inactive and super users.
if not self.user.is_active:
return False
if self.user.is_superuser:
return True
_, perm = perm.split(".", 1)
return perm in self.get_perms(obj)
def get_group_perms(self, obj):
content_type = ContentType.objects.get_for_model(obj)
perms_queryset = Permission.objects.filter(
content_type=ContentType.objects.get_for_model(obj)
)
fieldname = "group%spermission__group__user" % content_type.model
filters = {fieldname: self.user}
filters[
"group%spermission__%s" % (content_type.model, content_type.model)
] = obj
perms_queryset = perms_queryset.filter(**filters)
perms = set(perms_queryset.values_list("codename", flat=True))
# Add lower priority permissions the resulting set.
for perm in perms.copy():
for idx, lower_perm in enumerate(obj.PERMISSIONS_PRIORITY):
if idx > obj.PERMISSIONS_PRIORITY.index(
"%s.%s" % (content_type.app_label, perm)
):
perms.add(lower_perm.split(".", 1)[-1])
return perms
def get_perms(self, obj):
"""
Returns list of codenames of all permissions for given object.
:param obj: Django model instance for which permission should be checked
"""
if not self.user.is_active:
return []
content_type = ContentType.objects.get_for_model(obj)
if self.user.is_superuser:
perms = set(
chain(
*Permission.objects.filter(content_type=content_type).values_list(
"codename"
)
)
)
else:
perms = set(self.get_group_perms(obj))
return perms
......@@ -159,12 +159,13 @@ def parse_job_description(job):
job.save(update_fields=["pipeline_compatibility"])
def device_type_summary(visible=None):
def device_type_summary(user):
devices = (
Device.objects.filter(
~Q(health=Device.HEALTH_RETIRED) & Q(device_type__in=visible)
~Q(health=Device.HEALTH_RETIRED),
Q(device_type__in=DeviceType.objects.visible_by_user(user)),
)
.only("state", "health", "is_public", "device_type", "hostname")
.only("state", "health", "device_type", "hostname")
.values("device_type")
.annotate(
idle=Sum(
......@@ -202,13 +203,6 @@ def device_type_summary(visible=None):
output_field=IntegerField(),
)
),
restricted=Sum(
Case(
When(is_public=False, then=1),
default=0,
output_field=IntegerField(),
)
),
)
.order_by("device_type")
)
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -209,7 +209,7 @@ def schedule_jobs_for_device_type(logger, dt, available_devices):
# Add a random sort: with N devices and num(jobs) < N, if we don't sort
# randomly, the same devices will always be used while the others will
# never be used.
devices = devices.order_by("is_public", "?")
devices = devices.order_by("?")
jobs = []
for device in devices:
......
......@@ -164,7 +164,6 @@ def testjob_post_handler(sender, **kwargs):
"priority": instance.priority,
"submit_time": instance.submit_time.isoformat(),
"submitter": str(instance.submitter),
"visibility": instance.get_visibility_display(),
"health_check": instance.health_check,
}
if instance.is_multinode:
......
......@@ -113,43 +113,8 @@ class ExpandedStatusColumn(tables.Column):
return record.get_simple_state_display()
class RestrictedDeviceColumn(tables.Column):
def __init__(self, verbose_name="Submissions restricted to", **kw):
kw["verbose_name"] = verbose_name
super().__init__(**kw)
def render(self, record):
"""
If the strings here are changed, ensure the strings in the restriction_query
are changed to match.
:param record: a database record
:return: a text string describing the restrictions on this device.
"""
label = None
if record.health == Device.HEALTH_BAD:
return "Health check failed: no test jobs will be scheduled."
if record.health == Device.HEALTH_MAINTENANCE:
return "No test jobs will be scheduled."
if record.health == Device.HEALTH_RETIRED:
return "Retired: no submissions possible."
if record.is_public:
return ""
if record.user:
label = record.user.email
if record.group:
label = "group %s" % record.group
return label
def all_jobs_with_custom_sort():
jobs = TestJob.objects.select_related(
"actual_device",
"actual_device__user",
"actual_device__group",
"submitter",
"user",
"group",
).all()
def visible_jobs_with_custom_sort(user):
jobs = TestJob.objects.visible_by_user(user)
return jobs.order_by("-submit_time")
......@@ -276,8 +241,6 @@ class JobTable(LavaTable):
device_type = record.requested_device_type
if not device_type:
return "Error"
if not device_type.some_devices_visible_to(self.context.get("request").user):
return "Unavailable"
return retval
def render_description(self, value): # pylint: disable=no-self-use
......@@ -293,8 +256,6 @@ class JobTable(LavaTable):
# alternatively, use 'fields' value to include specific fields.
exclude = [
"is_public",
"user",
"group",
"sub_id",
"target_group",
"health_check",
......@@ -651,9 +612,6 @@ class DeviceTypeTable(LavaTable):
def render_busy(self, record): # pylint: disable=no-self-use
return record["busy"] if record["busy"] > 0 else ""
def render_restricted(self, record): # pylint: disable=no-self-use
return record["restricted"] if record["restricted"] > 0 else ""
def render_name(self, record): # pylint: disable=no-self-use
return pklink(DeviceType.objects.get(name=record["device_type"]))
......@@ -670,7 +628,6 @@ class DeviceTypeTable(LavaTable):
idle = tables.Column()
offline = tables.Column()
busy = tables.Column()
restricted = tables.Column()
# sadly, this needs to be not orderable as it would otherwise sort by the accessor.
queue = tables.Column(accessor="idle", verbose_name="Queue", orderable=False)
......@@ -681,7 +638,6 @@ class DeviceTypeTable(LavaTable):
exclude = [
"display",
"disable_health_check",
"owners_only",
"architecture",
"health_denominator",
"health_frequency",
......@@ -761,8 +717,6 @@ class DeviceTable(LavaTable):
)
device_type = tables.Column()
state = ExpandedStatusColumn("state")
owner = RestrictedDeviceColumn()
owner.orderable = False
health = tables.Column(verbose_name="Health")
tags = TagsColumn()
......@@ -771,9 +725,6 @@ class DeviceTable(LavaTable):
): # pylint: disable=too-few-public-methods,no-init,no-self-use
model = Device
exclude = [
"user",
"group",
"is_public",
"device_version",
"physical_owner",
"physical_group",
......@@ -781,20 +732,12 @@ class DeviceTable(LavaTable):
"current_job",
"last_health_report_job",
]
sequence = [
"hostname",
"worker_host",
"device_type",
"state",
"health",
"owner",
]
sequence = ["hostname", "worker_host", "device_type", "state", "health"]
searches = {"hostname": "contains"}
queries = {
"device_type_query": "device_type",
"device_state_query": "state",
"device_health_query": "health",
"restriction_query": "restrictions",
"tags_query": "tags",
}
......@@ -896,9 +839,6 @@ class NoWorkerDeviceTable(DeviceTable):
): # pylint: disable=too-few-public-methods,no-init,no-self-use
exclude = [
"worker_host",
"user",
"group",
"is_public",
"device_version",
"physical_owner",
"physical_group",
......@@ -1057,7 +997,6 @@ class RunningTable(LavaTable):
exclude = [
"display",
"disable_health_check",
"owners_only",
"architecture",
"processor",
"cpu_model",
......
......@@ -20,13 +20,6 @@
</div>
{% endif %}
{% if device.device_type.owners_only %}
<div class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<strong><i>{{ device.device_type.name }}</i> is a hidden device type:</strong> Only owners of one or more devices of that type can see this page.
</div>
{% endif %}
{% if user.is_staff and not device.get_health_check and not device.device_type.disable_health_check and device.health != device.HEALTH_RETIRED %}
<div class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert">&times;</button>
......@@ -51,18 +44,6 @@
</dd>
<dt>Device-type</dt>
<dd><a href="{{ device.device_type.get_absolute_url }}">{{ device.device_type.name }}</a> <a href="{% url 'lava.scheduler.device_type_report' device.device_type.pk %}"><span class="glyphicon glyphicon-stats"></span></a></dd>
<dt><abbr title="If specified, submissions are restricted to this user or group">Owner</abbr></dt>
<dd>
{% if device.user %}
<a href="mailto:{{ device.user.email }}">{{ device.user.get_full_name|default:user.username }}</a>
{% elif device.group %}
Group <em>{{ device.group }}</em>
{% else %}
...
{% endif %}
</dd>
<dt>Restriction</dt>
<dd><span class="label label-{{ device.is_public|yesno:"success,warning" }}">{{ device.is_public|yesno:"Public,Private" }}</span></dd>
<dt>Tags</dt>
{% if device.tags.all %}
<dd>
......
......@@ -18,15 +18,9 @@
{% endfor %}
{% if job.actual_device %}
<dt>Owner</dt>
<dt>Permissions</dt>
<dd>
{% if job.actual_device.user %}
<a href="mailto:{{ job.actual_device.user.email }}">{{ job.actual_device.user.email }}</a>
{% elif job.actual_device.group %}
Group <em>{{ job.actual_device.group }}</em>
{% else %}
<i>Unrestricted</i>
{% endif %}
<!-- display restrictive permissions if any -->
</dd>
<dt>Physical access</dt>
<dd>
......
......@@ -13,14 +13,6 @@
</div>
{% endif %}
{% if dt.owners_only %}
<div class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<p><strong><i>{{ dt.name }}</i> is a hidden device type.</strong></p>
Only owners of one or more devices of type <i>{{ dt }}</i> can see this information.
</div>
{% endif %}
<div class="row">
<div class="col-md-4">
<div class="dl-horizontal">
......
......@@ -15,14 +15,6 @@
{% endblock %}
{% block content %}
{% if device.device_type.owners_only %}
<div class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<p><strong><i>{{ device.device_type.name }}</i> is a hidden device type.</strong></p>
Only owners of one or more devices of type <i>{{ device.device_type }}</i> can see this information.
</div>
{% endif %}
<h2>Device dictionary
<a class="btn btn-xs btn-info"
href="{% url 'lava.scheduler.device.dictionary.plain' device.hostname %}"
......
......@@ -135,7 +135,7 @@
<dt>Priority</dt>
<dd>{{ job.get_priority_display }}</dd>
<dt>Visibility</dt>
<dd>{{ job.get_visibility_display }}{% if job.visibility == job.VISIBLE_GROUP %} ({{ job.viewing_groups.all|join:', ' }}){% endif %}</dd>
<dd>{{ job.is_public|yesno:"Public,Private,Not set" }}{% if job.viewing_groups.all %} ({{ job.viewing_groups.all|join:', ' }}){% endif %}</dd>
{% if job_tags %}
<dt>Required Tags</dt>
<dd>
......
{% load utils %}
{% can_view record request.user as can_view %}
<div class="text-nowrap">
<a class="btn btn-xs btn-success {% if not can_view or not record.results_link %}disabled{% endif %}"
title="{% if can_view and record.results_link %}View job results{% elif not can_view %}Insufficient permissions{% elif not record.results_link %}Disabled for V1 jobs{% endif %}"
{% if not can_view or not record.results_link %}
<a class="btn btn-xs btn-success {% if not record.results_link %}disabled{% endif %}"
title="{% if record.results_link %}View job results{% elif not record.results_link %}Disabled for V1 jobs{% endif %}"
{% if not record.results_link %}
onclick="javascript:void(0)" style="pointer-events: auto;"
{% else %}
href="{{ record.results_link }}"
{% endif %}>
<span class="glyphicon glyphicon-signal"></span>
</a>
<a class="btn btn-xs btn-info {% if not can_view %}disabled{% endif %}"
title="{% if can_view %}View job details{% else %}Insufficient permissions{% endif %}"
{% if not can_view %}
onclick="javascript:void(0)" style="pointer-events: auto;"
{% else %}
href="{{ record.get_absolute_url }}"
{% endif %}>
<a class="btn btn-xs btn-info" title="View job details"
href="{{ record.get_absolute_url }}">
<span class="glyphicon glyphicon-eye-open"></span>
</a>
<a class="btn btn-xs btn-primary {% if not can_view %}disabled{% endif %}" title="{% if can_view %}End of log{% else %}Insufficient permissions{% endif %}"
{% if not can_view %}
onclick="javascript:void(0)" style="pointer-events: auto;"
{% else %}
href="{{ record.get_absolute_url }}#bottom"
{% endif %}>
<a class="btn btn-xs btn-primary" title="End of log"
href="{{ record.get_absolute_url }}#bottom">
<span class="glyphicon glyphicon-fast-forward"></span>
</a>
</div>
......
......@@ -28,7 +28,6 @@ Worker: {{ job.actual_device.worker_host.hostname }}
Job details:
Priority: {{ job.get_priority_display() }}
Visibility: {{ job.get_visibility_display() }}
Description: {{ job.description }}
Submitted: {{ job.submit_time.strftime("%Y-%m-%d %H:%M:%S (%z %Z)") }}
Started: {{ job.start_time.strftime("%Y-%m-%d %H:%M:%S (%z %Z)") }}
......
# Sample JOB definition for a KVM
device_type: qemu
job_name: qemu-pipeline
timeouts:
job:
minutes: 20 # timeout for the whole job (default: ??h)
action:
minutes: 5 # default timeout applied for each action; can be overriden in the action itself (default: ?h)
priority: low
visibility: personal
actions:
- deploy:
timeout:
minutes: 20
to: tmpfs
images:
rootfs:
url: http://images.validation.linaro.org/kvm-debian-wheezy.img.gz
image_arg: -drive format=raw,file={rootfs}
compression: gz
os: debian
- boot:
method: qemu
media: tmpfs
prompts: ["root@debian:"]
failure_retry: 2
- test:
failure_retry: 3
# only s, m & h are supported.
timeout:
minutes: 5 # uses install:deps, so takes longer than singlenode01
definitions:
- repository: git://git.linaro.org/lava-team/lava-functional-tests.git
from: git
path: lava-test-shell/smoke-tests-basic.yaml
# name: if not present, use the name from the YAML. The name can
# also be overriden from the actual commands being run by
# calling the lava-test-suite-name API call (e.g.
# `lava-test-suite-name FOO`).
name: smoke-tests
- repository: http://git.linaro.org/lava-team/lava-functional-tests.git
from: git
path: lava-test-shell/single-node/singlenode03.yaml
name: singlenode-advanced
context:
arch: amd64
This diff is collapsed.
# -*- coding: utf-8 -*-
# Copyright (C) 2019 Linaro Limited
#
# Author: Stevan Radakovic <stevan.radakovic@linaro.org>
#
# This file is part of LAVA.
#
# LAVA is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License version 3
# as published by the Free Software Foundation
#
# LAVA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with LAVA. If not, see <http://www.gnu.org/licenses/>.
from itertools import chain
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission, AnonymousUser
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from lava_common.exceptions import PermissionNameError
from lava_scheduler_app.auth import PermissionAuth
from lava_scheduler_app.models import (
GroupObjectPermission,
GroupDeviceTypePermission,
GroupDevicePermission,
)
from lava_scheduler_app.tests.test_submission import TestCaseWithFactory
User = get_user_model()
class PermissionAuthTest(TestCaseWithFactory):
def setUp(self):
super().setUp()
self.admin_user = User.objects.create(
username=self.factory.get_unique_user(), is_superuser=True
)
self.group = self.factory.make_group(name="group1")
self.user = self.factory.make_user()
self.user.groups.add(self.group)
self.definition = self.factory.make_job_data_from_file(
"qemu-pipeline-first-job.yaml"
)
self.device_type = self.factory.make_device_type(name="qemu")
self.device = self.factory.make_device(
device_type=self.device_type, hostname="qemu-1"
)
def tearDown(self):
super().tearDown()
GroupDeviceTypePermission.objects.all().delete()
GroupDevicePermission.objects.all().delete()
def test_get_group_perms(self):
# Test group permission queries.
auth = PermissionAuth(self.user)
GroupDevicePermission.objects.assign_perm(
"admin_device", self.group, self.device
)
permissions = auth.get_group_perms(self.device)
self.assertEqual(
permissions, {"admin_device", "view_device", "submit_to_device"}
)
def test_anonymous_unrestricted_device_type(self):
guy_fawkes = AnonymousUser()
auth = PermissionAuth(guy_fawkes)
self.assertTrue(
auth.has_perm("lava_scheduler_app.view_devicetype", self.device_type)
)
def test_anonymous_restricted_device_type(self):
guy_fawkes = AnonymousUser()
auth = PermissionAuth(guy_fawkes)
GroupDeviceTypePermission.objects.assign_perm(
"view_devicetype", self.group, self.device_type
)
self.assertFalse(
auth.has_perm("lava_scheduler_app.view_devicetype", self.device_type)
)
def test_anonymous_restricted_device_type_by_non_view_permission(self):
guy_fawkes = AnonymousUser()
auth = PermissionAuth(guy_fawkes)
GroupDeviceTypePermission.objects.assign_perm(
"admin_devicetype", self.group, self.device_type
)
self.assertTrue(
auth.has_perm("lava_scheduler_app.view_devicetype", self.device_type)
)
def test_anonymous_unrestricted_device(self):
guy_fawkes = AnonymousUser()
auth = PermissionAuth(guy_fawkes)
self.assertTrue(auth.has_perm("lava_scheduler_app.view_device", self.device))
def test_anonymous_restricted_device(self):
guy_fawkes = AnonymousUser()
auth = PermissionAuth(guy_fawkes)
GroupDevicePermission.objects.assign_perm(
"view_device", self.group, self.device
)
self.assertFalse(auth.has_perm("lava_scheduler_app.view_device", self.device))
def test_anonymous_restricted_device_by_non_view_permission(self):
guy_fawkes = AnonymousUser()
auth = PermissionAuth(guy_fawkes)
GroupDevicePermission.objects.assign_perm(
"admin_device", self.group, self.device
)
self.assertTrue(auth.has_perm("lava_scheduler_app.view_device", self.device))
def test_has_perm_unsupported_model(self):
# Unsupported permission codename will raise PermissionNameError.
user = self.factory.make_user()
auth = PermissionAuth(user)
with TestCase.assertRaises(self, PermissionNameError):
GroupDevicePermission.objects.assign_perm(
"change_group", self.group, self.device
)
def test_superuser(self):
user = User.objects.create(username="superuser", is_superuser=True)
auth = PermissionAuth(user)
content_type = ContentType.objects.get_for_model(self.device)
perms = set(
chain(
*Permission.objects.filter(content_type=content_type).values_list(
"codename"
)
)
)
self.assertEqual(perms, auth.get_perms(self.device))
for perm in perms:
self.assertTrue(
auth.has_perm("%s.%s" % (content_type.app_label, perm), self.device)
)
def test_not_active_superuser(self):
user = User.objects.create(
username="not_active_superuser", is_superuser=True, is_active=False
)
check = PermissionAuth(user)
content_type = ContentType.objects.get_for_model(self.device)
perms = sorted(
chain(
*Permission.objects.filter(content_type=content_type).values_list(
"codename"
)
)
)
self.assertEqual(check.get_perms(self.device), [])
for perm in perms:
self.assertFalse(
check.has_perm("%s.%s" % (content_type.app_label, perm), self.device)
)
def test_not_active_user(self):
user = User.objects.create(username="notactive")
user.groups.add(self.group)
GroupDevicePermission.objects.assign_perm(
"admin_device", self.group, self.device
)
check = PermissionAuth(user)
self.assertTrue(check.has_perm("lava_scheduler_app.admin_device", self.device))
user.is_active = False
self.assertFalse(check.has_perm("lava_scheduler_app.admin_device", self.device))
def test_get_perms(self):
device1 = self.factory.make_device(
device_type=self.device_type, hostname="qemu-tmp-01"
)
device2 = self.factory.make_device(
device_type=self.device_type, hostname="qemu-tmp-02"
)
assign_perms = {device1: ("admin_device",), device2: ("view_device",)}
auth = PermissionAuth(self.user)
for obj, perms in assign_perms.items():
for perm in perms:
GroupDevicePermission.objects.assign_perm(perm, self.group, obj)
self.assertTrue(set(perms).issubset(auth.get_perms(obj)))
def test_ensure_users_group_has_associated_groups(self):
test_user = self.factory.make_user()
unwanted_group = self.factory.make_group(name="unwanted_group")
test_user.groups.add(unwanted_group)
group = GroupObjectPermission.ensure_users_group(test_user)
# Ensure that groups are not removed for this user.
self.assertEqual(test_user.groups.count(), 2)
# Test that our new group has correct name and
# only this user associated.
self.assertEqual(group.name, test_user.username)
self.assertEqual(group.user_set.count(), 1)
def test_ensure_users_group_has_associated_users(self):
test_user = self.factory.make_user()
unwanted_user = self.factory.make_user()
users_group = self.factory.make_group(name=test_user.username)
users_group.user_set.add(unwanted_user)
GroupObjectPermission.ensure_users_group(test_user)
self.assertEqual(test_user.groups.count(), 1)
self.assertEqual(test_user.groups.first().name, test_user.username)
self.assertEqual(test_user.groups.first().user_set.count(), 1)
def test_ensure_users_group(self):
test_user = self.factory.make_user()
group = GroupObjectPermission.ensure_users_group(test_user)