Skip to content
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

Kibana public/private access using IAM #4240

Merged
merged 8 commits into from
Jan 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Auth token key is not returned when registering without email verification (<https://github.com/openvinotoolkit/cvat/pull/4092>)
- Error in create project from backup for standard 3D annotation (<https://github.com/openvinotoolkit/cvat/pull/4160>)
- Annotations search does not work correctly in some corner cases (when use complex properties with width, height) (<https://github.com/openvinotoolkit/cvat/pull/4198>)
- Kibana requests are not proxied due to django-revproxy incompatibility with Django >3.2.x (<https://github.com/openvinotoolkit/cvat/issues/4085>)

### Security
- Updated ELK to 6.8.23 which uses log4j 2.17.1 (<https://github.com/openvinotoolkit/cvat/pull/4206>)
Expand Down
8 changes: 7 additions & 1 deletion components/analytics/docker-compose.analytics.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,15 @@ services:
environment:
DJANGO_LOG_SERVER_HOST: logstash
DJANGO_LOG_SERVER_PORT: 8080
CVAT_ANALYTICS: 1

traefik:
environment:
CVAT_HOST: ${CVAT_HOST:-localhost}
DJANGO_LOG_VIEWER_HOST: kibana
DJANGO_LOG_VIEWER_PORT: 5601
CVAT_ANALYTICS: 1
volumes:
- ./components/analytics/kibana_conf.yml:/etc/traefik/rules/kibana_conf.yml:ro

volumes:
cvat_events:
26 changes: 26 additions & 0 deletions components/analytics/kibana_conf.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
http:
routers:
kibana:
entryPoints:
- web
middlewares:
- analytics-auth
- strip-prefix
service: kibana
rule: Host(`{{ env "CVAT_HOST" }}`) && PathPrefix(`/analytics`)

middlewares:
analytics-auth:
forwardauth:
address: http://cvat:8080/analytics
strip-prefix:
stripprefix:
prefixes:
- /analytics

services:
kibana:
loadBalancer:
servers:
- url: http://{{ env "DJANGO_LOG_VIEWER_HOST" }}:{{ env "DJANGO_LOG_VIEWER_PORT" }}
passHostHeader: false
28 changes: 28 additions & 0 deletions cvat/apps/iam/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,34 @@ def scope(self):
'share': 'list:content'
}.get(self.view.action, None)

class LogViewerPermission(OpenPolicyAgentPermission):
@classmethod
def create(cls, request, view, obj):
permissions = []
if view.basename == 'analytics':
self = cls(request, view, obj)
permissions.append(self)

return permissions

def __init__(self, request, view, obj):
super().__init__(request, view, obj)
self.url = settings.IAM_OPA_DATA_URL + '/analytics/allow'
self.payload['input']['scope'] = self.scope
self.payload['input']['resource'] = self.resource

@property
def scope(self):
return {
'list': 'view',
}.get(self.view.action, None)

@property
def resource(self):
return {
'visibility': 'public' if settings.RESTRICTIONS['analytics_visibility'] else 'private',
}

class UserPermission(OpenPolicyAgentPermission):
@classmethod
def create(cls, request, view, obj):
Expand Down
3 changes: 3 additions & 0 deletions cvat/apps/iam/rules/analytics.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Scope,Resource,Context,Ownership,Limit,Method,URL,Privilege,Membership
nmanovic marked this conversation as resolved.
Show resolved Hide resolved
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
36 changes: 36 additions & 0 deletions cvat/apps/iam/rules/analytics.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package analytics
import data.utils

# input: {
# "scope": <"view"> or null,
# "auth": {
# "user": {
# "id": <num>,
# "privilege": <"admin"|"business"|"user"|"worker"> or null
# },
# "organization": {
# "id": <num>,
# "owner": {
# "id": <num>
# },
# "user": {
# "role": <"owner"|"maintainer"|"supervisor"|"worker"> or null
# }
# } or null,
# },
# "resource": {
# "visibility": <"public"|"private"> or null,
# }
# }

default allow = false

allow {
utils.is_admin
}

allow {
input.resource.visibility == utils.PUBLIC
input.scope == utils.VIEW
utils.has_perm(utils.BUSINESS)
}
404 changes: 404 additions & 0 deletions cvat/apps/iam/rules/analytics_test.gen.rego

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion cvat/apps/log_viewer/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@


class LogViewerConfig(AppConfig):
name = 'log_viewer'
name = 'cvat.apps.log_viewer'
12 changes: 7 additions & 5 deletions cvat/apps/log_viewer/urls.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@

# Copyright (C) 2018-2020 Intel Corporation
# Copyright (C) 2018-2022 Intel Corporation
#
# SPDX-License-Identifier: MIT

from django.urls import path
from rest_framework import routers

from . import views

urlpatterns = [
path('<path:path>', views.LogViewerProxy.as_view())
]
router = routers.DefaultRouter(trailing_slash=False)
router.register('analytics', views.LogViewerAccessViewSet, basename='analytics')

urlpatterns = router.urls
45 changes: 18 additions & 27 deletions cvat/apps/log_viewer/views.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,22 @@
# Copyright (C) 2018-2020 Intel Corporation
# Copyright (C) 2018-2022 Intel Corporation
#
# SPDX-License-Identifier: MIT

import os

from revproxy.views import ProxyView
from django.utils.decorators import method_decorator
from django.conf import settings
from rules.contrib.views import PermissionRequiredMixin

from cvat.apps.iam import login_required

@method_decorator(login_required, name='dispatch')
class LogViewerProxy(PermissionRequiredMixin, ProxyView):
permission_required = settings.RESTRICTIONS['analytics_access']

upstream = 'http://{}:{}'.format(os.getenv('DJANGO_LOG_VIEWER_HOST'),
os.getenv('DJANGO_LOG_VIEWER_PORT'))
add_remote_user = True

def get_request_headers(self):
headers = super().get_request_headers()
headers['X-Forwarded-User'] = headers['REMOTE_USER']

return headers

# Returns True if the user has any of the specified permissions
def has_permission(self):
perms = self.get_permission_required()
return any(self.request.user.has_perm(perm) for perm in perms)
from django.http import HttpResponsePermanentRedirect
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

class LogViewerAccessViewSet(viewsets.ViewSet):
serializer_class = None

def list(self, request):
return Response(status=status.HTTP_200_OK)

# All log view requests are proxied by Traefik in production mode which is not available in debug mode,
# In order not to duplicate settings, let's just redirect to the default page in debug mode
@action(detail=False, url_path='app/kibana')
def redirect(self, request):
if settings.DEBUG:
return HttpResponsePermanentRedirect('http://localhost:5601/app/kibana')
1 change: 0 additions & 1 deletion cvat/requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ sqlparse==0.4.2
django-sendfile2==0.6.1
dj-pagination==2.5.0
python-logstash-async==2.2.0
django-revproxy==0.10.0
rules==2.2
GitPython==3.1.8
coreapi==2.3.3
Expand Down
21 changes: 6 additions & 15 deletions cvat/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import subprocess
import mimetypes
from corsheaders.defaults import default_headers
from distutils.util import strtobool

mimetypes.add_type("application/wasm", ".wasm", True)

Expand Down Expand Up @@ -108,7 +109,6 @@ def add_ssh_keys():
'compressor',
'django_sendfile',
'dj_pagination',
'revproxy',
'rest_framework',
'rest_framework.authtoken',
'django_filters',
Expand All @@ -127,7 +127,7 @@ def add_ssh_keys():
'cvat.apps.dataset_repo',
'cvat.apps.restrictions',
'cvat.apps.lambda_manager',
'cvat.apps.opencv'
'cvat.apps.opencv',
]

SITE_ID = 1
Expand Down Expand Up @@ -185,7 +185,7 @@ def add_ssh_keys():
'PASSWORD_RESET_SERIALIZER': 'cvat.apps.iam.serializers.PasswordResetSerializerEx',
}

if os.getenv('DJANGO_LOG_VIEWER_HOST'):
if strtobool(os.getenv('CVAT_ANALYTICS', '0')):
INSTALLED_APPS += ['cvat.apps.log_viewer']

MIDDLEWARE = [
Expand Down Expand Up @@ -420,11 +420,6 @@ def add_ssh_keys():
'handlers': [],
'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG'),
},

'revproxy': {
'handlers': ['console', 'server_file'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG')
},
'django': {
'handlers': ['console', 'server_file'],
'level': 'INFO',
Expand Down Expand Up @@ -454,13 +449,9 @@ def add_ssh_keys():
# this setting reduces task visibility to owner and assignee only
'reduce_task_visibility': False,

# allow access to analytics component to users with the following roles
'analytics_access': (
'engine.role.observer',
'engine.role.annotator',
'engine.role.user',
'engine.role.admin',
),
# allow access to analytics component to users with business role
# otherwise, only the administrator has access
'analytics_visibility': True,
}

# http://www.grantjenks.com/docs/diskcache/tutorial.html#djangocache
Expand Down
2 changes: 1 addition & 1 deletion cvat/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
urlpatterns.append(path('git/repository/', include('cvat.apps.dataset_repo.urls')))

if apps.is_installed('cvat.apps.log_viewer'):
urlpatterns.append(path('analytics/', include('cvat.apps.log_viewer.urls')))
urlpatterns.append(path('', include('cvat.apps.log_viewer.urls')))

if apps.is_installed('cvat.apps.lambda_manager'):
urlpatterns.append(path('', include('cvat.apps.lambda_manager.urls')))
Expand Down
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ services:
- traefik.enable=true
- traefik.http.services.cvat.loadbalancer.server.port=8080
- traefik.http.routers.cvat.rule=Host(`${CVAT_HOST:-localhost}`) &&
PathPrefix(`/api/`, `/git/`, `/opencv/`, `/analytics/`, `/static/`, `/admin`, `/documentation/`, `/django-rq`)
PathPrefix(`/api/`, `/git/`, `/opencv/`, `/static/`, `/admin`, `/documentation/`, `/django-rq`)
- traefik.http.routers.cvat.entrypoints=web
volumes:
- cvat_data:/home/django/data
Expand Down Expand Up @@ -75,6 +75,7 @@ services:
- '--providers.docker.exposedByDefault=false'
- '--providers.docker.network=cvat'
- '--entryPoints.web.address=:8080'
- '--providers.file.directory=/etc/traefik/rules'
# Uncomment to get Traefik dashboard
# - "--entryPoints.dashboard.address=:8090"
# - "--api.dashboard=true"
Expand Down