diff --git a/CHANGELOG.md b/CHANGELOG.md index bb2ad040802d..2a9546c3fb5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 () - Error in create project from backup for standard 3D annotation () - Annotations search does not work correctly in some corner cases (when use complex properties with width, height) () +- Kibana requests are not proxied due to django-revproxy incompatibility with Django >3.2.x () ### Security - Updated ELK to 6.8.23 which uses log4j 2.17.1 () diff --git a/components/analytics/docker-compose.analytics.yml b/components/analytics/docker-compose.analytics.yml index eab35b2cd0b6..4a8c769c42a7 100644 --- a/components/analytics/docker-compose.analytics.yml +++ b/components/analytics/docker-compose.analytics.yml @@ -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: diff --git a/components/analytics/kibana_conf.yml b/components/analytics/kibana_conf.yml new file mode 100644 index 000000000000..c2eb5a95835e --- /dev/null +++ b/components/analytics/kibana_conf.yml @@ -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 diff --git a/cvat/apps/iam/permissions.py b/cvat/apps/iam/permissions.py index b9881ccc76f9..1107db4a49d6 100644 --- a/cvat/apps/iam/permissions.py +++ b/cvat/apps/iam/permissions.py @@ -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): diff --git a/cvat/apps/iam/rules/analytics.csv b/cvat/apps/iam/rules/analytics.csv new file mode 100644 index 000000000000..d38b0514425c --- /dev/null +++ b/cvat/apps/iam/rules/analytics.csv @@ -0,0 +1,3 @@ +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 diff --git a/cvat/apps/iam/rules/analytics.rego b/cvat/apps/iam/rules/analytics.rego new file mode 100644 index 000000000000..08c0096318ce --- /dev/null +++ b/cvat/apps/iam/rules/analytics.rego @@ -0,0 +1,36 @@ +package analytics +import data.utils + +# input: { +# "scope": <"view"> or null, +# "auth": { +# "user": { +# "id": , +# "privilege": <"admin"|"business"|"user"|"worker"> or null +# }, +# "organization": { +# "id": , +# "owner": { +# "id": +# }, +# "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) +} diff --git a/cvat/apps/iam/rules/analytics_test.gen.rego b/cvat/apps/iam/rules/analytics_test.gen.rego new file mode 100644 index 000000000000..5278e321475d --- /dev/null +++ b/cvat/apps/iam/rules/analytics_test.gen.rego @@ -0,0 +1,404 @@ +package analytics + +test_scope_VIEW_context_SANDBOX_ownership_NONE_privilege_ADMIN_membership_NONE { + allow with input as {"scope": "view", "auth": {"user": {"id": 76, "privilege": "admin"}, "organization": null}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_SANDBOX_ownership_NONE_privilege_ADMIN_membership_NONE { + allow with input as {"scope": "view", "auth": {"user": {"id": 70, "privilege": "admin"}, "organization": null}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_SANDBOX_ownership_NONE_privilege_BUSINESS_membership_NONE { + allow with input as {"scope": "view", "auth": {"user": {"id": 45, "privilege": "business"}, "organization": null}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_SANDBOX_ownership_NONE_privilege_BUSINESS_membership_NONE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 11, "privilege": "business"}, "organization": null}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_SANDBOX_ownership_NONE_privilege_USER_membership_NONE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 70, "privilege": "user"}, "organization": null}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_SANDBOX_ownership_NONE_privilege_USER_membership_NONE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 52, "privilege": "user"}, "organization": null}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_SANDBOX_ownership_NONE_privilege_WORKER_membership_NONE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 34, "privilege": "worker"}, "organization": null}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_SANDBOX_ownership_NONE_privilege_WORKER_membership_NONE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 70, "privilege": "worker"}, "organization": null}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_SANDBOX_ownership_NONE_privilege_NONE_membership_NONE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 73, "privilege": "none"}, "organization": null}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_SANDBOX_ownership_NONE_privilege_NONE_membership_NONE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 98, "privilege": "none"}, "organization": null}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_OWNER { + allow with input as {"scope": "view", "auth": {"user": {"id": 56, "privilege": "admin"}, "organization": {"id": 112, "owner": {"id": 56}, "user": {"role": "owner"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_OWNER { + allow with input as {"scope": "view", "auth": {"user": {"id": 98, "privilege": "admin"}, "organization": {"id": 114, "owner": {"id": 98}, "user": {"role": "owner"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_MAINTAINER { + allow with input as {"scope": "view", "auth": {"user": {"id": 31, "privilege": "admin"}, "organization": {"id": 115, "owner": {"id": 244}, "user": {"role": "maintainer"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_MAINTAINER { + allow with input as {"scope": "view", "auth": {"user": {"id": 40, "privilege": "admin"}, "organization": {"id": 190, "owner": {"id": 208}, "user": {"role": "maintainer"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_SUPERVISOR { + allow with input as {"scope": "view", "auth": {"user": {"id": 57, "privilege": "admin"}, "organization": {"id": 137, "owner": {"id": 294}, "user": {"role": "supervisor"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_SUPERVISOR { + allow with input as {"scope": "view", "auth": {"user": {"id": 65, "privilege": "admin"}, "organization": {"id": 193, "owner": {"id": 253}, "user": {"role": "supervisor"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_WORKER { + allow with input as {"scope": "view", "auth": {"user": {"id": 11, "privilege": "admin"}, "organization": {"id": 140, "owner": {"id": 257}, "user": {"role": "worker"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_WORKER { + allow with input as {"scope": "view", "auth": {"user": {"id": 29, "privilege": "admin"}, "organization": {"id": 133, "owner": {"id": 291}, "user": {"role": "worker"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_NONE { + allow with input as {"scope": "view", "auth": {"user": {"id": 25, "privilege": "admin"}, "organization": {"id": 185, "owner": {"id": 266}, "user": {"role": null}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_ADMIN_membership_NONE { + allow with input as {"scope": "view", "auth": {"user": {"id": 9, "privilege": "admin"}, "organization": {"id": 199, "owner": {"id": 225}, "user": {"role": null}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_OWNER { + allow with input as {"scope": "view", "auth": {"user": {"id": 40, "privilege": "business"}, "organization": {"id": 144, "owner": {"id": 40}, "user": {"role": "owner"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_OWNER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 74, "privilege": "business"}, "organization": {"id": 141, "owner": {"id": 74}, "user": {"role": "owner"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_MAINTAINER { + allow with input as {"scope": "view", "auth": {"user": {"id": 18, "privilege": "business"}, "organization": {"id": 137, "owner": {"id": 275}, "user": {"role": "maintainer"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_MAINTAINER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 4, "privilege": "business"}, "organization": {"id": 105, "owner": {"id": 285}, "user": {"role": "maintainer"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_SUPERVISOR { + allow with input as {"scope": "view", "auth": {"user": {"id": 45, "privilege": "business"}, "organization": {"id": 102, "owner": {"id": 291}, "user": {"role": "supervisor"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_SUPERVISOR { + not allow with input as {"scope": "view", "auth": {"user": {"id": 66, "privilege": "business"}, "organization": {"id": 152, "owner": {"id": 255}, "user": {"role": "supervisor"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_WORKER { + allow with input as {"scope": "view", "auth": {"user": {"id": 65, "privilege": "business"}, "organization": {"id": 198, "owner": {"id": 227}, "user": {"role": "worker"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_WORKER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 74, "privilege": "business"}, "organization": {"id": 125, "owner": {"id": 208}, "user": {"role": "worker"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_NONE { + allow with input as {"scope": "view", "auth": {"user": {"id": 99, "privilege": "business"}, "organization": {"id": 115, "owner": {"id": 276}, "user": {"role": null}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_BUSINESS_membership_NONE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 57, "privilege": "business"}, "organization": {"id": 190, "owner": {"id": 253}, "user": {"role": null}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_OWNER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 54, "privilege": "user"}, "organization": {"id": 130, "owner": {"id": 54}, "user": {"role": "owner"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_OWNER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 74, "privilege": "user"}, "organization": {"id": 145, "owner": {"id": 74}, "user": {"role": "owner"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_MAINTAINER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 44, "privilege": "user"}, "organization": {"id": 157, "owner": {"id": 223}, "user": {"role": "maintainer"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_MAINTAINER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 55, "privilege": "user"}, "organization": {"id": 142, "owner": {"id": 292}, "user": {"role": "maintainer"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_SUPERVISOR { + not allow with input as {"scope": "view", "auth": {"user": {"id": 7, "privilege": "user"}, "organization": {"id": 154, "owner": {"id": 243}, "user": {"role": "supervisor"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_SUPERVISOR { + not allow with input as {"scope": "view", "auth": {"user": {"id": 72, "privilege": "user"}, "organization": {"id": 199, "owner": {"id": 225}, "user": {"role": "supervisor"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_WORKER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 82, "privilege": "user"}, "organization": {"id": 148, "owner": {"id": 273}, "user": {"role": "worker"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_WORKER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 27, "privilege": "user"}, "organization": {"id": 147, "owner": {"id": 296}, "user": {"role": "worker"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_NONE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 35, "privilege": "user"}, "organization": {"id": 146, "owner": {"id": 298}, "user": {"role": null}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_USER_membership_NONE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 8, "privilege": "user"}, "organization": {"id": 118, "owner": {"id": 247}, "user": {"role": null}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_OWNER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 34, "privilege": "worker"}, "organization": {"id": 112, "owner": {"id": 34}, "user": {"role": "owner"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_OWNER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 47, "privilege": "worker"}, "organization": {"id": 149, "owner": {"id": 47}, "user": {"role": "owner"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_MAINTAINER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 4, "privilege": "worker"}, "organization": {"id": 147, "owner": {"id": 277}, "user": {"role": "maintainer"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_MAINTAINER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 52, "privilege": "worker"}, "organization": {"id": 150, "owner": {"id": 233}, "user": {"role": "maintainer"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_SUPERVISOR { + not allow with input as {"scope": "view", "auth": {"user": {"id": 58, "privilege": "worker"}, "organization": {"id": 102, "owner": {"id": 275}, "user": {"role": "supervisor"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_SUPERVISOR { + not allow with input as {"scope": "view", "auth": {"user": {"id": 63, "privilege": "worker"}, "organization": {"id": 106, "owner": {"id": 258}, "user": {"role": "supervisor"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_WORKER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 11, "privilege": "worker"}, "organization": {"id": 171, "owner": {"id": 212}, "user": {"role": "worker"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_WORKER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 57, "privilege": "worker"}, "organization": {"id": 150, "owner": {"id": 216}, "user": {"role": "worker"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_NONE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 62, "privilege": "worker"}, "organization": {"id": 112, "owner": {"id": 233}, "user": {"role": null}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_WORKER_membership_NONE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 75, "privilege": "worker"}, "organization": {"id": 146, "owner": {"id": 241}, "user": {"role": null}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_OWNER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 75, "privilege": "none"}, "organization": {"id": 122, "owner": {"id": 75}, "user": {"role": "owner"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_OWNER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 54, "privilege": "none"}, "organization": {"id": 181, "owner": {"id": 54}, "user": {"role": "owner"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_MAINTAINER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 44, "privilege": "none"}, "organization": {"id": 159, "owner": {"id": 238}, "user": {"role": "maintainer"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_MAINTAINER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 65, "privilege": "none"}, "organization": {"id": 152, "owner": {"id": 296}, "user": {"role": "maintainer"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_SUPERVISOR { + not allow with input as {"scope": "view", "auth": {"user": {"id": 41, "privilege": "none"}, "organization": {"id": 188, "owner": {"id": 223}, "user": {"role": "supervisor"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_SUPERVISOR { + not allow with input as {"scope": "view", "auth": {"user": {"id": 84, "privilege": "none"}, "organization": {"id": 132, "owner": {"id": 284}, "user": {"role": "supervisor"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_WORKER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 15, "privilege": "none"}, "organization": {"id": 136, "owner": {"id": 216}, "user": {"role": "worker"}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_WORKER { + not allow with input as {"scope": "view", "auth": {"user": {"id": 83, "privilege": "none"}, "organization": {"id": 106, "owner": {"id": 258}, "user": {"role": "worker"}}}, "resource": {"visibility": "private"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_NONE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 35, "privilege": "none"}, "organization": {"id": 178, "owner": {"id": 246}, "user": {"role": null}}}, "resource": {"visibility": "public"}} +} + +test_scope_VIEW_context_ORGANIZATION_ownership_NONE_privilege_NONE_membership_NONE { + not allow with input as {"scope": "view", "auth": {"user": {"id": 3, "privilege": "none"}, "organization": {"id": 181, "owner": {"id": 234}, "user": {"role": null}}}, "resource": {"visibility": "private"}} +} + + + +# analytics_test.gen.py +# # Copyright (C) 2022 Intel Corporation +# # +# # SPDX-License-Identifier: MIT +# +# import csv +# import json +# import random +# import sys +# import os +# from itertools import product +# from tkinter.messagebox import NO +# +# NAME = 'analytics' +# +# def read_rules(name): +# rules = [] +# with open(os.path.join(sys.argv[1], f'{name}.csv')) as f: +# reader = csv.DictReader(f) +# for row in reader: +# row = {k.lower():v.lower().replace('n/a','na') for k,v in row.items()} +# row['limit'] = row['limit'].replace('none', 'None') +# found = False +# for col,val in row.items(): +# if col in ["limit", "method", "url"]: +# continue +# complex_val = [v.strip() for v in val.split(',')] +# if len(complex_val) > 1: +# found = True +# for item in complex_val: +# new_row = row.copy() +# new_row[col] = item +# rules.append(new_row) +# if not found: +# rules.append(row) +# +# return rules +# +# simple_rules = read_rules(NAME) +# +# SCOPES = {rule['scope'] for rule in simple_rules} +# CONTEXTS = ['sandbox', 'organization'] +# OWNERSHIPS = ['none'] +# GROUPS = ['admin', 'business', 'user', 'worker', 'none'] +# ORG_ROLES = ['owner', 'maintainer', 'supervisor', 'worker', None] +# +# def RESOURCES(scope): +# if scope == 'view': +# return [ +# {'visibility': 'public'}, +# {'visibility': 'private'}, +# ] +# +# return [None] +# +# def eval_rule(scope, context, ownership, privilege, membership, data): +# if privilege == 'admin': +# return True +# +# rules = list(filter(lambda r: scope == r['scope'], simple_rules)) +# rules = list(filter(lambda r: r['context'] == 'na' or context == r['context'], rules)) +# rules = list(filter(lambda r: r['ownership'] == 'na' or ownership == r['ownership'], rules)) +# rules = list(filter(lambda r: r['membership'] == 'na' or +# ORG_ROLES.index(membership) <= ORG_ROLES.index(r['membership']), rules)) +# rules = list(filter(lambda r: GROUPS.index(privilege) <= GROUPS.index(r['privilege']), rules)) +# resource = data['resource'] +# rules = list(filter(lambda r: eval(r['limit'], {'resource': resource}), rules)) +# +# return bool(rules) +# +# def get_data(scope, context, ownership, privilege, membership, resource): +# data = { +# "scope": scope, +# "auth": { +# "user": { "id": random.randrange(0,100), "privilege": privilege }, +# "organization": { +# "id": random.randrange(100,200), +# "owner": { "id": random.randrange(200, 300) }, +# "user": { "role": membership } +# } if context == 'organization' else None +# }, +# "resource": resource +# } +# +# user_id = data['auth']['user']['id'] +# if context == 'organization': +# if data['auth']['organization']['user']['role'] == 'owner': +# data['auth']['organization']['owner']['id'] = user_id +# +# return data +# +# def _get_name(prefix, **kwargs): +# name = prefix +# for k,v in kwargs.items(): +# if k == 'resource': +# continue +# prefix = '_' + str(k) +# if isinstance(v, dict): +# if 'id' in v: +# v = v.copy() +# v.pop('id') +# if v: +# name += _get_name(prefix, **v) +# else: +# name += ''.join(map(lambda c: c if c.isalnum() else {'@':'_IN_'}.get(c, '_'), +# f'{prefix}_{str(v).upper()}')) +# +# return name +# +# def get_name(scope, context, ownership, privilege, membership, resource): +# return _get_name('test', **locals()) +# +# def is_valid(scope, context, ownership, privilege, membership, resource): +# if context == "sandbox" and membership: +# return False +# if scope == 'list' and ownership != 'None': +# return False +# +# return True +# +# def gen_test_rego(name): +# with open(f'{name}_test.gen.rego', 'wt') as f: +# f.write(f'package {name}\n\n') +# for scope, context, ownership, privilege, membership in product( +# SCOPES, CONTEXTS, OWNERSHIPS, GROUPS, ORG_ROLES): +# for resource in RESOURCES(scope): +# if not is_valid(scope, context, ownership, privilege, membership, resource): +# continue +# +# data = get_data(scope, context, ownership, privilege, membership, resource) +# test_name = get_name(scope, context, ownership, privilege, membership, resource) +# result = eval_rule(scope, context, ownership, privilege, membership, data) +# f.write('{test_name} {{\n {allow} with input as {data}\n}}\n\n'.format( +# test_name=test_name, allow='allow' if result else 'not allow', +# data=json.dumps(data))) +# +# # Write the script which is used to generate the file +# with open(sys.argv[0]) as this_file: +# f.write(f'\n\n# {os.path.split(sys.argv[0])[1]}\n') +# for line in this_file: +# if line.strip(): +# f.write(f'# {line}') +# else: +# f.write(f'#\n') +# +# # Write rules which are used to generate the file +# with open(os.path.join(sys.argv[1], f'{name}.csv')) as rego_file: +# f.write(f'\n\n# {name}.csv\n') +# for line in rego_file: +# if line.strip(): +# f.write(f'# {line}') +# else: +# f.write(f'#\n') +# +# gen_test_rego(NAME) + +# analytics.csv +# 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 diff --git a/cvat/apps/log_viewer/apps.py b/cvat/apps/log_viewer/apps.py index 8f4bd9ccfd81..1bbe5cebd5a0 100644 --- a/cvat/apps/log_viewer/apps.py +++ b/cvat/apps/log_viewer/apps.py @@ -2,4 +2,4 @@ class LogViewerConfig(AppConfig): - name = 'log_viewer' + name = 'cvat.apps.log_viewer' diff --git a/cvat/apps/log_viewer/urls.py b/cvat/apps/log_viewer/urls.py index eefad13066eb..0de56682a37e 100644 --- a/cvat/apps/log_viewer/urls.py +++ b/cvat/apps/log_viewer/urls.py @@ -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('', views.LogViewerProxy.as_view()) -] +router = routers.DefaultRouter(trailing_slash=False) +router.register('analytics', views.LogViewerAccessViewSet, basename='analytics') + +urlpatterns = router.urls diff --git a/cvat/apps/log_viewer/views.py b/cvat/apps/log_viewer/views.py index ab81a734d70b..8bc55603df4d 100644 --- a/cvat/apps/log_viewer/views.py +++ b/cvat/apps/log_viewer/views.py @@ -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') diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index cd8ca7feb0f3..03a0769eb666 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -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 diff --git a/cvat/settings/base.py b/cvat/settings/base.py index d8db3e86b43c..6a7d447bf006 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -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) @@ -108,7 +109,6 @@ def add_ssh_keys(): 'compressor', 'django_sendfile', 'dj_pagination', - 'revproxy', 'rest_framework', 'rest_framework.authtoken', 'django_filters', @@ -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 @@ -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 = [ @@ -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', @@ -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 diff --git a/cvat/urls.py b/cvat/urls.py index 54e18d2cbcae..999cbad15646 100644 --- a/cvat/urls.py +++ b/cvat/urls.py @@ -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'))) diff --git a/docker-compose.yml b/docker-compose.yml index 8c67534e54c7..dc09494f68b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 @@ -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"