-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Analytics access #8509
base: develop
Are you sure you want to change the base?
Analytics access #8509
Changes from all commits
3198116
275bba8
461c9ea
31ef9e4
8b9899d
b18cb2f
4395c3c
ddea49d
c891cd0
6d653ea
a662c55
bdb444c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
### Added | ||
|
||
- Access to /analytics can now be granted | ||
(<https://github.com/cvat-ai/cvat/pull/8509>) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -513,7 +513,7 @@ function HeaderComponent(props: Props): JSX.Element { | |
Models | ||
</Button> | ||
) : null} | ||
{isAnalyticsPluginActive && user.isSuperuser ? ( | ||
{isAnalyticsPluginActive && (user.isSuperuser || user.hasAnalyticsAccess) ? ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just rely on |
||
<Button | ||
className={getButtonClassName('analytics', false)} | ||
type='link' | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Generated by Django 4.2.15 on 2024-10-04 10:50 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
("engine", "0084_honeypot_support"), | ||
] | ||
|
||
operations = [ | ||
migrations.AddField( | ||
model_name="profile", | ||
name="has_analytics_access", | ||
field=models.BooleanField( | ||
default=False, | ||
help_text="Designates whether the user can access /analytics.", | ||
verbose_name="has /analytics access", | ||
), | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,19 +15,21 @@ | |
|
||
from django.conf import settings | ||
from django.contrib.auth.models import User | ||
from django.core.files.storage import FileSystemStorage | ||
from django.core.exceptions import ValidationError | ||
from django.core.files.storage import FileSystemStorage | ||
from django.db import IntegrityError, models, transaction | ||
from django.db.models.fields import FloatField | ||
from django.db.models import Q, TextChoices | ||
from django.db.models.fields import FloatField | ||
from django.utils.translation import gettext_lazy as _ | ||
from drf_spectacular.types import OpenApiTypes | ||
from drf_spectacular.utils import extend_schema_field | ||
|
||
from cvat.apps.engine.lazy_list import LazyList | ||
from cvat.apps.engine.model_utils import MaybeUndefined | ||
from cvat.apps.engine.utils import parse_specific_attributes, chunked_list | ||
from cvat.apps.engine.utils import chunked_list, parse_specific_attributes | ||
from cvat.apps.events.utils import cache_deleted | ||
|
||
|
||
class SafeCharField(models.CharField): | ||
def get_prep_value(self, value): | ||
value = super().get_prep_value(value) | ||
|
@@ -1085,9 +1087,16 @@ class TrackedShapeAttributeVal(AttributeVal): | |
shape = models.ForeignKey(TrackedShape, on_delete=models.DO_NOTHING, | ||
related_name='attributes', related_query_name='attribute') | ||
|
||
|
||
class Profile(models.Model): | ||
user = models.OneToOneField(User, on_delete=models.CASCADE) | ||
rating = models.FloatField(default=0.0) | ||
has_analytics_access = models.BooleanField( | ||
_("has /analytics access"), | ||
default=False, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not really think that it's the desired approach to have |
||
help_text=_("Designates whether the user can access /analytics."), | ||
) | ||
|
||
|
||
class Issue(TimestampedModel): | ||
frame = models.PositiveIntegerField() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -221,28 +221,35 @@ class Meta: | |
model = User | ||
fields = ('url', 'id', 'username', 'first_name', 'last_name') | ||
|
||
|
||
class UserSerializer(serializers.ModelSerializer): | ||
groups = serializers.SlugRelatedField(many=True, | ||
slug_field='name', queryset=Group.objects.all()) | ||
has_analytics_access = serializers.BooleanField( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it supposed to be changed only through django admin panel? If this is the case, then this field should be marked as |
||
source='profile.has_analytics_access', | ||
required=False, | ||
) | ||
|
||
class Meta: | ||
model = User | ||
fields = ('url', 'id', 'username', 'first_name', 'last_name', 'email', | ||
'groups', 'is_staff', 'is_superuser', 'is_active', 'last_login', | ||
'date_joined') | ||
'date_joined', 'has_analytics_access') | ||
read_only_fields = ('last_login', 'date_joined') | ||
write_only_fields = ('password', ) | ||
extra_kwargs = { | ||
'last_login': { 'allow_null': True } | ||
} | ||
|
||
|
||
class DelimitedStringListField(serializers.ListField): | ||
def to_representation(self, value): | ||
return super().to_representation(value.split('\n')) | ||
|
||
def to_internal_value(self, data): | ||
return '\n'.join(super().to_internal_value(data)) | ||
|
||
|
||
class AttributeSerializer(serializers.ModelSerializer): | ||
id = serializers.IntegerField(required=False) | ||
values = DelimitedStringListField(allow_empty=True, | ||
|
@@ -253,6 +260,7 @@ class Meta: | |
model = models.AttributeSpec | ||
fields = ('id', 'name', 'mutable', 'input_type', 'default_value', 'values') | ||
|
||
|
||
class SublabelSerializer(serializers.ModelSerializer): | ||
id = serializers.IntegerField(required=False) | ||
attributes = AttributeSerializer(many=True, source='attributespec_set', default=[], | ||
|
@@ -271,6 +279,7 @@ class Meta: | |
fields = ('id', 'name', 'color', 'attributes', 'type', 'has_parent', ) | ||
read_only_fields = ('parent',) | ||
|
||
|
||
class SkeletonSerializer(serializers.ModelSerializer): | ||
id = serializers.IntegerField(required=False) | ||
svg = serializers.CharField(allow_blank=True, required=False) | ||
|
@@ -279,6 +288,7 @@ class Meta: | |
model = models.Skeleton | ||
fields = ('id', 'svg',) | ||
|
||
|
||
class LabelSerializer(SublabelSerializer): | ||
deleted = serializers.BooleanField(required=False, write_only=True, | ||
help_text='Delete the label. Only applicable in the PATCH methods of a project or a task.') | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,7 +15,7 @@ | |
from attrs import define, field | ||
from django.apps import AppConfig | ||
from django.conf import settings | ||
from django.db.models import Q, Model | ||
from django.db.models import Model, Q | ||
from rest_framework.exceptions import PermissionDenied | ||
from rest_framework.permissions import BasePermission | ||
|
||
|
@@ -24,15 +24,18 @@ | |
|
||
from .utils import add_opa_rules_path | ||
|
||
|
||
class StrEnum(str, Enum): | ||
def __str__(self) -> str: | ||
return self.value | ||
|
||
|
||
@define | ||
class PermissionResult: | ||
allow: bool | ||
reasons: List[str] = field(factory=list) | ||
|
||
|
||
def get_organization(request, obj): | ||
# Try to get organization from an object otherwise, return the organization that is specified in query parameters | ||
if isinstance(obj, Organization): | ||
|
@@ -56,6 +59,7 @@ def get_organization(request, obj): | |
|
||
return request.iam_context['organization'] | ||
|
||
|
||
def get_membership(request, organization): | ||
if organization is None: | ||
return None | ||
|
@@ -66,9 +70,11 @@ def get_membership(request, organization): | |
is_active=True | ||
).first() | ||
|
||
|
||
def build_iam_context(request, organization: Optional[Organization], membership: Optional[Membership]): | ||
return { | ||
'user_id': request.user.id, | ||
'has_analytics_access': request.user.profile.has_analytics_access, | ||
Eldies marked this conversation as resolved.
Show resolved
Hide resolved
|
||
'group_name': request.iam_context['privilege'], | ||
'org_id': getattr(organization, 'id', None), | ||
'org_slug': getattr(organization, 'slug', None), | ||
|
@@ -94,6 +100,7 @@ class OpenPolicyAgentPermission(metaclass=ABCMeta): | |
org_role: Optional[str] | ||
scope: str | ||
obj: Optional[Any] | ||
has_analytics_access: bool | ||
|
||
@classmethod | ||
@abstractmethod | ||
|
@@ -126,7 +133,8 @@ def __init__(self, **kwargs): | |
'auth': { | ||
'user': { | ||
'id': self.user_id, | ||
'privilege': self.group_name | ||
'privilege': self.group_name, | ||
'has_analytics_access': self.has_analytics_access, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like these changes should be encapsulated in |
||
}, | ||
'organization': { | ||
'id': self.org_id, | ||
|
@@ -219,11 +227,14 @@ def get_per_field_update_scopes(cls, request, scopes_per_field): | |
|
||
return scopes | ||
|
||
|
||
T = TypeVar('T', bound=Model) | ||
|
||
|
||
def is_public_obj(obj: T) -> bool: | ||
return getattr(obj, "is_public", False) | ||
|
||
|
||
class PolicyEnforcer(BasePermission): | ||
# pylint: disable=no-self-use | ||
def check_permission(self, request, view, obj) -> bool: | ||
|
@@ -258,13 +269,15 @@ def is_metadata_request(request, view): | |
return request.method == 'OPTIONS' \ | ||
or (request.method == 'POST' and view.action == 'metadata' and len(request.data) == 0) | ||
|
||
|
||
class IsAuthenticatedOrReadPublicResource(BasePermission): | ||
def has_object_permission(self, request, view, obj) -> bool: | ||
return bool( | ||
request.user and request.user.is_authenticated or | ||
request.method == 'GET' and is_public_obj(obj) | ||
(request.user and request.user.is_authenticated) or | ||
(request.method == 'GET' and is_public_obj(obj)) | ||
) | ||
|
||
|
||
def load_app_permissions(config: AppConfig) -> None: | ||
""" | ||
Ensures that permissions and OPA rules from the given app are loaded. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
Scope,Resource,Context,Ownership,Limit,Method,URL,Privilege,Membership | ||
view,Analytics,N/A,N/A,resource['visibility']=='public',GET,"/analytics",business,N/A | ||
view,Analytics,N/A,N/A,,GET,"/analytics",admin,N/A | ||
Scope,Resource,Context,Ownership,Limit,Method,URL,Privilege,Membership,HasAnalyticsAccess | ||
view,Analytics,N/A,N/A,resource['visibility']=='public',GET,"/analytics",business,N/A,N/A | ||
view,Analytics,N/A,N/A,,GET,"/analytics",admin,N/A,N/A | ||
view,Analytics,N/A,N/A,,GET,"/analytics",none,N/A,true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SerializedUser
also should be updated