From cce45745c7a284efc540fa7899db9d7465c5fc6e Mon Sep 17 00:00:00 2001 From: Helder Souza <42891390+helllllllder@users.noreply.github.com> Date: Tue, 3 May 2022 15:59:10 -0300 Subject: [PATCH 1/9] Develop (#709) (#710) * Fix: Remove ai from project (#707) * remove document deletion from delete_nlp_logs task * inconsistency number and debug errors fixed * add user_email to remove_authorizations_project * Feature/health check blocklist (#708) * remove document deletion from delete_nlp_logs task * add a blocklist for not saving logs depending on the authorization user * inconsistency number and debug errors fixed * change the REPOSITORY_BLOCK_USER_LOGS values from users to repository authorizations * change readme * pass on sonarcloud * change admins settings * transform uuid into string * convert uuid into string at test_blocked_user --- README.md | 1 + bothub/api/grpc/connect_grpc_client.py | 8 +++- bothub/api/v2/nlp/serializers.py | 8 +++- bothub/api/v2/repository/views.py | 2 +- bothub/api/v2/tests/test_logs.py | 53 ++++++++++++++++++++++++++ bothub/common/tasks.py | 6 ++- bothub/settings.py | 9 +++-- docker-compose.yml | 31 ++++++++++----- 8 files changed, 99 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index d239aac1..1846f7f8 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ You can set environment variables in your OS, write on ```.env``` file or pass v | ELASTICSEARCH_LOGS_DELETE_AGE | ```string``` | ```90d``` | Specify the ILM delete age, when the index will be deleted. | GUNICORN_WORKERS | ``` int ``` | ``` multiprocessing.cpu_count() * 2 + 1 ``` | Gunicorn number of workers. | USE_ELASTICSEARCH | ```boolean``` | ```true``` | Change the logic in requirements_to_train to use either elasticsearch or postgres. +| REPOSITORY_BLOCK_USER_LOGS | ```list``` | ```[]``` | List of repository authorization(api bearer) that won't save logs ## Roadmap diff --git a/bothub/api/grpc/connect_grpc_client.py b/bothub/api/grpc/connect_grpc_client.py index 79fb94db..07b0cd51 100644 --- a/bothub/api/grpc/connect_grpc_client.py +++ b/bothub/api/grpc/connect_grpc_client.py @@ -57,14 +57,18 @@ def get_authorization_classifier( return next(classifier).get("uuid") - def remove_authorization(self, project_uuid: str, authorization_uuid: str): + def remove_authorization( + self, project_uuid: str, authorization_uuid: str, user_email: str + ): classifier_uuid = self.get_authorization_classifier( project_uuid, authorization_uuid ) stub = project_pb2_grpc.ProjectControllerStub(self.channel) stub.DestroyClassifier( - project_pb2.ClassifierDestroyRequest(uuid=classifier_uuid) + project_pb2.ClassifierDestroyRequest( + uuid=classifier_uuid, user_email=user_email + ) ) def create_classifier(self, **kwargs): diff --git a/bothub/api/v2/nlp/serializers.py b/bothub/api/v2/nlp/serializers.py index 1acbccb5..21446817 100644 --- a/bothub/api/v2/nlp/serializers.py +++ b/bothub/api/v2/nlp/serializers.py @@ -1,5 +1,7 @@ from rest_framework import serializers +from django.conf import settings + from bothub.common.models import ( QAKnowledgeBase, QALogs, @@ -45,8 +47,12 @@ class Meta: ) def create(self, validated_data): + repository_auth = validated_data.get("user") + user = repository_auth.user + if str(repository_auth.pk) in settings.REPOSITORY_BLOCK_USER_LOGS: + return validated_data log_intent = validated_data.pop("log_intent") - validated_data.update({"user": validated_data.get("user").user}) + validated_data.update({"user": user}) instance = self.Meta.model(**validated_data) instance.save() diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index 3fc582f0..a69918f1 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -336,7 +336,7 @@ def remove_repository_project(self, request, **kwargs): task = celery_app.send_task( name="remove_authorizations_project", - args=[project_uuid, list(authorizations_uuids)], + args=[project_uuid, list(authorizations_uuids), request.user.email], ) task.wait() diff --git a/bothub/api/v2/tests/test_logs.py b/bothub/api/v2/tests/test_logs.py index 710a2b1e..93e1a147 100644 --- a/bothub/api/v2/tests/test_logs.py +++ b/bothub/api/v2/tests/test_logs.py @@ -23,6 +23,7 @@ from bothub.common.documents.repositorynlplog import REPOSITORYNLPLOG_INDEX_NAME +@tag("elastic") class RepositoryNLPLogTestCase(TestCase): def setUp(self): self.factory = RequestFactory() @@ -59,6 +60,58 @@ def request(self, data): content_data = json.loads(response.content) return (response, content_data) + def list_request(self, data, token=None): + authorization_header = ( + {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} + ) + request = self.factory.get("/v2/repository/log/", data, **authorization_header) + response = RepositoryNLPLogViewSet.as_view({"get": "list"})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_blocked_user(self): + with self.settings(REPOSITORY_BLOCK_USER_LOGS=[str(self.repository_auth.pk)]): + data = { + "text": "test", + "user_agent": "python-requests/2.20.1", + "from_backend": True, + "user": str(self.repository_auth.pk), + "repository_version_language": int( + self.repository.current_version().pk + ), + "nlp_log": json.dumps( + { + "intent": {"name": "bias", "confidence": 0.9994810899625854}, + "intent_ranking": [ + {"name": "bias", "confidence": 0.9994810819625244}, + {"name": "doubt", "confidence": 0.039216167263031006}, + {"name": "negative", "confidence": 0.0}, + {"name": "affirmative", "confidence": 0.0}, + ], + "labels_list": [], + "entities_list": [], + "entities": {}, + "text": "test", + "repository_version": int(self.repository.current_version().pk), + "language": str(self.repository.language), + } + ), + "log_intent": [], + } + self.request(data) + response, content_data = self.list_request( + { + "repository_version_language": int( + self.repository.current_version().pk + ) + }, + self.owner_token, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(content_data.get("count"), 0) + def test_okay(self): data = { "text": "test", diff --git a/bothub/common/tasks.py b/bothub/common/tasks.py index a3f5fb8d..59d29e77 100644 --- a/bothub/common/tasks.py +++ b/bothub/common/tasks.py @@ -551,10 +551,12 @@ def get_project_organization(project_uuid: str): # pragma: no cover @app.task(name="remove_authorizations_project") -def remove_authorizations_project(project_uuid: str, authorizations_uuids: list): +def remove_authorizations_project( + project_uuid: str, authorizations_uuids: list, user_email: str +): grpc_client = ConnectGRPCClient() for authorization_uuid in authorizations_uuids: - grpc_client.remove_authorization(project_uuid, authorization_uuid) + grpc_client.remove_authorization(project_uuid, authorization_uuid, user_email) @app.task(name="create_repository_project") diff --git a/bothub/settings.py b/bothub/settings.py index ad8461c7..0f0b01fc 100644 --- a/bothub/settings.py +++ b/bothub/settings.py @@ -76,6 +76,7 @@ CONNECT_GRPC_SERVER_URL=(str, "localhost:8002"), CONNECT_CERTIFICATE_GRPC_CRT=(str, None), REPOSITORY_RESTRICT_ACCESS_NLP_LOGS=(list, []), + REPOSITORY_BLOCK_USER_LOGS=(list, []), REPOSITORY_KNOWLEDGE_BASE_DESCRIPTION_LIMIT=(int, 450), REPOSITORY_EXAMPLE_TEXT_WORDS_LIMIT=(int, 200), ELASTICSEARCH_DSL=(str, "localhost:9200"), @@ -263,7 +264,7 @@ envvar_EMAIL_HOST = env.str("EMAIL_HOST") -ADMINS = env.list("ADMINS") +ADMINS = [("Helder", "helder.souza@weni.ai")] # env.list("ADMINS") EMAIL_SUBJECT_PREFIX = "[bothub] " DEFAULT_FROM_EMAIL = env.str("DEFAULT_FROM_EMAIL") SERVER_EMAIL = env.str("SERVER_EMAIL") @@ -451,12 +452,12 @@ REPOSITORY_RESTRICT_ACCESS_NLP_LOGS = env.list("REPOSITORY_RESTRICT_ACCESS_NLP_LOGS") # Limit of characters for the knowledge base description -REPOSITORY_KNOWLEDGE_BASE_DESCRIPTION_LIMIT = env.list( +REPOSITORY_KNOWLEDGE_BASE_DESCRIPTION_LIMIT = env.int( "REPOSITORY_KNOWLEDGE_BASE_DESCRIPTION_LIMIT", default=450 ) # Limit of words for the example sentence -REPOSITORY_EXAMPLE_TEXT_WORDS_LIMIT = env.list( +REPOSITORY_EXAMPLE_TEXT_WORDS_LIMIT = env.int( "REPOSITORY_EXAMPLE_TEXT_WORDS_LIMIT", default=200 ) @@ -582,3 +583,5 @@ ELASTICSEARCH_DSL_SIGNAL_PROCESSOR = ELASTICSEARCH_SIGNAL_PROCESSOR_CLASSES[ env.str("ELASTICSEARCH_SIGNAL_PROCESSOR", default="realtime") ] + +REPOSITORY_BLOCK_USER_LOGS = env.list("REPOSITORY_BLOCK_USER_LOGS", default=[]) diff --git a/docker-compose.yml b/docker-compose.yml index 8d500f68..306cb714 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,16 +7,24 @@ version: '3.6' services: database: - image: postgres - ports: - - 5432:5432 - networks: - - default - - bothub - environment: - - POSTGRES_USER=${POSTGRES_USER:-bothub} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-bothub} - - POSTGRES_DB=${POSTGRES_DB:-bothub} + image: postgres + ports: + - 5432:5432 + volumes: + - postgres:/var/lib/postgresql/data + networks: + - default + - bothub + environment: + - POSTGRES_USER=${POSTGRES_USER:-bothub} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-bothub} + - POSTGRES_DB=${POSTGRES_DB:-bothub} + deploy: + mode: replicated + replicas: 1 + placement: + constraints: [node.role == manager] + bothub: image: ${DOCKER_IMAGE_NAME:-ilha/bothub}:${TAG:-latest} build: @@ -153,6 +161,9 @@ services: - default - bothub +volumes: + postgres: + networks: bothub: external: true From a1c7c577fcc52902f0c754c6059a36902d0ccab2 Mon Sep 17 00:00:00 2001 From: Helder Souza <42891390+helllllllder@users.noreply.github.com> Date: Thu, 21 Jul 2022 12:32:31 -0300 Subject: [PATCH 2/9] Feature/remove rest client from celery (#728) * Fix: Remove ai from project (#707) * remove document deletion from delete_nlp_logs task * inconsistency number and debug errors fixed * add user_email to remove_authorizations_project * Feature/health check blocklist (#708) * remove document deletion from delete_nlp_logs task * add a blocklist for not saving logs depending on the authorization user * inconsistency number and debug errors fixed * change the REPOSITORY_BLOCK_USER_LOGS values from users to repository authorizations * change readme * pass on sonarcloud * change admins settings * transform uuid into string * convert uuid into string at test_blocked_user * Develop (#709) * Fix: Remove ai from project (#707) * remove document deletion from delete_nlp_logs task * inconsistency number and debug errors fixed * add user_email to remove_authorizations_project * Feature/health check blocklist (#708) * remove document deletion from delete_nlp_logs task * add a blocklist for not saving logs depending on the authorization user * inconsistency number and debug errors fixed * change the REPOSITORY_BLOCK_USER_LOGS values from users to repository authorizations * change readme * pass on sonarcloud * change admins settings * transform uuid into string * convert uuid into string at test_blocked_user * Create internal module, module permission and updated WeniOIDCAuthenticationBackend to add permission if the user is a module (#712) * Feature/internal endpoint organization (#713) * Create internal module, module permission and updated WeniOIDCAuthenticationBackend to add permission if the user is a module * Add organization internal endpoints * Feature/internal endpoint repository (#714) * Fix: Remove ai from project (#707) * remove document deletion from delete_nlp_logs task * inconsistency number and debug errors fixed * add user_email to remove_authorizations_project * Feature/health check blocklist (#708) * remove document deletion from delete_nlp_logs task * add a blocklist for not saving logs depending on the authorization user * inconsistency number and debug errors fixed * change the REPOSITORY_BLOCK_USER_LOGS values from users to repository authorizations * change readme * pass on sonarcloud * change admins settings * transform uuid into string * convert uuid into string at test_blocked_user * Create internal module, module permission and updated WeniOIDCAuthenticationBackend to add permission if the user is a module * Add organization internal endpoints * add repository endpoints * code smells * duplicated lines * fix code smells * Feature/internal endpoint user (#715) * Create internal module, module permission and updated WeniOIDCAuthenticationBackend to add permission if the user is a module * Add organization internal endpoints * add repository endpoints * add user endpoints * flake8 and black * Feature/connect rest client (#716) * Create internal module, module permission and updated WeniOIDCAuthenticationBackend to add permission if the user is a module * Add organization internal endpoints * add repository endpoints * add user endpoints * flake8 and black * implement connect rest client * fix code smells * black * Feature/add csp (#717) * Develop (#709) (#710) * Fix: Remove ai from project (#707) * remove document deletion from delete_nlp_logs task * inconsistency number and debug errors fixed * add user_email to remove_authorizations_project * Feature/health check blocklist (#708) * remove document deletion from delete_nlp_logs task * add a blocklist for not saving logs depending on the authorization user * inconsistency number and debug errors fixed * change the REPOSITORY_BLOCK_USER_LOGS values from users to repository authorizations * change readme * pass on sonarcloud * change admins settings * transform uuid into string * convert uuid into string at test_blocked_user * add regex remotion of special characters from username when creating from keycloak * add django_csp and settings * configure csp * fix settings * black * Feature/internal endpoint user (#718) * Create internal module, module permission and updated WeniOIDCAuthenticationBackend to add permission if the user is a module * Add organization internal endpoints * add repository endpoints * add user endpoints * flake8 and black * fix user endpoints * Feature/add csp (#719) * Develop (#709) (#710) * Fix: Remove ai from project (#707) * remove document deletion from delete_nlp_logs task * inconsistency number and debug errors fixed * add user_email to remove_authorizations_project * Feature/health check blocklist (#708) * remove document deletion from delete_nlp_logs task * add a blocklist for not saving logs depending on the authorization user * inconsistency number and debug errors fixed * change the REPOSITORY_BLOCK_USER_LOGS values from users to repository authorizations * change readme * pass on sonarcloud * change admins settings * transform uuid into string * convert uuid into string at test_blocked_user * add regex remotion of special characters from username when creating from keycloak * add django_csp and settings * configure csp * fix settings * black * fix csp settings * fix csp settings * Feature/internal endpoint user (#720) * Create internal module, module permission and updated WeniOIDCAuthenticationBackend to add permission if the user is a module * Add organization internal endpoints * add repository endpoints * add user endpoints * flake8 and black * fix user endpoints * fix connect client * Fix the repository get_user_authorization method The updated method compares repository and organization authorizations and uses the highest role level without overwriting the repository's authorization level * Update rule with repository authorization role overwriting The method now overwrites the repository's authorization role if is lower than 4 (i.e. ROLE_TRANSLATOR), as of this level on, it no longer has corresponding roles at the client app. * Linting * feat: update list_project_organization endpoint removing the celery call * feat: update list_project_organization endpoint removing the celery call (#725) * feat: update all code that called celery tasks for communicating with connect to calls directly into ConnectRESTClient * Feature/remove rest client from celery (#726) * feat: update list_project_organization endpoint removing the celery call * feat: update all code that called celery tasks for communicating with connect to calls directly into ConnectRESTClient * bugfix: remove task.result after condition to user or not grpc endpoints, as it was conflicting with the rest call * feat: cache request result in list_classifiers Co-authored-by: Victor Salles Co-authored-by: vitusalis <49169676+vitusalis@users.noreply.github.com> --- README.md | 2 + bothub/api/grpc/repository/serializers.py | 23 +- bothub/api/v2/internal/__init__.py | 0 bothub/api/v2/internal/connect_rest_client.py | 88 +++++++ .../api/v2/internal/organization/__init__.py | 0 .../v2/internal/organization/permissions.py | 11 + .../v2/internal/organization/serializers.py | 83 +++++++ bothub/api/v2/internal/organization/tests.py | 232 ++++++++++++++++++ bothub/api/v2/internal/organization/views.py | 120 +++++++++ bothub/api/v2/internal/permissions.py | 9 + bothub/api/v2/internal/repository/__init__.py | 0 .../api/v2/internal/repository/serializers.py | 29 +++ bothub/api/v2/internal/repository/tests.py | 112 +++++++++ bothub/api/v2/internal/repository/views.py | 39 +++ bothub/api/v2/internal/user/__init__.py | 0 bothub/api/v2/internal/user/serializers.py | 37 +++ bothub/api/v2/internal/user/tests.py | 208 ++++++++++++++++ bothub/api/v2/internal/user/views.py | 98 ++++++++ bothub/api/v2/repository/serializers.py | 15 +- bothub/api/v2/repository/views.py | 116 ++++++--- bothub/api/v2/routers.py | 13 + bothub/api/v2/tests/test_repository.py | 47 +--- bothub/api/v2/tests/utils.py | 51 +++- bothub/authentication/authorization.py | 8 + bothub/common/models.py | 16 +- bothub/common/tasks.py | 26 +- bothub/common/tests.py | 53 ++++ bothub/settings.py | 45 +++- bothub/utils.py | 52 ++++ poetry.lock | 21 +- pyproject.toml | 1 + 31 files changed, 1439 insertions(+), 116 deletions(-) create mode 100644 bothub/api/v2/internal/__init__.py create mode 100644 bothub/api/v2/internal/connect_rest_client.py create mode 100644 bothub/api/v2/internal/organization/__init__.py create mode 100644 bothub/api/v2/internal/organization/permissions.py create mode 100644 bothub/api/v2/internal/organization/serializers.py create mode 100644 bothub/api/v2/internal/organization/tests.py create mode 100644 bothub/api/v2/internal/organization/views.py create mode 100644 bothub/api/v2/internal/permissions.py create mode 100644 bothub/api/v2/internal/repository/__init__.py create mode 100644 bothub/api/v2/internal/repository/serializers.py create mode 100644 bothub/api/v2/internal/repository/tests.py create mode 100644 bothub/api/v2/internal/repository/views.py create mode 100644 bothub/api/v2/internal/user/__init__.py create mode 100644 bothub/api/v2/internal/user/serializers.py create mode 100644 bothub/api/v2/internal/user/tests.py create mode 100644 bothub/api/v2/internal/user/views.py diff --git a/README.md b/README.md index 1846f7f8..9dc314fd 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,8 @@ You can set environment variables in your OS, write on ```.env``` file or pass v | OIDC_RP_SCOPES | ```string``` | ```openid email``` | The OpenID Connect scopes to request during login. | CONNECT_GRPC_SERVER_URL | ```string``` | ```localhost:8002``` | Define grpc connect server url | CONNECT_CERTIFICATE_GRPC_CRT | ```string``` | ```None``` | Absolute certificate path for secure grpc communication +| CONNECT_API_URL | ```string``` | ```None``` | Connect module api url +| USE_GRPC | ```bool``` | ```False``` | Use connect gRPC clients | RECAPTCHA_SECRET_KEY | ```string``` | ```''``` | Token of the recaptcha used in the validation of a user's registration. | REPOSITORY_NLP_LOG_LIMIT | ```int``` | ```10000``` | Limit of query size to repository log. | REPOSITORY_RESTRICT_ACCESS_NLP_LOGS | ```list``` | ```[]``` | Restricts log access to a particular or multiple intelligences diff --git a/bothub/api/grpc/repository/serializers.py b/bothub/api/grpc/repository/serializers.py index 901f6d3f..1c8536e6 100644 --- a/bothub/api/grpc/repository/serializers.py +++ b/bothub/api/grpc/repository/serializers.py @@ -5,6 +5,8 @@ from bothub.common.models import Repository from weni.protobuf.intelligence import repository_pb2 +from bothub.utils import internal_serializer_fields + class RepositoryProtoSerializer(proto_serializers.ModelProtoSerializer): owner__nickname = serializers.SerializerMethodField() @@ -15,26 +17,7 @@ class RepositoryProtoSerializer(proto_serializers.ModelProtoSerializer): class Meta: model = Repository proto_class = repository_pb2.Repository - fields = [ - "uuid", - "name", - "slug", - "description", - "is_private", - "created_at", - "language", - "owner", - "algorithm", - "use_competing_intents", - "use_name_entities", - "use_analyze_char", - "owner__nickname", - "intents", - "categories", - "available_languages", - "categories_list", - "repository_type", - ] + fields = internal_serializer_fields def get_owner__nickname(self, repository: Repository): return repository.owner.nickname diff --git a/bothub/api/v2/internal/__init__.py b/bothub/api/v2/internal/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bothub/api/v2/internal/connect_rest_client.py b/bothub/api/v2/internal/connect_rest_client.py new file mode 100644 index 00000000..a6693aff --- /dev/null +++ b/bothub/api/v2/internal/connect_rest_client.py @@ -0,0 +1,88 @@ +import requests +from typing import List, Dict + +from django.conf import settings +from django.core.cache import cache + + +class ConnectRESTClient: + def __init__(self): + self.base_url = settings.CONNECT_API_URL + self.headers = { + "Content-Type": "application/json; charset: utf-8", + "Authorization": self.get_auth_token(), + } + + def get_auth_token(self) -> str: + request = requests.post( + url=settings.OIDC_OP_TOKEN_ENDPOINT, + data={ + "client_id": settings.OIDC_RP_CLIENT_ID, + "client_secret": settings.OIDC_RP_CLIENT_SECRET, + "grant_type": "client_credentials", + }, + ) + token = request.json().get("access_token") + return f"Bearer {token}" + + def list_classifiers( + self, project_uuid: str, user_email: str + ) -> List[Dict[str, str]]: + cache_key = f"LIST_CLASSIFIER_CACHE_KEY:project_uuid:{project_uuid}:user_email:{user_email}" + cache_result = cache.get(cache_key, None) + if cache_result is None: + request = requests.get( + url=f"{self.base_url}/v1/organization/project/list_classifier/", + headers=self.headers, + params={"project_uuid": project_uuid, "user_email": user_email}, + ) + cache.set(cache_key, request.json().get("data"), settings.CACHE_TTL) + return request.json().get("data") + else: + return cache_result + + def list_authorizations(self, project_uuid: str, user_email: str) -> List[str]: + + classifiers = self.list_classifiers( + project_uuid=project_uuid, user_email=user_email + ) + + return [classifier.get("authorization_uuid") for classifier in classifiers] + + def get_authorization_classifier( + self, project_uuid: str, authorization_uuid: str, user_email: str + ) -> str: + """ + Receives a authorization UUID and returns the respective classifier UUID + """ + classifiers = self.list_classifiers(project_uuid, user_email) + classifier = filter( + lambda classifier: classifier["authorization_uuid"] == authorization_uuid, + classifiers, + ) + + return next(classifier).get("uuid") + + def remove_authorization( + self, project_uuid: str, authorization_uuid: str, user_email: str + ): + classifier_uuid = self.get_authorization_classifier( + project_uuid, + authorization_uuid, + user_email, + ) + request = requests.delete( + url=f"{self.base_url}/v1/organization/project/destroy_classifier/", + headers=self.headers, + params={"uuid": classifier_uuid, "user_email": user_email}, + ) + + return request.json() + + def create_classifier(self, **kwargs): + request = requests.post( + url=f"{self.base_url}/v1/organization/project/create_classifier/", + headers=self.headers, + params={**kwargs, "classifier_type": "bothub"}, + ) + return request.json() diff --git a/bothub/api/v2/internal/organization/__init__.py b/bothub/api/v2/internal/organization/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bothub/api/v2/internal/organization/permissions.py b/bothub/api/v2/internal/organization/permissions.py new file mode 100644 index 00000000..6edbf607 --- /dev/null +++ b/bothub/api/v2/internal/organization/permissions.py @@ -0,0 +1,11 @@ +from rest_framework import permissions + +from bothub.utils import get_user + + +class InternalOrganizationAdminHasPermission(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + authorization = obj.organization.get_organization_authorization( + get_user(request.query_params.get("user_email", None)) + ) + return authorization.is_admin diff --git a/bothub/api/v2/internal/organization/serializers.py b/bothub/api/v2/internal/organization/serializers.py new file mode 100644 index 00000000..26456b3a --- /dev/null +++ b/bothub/api/v2/internal/organization/serializers.py @@ -0,0 +1,83 @@ +from django.db.models import F +from django.utils.translation import ugettext_lazy as _ + +from rest_framework import serializers +from rest_framework.exceptions import ValidationError + +from bothub.authentication.models import User +from bothub.common.models import Organization, OrganizationAuthorization + + +class OrganizationSerializer(serializers.ModelSerializer): + + users = serializers.SerializerMethodField() + + def get_users(self, org: Organization): + return list( + org.organization_authorizations.exclude( + role=OrganizationAuthorization.LEVEL_NOTHING + ) + .annotate( + org_user_id=F("user__user_owner__pk"), + org_user_email=F("user__user_owner__email"), + org_user_nickname=F("user__user_owner__nickname"), + org_user_name=F("user__user_owner__name"), + ) + .values( + "org_user_id", "org_user_email", "org_user_nickname", "org_user_name" + ) + ) + + class Meta: + model = Organization + fields = ["id", "name", "users"] + + +class OrgCreateSerializer(serializers.ModelSerializer): + + organization_name = serializers.CharField() + user_email = serializers.CharField() + + def validate_user_email(self, value: str) -> str: + try: + User.objects.get(email=value) + except User.DoesNotExist: + raise ValidationError(_("{} not found!").format(value)) + + return value + + class Meta: + model = Organization + fields = ["organization_name", "user_email"] + + +class OrgUpdateSerializer(serializers.ModelSerializer): + + id = serializers.IntegerField() + name = serializers.CharField(required=False) + + def validate_id(self, value): + try: + Organization.objects.get(pk=value) + except Organization.DoesNotExist: + raise ValidationError(f"{value} not found!") + + return value + + def save(self): + data = dict(self.validated_data) + + org = Organization.objects.get(pk=data.get("id")) + + updated_fields = self.get_updated_fields(data) + + if updated_fields: + org.__dict__.update(**updated_fields) + org.save() + + def get_updated_fields(self, data): + return {key: value for key, value in data.items() if key not in ["id"]} + + class Meta: + model = Organization + fields = ["id", "name"] diff --git a/bothub/api/v2/internal/organization/tests.py b/bothub/api/v2/internal/organization/tests.py new file mode 100644 index 00000000..4ac09bf6 --- /dev/null +++ b/bothub/api/v2/internal/organization/tests.py @@ -0,0 +1,232 @@ +import json + +from django.test import tag +from django.test import RequestFactory +from django.test import TestCase +from rest_framework import status + +from bothub.api.v2.internal.organization.views import InternalOrganizationViewSet +from bothub.api.v2.tests.utils import create_user_and_token +from bothub.common.models import ( + Organization, + OrganizationAuthorization, + RepositoryCategory, +) +from bothub.api.v2.tests.utils import get_valid_mockups, create_repository_from_mockup + + +class InternalOrganizationTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + self.moduser, self.moduser_token = create_user_and_token("module", module=True) + self.owner, self.owner_token = create_user_and_token("owner") + self.category = RepositoryCategory.objects.create(name="Category 1") + self.org = Organization.objects.create( + name="Organization 1", nickname="organization1" + ) + self.org_2 = Organization.objects.create( + name="Organization 2", nickname="organization2" + ) + OrganizationAuthorization.objects.create( + user=self.owner, + organization=self.org, + role=OrganizationAuthorization.ROLE_ADMIN, + ) + OrganizationAuthorization.objects.create( + user=self.owner, + organization=self.org_2, + role=OrganizationAuthorization.ROLE_ADMIN, + ) + self.repositories = [ + create_repository_from_mockup(self.org, **mockup) + for mockup in get_valid_mockups([self.category]) + ] + for rep in self.repositories: + rep.get_user_authorization(self.org) + + self.repository_auth = self.repositories[0].get_user_authorization(self.owner) + + +def auth_header(token): + authorization_header = ( + {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} + ) + authorization_header["content_type"] = "application/json" + + return authorization_header + + +@tag("internal") +class InternalOrganizationListTestCase(InternalOrganizationTestCase): + def request(self, params, token=None): + authorization_header = auth_header(token) + + request = self.factory.get( + "/v2/internal/organization/", params, **authorization_header + ) + response = InternalOrganizationViewSet.as_view({"get": "list"})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + {"user_email": self.owner.email}, + self.moduser_token, + ) + self.assertEqual(len(content_data), 2) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_not_ok(self): + response, content_data = self.request( + {"user_email": self.owner.email}, + self.owner_token, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalOrganizationCreateTestCase(InternalOrganizationTestCase): + def request(self, data, token=None): + authorization_header = auth_header(token) + + request = self.factory.post( + "/v2/internal/organization/", data, **authorization_header + ) + + response = InternalOrganizationViewSet.as_view({"post": "create"})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + {"user_email": self.owner.email, "organization_name": "org3"}, + self.moduser_token, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + organization = Organization.objects.get(pk=content_data.get("id")) + self.assertEqual(organization.name, "org3") + organization_authorization = OrganizationAuthorization.objects.filter( + organization=organization, user=self.owner + ) + self.assertEqual(organization_authorization.count(), 1) + self.assertEqual( + organization_authorization.first().role, + OrganizationAuthorization.ROLE_ADMIN, + ) + + def test_not_ok(self): + response, content_data = self.request( + {"user_email": self.owner.email, "organization_name": "org3"}, + self.owner_token, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalOrganizationDestroyTestCase(InternalOrganizationTestCase): + def request(self, email, id, token=None): + authorization_header = auth_header(token) + + request = self.factory.delete( + f"/v2/internal/organization/?user_email={email}", **authorization_header + ) + + response = InternalOrganizationViewSet.as_view({"delete": "destroy"})( + request, pk=id + ) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + self.owner.email, + self.org_2.id, + self.moduser_token, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + organization = Organization.objects.filter(pk=self.org_2.pk) + self.assertEqual(organization.count(), 0) + + def test_not_ok(self): + response, content_data = self.request( + self.owner.email, + self.org_2.id, + self.owner_token, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalOrganizationUpdateTestCase(InternalOrganizationTestCase): + def request(self, data, email, id, token=None): + authorization_header = auth_header(token) + + request = self.factory.put( + f"/v2/internal/organization/?user_email={email}", + data, + **authorization_header, + ) + response = InternalOrganizationViewSet.as_view({"put": "update"})( + request, pk=id + ) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + {"name": "org222"}, + self.owner.email, + self.org_2.id, + self.moduser_token, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + organization = Organization.objects.get(pk=self.org_2.pk) + + self.assertEqual(organization.name, "org222") + + def test_not_ok(self): + response, content_data = self.request( + {"name": "org222"}, + self.owner.email, + self.org_2.id, + self.owner_token, + ) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalOrganizationRetrieveTestCase(InternalOrganizationTestCase): + def request(self, email, id, token=None): + authorization_header = auth_header(token) + + request = self.factory.get( + f"/v2/internal/organization/?user_email={email}", **authorization_header + ) + response = InternalOrganizationViewSet.as_view({"get": "retrieve"})( + request, pk=id + ) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + self.owner.email, + self.org.id, + self.moduser_token, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(content_data["repositories_count"], 2) + + def test_not_ok(self): + response, content_data = self.request( + self.owner.email, + self.org.id, + self.owner_token, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/bothub/api/v2/internal/organization/views.py b/bothub/api/v2/internal/organization/views.py new file mode 100644 index 00000000..3fe839f1 --- /dev/null +++ b/bothub/api/v2/internal/organization/views.py @@ -0,0 +1,120 @@ +from django.utils.translation import ugettext_lazy as _ +from django.db import models +from rest_framework import mixins +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + +from bothub.api.v2.metadata import Metadata +from bothub.authentication.models import User +from bothub.common.models import ( + Organization, + OrganizationAuthorization, + Repository, + RepositoryAuthorization, +) +from bothub.api.v2.internal.organization.serializers import ( + OrganizationSerializer, + OrgCreateSerializer, + OrgUpdateSerializer, +) +from bothub import utils +from bothub.api.v2.internal.permissions import ModuleHasPermission +from bothub.api.v2.internal.organization.permissions import ( + InternalOrganizationAdminHasPermission, +) + + +class InternalOrganizationViewSet( + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.CreateModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + GenericViewSet, +): + queryset = Organization.objects.all() + serializer_class = OrganizationSerializer + lookup_field = "pk" + permission_classes = [ModuleHasPermission] + metadata_class = Metadata + + def get_permissions(self): + """ + Instantiates and returns the list of permissions that this view requires. + """ + + permission_classes = self.permission_classes + if self.action in ["destroy", "update"]: + permission_classes.append(InternalOrganizationAdminHasPermission) + return [permission() for permission in self.permission_classes] + + def list(self, request, *args, **kwargs): + user = utils.get_user(request.query_params.get("user_email")) + serializer = OrganizationSerializer(user.get_user_organizations, many=True) + + return Response(serializer.data) + + def create(self, request, *args, **kwargs): + user, created = User.objects.get_or_create( + email=request.data.get("user_email", None), + defaults={"nickname": request.data.get("user_email", None)}, + ) + + serializer = OrgCreateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + validated_data = { + "name": serializer.validated_data.get("organization_name"), + "nickname": utils.organization_unique_slug_generator( + serializer.validated_data.get("organization_name") + ), + "description": "", + "locale": "", + } + + org = Organization.objects.create(**validated_data) + + org.organization_authorizations.create( + user=user, role=OrganizationAuthorization.ROLE_ADMIN + ) + org_serializer = OrganizationSerializer(org) + + return Response(org_serializer.data) + + def destroy(self, request, *args, **kwargs): + org = self.get_object() + user, created = User.objects.get_or_create( + email=request.query_params.get("user_email"), + defaults={"nickname": request.query_params.get("user_email")}, + ) + + org.delete() + return Response({_("Organization deleted with success")}) + + def update(self, request, *args, **kwargs): + org = self.get_object() + data = {"id": org.pk, **request.data} + serializer = OrgUpdateSerializer(data=data) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response(serializer.data) + + def retrieve(self, request, *args, **kwargs): + org = self.get_object() + + auths = ( + RepositoryAuthorization.objects.exclude(repository__owner=org) + .exclude(role=RepositoryAuthorization.ROLE_NOT_SETTED) + .filter(user=org) + ) + + response = { + "repositories_count": int( + Repository.objects.filter( + models.Q(uuid__in=auths) | models.Q(owner=org) + ).count() + ) + } + + return Response(response) diff --git a/bothub/api/v2/internal/permissions.py b/bothub/api/v2/internal/permissions.py new file mode 100644 index 00000000..4974aa76 --- /dev/null +++ b/bothub/api/v2/internal/permissions.py @@ -0,0 +1,9 @@ +from rest_framework import permissions + + +class ModuleHasPermission(permissions.BasePermission): + def has_permission(self, request, view): # pragma: no cover + return request.user.has_perm("authentication.can_communicate_internally") + + def has_object_permission(self, request, view, obj): + return self.has_permission(request, view) diff --git a/bothub/api/v2/internal/repository/__init__.py b/bothub/api/v2/internal/repository/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bothub/api/v2/internal/repository/serializers.py b/bothub/api/v2/internal/repository/serializers.py new file mode 100644 index 00000000..58351bfd --- /dev/null +++ b/bothub/api/v2/internal/repository/serializers.py @@ -0,0 +1,29 @@ +from rest_framework import serializers + +from bothub.api.v2.repository.serializers import RepositoryCategorySerializer +from bothub.common.models import Repository + +from bothub.utils import internal_serializer_fields + + +class InternalRepositorySerializer(serializers.ModelSerializer): + owner__nickname = serializers.SerializerMethodField() + intents = serializers.SerializerMethodField() + available_languages = serializers.SerializerMethodField() + categories_list = serializers.SerializerMethodField() + + class Meta: + model = Repository + fields = internal_serializer_fields + + def get_owner__nickname(self, repository: Repository): + return repository.owner.nickname + + def get_intents(self, repository: Repository): + return repository.get_formatted_intents() + + def get_available_languages(self, repository: Repository): + return repository.available_languages() + + def get_categories_list(self, repository: Repository): + return RepositoryCategorySerializer(repository.categories, many=True).data diff --git a/bothub/api/v2/internal/repository/tests.py b/bothub/api/v2/internal/repository/tests.py new file mode 100644 index 00000000..9b83fe13 --- /dev/null +++ b/bothub/api/v2/internal/repository/tests.py @@ -0,0 +1,112 @@ +import json + +from django.test import tag +from django.test import RequestFactory +from django.test import TestCase +from rest_framework import status + +from bothub.api.v2.internal.repository.views import InternalRepositoriesViewSet +from bothub.api.v2.tests.utils import create_user_and_token +from bothub.common.models import ( + Organization, + OrganizationAuthorization, +) +from bothub.common.models import RepositoryCategory + +from bothub.api.v2.tests.utils import get_valid_mockups, create_repository_from_mockup + + +class InternalRepositoryTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + self.moduser, self.moduser_token = create_user_and_token("module", module=True) + self.owner, self.owner_token = create_user_and_token("owner") + self.category = RepositoryCategory.objects.create(name="Category 1") + self.org = Organization.objects.create( + name="Organization 1", nickname="organization1" + ) + OrganizationAuthorization.objects.create( + user=self.owner, + organization=self.org, + role=OrganizationAuthorization.ROLE_ADMIN, + ) + self.repositories = [ + create_repository_from_mockup(self.org, **mockup) + for mockup in get_valid_mockups([self.category]) + ] + for rep in self.repositories: + rep.get_user_authorization(self.org) + + self.repository_auth = self.repositories[0].get_user_authorization(self.owner) + + +@tag("internal") +class InternalRepositoryListTestCase(InternalRepositoryTestCase): + def request(self, params, token=None): + authorization_header = ( + {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} + ) + + request = self.factory.get( + "/v2/internal/repository/", + params, + **authorization_header, + ) + response = InternalRepositoriesViewSet.as_view({"get": "list"})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + {"name": "Repository 1", "org_id": self.org.id}, self.moduser_token + ) + self.assertEqual(content_data["count"], 1) + self.assertEqual(len(content_data["results"]), 1) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_return_all_repos(self): + response, content_data = self.request( + {"org_id": self.org.id}, self.moduser_token + ) + + self.assertEqual(content_data["count"], 2) + self.assertEqual(len(content_data["results"]), 2) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_fail_module(self): + response, content_data = self.request({"org_id": self.org.pk}, self.owner_token) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalRepositoryRetrieveAuthorizationTestCase(InternalRepositoryTestCase): + def request(self, params, token=None): + authorization_header = ( + {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} + ) + request = self.factory.get( + "/v2/internal/repository/RetrieveAuthorization/", + params, + **authorization_header, + ) + response = InternalRepositoriesViewSet.as_view( + {"get": "retrieve_authorization"} + )(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok_repository_1(self): + response, content_data = self.request( + {"repository_authorization": self.repository_auth.pk}, self.moduser_token + ) + self.assertEqual(content_data["name"], "Repository 1") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_not_ok(self): + response, content_data = self.request( + {"repository_authorization": self.repository_auth.pk}, self.owner_token + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/bothub/api/v2/internal/repository/views.py b/bothub/api/v2/internal/repository/views.py new file mode 100644 index 00000000..5b769250 --- /dev/null +++ b/bothub/api/v2/internal/repository/views.py @@ -0,0 +1,39 @@ +from rest_framework import mixins +from rest_framework.decorators import action +from rest_framework.filters import SearchFilter +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + +from bothub.common.models import Repository + +from bothub.api.v2.internal.repository.serializers import InternalRepositorySerializer +from bothub.api.v2.internal.permissions import ModuleHasPermission + + +class InternalRepositoriesViewSet(mixins.ListModelMixin, GenericViewSet): + serializer_class = InternalRepositorySerializer + queryset = Repository.objects + permission_classes = [ModuleHasPermission] + filter_backends = [SearchFilter] + search_fields = ["$name", "^name", "=name"] + + def get_queryset(self, *args, **kwargs): + queryset = self.queryset.all() + name = self.request.query_params.get("name", None) + if name: + queryset = self.queryset.filter(name__icontains=name) + org_id = self.request.query_params.get("org_id", None) + if org_id: + queryset = queryset.filter(authorizations__user__pk=org_id) + + return queryset[:20] + + @action(detail=True, methods=["GET"], url_name="retrieve-authorization") + def retrieve_authorization(self, request, **kwargs): + auth = self.request.query_params.get("repository_authorization", None) + repository = Repository.objects.none() + if auth: + repository = Repository.objects.get(authorizations__uuid=auth) + serialized_data = InternalRepositorySerializer(repository) + + return Response(serialized_data.data) diff --git a/bothub/api/v2/internal/user/__init__.py b/bothub/api/v2/internal/user/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bothub/api/v2/internal/user/serializers.py b/bothub/api/v2/internal/user/serializers.py new file mode 100644 index 00000000..e58a7c70 --- /dev/null +++ b/bothub/api/v2/internal/user/serializers.py @@ -0,0 +1,37 @@ +from rest_framework import serializers + +from bothub.authentication.models import User + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = [ + "id", + "email", + "nickname", + "name", + "language", + "joined_at", + "is_active", + "is_superuser", + ] + + +class UserPermissionSerializer(serializers.Serializer): + role = serializers.IntegerField() + + +class UserLanguageSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ["language"] + read_only = [ + "id", + "email", + "nickname", + "name", + "joined_at", + "is_active", + "is_superuser", + ] diff --git a/bothub/api/v2/internal/user/tests.py b/bothub/api/v2/internal/user/tests.py new file mode 100644 index 00000000..20000083 --- /dev/null +++ b/bothub/api/v2/internal/user/tests.py @@ -0,0 +1,208 @@ +import json +from bothub.authentication.models import User + +from django.test import tag +from django.test import RequestFactory +from django.test import TestCase +from rest_framework import status + +from bothub.api.v2.internal.user.views import ( + UserPermissionViewSet, + UserViewSet, + UserLanguageViewSet, +) +from bothub.api.v2.tests.utils import create_user_and_token +from bothub.common.models import ( + Organization, + OrganizationAuthorization, + RepositoryCategory, +) + + +class InternalUserTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + self.moduser, self.moduser_token = create_user_and_token("module", module=True) + self.owner, self.owner_token = create_user_and_token("owner") + self.user, self.user_token = create_user_and_token("user") + self.category = RepositoryCategory.objects.create(name="Category 1") + self.org = Organization.objects.create( + name="Organization 1", nickname="organization1" + ) + OrganizationAuthorization.objects.create( + user=self.owner, + organization=self.org, + role=OrganizationAuthorization.ROLE_ADMIN, + ) + OrganizationAuthorization.objects.create( + user=self.user, + organization=self.org, + role=OrganizationAuthorization.ROLE_CONTRIBUTOR, + ) + + +@tag("internal") +class InternalUserPermissionRetrieveTestCase(InternalUserTestCase): + def request(self, params, token=None): + authorization_header = ( + {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} + ) + + request = self.factory.get( + "/v2/internal/user/permission", params, **authorization_header + ) + response = UserPermissionViewSet.as_view({"get": "retrieve"})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + {"user_email": self.owner.email, "org_id": self.org.pk}, + token=self.moduser_token, + ) + self.assertEqual(content_data["role"], OrganizationAuthorization.ROLE_ADMIN) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_not_ok(self): + response, content_data = self.request( + {"user_email": self.owner.email, "org_id": self.org.pk}, + token=self.owner_token, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalUserPermissionUpdateTestCase(InternalUserTestCase): + def request(self, params, user_email, org_id, token=None): + authorization_header = ( + {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} + ) + authorization_header["content_type"] = "application/json" + + request = self.factory.put( + f"/v2/internal/user/permission?user_email={user_email}&org_id={org_id}", + params, + **authorization_header, + ) + response = UserPermissionViewSet.as_view({"put": "update"})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + {"role": OrganizationAuthorization.ROLE_CONTRIBUTOR}, + user_email=self.owner.email, + org_id=self.org.pk, + token=self.moduser_token, + ) + auth = OrganizationAuthorization.objects.get( + user=self.owner, organization=self.org + ) + self.assertEqual( + content_data["role"], OrganizationAuthorization.ROLE_CONTRIBUTOR + ) + self.assertEqual(content_data["role"], auth.role) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_not_ok(self): + response, content_data = self.request( + {"role": OrganizationAuthorization.ROLE_CONTRIBUTOR}, + user_email=self.owner.email, + org_id=self.org.pk, + token=self.owner_token, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalUserPermissionDeleteTestCase(InternalUserTestCase): + def request(self, user_email, org_id, token=None): + authorization_header = ( + {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} + ) + authorization_header["content_type"] = "application/json" + + request = self.factory.delete( + f"/v2/internal/user/permission?user_email={user_email}&org_id={org_id}", + {}, + **authorization_header, + ) + response = UserPermissionViewSet.as_view({"delete": "destroy"})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + user_email=self.owner.email, org_id=self.org.pk, token=self.moduser_token + ) + self.assertEqual(content_data["role"], None) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_not_ok(self): + response, content_data = self.request( + user_email=self.owner.email, org_id=self.org.pk, token=self.owner_token + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalUserRetrieveTestCase(InternalUserTestCase): + def request(self, params, token=None): + authorization_header = ( + {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} + ) + + request = self.factory.get("/v2/internal/user", params, **authorization_header) + response = UserViewSet.as_view({"get": "retrieve"})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + {"user_email": self.owner.email}, token=self.moduser_token + ) + self.assertEqual(content_data["email"], self.owner.email) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_not_ok(self): + response, content_data = self.request( + {"user_email": self.owner.email}, token=self.owner_token + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalUserLanguageUpdateTestCase(InternalUserTestCase): + def request(self, params, user_email, token=None): + authorization_header = ( + {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} + ) + authorization_header["content_type"] = "application/json" + + request = self.factory.put( + f"/v2/internal/user/language?user_email={user_email}", + params, + **authorization_header, + ) + response = UserLanguageViewSet.as_view({"put": "update"})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + {"language": "es"}, user_email=self.owner.email, token=self.moduser_token + ) + user = User.objects.get(email=self.owner.email) + self.assertEqual(content_data["language"], user.language) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_not_ok(self): + response, content_data = self.request( + {"language": "es"}, user_email=self.owner.email, token=self.owner_token + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/bothub/api/v2/internal/user/views.py b/bothub/api/v2/internal/user/views.py new file mode 100644 index 00000000..7c206595 --- /dev/null +++ b/bothub/api/v2/internal/user/views.py @@ -0,0 +1,98 @@ +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet +from rest_framework import mixins + +from bothub.authentication.models import User +from bothub.common.models import Organization, OrganizationAuthorization +from bothub.api.v2.internal.user.serializers import ( + UserPermissionSerializer, + UserSerializer, + UserLanguageSerializer, +) +from bothub import utils +from bothub.api.v2.internal.permissions import ModuleHasPermission + + +class UserPermissionViewSet( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + GenericViewSet, +): + queryset = OrganizationAuthorization.objects.all() + permission_classes = [ModuleHasPermission] + serializer_class = UserPermissionSerializer + lookup_field = None + + def retrieve(self, request, **kwargs): + user, org = utils.get_user_and_organization( + request.query_params.get("user_email", None), + request.query_params.get("org_id", None), + ) + permissions = self._get_user_permissions(org, user) + serializer = UserPermissionSerializer(permissions) + + return Response(serializer.data) + + def update(self, request, **kwargs): + user, org = utils.get_user_and_organization( + request.query_params.get("user_email", None), + request.query_params.get("org_id", None), + ) + + org.set_user_permission(user=user, permission=request.data.get("role", None)) + + permission = self._get_user_permissions(org, user) + serializer = UserPermissionSerializer(permission) + + return Response(serializer.data) + + def destroy(self, request, **kwargs): + user, org = utils.get_user_and_organization( + request.query_params.get("user_email", None), + request.query_params.get("org_id", None), + ) + + self._get_user_permissions(org, user).delete() + + permissions = self._get_user_permissions(org, user) + serializer = UserPermissionSerializer(permissions) + + return Response(serializer.data) + + def _get_user_permissions(self, org: Organization, user: User) -> dict: + return org.organization_authorizations.filter(user=user).first() + + +class UserViewSet(mixins.RetrieveModelMixin, GenericViewSet): + serializer_class = UserSerializer + permission_classes = [ModuleHasPermission] + queryset = User.objects + lookup_field = None + + def retrieve(self, request, **kwargs): + user, created = User.objects.get_or_create( + email=request.query_params.get("user_email"), + defaults={"nickname": request.query_params.get("user_email")}, + ) + + return Response(UserSerializer(user).data) + + +class UserLanguageViewSet(mixins.UpdateModelMixin, GenericViewSet): + serializer_class = UserLanguageSerializer + permission_classes = [ModuleHasPermission] + queryset = User.objects + lookup_field = None + + def update(self, request, **kwargs): + user, created = User.objects.get_or_create( + email=request.query_params.get("user_email"), + defaults={"nickname": request.query_params.get("user_email")}, + ) + serializer = UserLanguageSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + user.language = request.data["language"] + user.save() + return Response(UserLanguageSerializer(user).data) diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index 22bfd3d9..c0370173 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -59,6 +59,10 @@ CanContributeInRepositoryTranslatedExampleValidator, ) +from bothub.api.v2.internal.connect_rest_client import ( + ConnectRESTClient as ConnectClient, +) + class RequestRepositoryAuthorizationSerializer(serializers.ModelSerializer): class Meta: @@ -1654,10 +1658,13 @@ class AddRepositoryProjectSerializer(serializers.Serializer): project_uuid = serializers.CharField(required=True) def create(self, validated_data): - task = celery_app.send_task( - name="create_repository_project", kwargs=validated_data - ) - task.wait() + if settings.USE_GRPC: + task = celery_app.send_task( + name="create_repository_project", kwargs=validated_data + ) + task.wait() + else: + task = ConnectClient().create_classifier(**validated_data) return validated_data def to_representation(self, instance): diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index a69918f1..d988c7f1 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -121,6 +121,10 @@ AddRepositoryProjectSerializer, ) +from bothub.api.v2.internal.connect_rest_client import ( + ConnectRESTClient as ConnectClient, +) + class NewRepositoryViewSet( MultipleFieldLookupMixin, mixins.RetrieveModelMixin, GenericViewSet @@ -249,19 +253,26 @@ def projectrepository(self, request, **kwargs): if not authorization.can_contribute: raise PermissionDenied() - task = celery_app.send_task( - name="get_project_organization", args=[project_uuid] - ) - task.wait() + if settings.USE_GRPC: + task = celery_app.send_task( + name="get_project_organization", args=[project_uuid] + ) + task.wait() + authorizations = task.result + repositories = repository.authorizations.filter(uuid__in=authorizations) - repositories = repository.authorizations.filter(uuid__in=task.result) + else: + authorizations = ConnectClient().list_authorizations( + project_uuid=project_uuid, user_email=request.user.email + ) + repositories = repository.authorizations.filter(uuid__in=authorizations) data = dict(in_project=repositories.exists()) if organization: organization_authorization = ( - organization.organization_authorizations.filter(uuid__in=task.result) + organization.organization_authorizations.filter(uuid__in=authorizations) ) data["in_project"] = ( data["in_project"] or organization_authorization.exists() @@ -301,16 +312,27 @@ def remove_repository_project(self, request, **kwargs): if not user_authorization.is_admin: raise PermissionDenied() - project_organization = celery_app.send_task( - name="get_project_organization", args=[project_uuid] - ) - project_organization.wait() + if settings.USE_GRPC: + project_organization = celery_app.send_task( + name="get_project_organization", args=[project_uuid] + ) + project_organization.wait() + project_organization = project_organization.result + authorizations = list( + repository.authorizations.filter( + uuid__in=project_organization + ).values_list("uuid", flat=True) + ) - authorizations = list( - repository.authorizations.filter( - uuid__in=project_organization.result - ).values_list("uuid", flat=True) - ) + else: + project_organization = ConnectClient().list_authorizations( + project_uuid=project_uuid, user_email=request.user.email + ) + authorizations = list( + repository.authorizations.filter( + uuid__in=project_organization + ).values_list("uuid", flat=True) + ) if organization: organization_authorization = organization.get_organization_authorization( @@ -321,7 +343,7 @@ def remove_repository_project(self, request, **kwargs): authorizations += list( organization.organization_authorizations.filter( - uuid__in=project_organization.result + uuid__in=project_organization ).values_list("uuid", flat=True) ) @@ -334,11 +356,20 @@ def remove_repository_project(self, request, **kwargs): lambda authorization: str(authorization), authorizations ) - task = celery_app.send_task( - name="remove_authorizations_project", - args=[project_uuid, list(authorizations_uuids), request.user.email], - ) - task.wait() + if settings.USE_GRPC: + task = celery_app.send_task( + name="remove_authorizations_project", + args=[project_uuid, list(authorizations_uuids), request.user.email], + ) + task.wait() + else: + task = ConnectClient().list_authorizations( + project_uuid=project_uuid, user_email=request.user.email + ) + for authorization_uuid in authorizations_uuids: + ConnectClient().remove_authorization( + project_uuid, authorization_uuid, request.user.email + ) return Response(status=status.HTTP_204_NO_CONTENT) @@ -381,14 +412,22 @@ def add_repository_project(self, request, **kwargs): project_uuid = serializer.validated_data.get("project_uuid") - task = celery_app.send_task( - name="get_project_organization", args=[project_uuid] - ) - task.wait() + if settings.USE_GRPC: + task = celery_app.send_task( + name="get_project_organization", args=[project_uuid] + ) + task.wait() - organization_authorization = organization.organization_authorizations.filter( - uuid__in=task.result - ) + organization_authorization = ( + organization.organization_authorizations.filter(uuid__in=task.result) + ) + else: + task = ConnectClient().list_authorizations( + project_uuid=project_uuid, user_email=request.user.email + ) + organization_authorization = ( + organization.organization_authorizations.filter(uuid__in=task) + ) if organization_authorization.exists(): raise ValidationError(_("Repository already added")) @@ -803,12 +842,21 @@ def list_project_organizatiton(self, request, **kwargs): if not project_uuid: raise ValidationError(_("Need to pass 'project_uuid' in query params")) - task = celery_app.send_task( - name="get_project_organization", args=[project_uuid] - ) - task.wait() - - repositories = Repository.objects.filter(authorizations__uuid__in=task.result) + if settings.USE_GRPC: + task = celery_app.send_task( + name="get_project_organization", args=[project_uuid, request.user.email] + ) + task.wait() + repositories = Repository.objects.filter( + authorizations__uuid__in=task.result + ) + else: + authorizations = ConnectClient().list_authorizations( + project_uuid=project_uuid, user_email=request.user.email + ) + repositories = Repository.objects.filter( + authorizations__uuid__in=authorizations + ) serialized_data = ShortRepositorySerializer(repositories, many=True) return Response(serialized_data.data) diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index 55e8b818..fbf67a88 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -66,6 +66,14 @@ RepositoryTranslatorViewSet, ) +from bothub.api.v2.internal.repository.views import InternalRepositoriesViewSet +from bothub.api.v2.internal.organization.views import InternalOrganizationViewSet +from bothub.api.v2.internal.user.views import ( + UserPermissionViewSet, + UserViewSet, + UserLanguageViewSet, +) + class Router(routers.SimpleRouter): routes = [ @@ -233,3 +241,8 @@ def get_lookup_regex(self, viewset, lookup_prefix=""): router.register("org/organization", OrganizationViewSet) router.register("org/profile", OrganizationProfileViewSet) router.register("org/authorizations", OrganizationAuthorizationViewSet) +router.register("internal/organization", InternalOrganizationViewSet) +router.register("internal/repository", InternalRepositoriesViewSet) +router.register("internal/user", UserViewSet) +router.register("internal/user/permission", UserPermissionViewSet) +router.register("internal/user/language", UserLanguageViewSet) diff --git a/bothub/api/v2/tests/test_repository.py b/bothub/api/v2/tests/test_repository.py index 9aa601e6..025753f9 100644 --- a/bothub/api/v2/tests/test_repository.py +++ b/bothub/api/v2/tests/test_repository.py @@ -42,48 +42,11 @@ from bothub.common.models import RepositoryVote from bothub.common.models import RequestRepositoryAuthorization - -def get_valid_mockups(categories): - return [ - { - "name": "Repository 1", - "slug": "repository-1", - "description": "", - "language": languages.LANGUAGE_EN, - "categories": [category.pk for category in categories], - }, - { - "name": "Repository 2", - "description": "", - "language": languages.LANGUAGE_PT, - "categories": [category.pk for category in categories], - }, - ] - - -def get_invalid_mockups(categories): - return [ - { - "name": "", - "slug": "repository-1", - "language": languages.LANGUAGE_EN, - "categories": [category.pk for category in categories], - }, - { - "name": "Repository 3", - "language": "out", - "categories": [category.pk for category in categories], - "is_private": False, - }, - ] - - -def create_repository_from_mockup(owner, categories, **mockup): - r = Repository.objects.create(owner_id=owner.id, **mockup) - r.current_version() - for category in categories: - r.categories.add(category) - return r +from bothub.api.v2.tests.utils import ( + get_valid_mockups, + get_invalid_mockups, + create_repository_from_mockup, +) class CreateRepositoryAPITestCase(TestCase): diff --git a/bothub/api/v2/tests/utils.py b/bothub/api/v2/tests/utils.py index 7ede7eb6..5d1c6901 100644 --- a/bothub/api/v2/tests/utils.py +++ b/bothub/api/v2/tests/utils.py @@ -1,9 +1,58 @@ +from bothub.utils import check_module_permission from rest_framework.authtoken.models import Token from bothub.authentication.models import User +from bothub.common import languages +from bothub.common.models import Repository -def create_user_and_token(nickname="fake"): +def create_user_and_token(nickname="fake", module=False): user = User.objects.create_user("{}@user.com".format(nickname), nickname) + if module is True: + check_module_permission({"can_communicate_internally": module}, user) + user = User.objects.get(email=user.email) token, create = Token.objects.get_or_create(user=user) return (user, token) + + +def get_valid_mockups(categories): + return [ + { + "name": "Repository 1", + "slug": "repository-1", + "description": "", + "language": languages.LANGUAGE_EN, + "categories": [category.pk for category in categories], + }, + { + "name": "Repository 2", + "description": "", + "language": languages.LANGUAGE_PT, + "categories": [category.pk for category in categories], + }, + ] + + +def get_invalid_mockups(categories): + return [ + { + "name": "", + "slug": "repository-1", + "language": languages.LANGUAGE_EN, + "categories": [category.pk for category in categories], + }, + { + "name": "Repository 3", + "language": "out", + "categories": [category.pk for category in categories], + "is_private": False, + }, + ] + + +def create_repository_from_mockup(owner, categories, **mockup): + r = Repository.objects.create(owner_id=owner.id, **mockup) + r.current_version() + for category in categories: + r.categories.add(category) + return r diff --git a/bothub/authentication/authorization.py b/bothub/authentication/authorization.py index ccc89e80..c2e4562c 100644 --- a/bothub/authentication/authorization.py +++ b/bothub/authentication/authorization.py @@ -1,6 +1,10 @@ import logging +import re from django.utils.translation import ugettext_lazy as _ +from bothub.utils import check_module_permission + + from mozilla_django_oidc.auth import OIDCAuthenticationBackend from rest_framework import exceptions from rest_framework.authentication import TokenAuthentication, get_authorization_header @@ -92,16 +96,20 @@ def create_user(self, claims): # Override existing create_user method in OIDCAuthenticationBackend email = claims.get("email") username = self.get_username(claims)[:16] + username = re.sub("[^A-Za-z0-9]+", "", username) user = self.UserModel.objects.create_user(email, username) user.name = claims.get("name", "") user.save() + check_module_permission(claims, user) + return user def update_user(self, user, claims): user.name = claims.get("name", "") user.email = claims.get("email", "") user.save() + check_module_permission(claims, user) return user diff --git a/bothub/common/models.py b/bothub/common/models.py index f9a8793e..05163b18 100644 --- a/bothub/common/models.py +++ b/bothub/common/models.py @@ -906,16 +906,18 @@ def get_specific_version_id(self, repository_version, language=None): def get_user_authorization(self, user): if user.is_anonymous: return RepositoryAuthorization(repository=self) - get, created = RepositoryAuthorization.objects.get_or_create( + repo_auth, created = RepositoryAuthorization.objects.get_or_create( user=user.repository_owner, repository=self ) if self.owner.is_organization: - org_role = self.owner.organization.get_organization_authorization(user).role - if get.role != org_role and get.role == 0: - get.role = org_role - get.save() - - return get + org_auth = self.owner.organization.get_organization_authorization(user) + + # Excluding ROLE_TRANSLATE as it does not correspond to the same role in the client app (connect). + # todo: update this conditional with corresponding role rule + if repo_auth.role < org_auth.role and org_auth.role < RepositoryAuthorization.ROLE_TRANSLATE: + repo_auth.role = org_auth.role + repo_auth.save(update_fields=['role']) + return repo_auth def get_absolute_url(self): return "{}dashboard/{}/{}/".format( diff --git a/bothub/common/tasks.py b/bothub/common/tasks.py index 59d29e77..1660e7f0 100644 --- a/bothub/common/tasks.py +++ b/bothub/common/tasks.py @@ -12,7 +12,6 @@ from django_elasticsearch_dsl.registries import registry from bothub import translate -from bothub.api.grpc.connect_grpc_client import ConnectGRPCClient from bothub.celery import app from bothub.common.models import ( RepositoryQueueTask, @@ -40,6 +39,14 @@ ) +if settings.USE_GRPC: + from bothub.api.grpc.connect_grpc_client import ConnectGRPCClient as ConnectClient +else: + from bothub.api.v2.internal.connect_rest_client import ( + ConnectRESTClient as ConnectClient, + ) + + @app.task(name="es_handle_save") def handle_save(pk, app_label, model_name): sender = apps.get_model(app_label, model_name) @@ -544,9 +551,16 @@ def evaluate_crossvalidation(data, authorization_token): # pragma: no cover @app.task(name="get_project_organization") -def get_project_organization(project_uuid: str): # pragma: no cover - grpc_client = ConnectGRPCClient() - authorizations = grpc_client.list_authorizations(project_uuid=project_uuid) +def get_project_organization( + project_uuid: str, user_email: str = "" +): # pragma: no cover + grpc_client = ConnectClient() + if settings.USE_GRPC: + authorizations = grpc_client.list_authorizations(project_uuid=project_uuid) + else: + authorizations = grpc_client.list_authorizations( + project_uuid=project_uuid, user_email=user_email + ) return authorizations @@ -554,13 +568,13 @@ def get_project_organization(project_uuid: str): # pragma: no cover def remove_authorizations_project( project_uuid: str, authorizations_uuids: list, user_email: str ): - grpc_client = ConnectGRPCClient() + grpc_client = ConnectClient() for authorization_uuid in authorizations_uuids: grpc_client.remove_authorization(project_uuid, authorization_uuid, user_email) @app.task(name="create_repository_project") def create_repository_project(**kwargs): - grpc_client = ConnectGRPCClient() + grpc_client = ConnectClient() grpc_client.create_classifier(**kwargs) return kwargs diff --git a/bothub/common/tests.py b/bothub/common/tests.py index 8b72ba04..a9bfe505 100644 --- a/bothub/common/tests.py +++ b/bothub/common/tests.py @@ -15,6 +15,8 @@ RepositoryQueueTask, QAKnowledgeBase, QAtext, + Organization, + OrganizationAuthorization ) from .models import RepositoryAuthorization from .models import RepositoryEntity @@ -544,6 +546,7 @@ class RepositoryAuthorizationTestCase(TestCase): def setUp(self): self.owner = User.objects.create_user("owner@user.com", "owner") self.user = User.objects.create_user("fake@user.com", "user") + self.collaborator = User.objects.create_user("colaborator@user.com", "collaborator") self.repository = Repository.objects.create( owner=self.owner.repository_owner, name="Test", slug="test" @@ -554,6 +557,12 @@ def setUp(self): slug="private", is_private=True, ) + self.organization = Organization.objects.create(name="Weni") + self.organization_repository = Repository.objects.create( + owner=self.organization, + name="Organization Repository", + slug="organization_repository" + ) def test_admin_level(self): authorization = self.repository.get_user_authorization(self.owner) @@ -682,6 +691,50 @@ def test_role_contributor_can_contribute(self): authorization_user.save() self.assertTrue(authorization_user.can_contribute) + def test_organization_auth_over_repository_auth(self): + """ + Tests that a User's authorization role is of the highest level possible in a Repository, + either using the RepositoryAuthorization or the OrganizationAuthorization. + The expected behavior is that the organization's authorization role should be passed to the repository's authorization for that user. + """ + + # The role that the user should inherit from the organization authorization inside the repository. + initial_organization_role = OrganizationAuthorization.ROLE_ADMIN + + # Set user's role to a low level at the Repository + collaborator_repository_auth, created = RepositoryAuthorization.objects.get_or_create( + user=self.collaborator, repository=self.organization_repository, role=RepositoryAuthorization.ROLE_USER + ) + # Set user's role to a high level at the Organization + collaborator_organization_auth = self.organization.organization_authorizations.create( + user=self.collaborator, role=initial_organization_role + ) + + # Validate that their access level corresponds to their role in the Organization and not the Repository, as it is higher at this point. + user_authorization = self.organization_repository.get_user_authorization(self.collaborator) + self.assertEqual(user_authorization.role, collaborator_organization_auth.role) + + # Lower their level inside the Organization + collaborator_organization_auth.role = OrganizationAuthorization.ROLE_NOT_SETTED + collaborator_organization_auth.save() + + # Validate that the repository authorization level was updated. + collaborator_repository_auth.refresh_from_db() + self.assertEqual(collaborator_repository_auth.role, initial_organization_role) + + # Validate that the user's level is now the Repository's and not the Organization's, as it is higher. + user_authorization = self.organization_repository.get_user_authorization(self.collaborator) + self.assertEqual(user_authorization.role, collaborator_repository_auth.role) + + # Verify that org auth with (role >= 4) will not update the repository's authorization + + # Set user's role to ROLE_TRANSLATE level at the Organization + collaborator_organization_auth.role = OrganizationAuthorization.ROLE_TRANSLATE + collaborator_organization_auth.save() + + user_authorization = self.organization_repository.get_user_authorization(self.collaborator) + self.assertEqual(user_authorization.role, collaborator_repository_auth.role) + class RepositoryVersionTrainingTestCase(TestCase): def setUp(self): diff --git a/bothub/settings.py b/bothub/settings.py index 0f0b01fc..6a473163 100644 --- a/bothub/settings.py +++ b/bothub/settings.py @@ -75,6 +75,8 @@ SECRET_KEY_CHECK_LEGACY_USER=(str, None), CONNECT_GRPC_SERVER_URL=(str, "localhost:8002"), CONNECT_CERTIFICATE_GRPC_CRT=(str, None), + USE_GRPC=(bool, False), + CONNECT_API_URL=(str, ""), REPOSITORY_RESTRICT_ACCESS_NLP_LOGS=(list, []), REPOSITORY_BLOCK_USER_LOGS=(list, []), REPOSITORY_KNOWLEDGE_BASE_DESCRIPTION_LIMIT=(int, 450), @@ -92,6 +94,17 @@ ELASTICSEARCH_LOGS_DELETE_AGE=(str, "90d"), ELASTICSEARCH_LOGS_ROLLOVER_AGE=(str, "1d"), ELASTICSEARCH_TIMESTAMP_PIPELINE_FIELD=(str, "created_at"), + CSP_DEFAULT_SRC=(tuple, "CSP_DEFAULT_SRC"), + CSP_FRAME_ANCESTORS=(tuple, "CSP_FRAME_ANCESTORS"), + CSP_FONT_SRC=(tuple, "CSP_FONT_SRC"), + CSP_STYLE_SRC=(tuple, "CSP_STYLE_SRC"), + CSP_STYLE_SRC_ELEM=(tuple, "CSP_STYLE_SRC_ELEM"), + CSP_SCRIPT_SRC=(tuple, "CSP_SCRIPT_SRC"), + CSP_SCRIPT_SRC_ELEM=(tuple, "CSP_SCRIPT_SRC_ELEM"), + CSP_FRAME_SRC=(tuple, "CSP_FRAME_SRC"), + CSP_CONNECT_SRC=(tuple, "CSP_CONNECT_SRC"), + CSP_WORKER_SRC=(tuple, "CSP_WORKER_SRC"), + CSP_IMG_SRC=(tuple, "CSP_IMG_SRC"), ) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -144,12 +157,12 @@ "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.locale.LocaleMiddleware", + "csp.middleware.CSPMiddleware", "corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", "bothub.api.v2.middleware.UserLanguageMiddleware", ] @@ -299,6 +312,30 @@ CSRF_COOKIE_SECURE = env.bool("CSRF_COOKIE_SECURE") +# CSP headers + +DEFAULT_CSP_SETTINGS = ("'self'",) +DEFAULT_CSP_WENI_SETTINGS = DEFAULT_CSP_SETTINGS + ("*.weni.ai",) + +CSP_DEFAULT_SRC = env.tuple("CSP_DEFAULT_SRC", default=DEFAULT_CSP_SETTINGS) +CSP_FRAME_ANCESTORS = env.tuple( + "CSP_FRAME_ANCESTORS", default=DEFAULT_CSP_WENI_SETTINGS +) +CSP_FONT_SRC = env.tuple("CSP_FONT_SRC", default=CSP_DEFAULT_SRC) +CSP_STYLE_SRC = env.tuple( + "CSP_STYLE_SRC", default=DEFAULT_CSP_SETTINGS + ("'unsafe-inline'", "'unsafe-eval'") +) +CSP_STYLE_SRC_ELEM = env.tuple("CSP_STYLE_SRC_ELEM", default=CSP_STYLE_SRC) +CSP_SCRIPT_SRC = env.tuple("CSP_SCRIPT_SRC", default=CSP_STYLE_SRC) +CSP_SCRIPT_SRC_ELEM = env.tuple("CSP_SCRIPT_SRC_ELEM", default=CSP_SCRIPT_SRC) +CSP_FRAME_SRC = env.tuple("CSP_FRAME_SRC", default=CSP_DEFAULT_SRC) +CSP_CONNECT_SRC = env.tuple("CSP_CONNECT_SRC", default=CSP_DEFAULT_SRC) +CSP_WORKER_SRC = env.tuple( + "CSP_WORKER_SRC", default=DEFAULT_CSP_WENI_SETTINGS + ("blob:", "data:") +) +CSP_IMG_SRC = env.tuple("CSP_IMG_SRC", default=CSP_WORKER_SRC) + + # Logging LOGGING = DEFAULT_LOGGING @@ -531,6 +568,10 @@ CONNECT_CERTIFICATE_GRPC_CRT = env.str("CONNECT_CERTIFICATE_GRPC_CRT") +USE_GRPC = env.bool("USE_GRPC", default=False) + +CONNECT_API_URL = env.str("CONNECT_API_URL", default="https://api.dev.cloud.weni.ai") + # ElasticSearch ELASTICSEARCH_DSL = { "default": {"hosts": env.str("ELASTICSEARCH_DSL", default="es:9200")} @@ -585,3 +626,5 @@ ] REPOSITORY_BLOCK_USER_LOGS = env.list("REPOSITORY_BLOCK_USER_LOGS", default=[]) + +CACHE_TTL = env.int("CACHE_TTL", default=180) diff --git a/bothub/utils.py b/bothub/utils.py index 38949d12..40e36dcd 100644 --- a/bothub/utils.py +++ b/bothub/utils.py @@ -20,6 +20,7 @@ from rest_framework import status from rest_framework.exceptions import APIException, ValidationError + entity_regex = re.compile( r"\[(?P[^\]]+)" r"\]\((?P[^:)]*?)" r"(?:\:(?P[^)]+))?\)" ) @@ -375,6 +376,17 @@ def get_organization(request, organization_id: int): ) +def get_user_and_organization(user_email: str, organization_id: int): + from bothub.authentication.models import User + from bothub.common.models import Organization + + org = Organization.objects.get(pk=organization_id) + user, created = User.objects.get_or_create( + email=user_email, defaults={"nickname": user_email} + ) + return user, org + + class TimeBasedDocument(Document): def save(self, action="create", **kwargs): return super().save(action=action, **kwargs) @@ -462,3 +474,43 @@ def filter_has_invalid_entities(self, queryset, name, value): return filter_validate_entities(queryset, value).exclude( original_entities_count=F("entities_count") ) + + +def check_module_permission(claims, user): + from django.contrib.auth.models import Permission + from django.contrib.contenttypes.models import ContentType + from bothub.common.models import User + + if claims.get("can_communicate_internally", False): + content_type = ContentType.objects.get_for_model(User) + permission, created = Permission.objects.get_or_create( + codename="can_communicate_internally", + name="can communicate internally", + content_type=content_type, + ) + if not user.has_perm("authentication.can_communicate_internally"): + user.user_permissions.add(permission) + return True + return False + + +internal_serializer_fields = [ + "uuid", + "name", + "slug", + "description", + "is_private", + "created_at", + "language", + "owner", + "algorithm", + "use_competing_intents", + "use_name_entities", + "use_analyze_char", + "owner__nickname", + "intents", + "categories", + "available_languages", + "categories_list", + "repository_type", +] diff --git a/poetry.lock b/poetry.lock index d67a0a89..6199b760 100644 --- a/poetry.lock +++ b/poetry.lock @@ -445,6 +445,21 @@ python-versions = ">=3.6" [package.dependencies] Django = ">=2.2" +[[package]] +name = "django-csp" +version = "3.7" +description = "Django Content Security Policy support." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Django = ">=1.8" + +[package.extras] +jinja2 = ["jinja2 (>=2.9.6)"] +tests = ["pytest (<4.0)", "pytest-django", "pytest-flakes (==1.0.1)", "pytest-pep8 (==1.0.6)", "pep8 (==1.4.6)", "mock (==1.0.1)", "six (==1.12.0)", "jinja2 (>=2.9.6)"] + [[package]] name = "django-elasticsearch-dsl" version = "7.2.2" @@ -1711,7 +1726,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "~=3.6.15" -content-hash = "dd6bfc5c1373deba58e4e85a9cb3114b91f7d5d5a33db1ac48afee53bbe395f8" +content-hash = "31ccdac8c205f34af79715c1d090792b5496b461d94085bc7715f635cfbf4dfa" [metadata.files] amqp = [ @@ -1970,6 +1985,10 @@ django-cors-headers = [ {file = "django-cors-headers-3.7.0.tar.gz", hash = "sha256:96069c4aaacace786a34ee7894ff680780ec2644e4268b31181044410fecd12e"}, {file = "django_cors_headers-3.7.0-py3-none-any.whl", hash = "sha256:1ac2b1213de75a251e2ba04448da15f99bcfcbe164288ae6b5ff929dc49b372f"}, ] +django-csp = [ + {file = "django_csp-3.7-py2.py3-none-any.whl", hash = "sha256:01443a07723f9a479d498bd7bb63571aaa771e690f64bde515db6cdb76e8041a"}, + {file = "django_csp-3.7.tar.gz", hash = "sha256:01eda02ad3f10261c74131cdc0b5a6a62b7c7ad4fd017fbefb7a14776e0a9727"}, +] django-elasticsearch-dsl = [ {file = "django-elasticsearch-dsl-7.2.2.tar.gz", hash = "sha256:811d3909b3387fd55c19d9bbcf0e9a9b234f085df3f8422d59e7519a5f733e0e"}, {file = "django_elasticsearch_dsl-7.2.2-py2.py3-none-any.whl", hash = "sha256:3c58a254a6318b169eb904d41d802924b99ea8e53ddc2c596ebba90506cf47fa"}, diff --git a/pyproject.toml b/pyproject.toml index c6d48f5e..0abed417 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ psycopg2-binary = "~=2.9.1" weni-protobuffers = "~=1.2.1" black = "21.7b0" Pillow = "~=8.4.0" +django-csp = "^3.7" [tool.poetry.dev-dependencies] flake8 = "~=4.0.0" From 25b1503aa4ae91ebe97c914ac65a5491dcc40de7 Mon Sep 17 00:00:00 2001 From: Helder Souza <42891390+helllllllder@users.noreply.github.com> Date: Thu, 21 Jul 2022 12:33:40 -0300 Subject: [PATCH 3/9] Revert "Feature/remove rest client from celery (#728)" (#729) This reverts commit a1c7c577fcc52902f0c754c6059a36902d0ccab2. --- README.md | 2 - bothub/api/grpc/repository/serializers.py | 23 +- bothub/api/v2/internal/__init__.py | 0 bothub/api/v2/internal/connect_rest_client.py | 88 ------- .../api/v2/internal/organization/__init__.py | 0 .../v2/internal/organization/permissions.py | 11 - .../v2/internal/organization/serializers.py | 83 ------- bothub/api/v2/internal/organization/tests.py | 232 ------------------ bothub/api/v2/internal/organization/views.py | 120 --------- bothub/api/v2/internal/permissions.py | 9 - bothub/api/v2/internal/repository/__init__.py | 0 .../api/v2/internal/repository/serializers.py | 29 --- bothub/api/v2/internal/repository/tests.py | 112 --------- bothub/api/v2/internal/repository/views.py | 39 --- bothub/api/v2/internal/user/__init__.py | 0 bothub/api/v2/internal/user/serializers.py | 37 --- bothub/api/v2/internal/user/tests.py | 208 ---------------- bothub/api/v2/internal/user/views.py | 98 -------- bothub/api/v2/repository/serializers.py | 15 +- bothub/api/v2/repository/views.py | 116 +++------ bothub/api/v2/routers.py | 13 - bothub/api/v2/tests/test_repository.py | 47 +++- bothub/api/v2/tests/utils.py | 51 +--- bothub/authentication/authorization.py | 8 - bothub/common/models.py | 16 +- bothub/common/tasks.py | 26 +- bothub/common/tests.py | 53 ---- bothub/settings.py | 45 +--- bothub/utils.py | 52 ---- poetry.lock | 21 +- pyproject.toml | 1 - 31 files changed, 116 insertions(+), 1439 deletions(-) delete mode 100644 bothub/api/v2/internal/__init__.py delete mode 100644 bothub/api/v2/internal/connect_rest_client.py delete mode 100644 bothub/api/v2/internal/organization/__init__.py delete mode 100644 bothub/api/v2/internal/organization/permissions.py delete mode 100644 bothub/api/v2/internal/organization/serializers.py delete mode 100644 bothub/api/v2/internal/organization/tests.py delete mode 100644 bothub/api/v2/internal/organization/views.py delete mode 100644 bothub/api/v2/internal/permissions.py delete mode 100644 bothub/api/v2/internal/repository/__init__.py delete mode 100644 bothub/api/v2/internal/repository/serializers.py delete mode 100644 bothub/api/v2/internal/repository/tests.py delete mode 100644 bothub/api/v2/internal/repository/views.py delete mode 100644 bothub/api/v2/internal/user/__init__.py delete mode 100644 bothub/api/v2/internal/user/serializers.py delete mode 100644 bothub/api/v2/internal/user/tests.py delete mode 100644 bothub/api/v2/internal/user/views.py diff --git a/README.md b/README.md index 9dc314fd..1846f7f8 100644 --- a/README.md +++ b/README.md @@ -162,8 +162,6 @@ You can set environment variables in your OS, write on ```.env``` file or pass v | OIDC_RP_SCOPES | ```string``` | ```openid email``` | The OpenID Connect scopes to request during login. | CONNECT_GRPC_SERVER_URL | ```string``` | ```localhost:8002``` | Define grpc connect server url | CONNECT_CERTIFICATE_GRPC_CRT | ```string``` | ```None``` | Absolute certificate path for secure grpc communication -| CONNECT_API_URL | ```string``` | ```None``` | Connect module api url -| USE_GRPC | ```bool``` | ```False``` | Use connect gRPC clients | RECAPTCHA_SECRET_KEY | ```string``` | ```''``` | Token of the recaptcha used in the validation of a user's registration. | REPOSITORY_NLP_LOG_LIMIT | ```int``` | ```10000``` | Limit of query size to repository log. | REPOSITORY_RESTRICT_ACCESS_NLP_LOGS | ```list``` | ```[]``` | Restricts log access to a particular or multiple intelligences diff --git a/bothub/api/grpc/repository/serializers.py b/bothub/api/grpc/repository/serializers.py index 1c8536e6..901f6d3f 100644 --- a/bothub/api/grpc/repository/serializers.py +++ b/bothub/api/grpc/repository/serializers.py @@ -5,8 +5,6 @@ from bothub.common.models import Repository from weni.protobuf.intelligence import repository_pb2 -from bothub.utils import internal_serializer_fields - class RepositoryProtoSerializer(proto_serializers.ModelProtoSerializer): owner__nickname = serializers.SerializerMethodField() @@ -17,7 +15,26 @@ class RepositoryProtoSerializer(proto_serializers.ModelProtoSerializer): class Meta: model = Repository proto_class = repository_pb2.Repository - fields = internal_serializer_fields + fields = [ + "uuid", + "name", + "slug", + "description", + "is_private", + "created_at", + "language", + "owner", + "algorithm", + "use_competing_intents", + "use_name_entities", + "use_analyze_char", + "owner__nickname", + "intents", + "categories", + "available_languages", + "categories_list", + "repository_type", + ] def get_owner__nickname(self, repository: Repository): return repository.owner.nickname diff --git a/bothub/api/v2/internal/__init__.py b/bothub/api/v2/internal/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bothub/api/v2/internal/connect_rest_client.py b/bothub/api/v2/internal/connect_rest_client.py deleted file mode 100644 index a6693aff..00000000 --- a/bothub/api/v2/internal/connect_rest_client.py +++ /dev/null @@ -1,88 +0,0 @@ -import requests -from typing import List, Dict - -from django.conf import settings -from django.core.cache import cache - - -class ConnectRESTClient: - def __init__(self): - self.base_url = settings.CONNECT_API_URL - self.headers = { - "Content-Type": "application/json; charset: utf-8", - "Authorization": self.get_auth_token(), - } - - def get_auth_token(self) -> str: - request = requests.post( - url=settings.OIDC_OP_TOKEN_ENDPOINT, - data={ - "client_id": settings.OIDC_RP_CLIENT_ID, - "client_secret": settings.OIDC_RP_CLIENT_SECRET, - "grant_type": "client_credentials", - }, - ) - token = request.json().get("access_token") - return f"Bearer {token}" - - def list_classifiers( - self, project_uuid: str, user_email: str - ) -> List[Dict[str, str]]: - cache_key = f"LIST_CLASSIFIER_CACHE_KEY:project_uuid:{project_uuid}:user_email:{user_email}" - cache_result = cache.get(cache_key, None) - if cache_result is None: - request = requests.get( - url=f"{self.base_url}/v1/organization/project/list_classifier/", - headers=self.headers, - params={"project_uuid": project_uuid, "user_email": user_email}, - ) - cache.set(cache_key, request.json().get("data"), settings.CACHE_TTL) - return request.json().get("data") - else: - return cache_result - - def list_authorizations(self, project_uuid: str, user_email: str) -> List[str]: - - classifiers = self.list_classifiers( - project_uuid=project_uuid, user_email=user_email - ) - - return [classifier.get("authorization_uuid") for classifier in classifiers] - - def get_authorization_classifier( - self, project_uuid: str, authorization_uuid: str, user_email: str - ) -> str: - """ - Receives a authorization UUID and returns the respective classifier UUID - """ - classifiers = self.list_classifiers(project_uuid, user_email) - classifier = filter( - lambda classifier: classifier["authorization_uuid"] == authorization_uuid, - classifiers, - ) - - return next(classifier).get("uuid") - - def remove_authorization( - self, project_uuid: str, authorization_uuid: str, user_email: str - ): - classifier_uuid = self.get_authorization_classifier( - project_uuid, - authorization_uuid, - user_email, - ) - request = requests.delete( - url=f"{self.base_url}/v1/organization/project/destroy_classifier/", - headers=self.headers, - params={"uuid": classifier_uuid, "user_email": user_email}, - ) - - return request.json() - - def create_classifier(self, **kwargs): - request = requests.post( - url=f"{self.base_url}/v1/organization/project/create_classifier/", - headers=self.headers, - params={**kwargs, "classifier_type": "bothub"}, - ) - return request.json() diff --git a/bothub/api/v2/internal/organization/__init__.py b/bothub/api/v2/internal/organization/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bothub/api/v2/internal/organization/permissions.py b/bothub/api/v2/internal/organization/permissions.py deleted file mode 100644 index 6edbf607..00000000 --- a/bothub/api/v2/internal/organization/permissions.py +++ /dev/null @@ -1,11 +0,0 @@ -from rest_framework import permissions - -from bothub.utils import get_user - - -class InternalOrganizationAdminHasPermission(permissions.BasePermission): - def has_object_permission(self, request, view, obj): - authorization = obj.organization.get_organization_authorization( - get_user(request.query_params.get("user_email", None)) - ) - return authorization.is_admin diff --git a/bothub/api/v2/internal/organization/serializers.py b/bothub/api/v2/internal/organization/serializers.py deleted file mode 100644 index 26456b3a..00000000 --- a/bothub/api/v2/internal/organization/serializers.py +++ /dev/null @@ -1,83 +0,0 @@ -from django.db.models import F -from django.utils.translation import ugettext_lazy as _ - -from rest_framework import serializers -from rest_framework.exceptions import ValidationError - -from bothub.authentication.models import User -from bothub.common.models import Organization, OrganizationAuthorization - - -class OrganizationSerializer(serializers.ModelSerializer): - - users = serializers.SerializerMethodField() - - def get_users(self, org: Organization): - return list( - org.organization_authorizations.exclude( - role=OrganizationAuthorization.LEVEL_NOTHING - ) - .annotate( - org_user_id=F("user__user_owner__pk"), - org_user_email=F("user__user_owner__email"), - org_user_nickname=F("user__user_owner__nickname"), - org_user_name=F("user__user_owner__name"), - ) - .values( - "org_user_id", "org_user_email", "org_user_nickname", "org_user_name" - ) - ) - - class Meta: - model = Organization - fields = ["id", "name", "users"] - - -class OrgCreateSerializer(serializers.ModelSerializer): - - organization_name = serializers.CharField() - user_email = serializers.CharField() - - def validate_user_email(self, value: str) -> str: - try: - User.objects.get(email=value) - except User.DoesNotExist: - raise ValidationError(_("{} not found!").format(value)) - - return value - - class Meta: - model = Organization - fields = ["organization_name", "user_email"] - - -class OrgUpdateSerializer(serializers.ModelSerializer): - - id = serializers.IntegerField() - name = serializers.CharField(required=False) - - def validate_id(self, value): - try: - Organization.objects.get(pk=value) - except Organization.DoesNotExist: - raise ValidationError(f"{value} not found!") - - return value - - def save(self): - data = dict(self.validated_data) - - org = Organization.objects.get(pk=data.get("id")) - - updated_fields = self.get_updated_fields(data) - - if updated_fields: - org.__dict__.update(**updated_fields) - org.save() - - def get_updated_fields(self, data): - return {key: value for key, value in data.items() if key not in ["id"]} - - class Meta: - model = Organization - fields = ["id", "name"] diff --git a/bothub/api/v2/internal/organization/tests.py b/bothub/api/v2/internal/organization/tests.py deleted file mode 100644 index 4ac09bf6..00000000 --- a/bothub/api/v2/internal/organization/tests.py +++ /dev/null @@ -1,232 +0,0 @@ -import json - -from django.test import tag -from django.test import RequestFactory -from django.test import TestCase -from rest_framework import status - -from bothub.api.v2.internal.organization.views import InternalOrganizationViewSet -from bothub.api.v2.tests.utils import create_user_and_token -from bothub.common.models import ( - Organization, - OrganizationAuthorization, - RepositoryCategory, -) -from bothub.api.v2.tests.utils import get_valid_mockups, create_repository_from_mockup - - -class InternalOrganizationTestCase(TestCase): - def setUp(self): - self.factory = RequestFactory() - self.moduser, self.moduser_token = create_user_and_token("module", module=True) - self.owner, self.owner_token = create_user_and_token("owner") - self.category = RepositoryCategory.objects.create(name="Category 1") - self.org = Organization.objects.create( - name="Organization 1", nickname="organization1" - ) - self.org_2 = Organization.objects.create( - name="Organization 2", nickname="organization2" - ) - OrganizationAuthorization.objects.create( - user=self.owner, - organization=self.org, - role=OrganizationAuthorization.ROLE_ADMIN, - ) - OrganizationAuthorization.objects.create( - user=self.owner, - organization=self.org_2, - role=OrganizationAuthorization.ROLE_ADMIN, - ) - self.repositories = [ - create_repository_from_mockup(self.org, **mockup) - for mockup in get_valid_mockups([self.category]) - ] - for rep in self.repositories: - rep.get_user_authorization(self.org) - - self.repository_auth = self.repositories[0].get_user_authorization(self.owner) - - -def auth_header(token): - authorization_header = ( - {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} - ) - authorization_header["content_type"] = "application/json" - - return authorization_header - - -@tag("internal") -class InternalOrganizationListTestCase(InternalOrganizationTestCase): - def request(self, params, token=None): - authorization_header = auth_header(token) - - request = self.factory.get( - "/v2/internal/organization/", params, **authorization_header - ) - response = InternalOrganizationViewSet.as_view({"get": "list"})(request) - response.render() - content_data = json.loads(response.content) - return (response, content_data) - - def test_ok(self): - response, content_data = self.request( - {"user_email": self.owner.email}, - self.moduser_token, - ) - self.assertEqual(len(content_data), 2) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_not_ok(self): - response, content_data = self.request( - {"user_email": self.owner.email}, - self.owner_token, - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - -@tag("internal") -class InternalOrganizationCreateTestCase(InternalOrganizationTestCase): - def request(self, data, token=None): - authorization_header = auth_header(token) - - request = self.factory.post( - "/v2/internal/organization/", data, **authorization_header - ) - - response = InternalOrganizationViewSet.as_view({"post": "create"})(request) - response.render() - content_data = json.loads(response.content) - return (response, content_data) - - def test_ok(self): - response, content_data = self.request( - {"user_email": self.owner.email, "organization_name": "org3"}, - self.moduser_token, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - organization = Organization.objects.get(pk=content_data.get("id")) - self.assertEqual(organization.name, "org3") - organization_authorization = OrganizationAuthorization.objects.filter( - organization=organization, user=self.owner - ) - self.assertEqual(organization_authorization.count(), 1) - self.assertEqual( - organization_authorization.first().role, - OrganizationAuthorization.ROLE_ADMIN, - ) - - def test_not_ok(self): - response, content_data = self.request( - {"user_email": self.owner.email, "organization_name": "org3"}, - self.owner_token, - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - -@tag("internal") -class InternalOrganizationDestroyTestCase(InternalOrganizationTestCase): - def request(self, email, id, token=None): - authorization_header = auth_header(token) - - request = self.factory.delete( - f"/v2/internal/organization/?user_email={email}", **authorization_header - ) - - response = InternalOrganizationViewSet.as_view({"delete": "destroy"})( - request, pk=id - ) - response.render() - content_data = json.loads(response.content) - return (response, content_data) - - def test_ok(self): - response, content_data = self.request( - self.owner.email, - self.org_2.id, - self.moduser_token, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - organization = Organization.objects.filter(pk=self.org_2.pk) - self.assertEqual(organization.count(), 0) - - def test_not_ok(self): - response, content_data = self.request( - self.owner.email, - self.org_2.id, - self.owner_token, - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - -@tag("internal") -class InternalOrganizationUpdateTestCase(InternalOrganizationTestCase): - def request(self, data, email, id, token=None): - authorization_header = auth_header(token) - - request = self.factory.put( - f"/v2/internal/organization/?user_email={email}", - data, - **authorization_header, - ) - response = InternalOrganizationViewSet.as_view({"put": "update"})( - request, pk=id - ) - response.render() - content_data = json.loads(response.content) - return (response, content_data) - - def test_ok(self): - response, content_data = self.request( - {"name": "org222"}, - self.owner.email, - self.org_2.id, - self.moduser_token, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - organization = Organization.objects.get(pk=self.org_2.pk) - - self.assertEqual(organization.name, "org222") - - def test_not_ok(self): - response, content_data = self.request( - {"name": "org222"}, - self.owner.email, - self.org_2.id, - self.owner_token, - ) - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - -@tag("internal") -class InternalOrganizationRetrieveTestCase(InternalOrganizationTestCase): - def request(self, email, id, token=None): - authorization_header = auth_header(token) - - request = self.factory.get( - f"/v2/internal/organization/?user_email={email}", **authorization_header - ) - response = InternalOrganizationViewSet.as_view({"get": "retrieve"})( - request, pk=id - ) - response.render() - content_data = json.loads(response.content) - return (response, content_data) - - def test_ok(self): - response, content_data = self.request( - self.owner.email, - self.org.id, - self.moduser_token, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(content_data["repositories_count"], 2) - - def test_not_ok(self): - response, content_data = self.request( - self.owner.email, - self.org.id, - self.owner_token, - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/bothub/api/v2/internal/organization/views.py b/bothub/api/v2/internal/organization/views.py deleted file mode 100644 index 3fe839f1..00000000 --- a/bothub/api/v2/internal/organization/views.py +++ /dev/null @@ -1,120 +0,0 @@ -from django.utils.translation import ugettext_lazy as _ -from django.db import models -from rest_framework import mixins -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from bothub.api.v2.metadata import Metadata -from bothub.authentication.models import User -from bothub.common.models import ( - Organization, - OrganizationAuthorization, - Repository, - RepositoryAuthorization, -) -from bothub.api.v2.internal.organization.serializers import ( - OrganizationSerializer, - OrgCreateSerializer, - OrgUpdateSerializer, -) -from bothub import utils -from bothub.api.v2.internal.permissions import ModuleHasPermission -from bothub.api.v2.internal.organization.permissions import ( - InternalOrganizationAdminHasPermission, -) - - -class InternalOrganizationViewSet( - mixins.ListModelMixin, - mixins.RetrieveModelMixin, - mixins.CreateModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - GenericViewSet, -): - queryset = Organization.objects.all() - serializer_class = OrganizationSerializer - lookup_field = "pk" - permission_classes = [ModuleHasPermission] - metadata_class = Metadata - - def get_permissions(self): - """ - Instantiates and returns the list of permissions that this view requires. - """ - - permission_classes = self.permission_classes - if self.action in ["destroy", "update"]: - permission_classes.append(InternalOrganizationAdminHasPermission) - return [permission() for permission in self.permission_classes] - - def list(self, request, *args, **kwargs): - user = utils.get_user(request.query_params.get("user_email")) - serializer = OrganizationSerializer(user.get_user_organizations, many=True) - - return Response(serializer.data) - - def create(self, request, *args, **kwargs): - user, created = User.objects.get_or_create( - email=request.data.get("user_email", None), - defaults={"nickname": request.data.get("user_email", None)}, - ) - - serializer = OrgCreateSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - - validated_data = { - "name": serializer.validated_data.get("organization_name"), - "nickname": utils.organization_unique_slug_generator( - serializer.validated_data.get("organization_name") - ), - "description": "", - "locale": "", - } - - org = Organization.objects.create(**validated_data) - - org.organization_authorizations.create( - user=user, role=OrganizationAuthorization.ROLE_ADMIN - ) - org_serializer = OrganizationSerializer(org) - - return Response(org_serializer.data) - - def destroy(self, request, *args, **kwargs): - org = self.get_object() - user, created = User.objects.get_or_create( - email=request.query_params.get("user_email"), - defaults={"nickname": request.query_params.get("user_email")}, - ) - - org.delete() - return Response({_("Organization deleted with success")}) - - def update(self, request, *args, **kwargs): - org = self.get_object() - data = {"id": org.pk, **request.data} - serializer = OrgUpdateSerializer(data=data) - serializer.is_valid(raise_exception=True) - serializer.save() - - return Response(serializer.data) - - def retrieve(self, request, *args, **kwargs): - org = self.get_object() - - auths = ( - RepositoryAuthorization.objects.exclude(repository__owner=org) - .exclude(role=RepositoryAuthorization.ROLE_NOT_SETTED) - .filter(user=org) - ) - - response = { - "repositories_count": int( - Repository.objects.filter( - models.Q(uuid__in=auths) | models.Q(owner=org) - ).count() - ) - } - - return Response(response) diff --git a/bothub/api/v2/internal/permissions.py b/bothub/api/v2/internal/permissions.py deleted file mode 100644 index 4974aa76..00000000 --- a/bothub/api/v2/internal/permissions.py +++ /dev/null @@ -1,9 +0,0 @@ -from rest_framework import permissions - - -class ModuleHasPermission(permissions.BasePermission): - def has_permission(self, request, view): # pragma: no cover - return request.user.has_perm("authentication.can_communicate_internally") - - def has_object_permission(self, request, view, obj): - return self.has_permission(request, view) diff --git a/bothub/api/v2/internal/repository/__init__.py b/bothub/api/v2/internal/repository/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bothub/api/v2/internal/repository/serializers.py b/bothub/api/v2/internal/repository/serializers.py deleted file mode 100644 index 58351bfd..00000000 --- a/bothub/api/v2/internal/repository/serializers.py +++ /dev/null @@ -1,29 +0,0 @@ -from rest_framework import serializers - -from bothub.api.v2.repository.serializers import RepositoryCategorySerializer -from bothub.common.models import Repository - -from bothub.utils import internal_serializer_fields - - -class InternalRepositorySerializer(serializers.ModelSerializer): - owner__nickname = serializers.SerializerMethodField() - intents = serializers.SerializerMethodField() - available_languages = serializers.SerializerMethodField() - categories_list = serializers.SerializerMethodField() - - class Meta: - model = Repository - fields = internal_serializer_fields - - def get_owner__nickname(self, repository: Repository): - return repository.owner.nickname - - def get_intents(self, repository: Repository): - return repository.get_formatted_intents() - - def get_available_languages(self, repository: Repository): - return repository.available_languages() - - def get_categories_list(self, repository: Repository): - return RepositoryCategorySerializer(repository.categories, many=True).data diff --git a/bothub/api/v2/internal/repository/tests.py b/bothub/api/v2/internal/repository/tests.py deleted file mode 100644 index 9b83fe13..00000000 --- a/bothub/api/v2/internal/repository/tests.py +++ /dev/null @@ -1,112 +0,0 @@ -import json - -from django.test import tag -from django.test import RequestFactory -from django.test import TestCase -from rest_framework import status - -from bothub.api.v2.internal.repository.views import InternalRepositoriesViewSet -from bothub.api.v2.tests.utils import create_user_and_token -from bothub.common.models import ( - Organization, - OrganizationAuthorization, -) -from bothub.common.models import RepositoryCategory - -from bothub.api.v2.tests.utils import get_valid_mockups, create_repository_from_mockup - - -class InternalRepositoryTestCase(TestCase): - def setUp(self): - self.factory = RequestFactory() - self.moduser, self.moduser_token = create_user_and_token("module", module=True) - self.owner, self.owner_token = create_user_and_token("owner") - self.category = RepositoryCategory.objects.create(name="Category 1") - self.org = Organization.objects.create( - name="Organization 1", nickname="organization1" - ) - OrganizationAuthorization.objects.create( - user=self.owner, - organization=self.org, - role=OrganizationAuthorization.ROLE_ADMIN, - ) - self.repositories = [ - create_repository_from_mockup(self.org, **mockup) - for mockup in get_valid_mockups([self.category]) - ] - for rep in self.repositories: - rep.get_user_authorization(self.org) - - self.repository_auth = self.repositories[0].get_user_authorization(self.owner) - - -@tag("internal") -class InternalRepositoryListTestCase(InternalRepositoryTestCase): - def request(self, params, token=None): - authorization_header = ( - {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} - ) - - request = self.factory.get( - "/v2/internal/repository/", - params, - **authorization_header, - ) - response = InternalRepositoriesViewSet.as_view({"get": "list"})(request) - response.render() - content_data = json.loads(response.content) - return (response, content_data) - - def test_ok(self): - response, content_data = self.request( - {"name": "Repository 1", "org_id": self.org.id}, self.moduser_token - ) - self.assertEqual(content_data["count"], 1) - self.assertEqual(len(content_data["results"]), 1) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_return_all_repos(self): - response, content_data = self.request( - {"org_id": self.org.id}, self.moduser_token - ) - - self.assertEqual(content_data["count"], 2) - self.assertEqual(len(content_data["results"]), 2) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_fail_module(self): - response, content_data = self.request({"org_id": self.org.pk}, self.owner_token) - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - -@tag("internal") -class InternalRepositoryRetrieveAuthorizationTestCase(InternalRepositoryTestCase): - def request(self, params, token=None): - authorization_header = ( - {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} - ) - request = self.factory.get( - "/v2/internal/repository/RetrieveAuthorization/", - params, - **authorization_header, - ) - response = InternalRepositoriesViewSet.as_view( - {"get": "retrieve_authorization"} - )(request) - response.render() - content_data = json.loads(response.content) - return (response, content_data) - - def test_ok_repository_1(self): - response, content_data = self.request( - {"repository_authorization": self.repository_auth.pk}, self.moduser_token - ) - self.assertEqual(content_data["name"], "Repository 1") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_not_ok(self): - response, content_data = self.request( - {"repository_authorization": self.repository_auth.pk}, self.owner_token - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/bothub/api/v2/internal/repository/views.py b/bothub/api/v2/internal/repository/views.py deleted file mode 100644 index 5b769250..00000000 --- a/bothub/api/v2/internal/repository/views.py +++ /dev/null @@ -1,39 +0,0 @@ -from rest_framework import mixins -from rest_framework.decorators import action -from rest_framework.filters import SearchFilter -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from bothub.common.models import Repository - -from bothub.api.v2.internal.repository.serializers import InternalRepositorySerializer -from bothub.api.v2.internal.permissions import ModuleHasPermission - - -class InternalRepositoriesViewSet(mixins.ListModelMixin, GenericViewSet): - serializer_class = InternalRepositorySerializer - queryset = Repository.objects - permission_classes = [ModuleHasPermission] - filter_backends = [SearchFilter] - search_fields = ["$name", "^name", "=name"] - - def get_queryset(self, *args, **kwargs): - queryset = self.queryset.all() - name = self.request.query_params.get("name", None) - if name: - queryset = self.queryset.filter(name__icontains=name) - org_id = self.request.query_params.get("org_id", None) - if org_id: - queryset = queryset.filter(authorizations__user__pk=org_id) - - return queryset[:20] - - @action(detail=True, methods=["GET"], url_name="retrieve-authorization") - def retrieve_authorization(self, request, **kwargs): - auth = self.request.query_params.get("repository_authorization", None) - repository = Repository.objects.none() - if auth: - repository = Repository.objects.get(authorizations__uuid=auth) - serialized_data = InternalRepositorySerializer(repository) - - return Response(serialized_data.data) diff --git a/bothub/api/v2/internal/user/__init__.py b/bothub/api/v2/internal/user/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bothub/api/v2/internal/user/serializers.py b/bothub/api/v2/internal/user/serializers.py deleted file mode 100644 index e58a7c70..00000000 --- a/bothub/api/v2/internal/user/serializers.py +++ /dev/null @@ -1,37 +0,0 @@ -from rest_framework import serializers - -from bothub.authentication.models import User - - -class UserSerializer(serializers.ModelSerializer): - class Meta: - model = User - fields = [ - "id", - "email", - "nickname", - "name", - "language", - "joined_at", - "is_active", - "is_superuser", - ] - - -class UserPermissionSerializer(serializers.Serializer): - role = serializers.IntegerField() - - -class UserLanguageSerializer(serializers.ModelSerializer): - class Meta: - model = User - fields = ["language"] - read_only = [ - "id", - "email", - "nickname", - "name", - "joined_at", - "is_active", - "is_superuser", - ] diff --git a/bothub/api/v2/internal/user/tests.py b/bothub/api/v2/internal/user/tests.py deleted file mode 100644 index 20000083..00000000 --- a/bothub/api/v2/internal/user/tests.py +++ /dev/null @@ -1,208 +0,0 @@ -import json -from bothub.authentication.models import User - -from django.test import tag -from django.test import RequestFactory -from django.test import TestCase -from rest_framework import status - -from bothub.api.v2.internal.user.views import ( - UserPermissionViewSet, - UserViewSet, - UserLanguageViewSet, -) -from bothub.api.v2.tests.utils import create_user_and_token -from bothub.common.models import ( - Organization, - OrganizationAuthorization, - RepositoryCategory, -) - - -class InternalUserTestCase(TestCase): - def setUp(self): - self.factory = RequestFactory() - self.moduser, self.moduser_token = create_user_and_token("module", module=True) - self.owner, self.owner_token = create_user_and_token("owner") - self.user, self.user_token = create_user_and_token("user") - self.category = RepositoryCategory.objects.create(name="Category 1") - self.org = Organization.objects.create( - name="Organization 1", nickname="organization1" - ) - OrganizationAuthorization.objects.create( - user=self.owner, - organization=self.org, - role=OrganizationAuthorization.ROLE_ADMIN, - ) - OrganizationAuthorization.objects.create( - user=self.user, - organization=self.org, - role=OrganizationAuthorization.ROLE_CONTRIBUTOR, - ) - - -@tag("internal") -class InternalUserPermissionRetrieveTestCase(InternalUserTestCase): - def request(self, params, token=None): - authorization_header = ( - {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} - ) - - request = self.factory.get( - "/v2/internal/user/permission", params, **authorization_header - ) - response = UserPermissionViewSet.as_view({"get": "retrieve"})(request) - response.render() - content_data = json.loads(response.content) - return (response, content_data) - - def test_ok(self): - response, content_data = self.request( - {"user_email": self.owner.email, "org_id": self.org.pk}, - token=self.moduser_token, - ) - self.assertEqual(content_data["role"], OrganizationAuthorization.ROLE_ADMIN) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_not_ok(self): - response, content_data = self.request( - {"user_email": self.owner.email, "org_id": self.org.pk}, - token=self.owner_token, - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - -@tag("internal") -class InternalUserPermissionUpdateTestCase(InternalUserTestCase): - def request(self, params, user_email, org_id, token=None): - authorization_header = ( - {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} - ) - authorization_header["content_type"] = "application/json" - - request = self.factory.put( - f"/v2/internal/user/permission?user_email={user_email}&org_id={org_id}", - params, - **authorization_header, - ) - response = UserPermissionViewSet.as_view({"put": "update"})(request) - response.render() - content_data = json.loads(response.content) - return (response, content_data) - - def test_ok(self): - response, content_data = self.request( - {"role": OrganizationAuthorization.ROLE_CONTRIBUTOR}, - user_email=self.owner.email, - org_id=self.org.pk, - token=self.moduser_token, - ) - auth = OrganizationAuthorization.objects.get( - user=self.owner, organization=self.org - ) - self.assertEqual( - content_data["role"], OrganizationAuthorization.ROLE_CONTRIBUTOR - ) - self.assertEqual(content_data["role"], auth.role) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_not_ok(self): - response, content_data = self.request( - {"role": OrganizationAuthorization.ROLE_CONTRIBUTOR}, - user_email=self.owner.email, - org_id=self.org.pk, - token=self.owner_token, - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - -@tag("internal") -class InternalUserPermissionDeleteTestCase(InternalUserTestCase): - def request(self, user_email, org_id, token=None): - authorization_header = ( - {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} - ) - authorization_header["content_type"] = "application/json" - - request = self.factory.delete( - f"/v2/internal/user/permission?user_email={user_email}&org_id={org_id}", - {}, - **authorization_header, - ) - response = UserPermissionViewSet.as_view({"delete": "destroy"})(request) - response.render() - content_data = json.loads(response.content) - return (response, content_data) - - def test_ok(self): - response, content_data = self.request( - user_email=self.owner.email, org_id=self.org.pk, token=self.moduser_token - ) - self.assertEqual(content_data["role"], None) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_not_ok(self): - response, content_data = self.request( - user_email=self.owner.email, org_id=self.org.pk, token=self.owner_token - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - -@tag("internal") -class InternalUserRetrieveTestCase(InternalUserTestCase): - def request(self, params, token=None): - authorization_header = ( - {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} - ) - - request = self.factory.get("/v2/internal/user", params, **authorization_header) - response = UserViewSet.as_view({"get": "retrieve"})(request) - response.render() - content_data = json.loads(response.content) - return (response, content_data) - - def test_ok(self): - response, content_data = self.request( - {"user_email": self.owner.email}, token=self.moduser_token - ) - self.assertEqual(content_data["email"], self.owner.email) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_not_ok(self): - response, content_data = self.request( - {"user_email": self.owner.email}, token=self.owner_token - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - -@tag("internal") -class InternalUserLanguageUpdateTestCase(InternalUserTestCase): - def request(self, params, user_email, token=None): - authorization_header = ( - {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} - ) - authorization_header["content_type"] = "application/json" - - request = self.factory.put( - f"/v2/internal/user/language?user_email={user_email}", - params, - **authorization_header, - ) - response = UserLanguageViewSet.as_view({"put": "update"})(request) - response.render() - content_data = json.loads(response.content) - return (response, content_data) - - def test_ok(self): - response, content_data = self.request( - {"language": "es"}, user_email=self.owner.email, token=self.moduser_token - ) - user = User.objects.get(email=self.owner.email) - self.assertEqual(content_data["language"], user.language) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_not_ok(self): - response, content_data = self.request( - {"language": "es"}, user_email=self.owner.email, token=self.owner_token - ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/bothub/api/v2/internal/user/views.py b/bothub/api/v2/internal/user/views.py deleted file mode 100644 index 7c206595..00000000 --- a/bothub/api/v2/internal/user/views.py +++ /dev/null @@ -1,98 +0,0 @@ -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet -from rest_framework import mixins - -from bothub.authentication.models import User -from bothub.common.models import Organization, OrganizationAuthorization -from bothub.api.v2.internal.user.serializers import ( - UserPermissionSerializer, - UserSerializer, - UserLanguageSerializer, -) -from bothub import utils -from bothub.api.v2.internal.permissions import ModuleHasPermission - - -class UserPermissionViewSet( - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - GenericViewSet, -): - queryset = OrganizationAuthorization.objects.all() - permission_classes = [ModuleHasPermission] - serializer_class = UserPermissionSerializer - lookup_field = None - - def retrieve(self, request, **kwargs): - user, org = utils.get_user_and_organization( - request.query_params.get("user_email", None), - request.query_params.get("org_id", None), - ) - permissions = self._get_user_permissions(org, user) - serializer = UserPermissionSerializer(permissions) - - return Response(serializer.data) - - def update(self, request, **kwargs): - user, org = utils.get_user_and_organization( - request.query_params.get("user_email", None), - request.query_params.get("org_id", None), - ) - - org.set_user_permission(user=user, permission=request.data.get("role", None)) - - permission = self._get_user_permissions(org, user) - serializer = UserPermissionSerializer(permission) - - return Response(serializer.data) - - def destroy(self, request, **kwargs): - user, org = utils.get_user_and_organization( - request.query_params.get("user_email", None), - request.query_params.get("org_id", None), - ) - - self._get_user_permissions(org, user).delete() - - permissions = self._get_user_permissions(org, user) - serializer = UserPermissionSerializer(permissions) - - return Response(serializer.data) - - def _get_user_permissions(self, org: Organization, user: User) -> dict: - return org.organization_authorizations.filter(user=user).first() - - -class UserViewSet(mixins.RetrieveModelMixin, GenericViewSet): - serializer_class = UserSerializer - permission_classes = [ModuleHasPermission] - queryset = User.objects - lookup_field = None - - def retrieve(self, request, **kwargs): - user, created = User.objects.get_or_create( - email=request.query_params.get("user_email"), - defaults={"nickname": request.query_params.get("user_email")}, - ) - - return Response(UserSerializer(user).data) - - -class UserLanguageViewSet(mixins.UpdateModelMixin, GenericViewSet): - serializer_class = UserLanguageSerializer - permission_classes = [ModuleHasPermission] - queryset = User.objects - lookup_field = None - - def update(self, request, **kwargs): - user, created = User.objects.get_or_create( - email=request.query_params.get("user_email"), - defaults={"nickname": request.query_params.get("user_email")}, - ) - serializer = UserLanguageSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - - user.language = request.data["language"] - user.save() - return Response(UserLanguageSerializer(user).data) diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index c0370173..22bfd3d9 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -59,10 +59,6 @@ CanContributeInRepositoryTranslatedExampleValidator, ) -from bothub.api.v2.internal.connect_rest_client import ( - ConnectRESTClient as ConnectClient, -) - class RequestRepositoryAuthorizationSerializer(serializers.ModelSerializer): class Meta: @@ -1658,13 +1654,10 @@ class AddRepositoryProjectSerializer(serializers.Serializer): project_uuid = serializers.CharField(required=True) def create(self, validated_data): - if settings.USE_GRPC: - task = celery_app.send_task( - name="create_repository_project", kwargs=validated_data - ) - task.wait() - else: - task = ConnectClient().create_classifier(**validated_data) + task = celery_app.send_task( + name="create_repository_project", kwargs=validated_data + ) + task.wait() return validated_data def to_representation(self, instance): diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index d988c7f1..a69918f1 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -121,10 +121,6 @@ AddRepositoryProjectSerializer, ) -from bothub.api.v2.internal.connect_rest_client import ( - ConnectRESTClient as ConnectClient, -) - class NewRepositoryViewSet( MultipleFieldLookupMixin, mixins.RetrieveModelMixin, GenericViewSet @@ -253,26 +249,19 @@ def projectrepository(self, request, **kwargs): if not authorization.can_contribute: raise PermissionDenied() - if settings.USE_GRPC: - task = celery_app.send_task( - name="get_project_organization", args=[project_uuid] - ) - task.wait() - authorizations = task.result - repositories = repository.authorizations.filter(uuid__in=authorizations) + task = celery_app.send_task( + name="get_project_organization", args=[project_uuid] + ) + task.wait() - else: - authorizations = ConnectClient().list_authorizations( - project_uuid=project_uuid, user_email=request.user.email - ) - repositories = repository.authorizations.filter(uuid__in=authorizations) + repositories = repository.authorizations.filter(uuid__in=task.result) data = dict(in_project=repositories.exists()) if organization: organization_authorization = ( - organization.organization_authorizations.filter(uuid__in=authorizations) + organization.organization_authorizations.filter(uuid__in=task.result) ) data["in_project"] = ( data["in_project"] or organization_authorization.exists() @@ -312,27 +301,16 @@ def remove_repository_project(self, request, **kwargs): if not user_authorization.is_admin: raise PermissionDenied() - if settings.USE_GRPC: - project_organization = celery_app.send_task( - name="get_project_organization", args=[project_uuid] - ) - project_organization.wait() - project_organization = project_organization.result - authorizations = list( - repository.authorizations.filter( - uuid__in=project_organization - ).values_list("uuid", flat=True) - ) + project_organization = celery_app.send_task( + name="get_project_organization", args=[project_uuid] + ) + project_organization.wait() - else: - project_organization = ConnectClient().list_authorizations( - project_uuid=project_uuid, user_email=request.user.email - ) - authorizations = list( - repository.authorizations.filter( - uuid__in=project_organization - ).values_list("uuid", flat=True) - ) + authorizations = list( + repository.authorizations.filter( + uuid__in=project_organization.result + ).values_list("uuid", flat=True) + ) if organization: organization_authorization = organization.get_organization_authorization( @@ -343,7 +321,7 @@ def remove_repository_project(self, request, **kwargs): authorizations += list( organization.organization_authorizations.filter( - uuid__in=project_organization + uuid__in=project_organization.result ).values_list("uuid", flat=True) ) @@ -356,20 +334,11 @@ def remove_repository_project(self, request, **kwargs): lambda authorization: str(authorization), authorizations ) - if settings.USE_GRPC: - task = celery_app.send_task( - name="remove_authorizations_project", - args=[project_uuid, list(authorizations_uuids), request.user.email], - ) - task.wait() - else: - task = ConnectClient().list_authorizations( - project_uuid=project_uuid, user_email=request.user.email - ) - for authorization_uuid in authorizations_uuids: - ConnectClient().remove_authorization( - project_uuid, authorization_uuid, request.user.email - ) + task = celery_app.send_task( + name="remove_authorizations_project", + args=[project_uuid, list(authorizations_uuids), request.user.email], + ) + task.wait() return Response(status=status.HTTP_204_NO_CONTENT) @@ -412,22 +381,14 @@ def add_repository_project(self, request, **kwargs): project_uuid = serializer.validated_data.get("project_uuid") - if settings.USE_GRPC: - task = celery_app.send_task( - name="get_project_organization", args=[project_uuid] - ) - task.wait() + task = celery_app.send_task( + name="get_project_organization", args=[project_uuid] + ) + task.wait() - organization_authorization = ( - organization.organization_authorizations.filter(uuid__in=task.result) - ) - else: - task = ConnectClient().list_authorizations( - project_uuid=project_uuid, user_email=request.user.email - ) - organization_authorization = ( - organization.organization_authorizations.filter(uuid__in=task) - ) + organization_authorization = organization.organization_authorizations.filter( + uuid__in=task.result + ) if organization_authorization.exists(): raise ValidationError(_("Repository already added")) @@ -842,21 +803,12 @@ def list_project_organizatiton(self, request, **kwargs): if not project_uuid: raise ValidationError(_("Need to pass 'project_uuid' in query params")) - if settings.USE_GRPC: - task = celery_app.send_task( - name="get_project_organization", args=[project_uuid, request.user.email] - ) - task.wait() - repositories = Repository.objects.filter( - authorizations__uuid__in=task.result - ) - else: - authorizations = ConnectClient().list_authorizations( - project_uuid=project_uuid, user_email=request.user.email - ) - repositories = Repository.objects.filter( - authorizations__uuid__in=authorizations - ) + task = celery_app.send_task( + name="get_project_organization", args=[project_uuid] + ) + task.wait() + + repositories = Repository.objects.filter(authorizations__uuid__in=task.result) serialized_data = ShortRepositorySerializer(repositories, many=True) return Response(serialized_data.data) diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index fbf67a88..55e8b818 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -66,14 +66,6 @@ RepositoryTranslatorViewSet, ) -from bothub.api.v2.internal.repository.views import InternalRepositoriesViewSet -from bothub.api.v2.internal.organization.views import InternalOrganizationViewSet -from bothub.api.v2.internal.user.views import ( - UserPermissionViewSet, - UserViewSet, - UserLanguageViewSet, -) - class Router(routers.SimpleRouter): routes = [ @@ -241,8 +233,3 @@ def get_lookup_regex(self, viewset, lookup_prefix=""): router.register("org/organization", OrganizationViewSet) router.register("org/profile", OrganizationProfileViewSet) router.register("org/authorizations", OrganizationAuthorizationViewSet) -router.register("internal/organization", InternalOrganizationViewSet) -router.register("internal/repository", InternalRepositoriesViewSet) -router.register("internal/user", UserViewSet) -router.register("internal/user/permission", UserPermissionViewSet) -router.register("internal/user/language", UserLanguageViewSet) diff --git a/bothub/api/v2/tests/test_repository.py b/bothub/api/v2/tests/test_repository.py index 025753f9..9aa601e6 100644 --- a/bothub/api/v2/tests/test_repository.py +++ b/bothub/api/v2/tests/test_repository.py @@ -42,11 +42,48 @@ from bothub.common.models import RepositoryVote from bothub.common.models import RequestRepositoryAuthorization -from bothub.api.v2.tests.utils import ( - get_valid_mockups, - get_invalid_mockups, - create_repository_from_mockup, -) + +def get_valid_mockups(categories): + return [ + { + "name": "Repository 1", + "slug": "repository-1", + "description": "", + "language": languages.LANGUAGE_EN, + "categories": [category.pk for category in categories], + }, + { + "name": "Repository 2", + "description": "", + "language": languages.LANGUAGE_PT, + "categories": [category.pk for category in categories], + }, + ] + + +def get_invalid_mockups(categories): + return [ + { + "name": "", + "slug": "repository-1", + "language": languages.LANGUAGE_EN, + "categories": [category.pk for category in categories], + }, + { + "name": "Repository 3", + "language": "out", + "categories": [category.pk for category in categories], + "is_private": False, + }, + ] + + +def create_repository_from_mockup(owner, categories, **mockup): + r = Repository.objects.create(owner_id=owner.id, **mockup) + r.current_version() + for category in categories: + r.categories.add(category) + return r class CreateRepositoryAPITestCase(TestCase): diff --git a/bothub/api/v2/tests/utils.py b/bothub/api/v2/tests/utils.py index 5d1c6901..7ede7eb6 100644 --- a/bothub/api/v2/tests/utils.py +++ b/bothub/api/v2/tests/utils.py @@ -1,58 +1,9 @@ -from bothub.utils import check_module_permission from rest_framework.authtoken.models import Token from bothub.authentication.models import User -from bothub.common import languages -from bothub.common.models import Repository -def create_user_and_token(nickname="fake", module=False): +def create_user_and_token(nickname="fake"): user = User.objects.create_user("{}@user.com".format(nickname), nickname) - if module is True: - check_module_permission({"can_communicate_internally": module}, user) - user = User.objects.get(email=user.email) token, create = Token.objects.get_or_create(user=user) return (user, token) - - -def get_valid_mockups(categories): - return [ - { - "name": "Repository 1", - "slug": "repository-1", - "description": "", - "language": languages.LANGUAGE_EN, - "categories": [category.pk for category in categories], - }, - { - "name": "Repository 2", - "description": "", - "language": languages.LANGUAGE_PT, - "categories": [category.pk for category in categories], - }, - ] - - -def get_invalid_mockups(categories): - return [ - { - "name": "", - "slug": "repository-1", - "language": languages.LANGUAGE_EN, - "categories": [category.pk for category in categories], - }, - { - "name": "Repository 3", - "language": "out", - "categories": [category.pk for category in categories], - "is_private": False, - }, - ] - - -def create_repository_from_mockup(owner, categories, **mockup): - r = Repository.objects.create(owner_id=owner.id, **mockup) - r.current_version() - for category in categories: - r.categories.add(category) - return r diff --git a/bothub/authentication/authorization.py b/bothub/authentication/authorization.py index c2e4562c..ccc89e80 100644 --- a/bothub/authentication/authorization.py +++ b/bothub/authentication/authorization.py @@ -1,10 +1,6 @@ import logging -import re from django.utils.translation import ugettext_lazy as _ -from bothub.utils import check_module_permission - - from mozilla_django_oidc.auth import OIDCAuthenticationBackend from rest_framework import exceptions from rest_framework.authentication import TokenAuthentication, get_authorization_header @@ -96,20 +92,16 @@ def create_user(self, claims): # Override existing create_user method in OIDCAuthenticationBackend email = claims.get("email") username = self.get_username(claims)[:16] - username = re.sub("[^A-Za-z0-9]+", "", username) user = self.UserModel.objects.create_user(email, username) user.name = claims.get("name", "") user.save() - check_module_permission(claims, user) - return user def update_user(self, user, claims): user.name = claims.get("name", "") user.email = claims.get("email", "") user.save() - check_module_permission(claims, user) return user diff --git a/bothub/common/models.py b/bothub/common/models.py index 05163b18..f9a8793e 100644 --- a/bothub/common/models.py +++ b/bothub/common/models.py @@ -906,18 +906,16 @@ def get_specific_version_id(self, repository_version, language=None): def get_user_authorization(self, user): if user.is_anonymous: return RepositoryAuthorization(repository=self) - repo_auth, created = RepositoryAuthorization.objects.get_or_create( + get, created = RepositoryAuthorization.objects.get_or_create( user=user.repository_owner, repository=self ) if self.owner.is_organization: - org_auth = self.owner.organization.get_organization_authorization(user) - - # Excluding ROLE_TRANSLATE as it does not correspond to the same role in the client app (connect). - # todo: update this conditional with corresponding role rule - if repo_auth.role < org_auth.role and org_auth.role < RepositoryAuthorization.ROLE_TRANSLATE: - repo_auth.role = org_auth.role - repo_auth.save(update_fields=['role']) - return repo_auth + org_role = self.owner.organization.get_organization_authorization(user).role + if get.role != org_role and get.role == 0: + get.role = org_role + get.save() + + return get def get_absolute_url(self): return "{}dashboard/{}/{}/".format( diff --git a/bothub/common/tasks.py b/bothub/common/tasks.py index 1660e7f0..59d29e77 100644 --- a/bothub/common/tasks.py +++ b/bothub/common/tasks.py @@ -12,6 +12,7 @@ from django_elasticsearch_dsl.registries import registry from bothub import translate +from bothub.api.grpc.connect_grpc_client import ConnectGRPCClient from bothub.celery import app from bothub.common.models import ( RepositoryQueueTask, @@ -39,14 +40,6 @@ ) -if settings.USE_GRPC: - from bothub.api.grpc.connect_grpc_client import ConnectGRPCClient as ConnectClient -else: - from bothub.api.v2.internal.connect_rest_client import ( - ConnectRESTClient as ConnectClient, - ) - - @app.task(name="es_handle_save") def handle_save(pk, app_label, model_name): sender = apps.get_model(app_label, model_name) @@ -551,16 +544,9 @@ def evaluate_crossvalidation(data, authorization_token): # pragma: no cover @app.task(name="get_project_organization") -def get_project_organization( - project_uuid: str, user_email: str = "" -): # pragma: no cover - grpc_client = ConnectClient() - if settings.USE_GRPC: - authorizations = grpc_client.list_authorizations(project_uuid=project_uuid) - else: - authorizations = grpc_client.list_authorizations( - project_uuid=project_uuid, user_email=user_email - ) +def get_project_organization(project_uuid: str): # pragma: no cover + grpc_client = ConnectGRPCClient() + authorizations = grpc_client.list_authorizations(project_uuid=project_uuid) return authorizations @@ -568,13 +554,13 @@ def get_project_organization( def remove_authorizations_project( project_uuid: str, authorizations_uuids: list, user_email: str ): - grpc_client = ConnectClient() + grpc_client = ConnectGRPCClient() for authorization_uuid in authorizations_uuids: grpc_client.remove_authorization(project_uuid, authorization_uuid, user_email) @app.task(name="create_repository_project") def create_repository_project(**kwargs): - grpc_client = ConnectClient() + grpc_client = ConnectGRPCClient() grpc_client.create_classifier(**kwargs) return kwargs diff --git a/bothub/common/tests.py b/bothub/common/tests.py index a9bfe505..8b72ba04 100644 --- a/bothub/common/tests.py +++ b/bothub/common/tests.py @@ -15,8 +15,6 @@ RepositoryQueueTask, QAKnowledgeBase, QAtext, - Organization, - OrganizationAuthorization ) from .models import RepositoryAuthorization from .models import RepositoryEntity @@ -546,7 +544,6 @@ class RepositoryAuthorizationTestCase(TestCase): def setUp(self): self.owner = User.objects.create_user("owner@user.com", "owner") self.user = User.objects.create_user("fake@user.com", "user") - self.collaborator = User.objects.create_user("colaborator@user.com", "collaborator") self.repository = Repository.objects.create( owner=self.owner.repository_owner, name="Test", slug="test" @@ -557,12 +554,6 @@ def setUp(self): slug="private", is_private=True, ) - self.organization = Organization.objects.create(name="Weni") - self.organization_repository = Repository.objects.create( - owner=self.organization, - name="Organization Repository", - slug="organization_repository" - ) def test_admin_level(self): authorization = self.repository.get_user_authorization(self.owner) @@ -691,50 +682,6 @@ def test_role_contributor_can_contribute(self): authorization_user.save() self.assertTrue(authorization_user.can_contribute) - def test_organization_auth_over_repository_auth(self): - """ - Tests that a User's authorization role is of the highest level possible in a Repository, - either using the RepositoryAuthorization or the OrganizationAuthorization. - The expected behavior is that the organization's authorization role should be passed to the repository's authorization for that user. - """ - - # The role that the user should inherit from the organization authorization inside the repository. - initial_organization_role = OrganizationAuthorization.ROLE_ADMIN - - # Set user's role to a low level at the Repository - collaborator_repository_auth, created = RepositoryAuthorization.objects.get_or_create( - user=self.collaborator, repository=self.organization_repository, role=RepositoryAuthorization.ROLE_USER - ) - # Set user's role to a high level at the Organization - collaborator_organization_auth = self.organization.organization_authorizations.create( - user=self.collaborator, role=initial_organization_role - ) - - # Validate that their access level corresponds to their role in the Organization and not the Repository, as it is higher at this point. - user_authorization = self.organization_repository.get_user_authorization(self.collaborator) - self.assertEqual(user_authorization.role, collaborator_organization_auth.role) - - # Lower their level inside the Organization - collaborator_organization_auth.role = OrganizationAuthorization.ROLE_NOT_SETTED - collaborator_organization_auth.save() - - # Validate that the repository authorization level was updated. - collaborator_repository_auth.refresh_from_db() - self.assertEqual(collaborator_repository_auth.role, initial_organization_role) - - # Validate that the user's level is now the Repository's and not the Organization's, as it is higher. - user_authorization = self.organization_repository.get_user_authorization(self.collaborator) - self.assertEqual(user_authorization.role, collaborator_repository_auth.role) - - # Verify that org auth with (role >= 4) will not update the repository's authorization - - # Set user's role to ROLE_TRANSLATE level at the Organization - collaborator_organization_auth.role = OrganizationAuthorization.ROLE_TRANSLATE - collaborator_organization_auth.save() - - user_authorization = self.organization_repository.get_user_authorization(self.collaborator) - self.assertEqual(user_authorization.role, collaborator_repository_auth.role) - class RepositoryVersionTrainingTestCase(TestCase): def setUp(self): diff --git a/bothub/settings.py b/bothub/settings.py index 6a473163..0f0b01fc 100644 --- a/bothub/settings.py +++ b/bothub/settings.py @@ -75,8 +75,6 @@ SECRET_KEY_CHECK_LEGACY_USER=(str, None), CONNECT_GRPC_SERVER_URL=(str, "localhost:8002"), CONNECT_CERTIFICATE_GRPC_CRT=(str, None), - USE_GRPC=(bool, False), - CONNECT_API_URL=(str, ""), REPOSITORY_RESTRICT_ACCESS_NLP_LOGS=(list, []), REPOSITORY_BLOCK_USER_LOGS=(list, []), REPOSITORY_KNOWLEDGE_BASE_DESCRIPTION_LIMIT=(int, 450), @@ -94,17 +92,6 @@ ELASTICSEARCH_LOGS_DELETE_AGE=(str, "90d"), ELASTICSEARCH_LOGS_ROLLOVER_AGE=(str, "1d"), ELASTICSEARCH_TIMESTAMP_PIPELINE_FIELD=(str, "created_at"), - CSP_DEFAULT_SRC=(tuple, "CSP_DEFAULT_SRC"), - CSP_FRAME_ANCESTORS=(tuple, "CSP_FRAME_ANCESTORS"), - CSP_FONT_SRC=(tuple, "CSP_FONT_SRC"), - CSP_STYLE_SRC=(tuple, "CSP_STYLE_SRC"), - CSP_STYLE_SRC_ELEM=(tuple, "CSP_STYLE_SRC_ELEM"), - CSP_SCRIPT_SRC=(tuple, "CSP_SCRIPT_SRC"), - CSP_SCRIPT_SRC_ELEM=(tuple, "CSP_SCRIPT_SRC_ELEM"), - CSP_FRAME_SRC=(tuple, "CSP_FRAME_SRC"), - CSP_CONNECT_SRC=(tuple, "CSP_CONNECT_SRC"), - CSP_WORKER_SRC=(tuple, "CSP_WORKER_SRC"), - CSP_IMG_SRC=(tuple, "CSP_IMG_SRC"), ) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -157,12 +144,12 @@ "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.locale.LocaleMiddleware", - "csp.middleware.CSPMiddleware", "corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", "bothub.api.v2.middleware.UserLanguageMiddleware", ] @@ -312,30 +299,6 @@ CSRF_COOKIE_SECURE = env.bool("CSRF_COOKIE_SECURE") -# CSP headers - -DEFAULT_CSP_SETTINGS = ("'self'",) -DEFAULT_CSP_WENI_SETTINGS = DEFAULT_CSP_SETTINGS + ("*.weni.ai",) - -CSP_DEFAULT_SRC = env.tuple("CSP_DEFAULT_SRC", default=DEFAULT_CSP_SETTINGS) -CSP_FRAME_ANCESTORS = env.tuple( - "CSP_FRAME_ANCESTORS", default=DEFAULT_CSP_WENI_SETTINGS -) -CSP_FONT_SRC = env.tuple("CSP_FONT_SRC", default=CSP_DEFAULT_SRC) -CSP_STYLE_SRC = env.tuple( - "CSP_STYLE_SRC", default=DEFAULT_CSP_SETTINGS + ("'unsafe-inline'", "'unsafe-eval'") -) -CSP_STYLE_SRC_ELEM = env.tuple("CSP_STYLE_SRC_ELEM", default=CSP_STYLE_SRC) -CSP_SCRIPT_SRC = env.tuple("CSP_SCRIPT_SRC", default=CSP_STYLE_SRC) -CSP_SCRIPT_SRC_ELEM = env.tuple("CSP_SCRIPT_SRC_ELEM", default=CSP_SCRIPT_SRC) -CSP_FRAME_SRC = env.tuple("CSP_FRAME_SRC", default=CSP_DEFAULT_SRC) -CSP_CONNECT_SRC = env.tuple("CSP_CONNECT_SRC", default=CSP_DEFAULT_SRC) -CSP_WORKER_SRC = env.tuple( - "CSP_WORKER_SRC", default=DEFAULT_CSP_WENI_SETTINGS + ("blob:", "data:") -) -CSP_IMG_SRC = env.tuple("CSP_IMG_SRC", default=CSP_WORKER_SRC) - - # Logging LOGGING = DEFAULT_LOGGING @@ -568,10 +531,6 @@ CONNECT_CERTIFICATE_GRPC_CRT = env.str("CONNECT_CERTIFICATE_GRPC_CRT") -USE_GRPC = env.bool("USE_GRPC", default=False) - -CONNECT_API_URL = env.str("CONNECT_API_URL", default="https://api.dev.cloud.weni.ai") - # ElasticSearch ELASTICSEARCH_DSL = { "default": {"hosts": env.str("ELASTICSEARCH_DSL", default="es:9200")} @@ -626,5 +585,3 @@ ] REPOSITORY_BLOCK_USER_LOGS = env.list("REPOSITORY_BLOCK_USER_LOGS", default=[]) - -CACHE_TTL = env.int("CACHE_TTL", default=180) diff --git a/bothub/utils.py b/bothub/utils.py index 40e36dcd..38949d12 100644 --- a/bothub/utils.py +++ b/bothub/utils.py @@ -20,7 +20,6 @@ from rest_framework import status from rest_framework.exceptions import APIException, ValidationError - entity_regex = re.compile( r"\[(?P[^\]]+)" r"\]\((?P[^:)]*?)" r"(?:\:(?P[^)]+))?\)" ) @@ -376,17 +375,6 @@ def get_organization(request, organization_id: int): ) -def get_user_and_organization(user_email: str, organization_id: int): - from bothub.authentication.models import User - from bothub.common.models import Organization - - org = Organization.objects.get(pk=organization_id) - user, created = User.objects.get_or_create( - email=user_email, defaults={"nickname": user_email} - ) - return user, org - - class TimeBasedDocument(Document): def save(self, action="create", **kwargs): return super().save(action=action, **kwargs) @@ -474,43 +462,3 @@ def filter_has_invalid_entities(self, queryset, name, value): return filter_validate_entities(queryset, value).exclude( original_entities_count=F("entities_count") ) - - -def check_module_permission(claims, user): - from django.contrib.auth.models import Permission - from django.contrib.contenttypes.models import ContentType - from bothub.common.models import User - - if claims.get("can_communicate_internally", False): - content_type = ContentType.objects.get_for_model(User) - permission, created = Permission.objects.get_or_create( - codename="can_communicate_internally", - name="can communicate internally", - content_type=content_type, - ) - if not user.has_perm("authentication.can_communicate_internally"): - user.user_permissions.add(permission) - return True - return False - - -internal_serializer_fields = [ - "uuid", - "name", - "slug", - "description", - "is_private", - "created_at", - "language", - "owner", - "algorithm", - "use_competing_intents", - "use_name_entities", - "use_analyze_char", - "owner__nickname", - "intents", - "categories", - "available_languages", - "categories_list", - "repository_type", -] diff --git a/poetry.lock b/poetry.lock index 6199b760..d67a0a89 100644 --- a/poetry.lock +++ b/poetry.lock @@ -445,21 +445,6 @@ python-versions = ">=3.6" [package.dependencies] Django = ">=2.2" -[[package]] -name = "django-csp" -version = "3.7" -description = "Django Content Security Policy support." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -Django = ">=1.8" - -[package.extras] -jinja2 = ["jinja2 (>=2.9.6)"] -tests = ["pytest (<4.0)", "pytest-django", "pytest-flakes (==1.0.1)", "pytest-pep8 (==1.0.6)", "pep8 (==1.4.6)", "mock (==1.0.1)", "six (==1.12.0)", "jinja2 (>=2.9.6)"] - [[package]] name = "django-elasticsearch-dsl" version = "7.2.2" @@ -1726,7 +1711,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "~=3.6.15" -content-hash = "31ccdac8c205f34af79715c1d090792b5496b461d94085bc7715f635cfbf4dfa" +content-hash = "dd6bfc5c1373deba58e4e85a9cb3114b91f7d5d5a33db1ac48afee53bbe395f8" [metadata.files] amqp = [ @@ -1985,10 +1970,6 @@ django-cors-headers = [ {file = "django-cors-headers-3.7.0.tar.gz", hash = "sha256:96069c4aaacace786a34ee7894ff680780ec2644e4268b31181044410fecd12e"}, {file = "django_cors_headers-3.7.0-py3-none-any.whl", hash = "sha256:1ac2b1213de75a251e2ba04448da15f99bcfcbe164288ae6b5ff929dc49b372f"}, ] -django-csp = [ - {file = "django_csp-3.7-py2.py3-none-any.whl", hash = "sha256:01443a07723f9a479d498bd7bb63571aaa771e690f64bde515db6cdb76e8041a"}, - {file = "django_csp-3.7.tar.gz", hash = "sha256:01eda02ad3f10261c74131cdc0b5a6a62b7c7ad4fd017fbefb7a14776e0a9727"}, -] django-elasticsearch-dsl = [ {file = "django-elasticsearch-dsl-7.2.2.tar.gz", hash = "sha256:811d3909b3387fd55c19d9bbcf0e9a9b234f085df3f8422d59e7519a5f733e0e"}, {file = "django_elasticsearch_dsl-7.2.2-py2.py3-none-any.whl", hash = "sha256:3c58a254a6318b169eb904d41d802924b99ea8e53ddc2c596ebba90506cf47fa"}, diff --git a/pyproject.toml b/pyproject.toml index 0abed417..c6d48f5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,6 @@ psycopg2-binary = "~=2.9.1" weni-protobuffers = "~=1.2.1" black = "21.7b0" Pillow = "~=8.4.0" -django-csp = "^3.7" [tool.poetry.dev-dependencies] flake8 = "~=4.0.0" From e3cf9261679a24f48ae3504dfd80a1fd581a6483 Mon Sep 17 00:00:00 2001 From: Helder Souza <42891390+helllllllder@users.noreply.github.com> Date: Wed, 27 Jul 2022 17:11:59 -0300 Subject: [PATCH 4/9] feat: use rest instead of grpc (#738) * Develop (#709) * Fix: Remove ai from project (#707) * remove document deletion from delete_nlp_logs task * inconsistency number and debug errors fixed * add user_email to remove_authorizations_project * Feature/health check blocklist (#708) * remove document deletion from delete_nlp_logs task * add a blocklist for not saving logs depending on the authorization user * inconsistency number and debug errors fixed * change the REPOSITORY_BLOCK_USER_LOGS values from users to repository authorizations * change readme * pass on sonarcloud * change admins settings * transform uuid into string * convert uuid into string at test_blocked_user * feat: Change gRPC call to REST calls * Fix: Remove ai from project (#707) * remove document deletion from delete_nlp_logs task * inconsistency number and debug errors fixed * add user_email to remove_authorizations_project * Feature/health check blocklist (#708) * remove document deletion from delete_nlp_logs task * add a blocklist for not saving logs depending on the authorization user * inconsistency number and debug errors fixed * change the REPOSITORY_BLOCK_USER_LOGS values from users to repository authorizations * change readme * pass on sonarcloud * change admins settings * transform uuid into string * convert uuid into string at test_blocked_user * Create internal module, module permission and updated WeniOIDCAuthenticationBackend to add permission if the user is a module (#712) * Feature/internal endpoint organization (#713) * Create internal module, module permission and updated WeniOIDCAuthenticationBackend to add permission if the user is a module * Add organization internal endpoints * Feature/internal endpoint repository (#714) * Fix: Remove ai from project (#707) * remove document deletion from delete_nlp_logs task * inconsistency number and debug errors fixed * add user_email to remove_authorizations_project * Feature/health check blocklist (#708) * remove document deletion from delete_nlp_logs task * add a blocklist for not saving logs depending on the authorization user * inconsistency number and debug errors fixed * change the REPOSITORY_BLOCK_USER_LOGS values from users to repository authorizations * change readme * pass on sonarcloud * change admins settings * transform uuid into string * convert uuid into string at test_blocked_user * Create internal module, module permission and updated WeniOIDCAuthenticationBackend to add permission if the user is a module * Add organization internal endpoints * add repository endpoints * code smells * duplicated lines * fix code smells * Feature/internal endpoint user (#715) * Create internal module, module permission and updated WeniOIDCAuthenticationBackend to add permission if the user is a module * Add organization internal endpoints * add repository endpoints * add user endpoints * flake8 and black * Feature/connect rest client (#716) * Create internal module, module permission and updated WeniOIDCAuthenticationBackend to add permission if the user is a module * Add organization internal endpoints * add repository endpoints * add user endpoints * flake8 and black * implement connect rest client * fix code smells * black * Feature/add csp (#717) * Develop (#709) (#710) * Fix: Remove ai from project (#707) * remove document deletion from delete_nlp_logs task * inconsistency number and debug errors fixed * add user_email to remove_authorizations_project * Feature/health check blocklist (#708) * remove document deletion from delete_nlp_logs task * add a blocklist for not saving logs depending on the authorization user * inconsistency number and debug errors fixed * change the REPOSITORY_BLOCK_USER_LOGS values from users to repository authorizations * change readme * pass on sonarcloud * change admins settings * transform uuid into string * convert uuid into string at test_blocked_user * add regex remotion of special characters from username when creating from keycloak * add django_csp and settings * configure csp * fix settings * black * Feature/internal endpoint user (#718) * Create internal module, module permission and updated WeniOIDCAuthenticationBackend to add permission if the user is a module * Add organization internal endpoints * add repository endpoints * add user endpoints * flake8 and black * fix user endpoints * Feature/add csp (#719) * Develop (#709) (#710) * Fix: Remove ai from project (#707) * remove document deletion from delete_nlp_logs task * inconsistency number and debug errors fixed * add user_email to remove_authorizations_project * Feature/health check blocklist (#708) * remove document deletion from delete_nlp_logs task * add a blocklist for not saving logs depending on the authorization user * inconsistency number and debug errors fixed * change the REPOSITORY_BLOCK_USER_LOGS values from users to repository authorizations * change readme * pass on sonarcloud * change admins settings * transform uuid into string * convert uuid into string at test_blocked_user * add regex remotion of special characters from username when creating from keycloak * add django_csp and settings * configure csp * fix settings * black * fix csp settings * fix csp settings * Feature/internal endpoint user (#720) * Create internal module, module permission and updated WeniOIDCAuthenticationBackend to add permission if the user is a module * Add organization internal endpoints * add repository endpoints * add user endpoints * flake8 and black * fix user endpoints * fix connect client * Fix the repository get_user_authorization method The updated method compares repository and organization authorizations and uses the highest role level without overwriting the repository's authorization level * Update rule with repository authorization role overwriting The method now overwrites the repository's authorization role if is lower than 4 (i.e. ROLE_TRANSLATOR), as of this level on, it no longer has corresponding roles at the client app. * Linting * feat: update list_project_organization endpoint removing the celery call (#725) * Feature/remove rest client from celery (#726) * feat: update list_project_organization endpoint removing the celery call * feat: update all code that called celery tasks for communicating with connect to calls directly into ConnectRESTClient * Feature/remove rest client from celery (#727) * feat: update list_project_organization endpoint removing the celery call * feat: update all code that called celery tasks for communicating with connect to calls directly into ConnectRESTClient * bugfix: remove task.result after condition to user or not grpc endpoints, as it was conflicting with the rest call * Feature/remove rest client from celery (#730) * Develop (#709) (#710) * Fix: Remove ai from project (#707) * remove document deletion from delete_nlp_logs task * inconsistency number and debug errors fixed * add user_email to remove_authorizations_project * Feature/health check blocklist (#708) * remove document deletion from delete_nlp_logs task * add a blocklist for not saving logs depending on the authorization user * inconsistency number and debug errors fixed * change the REPOSITORY_BLOCK_USER_LOGS values from users to repository authorizations * change readme * pass on sonarcloud * change admins settings * transform uuid into string * convert uuid into string at test_blocked_user * feat: update list_project_organization endpoint removing the celery call * feat: update all code that called celery tasks for communicating with connect to calls directly into ConnectRESTClient * bugfix: remove task.result after condition to user or not grpc endpoints, as it was conflicting with the rest call * feat: cache request result in list_classifiers * Feature/remove rest client from celery (#731) * Develop (#709) (#710) * Fix: Remove ai from project (#707) * remove document deletion from delete_nlp_logs task * inconsistency number and debug errors fixed * add user_email to remove_authorizations_project * Feature/health check blocklist (#708) * remove document deletion from delete_nlp_logs task * add a blocklist for not saving logs depending on the authorization user * inconsistency number and debug errors fixed * change the REPOSITORY_BLOCK_USER_LOGS values from users to repository authorizations * change readme * pass on sonarcloud * change admins settings * transform uuid into string * convert uuid into string at test_blocked_user * feat: update list_project_organization endpoint removing the celery call * feat: update all code that called celery tasks for communicating with connect to calls directly into ConnectRESTClient * bugfix: remove task.result after condition to user or not grpc endpoints, as it was conflicting with the rest call * feat: cache request result in list_classifiers * feat: delete cache when creating and removing a classifier * Revert "Feature/remove rest client from celery (#731)" (#733) This reverts commit 4a0b7f2abb9d767ef2b4a010c07e2ffd84bd63eb. * Revert "Feature/remove rest client from celery (#730)" (#734) This reverts commit 8e42c7dafe890f0674f53d9a4dbbf8c379bef94e. Co-authored-by: Victor Salles Co-authored-by: vitusalis <49169676+vitusalis@users.noreply.github.com> Co-authored-by: Victor Salles Co-authored-by: vitusalis <49169676+vitusalis@users.noreply.github.com> --- README.md | 2 + bothub/api/grpc/repository/serializers.py | 23 +- bothub/api/v2/internal/__init__.py | 0 bothub/api/v2/internal/connect_rest_client.py | 81 ++++++ .../api/v2/internal/organization/__init__.py | 0 .../v2/internal/organization/permissions.py | 11 + .../v2/internal/organization/serializers.py | 83 +++++++ bothub/api/v2/internal/organization/tests.py | 232 ++++++++++++++++++ bothub/api/v2/internal/organization/views.py | 120 +++++++++ bothub/api/v2/internal/permissions.py | 9 + bothub/api/v2/internal/repository/__init__.py | 0 .../api/v2/internal/repository/serializers.py | 29 +++ bothub/api/v2/internal/repository/tests.py | 112 +++++++++ bothub/api/v2/internal/repository/views.py | 39 +++ bothub/api/v2/internal/user/__init__.py | 0 bothub/api/v2/internal/user/serializers.py | 37 +++ bothub/api/v2/internal/user/tests.py | 208 ++++++++++++++++ bothub/api/v2/internal/user/views.py | 98 ++++++++ bothub/api/v2/repository/serializers.py | 15 +- bothub/api/v2/repository/views.py | 116 ++++++--- bothub/api/v2/routers.py | 13 + bothub/api/v2/tests/test_repository.py | 47 +--- bothub/api/v2/tests/utils.py | 51 +++- bothub/authentication/authorization.py | 8 + bothub/common/models.py | 16 +- bothub/common/tasks.py | 26 +- bothub/common/tests.py | 53 ++++ bothub/settings.py | 43 +++- bothub/utils.py | 52 ++++ poetry.lock | 21 +- pyproject.toml | 1 + 31 files changed, 1430 insertions(+), 116 deletions(-) create mode 100644 bothub/api/v2/internal/__init__.py create mode 100644 bothub/api/v2/internal/connect_rest_client.py create mode 100644 bothub/api/v2/internal/organization/__init__.py create mode 100644 bothub/api/v2/internal/organization/permissions.py create mode 100644 bothub/api/v2/internal/organization/serializers.py create mode 100644 bothub/api/v2/internal/organization/tests.py create mode 100644 bothub/api/v2/internal/organization/views.py create mode 100644 bothub/api/v2/internal/permissions.py create mode 100644 bothub/api/v2/internal/repository/__init__.py create mode 100644 bothub/api/v2/internal/repository/serializers.py create mode 100644 bothub/api/v2/internal/repository/tests.py create mode 100644 bothub/api/v2/internal/repository/views.py create mode 100644 bothub/api/v2/internal/user/__init__.py create mode 100644 bothub/api/v2/internal/user/serializers.py create mode 100644 bothub/api/v2/internal/user/tests.py create mode 100644 bothub/api/v2/internal/user/views.py diff --git a/README.md b/README.md index 1846f7f8..9dc314fd 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,8 @@ You can set environment variables in your OS, write on ```.env``` file or pass v | OIDC_RP_SCOPES | ```string``` | ```openid email``` | The OpenID Connect scopes to request during login. | CONNECT_GRPC_SERVER_URL | ```string``` | ```localhost:8002``` | Define grpc connect server url | CONNECT_CERTIFICATE_GRPC_CRT | ```string``` | ```None``` | Absolute certificate path for secure grpc communication +| CONNECT_API_URL | ```string``` | ```None``` | Connect module api url +| USE_GRPC | ```bool``` | ```False``` | Use connect gRPC clients | RECAPTCHA_SECRET_KEY | ```string``` | ```''``` | Token of the recaptcha used in the validation of a user's registration. | REPOSITORY_NLP_LOG_LIMIT | ```int``` | ```10000``` | Limit of query size to repository log. | REPOSITORY_RESTRICT_ACCESS_NLP_LOGS | ```list``` | ```[]``` | Restricts log access to a particular or multiple intelligences diff --git a/bothub/api/grpc/repository/serializers.py b/bothub/api/grpc/repository/serializers.py index 901f6d3f..1c8536e6 100644 --- a/bothub/api/grpc/repository/serializers.py +++ b/bothub/api/grpc/repository/serializers.py @@ -5,6 +5,8 @@ from bothub.common.models import Repository from weni.protobuf.intelligence import repository_pb2 +from bothub.utils import internal_serializer_fields + class RepositoryProtoSerializer(proto_serializers.ModelProtoSerializer): owner__nickname = serializers.SerializerMethodField() @@ -15,26 +17,7 @@ class RepositoryProtoSerializer(proto_serializers.ModelProtoSerializer): class Meta: model = Repository proto_class = repository_pb2.Repository - fields = [ - "uuid", - "name", - "slug", - "description", - "is_private", - "created_at", - "language", - "owner", - "algorithm", - "use_competing_intents", - "use_name_entities", - "use_analyze_char", - "owner__nickname", - "intents", - "categories", - "available_languages", - "categories_list", - "repository_type", - ] + fields = internal_serializer_fields def get_owner__nickname(self, repository: Repository): return repository.owner.nickname diff --git a/bothub/api/v2/internal/__init__.py b/bothub/api/v2/internal/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bothub/api/v2/internal/connect_rest_client.py b/bothub/api/v2/internal/connect_rest_client.py new file mode 100644 index 00000000..df2409d0 --- /dev/null +++ b/bothub/api/v2/internal/connect_rest_client.py @@ -0,0 +1,81 @@ +import requests +from typing import List, Dict + +from django.conf import settings + + +class ConnectRESTClient: + def __init__(self): + self.base_url = settings.CONNECT_API_URL + self.headers = { + "Content-Type": "application/json; charset: utf-8", + "Authorization": self.get_auth_token(), + } + + def get_auth_token(self) -> str: + request = requests.post( + url=settings.OIDC_OP_TOKEN_ENDPOINT, + data={ + "client_id": settings.OIDC_RP_CLIENT_ID, + "client_secret": settings.OIDC_RP_CLIENT_SECRET, + "grant_type": "client_credentials", + }, + ) + token = request.json().get("access_token") + return f"Bearer {token}" + + def list_classifiers( + self, project_uuid: str, user_email: str + ) -> List[Dict[str, str]]: + request = requests.get( + url=f"{self.base_url}/v1/organization/project/list_classifier/", + headers=self.headers, + params={"project_uuid": project_uuid, "user_email": user_email}, + ) + + return request.json()["data"] + + def list_authorizations(self, project_uuid: str, user_email: str) -> List[str]: + classifiers = self.list_classifiers( + project_uuid=project_uuid, user_email=user_email + ) + + return [classifier.get("authorization_uuid") for classifier in classifiers] + + def get_authorization_classifier( + self, project_uuid: str, authorization_uuid: str, user_email: str + ) -> str: + """ + Recives a authorization UUID and returns the respective classifier UUID + """ + classifiers = self.list_classifiers(project_uuid, user_email) + classifier = filter( + lambda classifier: classifier["authorization_uuid"] == authorization_uuid, + classifiers, + ) + + return next(classifier).get("uuid") + + def remove_authorization( + self, project_uuid: str, authorization_uuid: str, user_email: str + ): + classifier_uuid = self.get_authorization_classifier( + project_uuid, + authorization_uuid, + user_email, + ) + request = requests.delete( + url=f"{self.base_url}/v1/organization/project/destroy_classifier/", + headers=self.headers, + params={"uuid": classifier_uuid, "user_email": user_email}, + ) + + return request.json() + + def create_classifier(self, **kwargs): + request = requests.post( + url=f"{self.base_url}/v1/organization/project/create_classifier/", + headers=self.headers, + params={**kwargs, "classifier_type": "bothub"}, + ) + return request.json() diff --git a/bothub/api/v2/internal/organization/__init__.py b/bothub/api/v2/internal/organization/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bothub/api/v2/internal/organization/permissions.py b/bothub/api/v2/internal/organization/permissions.py new file mode 100644 index 00000000..6edbf607 --- /dev/null +++ b/bothub/api/v2/internal/organization/permissions.py @@ -0,0 +1,11 @@ +from rest_framework import permissions + +from bothub.utils import get_user + + +class InternalOrganizationAdminHasPermission(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + authorization = obj.organization.get_organization_authorization( + get_user(request.query_params.get("user_email", None)) + ) + return authorization.is_admin diff --git a/bothub/api/v2/internal/organization/serializers.py b/bothub/api/v2/internal/organization/serializers.py new file mode 100644 index 00000000..26456b3a --- /dev/null +++ b/bothub/api/v2/internal/organization/serializers.py @@ -0,0 +1,83 @@ +from django.db.models import F +from django.utils.translation import ugettext_lazy as _ + +from rest_framework import serializers +from rest_framework.exceptions import ValidationError + +from bothub.authentication.models import User +from bothub.common.models import Organization, OrganizationAuthorization + + +class OrganizationSerializer(serializers.ModelSerializer): + + users = serializers.SerializerMethodField() + + def get_users(self, org: Organization): + return list( + org.organization_authorizations.exclude( + role=OrganizationAuthorization.LEVEL_NOTHING + ) + .annotate( + org_user_id=F("user__user_owner__pk"), + org_user_email=F("user__user_owner__email"), + org_user_nickname=F("user__user_owner__nickname"), + org_user_name=F("user__user_owner__name"), + ) + .values( + "org_user_id", "org_user_email", "org_user_nickname", "org_user_name" + ) + ) + + class Meta: + model = Organization + fields = ["id", "name", "users"] + + +class OrgCreateSerializer(serializers.ModelSerializer): + + organization_name = serializers.CharField() + user_email = serializers.CharField() + + def validate_user_email(self, value: str) -> str: + try: + User.objects.get(email=value) + except User.DoesNotExist: + raise ValidationError(_("{} not found!").format(value)) + + return value + + class Meta: + model = Organization + fields = ["organization_name", "user_email"] + + +class OrgUpdateSerializer(serializers.ModelSerializer): + + id = serializers.IntegerField() + name = serializers.CharField(required=False) + + def validate_id(self, value): + try: + Organization.objects.get(pk=value) + except Organization.DoesNotExist: + raise ValidationError(f"{value} not found!") + + return value + + def save(self): + data = dict(self.validated_data) + + org = Organization.objects.get(pk=data.get("id")) + + updated_fields = self.get_updated_fields(data) + + if updated_fields: + org.__dict__.update(**updated_fields) + org.save() + + def get_updated_fields(self, data): + return {key: value for key, value in data.items() if key not in ["id"]} + + class Meta: + model = Organization + fields = ["id", "name"] diff --git a/bothub/api/v2/internal/organization/tests.py b/bothub/api/v2/internal/organization/tests.py new file mode 100644 index 00000000..4ac09bf6 --- /dev/null +++ b/bothub/api/v2/internal/organization/tests.py @@ -0,0 +1,232 @@ +import json + +from django.test import tag +from django.test import RequestFactory +from django.test import TestCase +from rest_framework import status + +from bothub.api.v2.internal.organization.views import InternalOrganizationViewSet +from bothub.api.v2.tests.utils import create_user_and_token +from bothub.common.models import ( + Organization, + OrganizationAuthorization, + RepositoryCategory, +) +from bothub.api.v2.tests.utils import get_valid_mockups, create_repository_from_mockup + + +class InternalOrganizationTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + self.moduser, self.moduser_token = create_user_and_token("module", module=True) + self.owner, self.owner_token = create_user_and_token("owner") + self.category = RepositoryCategory.objects.create(name="Category 1") + self.org = Organization.objects.create( + name="Organization 1", nickname="organization1" + ) + self.org_2 = Organization.objects.create( + name="Organization 2", nickname="organization2" + ) + OrganizationAuthorization.objects.create( + user=self.owner, + organization=self.org, + role=OrganizationAuthorization.ROLE_ADMIN, + ) + OrganizationAuthorization.objects.create( + user=self.owner, + organization=self.org_2, + role=OrganizationAuthorization.ROLE_ADMIN, + ) + self.repositories = [ + create_repository_from_mockup(self.org, **mockup) + for mockup in get_valid_mockups([self.category]) + ] + for rep in self.repositories: + rep.get_user_authorization(self.org) + + self.repository_auth = self.repositories[0].get_user_authorization(self.owner) + + +def auth_header(token): + authorization_header = ( + {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} + ) + authorization_header["content_type"] = "application/json" + + return authorization_header + + +@tag("internal") +class InternalOrganizationListTestCase(InternalOrganizationTestCase): + def request(self, params, token=None): + authorization_header = auth_header(token) + + request = self.factory.get( + "/v2/internal/organization/", params, **authorization_header + ) + response = InternalOrganizationViewSet.as_view({"get": "list"})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + {"user_email": self.owner.email}, + self.moduser_token, + ) + self.assertEqual(len(content_data), 2) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_not_ok(self): + response, content_data = self.request( + {"user_email": self.owner.email}, + self.owner_token, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalOrganizationCreateTestCase(InternalOrganizationTestCase): + def request(self, data, token=None): + authorization_header = auth_header(token) + + request = self.factory.post( + "/v2/internal/organization/", data, **authorization_header + ) + + response = InternalOrganizationViewSet.as_view({"post": "create"})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + {"user_email": self.owner.email, "organization_name": "org3"}, + self.moduser_token, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + organization = Organization.objects.get(pk=content_data.get("id")) + self.assertEqual(organization.name, "org3") + organization_authorization = OrganizationAuthorization.objects.filter( + organization=organization, user=self.owner + ) + self.assertEqual(organization_authorization.count(), 1) + self.assertEqual( + organization_authorization.first().role, + OrganizationAuthorization.ROLE_ADMIN, + ) + + def test_not_ok(self): + response, content_data = self.request( + {"user_email": self.owner.email, "organization_name": "org3"}, + self.owner_token, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalOrganizationDestroyTestCase(InternalOrganizationTestCase): + def request(self, email, id, token=None): + authorization_header = auth_header(token) + + request = self.factory.delete( + f"/v2/internal/organization/?user_email={email}", **authorization_header + ) + + response = InternalOrganizationViewSet.as_view({"delete": "destroy"})( + request, pk=id + ) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + self.owner.email, + self.org_2.id, + self.moduser_token, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + organization = Organization.objects.filter(pk=self.org_2.pk) + self.assertEqual(organization.count(), 0) + + def test_not_ok(self): + response, content_data = self.request( + self.owner.email, + self.org_2.id, + self.owner_token, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalOrganizationUpdateTestCase(InternalOrganizationTestCase): + def request(self, data, email, id, token=None): + authorization_header = auth_header(token) + + request = self.factory.put( + f"/v2/internal/organization/?user_email={email}", + data, + **authorization_header, + ) + response = InternalOrganizationViewSet.as_view({"put": "update"})( + request, pk=id + ) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + {"name": "org222"}, + self.owner.email, + self.org_2.id, + self.moduser_token, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + organization = Organization.objects.get(pk=self.org_2.pk) + + self.assertEqual(organization.name, "org222") + + def test_not_ok(self): + response, content_data = self.request( + {"name": "org222"}, + self.owner.email, + self.org_2.id, + self.owner_token, + ) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalOrganizationRetrieveTestCase(InternalOrganizationTestCase): + def request(self, email, id, token=None): + authorization_header = auth_header(token) + + request = self.factory.get( + f"/v2/internal/organization/?user_email={email}", **authorization_header + ) + response = InternalOrganizationViewSet.as_view({"get": "retrieve"})( + request, pk=id + ) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + self.owner.email, + self.org.id, + self.moduser_token, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(content_data["repositories_count"], 2) + + def test_not_ok(self): + response, content_data = self.request( + self.owner.email, + self.org.id, + self.owner_token, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/bothub/api/v2/internal/organization/views.py b/bothub/api/v2/internal/organization/views.py new file mode 100644 index 00000000..3fe839f1 --- /dev/null +++ b/bothub/api/v2/internal/organization/views.py @@ -0,0 +1,120 @@ +from django.utils.translation import ugettext_lazy as _ +from django.db import models +from rest_framework import mixins +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + +from bothub.api.v2.metadata import Metadata +from bothub.authentication.models import User +from bothub.common.models import ( + Organization, + OrganizationAuthorization, + Repository, + RepositoryAuthorization, +) +from bothub.api.v2.internal.organization.serializers import ( + OrganizationSerializer, + OrgCreateSerializer, + OrgUpdateSerializer, +) +from bothub import utils +from bothub.api.v2.internal.permissions import ModuleHasPermission +from bothub.api.v2.internal.organization.permissions import ( + InternalOrganizationAdminHasPermission, +) + + +class InternalOrganizationViewSet( + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.CreateModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + GenericViewSet, +): + queryset = Organization.objects.all() + serializer_class = OrganizationSerializer + lookup_field = "pk" + permission_classes = [ModuleHasPermission] + metadata_class = Metadata + + def get_permissions(self): + """ + Instantiates and returns the list of permissions that this view requires. + """ + + permission_classes = self.permission_classes + if self.action in ["destroy", "update"]: + permission_classes.append(InternalOrganizationAdminHasPermission) + return [permission() for permission in self.permission_classes] + + def list(self, request, *args, **kwargs): + user = utils.get_user(request.query_params.get("user_email")) + serializer = OrganizationSerializer(user.get_user_organizations, many=True) + + return Response(serializer.data) + + def create(self, request, *args, **kwargs): + user, created = User.objects.get_or_create( + email=request.data.get("user_email", None), + defaults={"nickname": request.data.get("user_email", None)}, + ) + + serializer = OrgCreateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + validated_data = { + "name": serializer.validated_data.get("organization_name"), + "nickname": utils.organization_unique_slug_generator( + serializer.validated_data.get("organization_name") + ), + "description": "", + "locale": "", + } + + org = Organization.objects.create(**validated_data) + + org.organization_authorizations.create( + user=user, role=OrganizationAuthorization.ROLE_ADMIN + ) + org_serializer = OrganizationSerializer(org) + + return Response(org_serializer.data) + + def destroy(self, request, *args, **kwargs): + org = self.get_object() + user, created = User.objects.get_or_create( + email=request.query_params.get("user_email"), + defaults={"nickname": request.query_params.get("user_email")}, + ) + + org.delete() + return Response({_("Organization deleted with success")}) + + def update(self, request, *args, **kwargs): + org = self.get_object() + data = {"id": org.pk, **request.data} + serializer = OrgUpdateSerializer(data=data) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response(serializer.data) + + def retrieve(self, request, *args, **kwargs): + org = self.get_object() + + auths = ( + RepositoryAuthorization.objects.exclude(repository__owner=org) + .exclude(role=RepositoryAuthorization.ROLE_NOT_SETTED) + .filter(user=org) + ) + + response = { + "repositories_count": int( + Repository.objects.filter( + models.Q(uuid__in=auths) | models.Q(owner=org) + ).count() + ) + } + + return Response(response) diff --git a/bothub/api/v2/internal/permissions.py b/bothub/api/v2/internal/permissions.py new file mode 100644 index 00000000..4974aa76 --- /dev/null +++ b/bothub/api/v2/internal/permissions.py @@ -0,0 +1,9 @@ +from rest_framework import permissions + + +class ModuleHasPermission(permissions.BasePermission): + def has_permission(self, request, view): # pragma: no cover + return request.user.has_perm("authentication.can_communicate_internally") + + def has_object_permission(self, request, view, obj): + return self.has_permission(request, view) diff --git a/bothub/api/v2/internal/repository/__init__.py b/bothub/api/v2/internal/repository/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bothub/api/v2/internal/repository/serializers.py b/bothub/api/v2/internal/repository/serializers.py new file mode 100644 index 00000000..58351bfd --- /dev/null +++ b/bothub/api/v2/internal/repository/serializers.py @@ -0,0 +1,29 @@ +from rest_framework import serializers + +from bothub.api.v2.repository.serializers import RepositoryCategorySerializer +from bothub.common.models import Repository + +from bothub.utils import internal_serializer_fields + + +class InternalRepositorySerializer(serializers.ModelSerializer): + owner__nickname = serializers.SerializerMethodField() + intents = serializers.SerializerMethodField() + available_languages = serializers.SerializerMethodField() + categories_list = serializers.SerializerMethodField() + + class Meta: + model = Repository + fields = internal_serializer_fields + + def get_owner__nickname(self, repository: Repository): + return repository.owner.nickname + + def get_intents(self, repository: Repository): + return repository.get_formatted_intents() + + def get_available_languages(self, repository: Repository): + return repository.available_languages() + + def get_categories_list(self, repository: Repository): + return RepositoryCategorySerializer(repository.categories, many=True).data diff --git a/bothub/api/v2/internal/repository/tests.py b/bothub/api/v2/internal/repository/tests.py new file mode 100644 index 00000000..9b83fe13 --- /dev/null +++ b/bothub/api/v2/internal/repository/tests.py @@ -0,0 +1,112 @@ +import json + +from django.test import tag +from django.test import RequestFactory +from django.test import TestCase +from rest_framework import status + +from bothub.api.v2.internal.repository.views import InternalRepositoriesViewSet +from bothub.api.v2.tests.utils import create_user_and_token +from bothub.common.models import ( + Organization, + OrganizationAuthorization, +) +from bothub.common.models import RepositoryCategory + +from bothub.api.v2.tests.utils import get_valid_mockups, create_repository_from_mockup + + +class InternalRepositoryTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + self.moduser, self.moduser_token = create_user_and_token("module", module=True) + self.owner, self.owner_token = create_user_and_token("owner") + self.category = RepositoryCategory.objects.create(name="Category 1") + self.org = Organization.objects.create( + name="Organization 1", nickname="organization1" + ) + OrganizationAuthorization.objects.create( + user=self.owner, + organization=self.org, + role=OrganizationAuthorization.ROLE_ADMIN, + ) + self.repositories = [ + create_repository_from_mockup(self.org, **mockup) + for mockup in get_valid_mockups([self.category]) + ] + for rep in self.repositories: + rep.get_user_authorization(self.org) + + self.repository_auth = self.repositories[0].get_user_authorization(self.owner) + + +@tag("internal") +class InternalRepositoryListTestCase(InternalRepositoryTestCase): + def request(self, params, token=None): + authorization_header = ( + {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} + ) + + request = self.factory.get( + "/v2/internal/repository/", + params, + **authorization_header, + ) + response = InternalRepositoriesViewSet.as_view({"get": "list"})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + {"name": "Repository 1", "org_id": self.org.id}, self.moduser_token + ) + self.assertEqual(content_data["count"], 1) + self.assertEqual(len(content_data["results"]), 1) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_return_all_repos(self): + response, content_data = self.request( + {"org_id": self.org.id}, self.moduser_token + ) + + self.assertEqual(content_data["count"], 2) + self.assertEqual(len(content_data["results"]), 2) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_fail_module(self): + response, content_data = self.request({"org_id": self.org.pk}, self.owner_token) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalRepositoryRetrieveAuthorizationTestCase(InternalRepositoryTestCase): + def request(self, params, token=None): + authorization_header = ( + {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} + ) + request = self.factory.get( + "/v2/internal/repository/RetrieveAuthorization/", + params, + **authorization_header, + ) + response = InternalRepositoriesViewSet.as_view( + {"get": "retrieve_authorization"} + )(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok_repository_1(self): + response, content_data = self.request( + {"repository_authorization": self.repository_auth.pk}, self.moduser_token + ) + self.assertEqual(content_data["name"], "Repository 1") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_not_ok(self): + response, content_data = self.request( + {"repository_authorization": self.repository_auth.pk}, self.owner_token + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/bothub/api/v2/internal/repository/views.py b/bothub/api/v2/internal/repository/views.py new file mode 100644 index 00000000..5b769250 --- /dev/null +++ b/bothub/api/v2/internal/repository/views.py @@ -0,0 +1,39 @@ +from rest_framework import mixins +from rest_framework.decorators import action +from rest_framework.filters import SearchFilter +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + +from bothub.common.models import Repository + +from bothub.api.v2.internal.repository.serializers import InternalRepositorySerializer +from bothub.api.v2.internal.permissions import ModuleHasPermission + + +class InternalRepositoriesViewSet(mixins.ListModelMixin, GenericViewSet): + serializer_class = InternalRepositorySerializer + queryset = Repository.objects + permission_classes = [ModuleHasPermission] + filter_backends = [SearchFilter] + search_fields = ["$name", "^name", "=name"] + + def get_queryset(self, *args, **kwargs): + queryset = self.queryset.all() + name = self.request.query_params.get("name", None) + if name: + queryset = self.queryset.filter(name__icontains=name) + org_id = self.request.query_params.get("org_id", None) + if org_id: + queryset = queryset.filter(authorizations__user__pk=org_id) + + return queryset[:20] + + @action(detail=True, methods=["GET"], url_name="retrieve-authorization") + def retrieve_authorization(self, request, **kwargs): + auth = self.request.query_params.get("repository_authorization", None) + repository = Repository.objects.none() + if auth: + repository = Repository.objects.get(authorizations__uuid=auth) + serialized_data = InternalRepositorySerializer(repository) + + return Response(serialized_data.data) diff --git a/bothub/api/v2/internal/user/__init__.py b/bothub/api/v2/internal/user/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bothub/api/v2/internal/user/serializers.py b/bothub/api/v2/internal/user/serializers.py new file mode 100644 index 00000000..e58a7c70 --- /dev/null +++ b/bothub/api/v2/internal/user/serializers.py @@ -0,0 +1,37 @@ +from rest_framework import serializers + +from bothub.authentication.models import User + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = [ + "id", + "email", + "nickname", + "name", + "language", + "joined_at", + "is_active", + "is_superuser", + ] + + +class UserPermissionSerializer(serializers.Serializer): + role = serializers.IntegerField() + + +class UserLanguageSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ["language"] + read_only = [ + "id", + "email", + "nickname", + "name", + "joined_at", + "is_active", + "is_superuser", + ] diff --git a/bothub/api/v2/internal/user/tests.py b/bothub/api/v2/internal/user/tests.py new file mode 100644 index 00000000..20000083 --- /dev/null +++ b/bothub/api/v2/internal/user/tests.py @@ -0,0 +1,208 @@ +import json +from bothub.authentication.models import User + +from django.test import tag +from django.test import RequestFactory +from django.test import TestCase +from rest_framework import status + +from bothub.api.v2.internal.user.views import ( + UserPermissionViewSet, + UserViewSet, + UserLanguageViewSet, +) +from bothub.api.v2.tests.utils import create_user_and_token +from bothub.common.models import ( + Organization, + OrganizationAuthorization, + RepositoryCategory, +) + + +class InternalUserTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + self.moduser, self.moduser_token = create_user_and_token("module", module=True) + self.owner, self.owner_token = create_user_and_token("owner") + self.user, self.user_token = create_user_and_token("user") + self.category = RepositoryCategory.objects.create(name="Category 1") + self.org = Organization.objects.create( + name="Organization 1", nickname="organization1" + ) + OrganizationAuthorization.objects.create( + user=self.owner, + organization=self.org, + role=OrganizationAuthorization.ROLE_ADMIN, + ) + OrganizationAuthorization.objects.create( + user=self.user, + organization=self.org, + role=OrganizationAuthorization.ROLE_CONTRIBUTOR, + ) + + +@tag("internal") +class InternalUserPermissionRetrieveTestCase(InternalUserTestCase): + def request(self, params, token=None): + authorization_header = ( + {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} + ) + + request = self.factory.get( + "/v2/internal/user/permission", params, **authorization_header + ) + response = UserPermissionViewSet.as_view({"get": "retrieve"})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + {"user_email": self.owner.email, "org_id": self.org.pk}, + token=self.moduser_token, + ) + self.assertEqual(content_data["role"], OrganizationAuthorization.ROLE_ADMIN) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_not_ok(self): + response, content_data = self.request( + {"user_email": self.owner.email, "org_id": self.org.pk}, + token=self.owner_token, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalUserPermissionUpdateTestCase(InternalUserTestCase): + def request(self, params, user_email, org_id, token=None): + authorization_header = ( + {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} + ) + authorization_header["content_type"] = "application/json" + + request = self.factory.put( + f"/v2/internal/user/permission?user_email={user_email}&org_id={org_id}", + params, + **authorization_header, + ) + response = UserPermissionViewSet.as_view({"put": "update"})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + {"role": OrganizationAuthorization.ROLE_CONTRIBUTOR}, + user_email=self.owner.email, + org_id=self.org.pk, + token=self.moduser_token, + ) + auth = OrganizationAuthorization.objects.get( + user=self.owner, organization=self.org + ) + self.assertEqual( + content_data["role"], OrganizationAuthorization.ROLE_CONTRIBUTOR + ) + self.assertEqual(content_data["role"], auth.role) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_not_ok(self): + response, content_data = self.request( + {"role": OrganizationAuthorization.ROLE_CONTRIBUTOR}, + user_email=self.owner.email, + org_id=self.org.pk, + token=self.owner_token, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalUserPermissionDeleteTestCase(InternalUserTestCase): + def request(self, user_email, org_id, token=None): + authorization_header = ( + {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} + ) + authorization_header["content_type"] = "application/json" + + request = self.factory.delete( + f"/v2/internal/user/permission?user_email={user_email}&org_id={org_id}", + {}, + **authorization_header, + ) + response = UserPermissionViewSet.as_view({"delete": "destroy"})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + user_email=self.owner.email, org_id=self.org.pk, token=self.moduser_token + ) + self.assertEqual(content_data["role"], None) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_not_ok(self): + response, content_data = self.request( + user_email=self.owner.email, org_id=self.org.pk, token=self.owner_token + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalUserRetrieveTestCase(InternalUserTestCase): + def request(self, params, token=None): + authorization_header = ( + {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} + ) + + request = self.factory.get("/v2/internal/user", params, **authorization_header) + response = UserViewSet.as_view({"get": "retrieve"})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + {"user_email": self.owner.email}, token=self.moduser_token + ) + self.assertEqual(content_data["email"], self.owner.email) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_not_ok(self): + response, content_data = self.request( + {"user_email": self.owner.email}, token=self.owner_token + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +@tag("internal") +class InternalUserLanguageUpdateTestCase(InternalUserTestCase): + def request(self, params, user_email, token=None): + authorization_header = ( + {"HTTP_AUTHORIZATION": "Token {}".format(token.key)} if token else {} + ) + authorization_header["content_type"] = "application/json" + + request = self.factory.put( + f"/v2/internal/user/language?user_email={user_email}", + params, + **authorization_header, + ) + response = UserLanguageViewSet.as_view({"put": "update"})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data) + + def test_ok(self): + response, content_data = self.request( + {"language": "es"}, user_email=self.owner.email, token=self.moduser_token + ) + user = User.objects.get(email=self.owner.email) + self.assertEqual(content_data["language"], user.language) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_not_ok(self): + response, content_data = self.request( + {"language": "es"}, user_email=self.owner.email, token=self.owner_token + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/bothub/api/v2/internal/user/views.py b/bothub/api/v2/internal/user/views.py new file mode 100644 index 00000000..7c206595 --- /dev/null +++ b/bothub/api/v2/internal/user/views.py @@ -0,0 +1,98 @@ +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet +from rest_framework import mixins + +from bothub.authentication.models import User +from bothub.common.models import Organization, OrganizationAuthorization +from bothub.api.v2.internal.user.serializers import ( + UserPermissionSerializer, + UserSerializer, + UserLanguageSerializer, +) +from bothub import utils +from bothub.api.v2.internal.permissions import ModuleHasPermission + + +class UserPermissionViewSet( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + GenericViewSet, +): + queryset = OrganizationAuthorization.objects.all() + permission_classes = [ModuleHasPermission] + serializer_class = UserPermissionSerializer + lookup_field = None + + def retrieve(self, request, **kwargs): + user, org = utils.get_user_and_organization( + request.query_params.get("user_email", None), + request.query_params.get("org_id", None), + ) + permissions = self._get_user_permissions(org, user) + serializer = UserPermissionSerializer(permissions) + + return Response(serializer.data) + + def update(self, request, **kwargs): + user, org = utils.get_user_and_organization( + request.query_params.get("user_email", None), + request.query_params.get("org_id", None), + ) + + org.set_user_permission(user=user, permission=request.data.get("role", None)) + + permission = self._get_user_permissions(org, user) + serializer = UserPermissionSerializer(permission) + + return Response(serializer.data) + + def destroy(self, request, **kwargs): + user, org = utils.get_user_and_organization( + request.query_params.get("user_email", None), + request.query_params.get("org_id", None), + ) + + self._get_user_permissions(org, user).delete() + + permissions = self._get_user_permissions(org, user) + serializer = UserPermissionSerializer(permissions) + + return Response(serializer.data) + + def _get_user_permissions(self, org: Organization, user: User) -> dict: + return org.organization_authorizations.filter(user=user).first() + + +class UserViewSet(mixins.RetrieveModelMixin, GenericViewSet): + serializer_class = UserSerializer + permission_classes = [ModuleHasPermission] + queryset = User.objects + lookup_field = None + + def retrieve(self, request, **kwargs): + user, created = User.objects.get_or_create( + email=request.query_params.get("user_email"), + defaults={"nickname": request.query_params.get("user_email")}, + ) + + return Response(UserSerializer(user).data) + + +class UserLanguageViewSet(mixins.UpdateModelMixin, GenericViewSet): + serializer_class = UserLanguageSerializer + permission_classes = [ModuleHasPermission] + queryset = User.objects + lookup_field = None + + def update(self, request, **kwargs): + user, created = User.objects.get_or_create( + email=request.query_params.get("user_email"), + defaults={"nickname": request.query_params.get("user_email")}, + ) + serializer = UserLanguageSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + user.language = request.data["language"] + user.save() + return Response(UserLanguageSerializer(user).data) diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index 22bfd3d9..c0370173 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -59,6 +59,10 @@ CanContributeInRepositoryTranslatedExampleValidator, ) +from bothub.api.v2.internal.connect_rest_client import ( + ConnectRESTClient as ConnectClient, +) + class RequestRepositoryAuthorizationSerializer(serializers.ModelSerializer): class Meta: @@ -1654,10 +1658,13 @@ class AddRepositoryProjectSerializer(serializers.Serializer): project_uuid = serializers.CharField(required=True) def create(self, validated_data): - task = celery_app.send_task( - name="create_repository_project", kwargs=validated_data - ) - task.wait() + if settings.USE_GRPC: + task = celery_app.send_task( + name="create_repository_project", kwargs=validated_data + ) + task.wait() + else: + task = ConnectClient().create_classifier(**validated_data) return validated_data def to_representation(self, instance): diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index a69918f1..d988c7f1 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -121,6 +121,10 @@ AddRepositoryProjectSerializer, ) +from bothub.api.v2.internal.connect_rest_client import ( + ConnectRESTClient as ConnectClient, +) + class NewRepositoryViewSet( MultipleFieldLookupMixin, mixins.RetrieveModelMixin, GenericViewSet @@ -249,19 +253,26 @@ def projectrepository(self, request, **kwargs): if not authorization.can_contribute: raise PermissionDenied() - task = celery_app.send_task( - name="get_project_organization", args=[project_uuid] - ) - task.wait() + if settings.USE_GRPC: + task = celery_app.send_task( + name="get_project_organization", args=[project_uuid] + ) + task.wait() + authorizations = task.result + repositories = repository.authorizations.filter(uuid__in=authorizations) - repositories = repository.authorizations.filter(uuid__in=task.result) + else: + authorizations = ConnectClient().list_authorizations( + project_uuid=project_uuid, user_email=request.user.email + ) + repositories = repository.authorizations.filter(uuid__in=authorizations) data = dict(in_project=repositories.exists()) if organization: organization_authorization = ( - organization.organization_authorizations.filter(uuid__in=task.result) + organization.organization_authorizations.filter(uuid__in=authorizations) ) data["in_project"] = ( data["in_project"] or organization_authorization.exists() @@ -301,16 +312,27 @@ def remove_repository_project(self, request, **kwargs): if not user_authorization.is_admin: raise PermissionDenied() - project_organization = celery_app.send_task( - name="get_project_organization", args=[project_uuid] - ) - project_organization.wait() + if settings.USE_GRPC: + project_organization = celery_app.send_task( + name="get_project_organization", args=[project_uuid] + ) + project_organization.wait() + project_organization = project_organization.result + authorizations = list( + repository.authorizations.filter( + uuid__in=project_organization + ).values_list("uuid", flat=True) + ) - authorizations = list( - repository.authorizations.filter( - uuid__in=project_organization.result - ).values_list("uuid", flat=True) - ) + else: + project_organization = ConnectClient().list_authorizations( + project_uuid=project_uuid, user_email=request.user.email + ) + authorizations = list( + repository.authorizations.filter( + uuid__in=project_organization + ).values_list("uuid", flat=True) + ) if organization: organization_authorization = organization.get_organization_authorization( @@ -321,7 +343,7 @@ def remove_repository_project(self, request, **kwargs): authorizations += list( organization.organization_authorizations.filter( - uuid__in=project_organization.result + uuid__in=project_organization ).values_list("uuid", flat=True) ) @@ -334,11 +356,20 @@ def remove_repository_project(self, request, **kwargs): lambda authorization: str(authorization), authorizations ) - task = celery_app.send_task( - name="remove_authorizations_project", - args=[project_uuid, list(authorizations_uuids), request.user.email], - ) - task.wait() + if settings.USE_GRPC: + task = celery_app.send_task( + name="remove_authorizations_project", + args=[project_uuid, list(authorizations_uuids), request.user.email], + ) + task.wait() + else: + task = ConnectClient().list_authorizations( + project_uuid=project_uuid, user_email=request.user.email + ) + for authorization_uuid in authorizations_uuids: + ConnectClient().remove_authorization( + project_uuid, authorization_uuid, request.user.email + ) return Response(status=status.HTTP_204_NO_CONTENT) @@ -381,14 +412,22 @@ def add_repository_project(self, request, **kwargs): project_uuid = serializer.validated_data.get("project_uuid") - task = celery_app.send_task( - name="get_project_organization", args=[project_uuid] - ) - task.wait() + if settings.USE_GRPC: + task = celery_app.send_task( + name="get_project_organization", args=[project_uuid] + ) + task.wait() - organization_authorization = organization.organization_authorizations.filter( - uuid__in=task.result - ) + organization_authorization = ( + organization.organization_authorizations.filter(uuid__in=task.result) + ) + else: + task = ConnectClient().list_authorizations( + project_uuid=project_uuid, user_email=request.user.email + ) + organization_authorization = ( + organization.organization_authorizations.filter(uuid__in=task) + ) if organization_authorization.exists(): raise ValidationError(_("Repository already added")) @@ -803,12 +842,21 @@ def list_project_organizatiton(self, request, **kwargs): if not project_uuid: raise ValidationError(_("Need to pass 'project_uuid' in query params")) - task = celery_app.send_task( - name="get_project_organization", args=[project_uuid] - ) - task.wait() - - repositories = Repository.objects.filter(authorizations__uuid__in=task.result) + if settings.USE_GRPC: + task = celery_app.send_task( + name="get_project_organization", args=[project_uuid, request.user.email] + ) + task.wait() + repositories = Repository.objects.filter( + authorizations__uuid__in=task.result + ) + else: + authorizations = ConnectClient().list_authorizations( + project_uuid=project_uuid, user_email=request.user.email + ) + repositories = Repository.objects.filter( + authorizations__uuid__in=authorizations + ) serialized_data = ShortRepositorySerializer(repositories, many=True) return Response(serialized_data.data) diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index 55e8b818..fbf67a88 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -66,6 +66,14 @@ RepositoryTranslatorViewSet, ) +from bothub.api.v2.internal.repository.views import InternalRepositoriesViewSet +from bothub.api.v2.internal.organization.views import InternalOrganizationViewSet +from bothub.api.v2.internal.user.views import ( + UserPermissionViewSet, + UserViewSet, + UserLanguageViewSet, +) + class Router(routers.SimpleRouter): routes = [ @@ -233,3 +241,8 @@ def get_lookup_regex(self, viewset, lookup_prefix=""): router.register("org/organization", OrganizationViewSet) router.register("org/profile", OrganizationProfileViewSet) router.register("org/authorizations", OrganizationAuthorizationViewSet) +router.register("internal/organization", InternalOrganizationViewSet) +router.register("internal/repository", InternalRepositoriesViewSet) +router.register("internal/user", UserViewSet) +router.register("internal/user/permission", UserPermissionViewSet) +router.register("internal/user/language", UserLanguageViewSet) diff --git a/bothub/api/v2/tests/test_repository.py b/bothub/api/v2/tests/test_repository.py index 9aa601e6..025753f9 100644 --- a/bothub/api/v2/tests/test_repository.py +++ b/bothub/api/v2/tests/test_repository.py @@ -42,48 +42,11 @@ from bothub.common.models import RepositoryVote from bothub.common.models import RequestRepositoryAuthorization - -def get_valid_mockups(categories): - return [ - { - "name": "Repository 1", - "slug": "repository-1", - "description": "", - "language": languages.LANGUAGE_EN, - "categories": [category.pk for category in categories], - }, - { - "name": "Repository 2", - "description": "", - "language": languages.LANGUAGE_PT, - "categories": [category.pk for category in categories], - }, - ] - - -def get_invalid_mockups(categories): - return [ - { - "name": "", - "slug": "repository-1", - "language": languages.LANGUAGE_EN, - "categories": [category.pk for category in categories], - }, - { - "name": "Repository 3", - "language": "out", - "categories": [category.pk for category in categories], - "is_private": False, - }, - ] - - -def create_repository_from_mockup(owner, categories, **mockup): - r = Repository.objects.create(owner_id=owner.id, **mockup) - r.current_version() - for category in categories: - r.categories.add(category) - return r +from bothub.api.v2.tests.utils import ( + get_valid_mockups, + get_invalid_mockups, + create_repository_from_mockup, +) class CreateRepositoryAPITestCase(TestCase): diff --git a/bothub/api/v2/tests/utils.py b/bothub/api/v2/tests/utils.py index 7ede7eb6..5d1c6901 100644 --- a/bothub/api/v2/tests/utils.py +++ b/bothub/api/v2/tests/utils.py @@ -1,9 +1,58 @@ +from bothub.utils import check_module_permission from rest_framework.authtoken.models import Token from bothub.authentication.models import User +from bothub.common import languages +from bothub.common.models import Repository -def create_user_and_token(nickname="fake"): +def create_user_and_token(nickname="fake", module=False): user = User.objects.create_user("{}@user.com".format(nickname), nickname) + if module is True: + check_module_permission({"can_communicate_internally": module}, user) + user = User.objects.get(email=user.email) token, create = Token.objects.get_or_create(user=user) return (user, token) + + +def get_valid_mockups(categories): + return [ + { + "name": "Repository 1", + "slug": "repository-1", + "description": "", + "language": languages.LANGUAGE_EN, + "categories": [category.pk for category in categories], + }, + { + "name": "Repository 2", + "description": "", + "language": languages.LANGUAGE_PT, + "categories": [category.pk for category in categories], + }, + ] + + +def get_invalid_mockups(categories): + return [ + { + "name": "", + "slug": "repository-1", + "language": languages.LANGUAGE_EN, + "categories": [category.pk for category in categories], + }, + { + "name": "Repository 3", + "language": "out", + "categories": [category.pk for category in categories], + "is_private": False, + }, + ] + + +def create_repository_from_mockup(owner, categories, **mockup): + r = Repository.objects.create(owner_id=owner.id, **mockup) + r.current_version() + for category in categories: + r.categories.add(category) + return r diff --git a/bothub/authentication/authorization.py b/bothub/authentication/authorization.py index ccc89e80..c2e4562c 100644 --- a/bothub/authentication/authorization.py +++ b/bothub/authentication/authorization.py @@ -1,6 +1,10 @@ import logging +import re from django.utils.translation import ugettext_lazy as _ +from bothub.utils import check_module_permission + + from mozilla_django_oidc.auth import OIDCAuthenticationBackend from rest_framework import exceptions from rest_framework.authentication import TokenAuthentication, get_authorization_header @@ -92,16 +96,20 @@ def create_user(self, claims): # Override existing create_user method in OIDCAuthenticationBackend email = claims.get("email") username = self.get_username(claims)[:16] + username = re.sub("[^A-Za-z0-9]+", "", username) user = self.UserModel.objects.create_user(email, username) user.name = claims.get("name", "") user.save() + check_module_permission(claims, user) + return user def update_user(self, user, claims): user.name = claims.get("name", "") user.email = claims.get("email", "") user.save() + check_module_permission(claims, user) return user diff --git a/bothub/common/models.py b/bothub/common/models.py index f9a8793e..05163b18 100644 --- a/bothub/common/models.py +++ b/bothub/common/models.py @@ -906,16 +906,18 @@ def get_specific_version_id(self, repository_version, language=None): def get_user_authorization(self, user): if user.is_anonymous: return RepositoryAuthorization(repository=self) - get, created = RepositoryAuthorization.objects.get_or_create( + repo_auth, created = RepositoryAuthorization.objects.get_or_create( user=user.repository_owner, repository=self ) if self.owner.is_organization: - org_role = self.owner.organization.get_organization_authorization(user).role - if get.role != org_role and get.role == 0: - get.role = org_role - get.save() - - return get + org_auth = self.owner.organization.get_organization_authorization(user) + + # Excluding ROLE_TRANSLATE as it does not correspond to the same role in the client app (connect). + # todo: update this conditional with corresponding role rule + if repo_auth.role < org_auth.role and org_auth.role < RepositoryAuthorization.ROLE_TRANSLATE: + repo_auth.role = org_auth.role + repo_auth.save(update_fields=['role']) + return repo_auth def get_absolute_url(self): return "{}dashboard/{}/{}/".format( diff --git a/bothub/common/tasks.py b/bothub/common/tasks.py index 59d29e77..1660e7f0 100644 --- a/bothub/common/tasks.py +++ b/bothub/common/tasks.py @@ -12,7 +12,6 @@ from django_elasticsearch_dsl.registries import registry from bothub import translate -from bothub.api.grpc.connect_grpc_client import ConnectGRPCClient from bothub.celery import app from bothub.common.models import ( RepositoryQueueTask, @@ -40,6 +39,14 @@ ) +if settings.USE_GRPC: + from bothub.api.grpc.connect_grpc_client import ConnectGRPCClient as ConnectClient +else: + from bothub.api.v2.internal.connect_rest_client import ( + ConnectRESTClient as ConnectClient, + ) + + @app.task(name="es_handle_save") def handle_save(pk, app_label, model_name): sender = apps.get_model(app_label, model_name) @@ -544,9 +551,16 @@ def evaluate_crossvalidation(data, authorization_token): # pragma: no cover @app.task(name="get_project_organization") -def get_project_organization(project_uuid: str): # pragma: no cover - grpc_client = ConnectGRPCClient() - authorizations = grpc_client.list_authorizations(project_uuid=project_uuid) +def get_project_organization( + project_uuid: str, user_email: str = "" +): # pragma: no cover + grpc_client = ConnectClient() + if settings.USE_GRPC: + authorizations = grpc_client.list_authorizations(project_uuid=project_uuid) + else: + authorizations = grpc_client.list_authorizations( + project_uuid=project_uuid, user_email=user_email + ) return authorizations @@ -554,13 +568,13 @@ def get_project_organization(project_uuid: str): # pragma: no cover def remove_authorizations_project( project_uuid: str, authorizations_uuids: list, user_email: str ): - grpc_client = ConnectGRPCClient() + grpc_client = ConnectClient() for authorization_uuid in authorizations_uuids: grpc_client.remove_authorization(project_uuid, authorization_uuid, user_email) @app.task(name="create_repository_project") def create_repository_project(**kwargs): - grpc_client = ConnectGRPCClient() + grpc_client = ConnectClient() grpc_client.create_classifier(**kwargs) return kwargs diff --git a/bothub/common/tests.py b/bothub/common/tests.py index 8b72ba04..a9bfe505 100644 --- a/bothub/common/tests.py +++ b/bothub/common/tests.py @@ -15,6 +15,8 @@ RepositoryQueueTask, QAKnowledgeBase, QAtext, + Organization, + OrganizationAuthorization ) from .models import RepositoryAuthorization from .models import RepositoryEntity @@ -544,6 +546,7 @@ class RepositoryAuthorizationTestCase(TestCase): def setUp(self): self.owner = User.objects.create_user("owner@user.com", "owner") self.user = User.objects.create_user("fake@user.com", "user") + self.collaborator = User.objects.create_user("colaborator@user.com", "collaborator") self.repository = Repository.objects.create( owner=self.owner.repository_owner, name="Test", slug="test" @@ -554,6 +557,12 @@ def setUp(self): slug="private", is_private=True, ) + self.organization = Organization.objects.create(name="Weni") + self.organization_repository = Repository.objects.create( + owner=self.organization, + name="Organization Repository", + slug="organization_repository" + ) def test_admin_level(self): authorization = self.repository.get_user_authorization(self.owner) @@ -682,6 +691,50 @@ def test_role_contributor_can_contribute(self): authorization_user.save() self.assertTrue(authorization_user.can_contribute) + def test_organization_auth_over_repository_auth(self): + """ + Tests that a User's authorization role is of the highest level possible in a Repository, + either using the RepositoryAuthorization or the OrganizationAuthorization. + The expected behavior is that the organization's authorization role should be passed to the repository's authorization for that user. + """ + + # The role that the user should inherit from the organization authorization inside the repository. + initial_organization_role = OrganizationAuthorization.ROLE_ADMIN + + # Set user's role to a low level at the Repository + collaborator_repository_auth, created = RepositoryAuthorization.objects.get_or_create( + user=self.collaborator, repository=self.organization_repository, role=RepositoryAuthorization.ROLE_USER + ) + # Set user's role to a high level at the Organization + collaborator_organization_auth = self.organization.organization_authorizations.create( + user=self.collaborator, role=initial_organization_role + ) + + # Validate that their access level corresponds to their role in the Organization and not the Repository, as it is higher at this point. + user_authorization = self.organization_repository.get_user_authorization(self.collaborator) + self.assertEqual(user_authorization.role, collaborator_organization_auth.role) + + # Lower their level inside the Organization + collaborator_organization_auth.role = OrganizationAuthorization.ROLE_NOT_SETTED + collaborator_organization_auth.save() + + # Validate that the repository authorization level was updated. + collaborator_repository_auth.refresh_from_db() + self.assertEqual(collaborator_repository_auth.role, initial_organization_role) + + # Validate that the user's level is now the Repository's and not the Organization's, as it is higher. + user_authorization = self.organization_repository.get_user_authorization(self.collaborator) + self.assertEqual(user_authorization.role, collaborator_repository_auth.role) + + # Verify that org auth with (role >= 4) will not update the repository's authorization + + # Set user's role to ROLE_TRANSLATE level at the Organization + collaborator_organization_auth.role = OrganizationAuthorization.ROLE_TRANSLATE + collaborator_organization_auth.save() + + user_authorization = self.organization_repository.get_user_authorization(self.collaborator) + self.assertEqual(user_authorization.role, collaborator_repository_auth.role) + class RepositoryVersionTrainingTestCase(TestCase): def setUp(self): diff --git a/bothub/settings.py b/bothub/settings.py index 0f0b01fc..a4ee2ac6 100644 --- a/bothub/settings.py +++ b/bothub/settings.py @@ -75,6 +75,8 @@ SECRET_KEY_CHECK_LEGACY_USER=(str, None), CONNECT_GRPC_SERVER_URL=(str, "localhost:8002"), CONNECT_CERTIFICATE_GRPC_CRT=(str, None), + USE_GRPC=(bool, False), + CONNECT_API_URL=(str, ""), REPOSITORY_RESTRICT_ACCESS_NLP_LOGS=(list, []), REPOSITORY_BLOCK_USER_LOGS=(list, []), REPOSITORY_KNOWLEDGE_BASE_DESCRIPTION_LIMIT=(int, 450), @@ -92,6 +94,17 @@ ELASTICSEARCH_LOGS_DELETE_AGE=(str, "90d"), ELASTICSEARCH_LOGS_ROLLOVER_AGE=(str, "1d"), ELASTICSEARCH_TIMESTAMP_PIPELINE_FIELD=(str, "created_at"), + CSP_DEFAULT_SRC=(tuple, "CSP_DEFAULT_SRC"), + CSP_FRAME_ANCESTORS=(tuple, "CSP_FRAME_ANCESTORS"), + CSP_FONT_SRC=(tuple, "CSP_FONT_SRC"), + CSP_STYLE_SRC=(tuple, "CSP_STYLE_SRC"), + CSP_STYLE_SRC_ELEM=(tuple, "CSP_STYLE_SRC_ELEM"), + CSP_SCRIPT_SRC=(tuple, "CSP_SCRIPT_SRC"), + CSP_SCRIPT_SRC_ELEM=(tuple, "CSP_SCRIPT_SRC_ELEM"), + CSP_FRAME_SRC=(tuple, "CSP_FRAME_SRC"), + CSP_CONNECT_SRC=(tuple, "CSP_CONNECT_SRC"), + CSP_WORKER_SRC=(tuple, "CSP_WORKER_SRC"), + CSP_IMG_SRC=(tuple, "CSP_IMG_SRC"), ) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -144,12 +157,12 @@ "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.locale.LocaleMiddleware", + "csp.middleware.CSPMiddleware", "corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", "bothub.api.v2.middleware.UserLanguageMiddleware", ] @@ -299,6 +312,30 @@ CSRF_COOKIE_SECURE = env.bool("CSRF_COOKIE_SECURE") +# CSP headers + +DEFAULT_CSP_SETTINGS = ("'self'",) +DEFAULT_CSP_WENI_SETTINGS = DEFAULT_CSP_SETTINGS + ("*.weni.ai",) + +CSP_DEFAULT_SRC = env.tuple("CSP_DEFAULT_SRC", default=DEFAULT_CSP_SETTINGS) +CSP_FRAME_ANCESTORS = env.tuple( + "CSP_FRAME_ANCESTORS", default=DEFAULT_CSP_WENI_SETTINGS +) +CSP_FONT_SRC = env.tuple("CSP_FONT_SRC", default=CSP_DEFAULT_SRC) +CSP_STYLE_SRC = env.tuple( + "CSP_STYLE_SRC", default=DEFAULT_CSP_SETTINGS + ("'unsafe-inline'", "'unsafe-eval'") +) +CSP_STYLE_SRC_ELEM = env.tuple("CSP_STYLE_SRC_ELEM", default=CSP_STYLE_SRC) +CSP_SCRIPT_SRC = env.tuple("CSP_SCRIPT_SRC", default=CSP_STYLE_SRC) +CSP_SCRIPT_SRC_ELEM = env.tuple("CSP_SCRIPT_SRC_ELEM", default=CSP_SCRIPT_SRC) +CSP_FRAME_SRC = env.tuple("CSP_FRAME_SRC", default=CSP_DEFAULT_SRC) +CSP_CONNECT_SRC = env.tuple("CSP_CONNECT_SRC", default=CSP_DEFAULT_SRC) +CSP_WORKER_SRC = env.tuple( + "CSP_WORKER_SRC", default=DEFAULT_CSP_WENI_SETTINGS + ("blob:", "data:") +) +CSP_IMG_SRC = env.tuple("CSP_IMG_SRC", default=CSP_WORKER_SRC) + + # Logging LOGGING = DEFAULT_LOGGING @@ -531,6 +568,10 @@ CONNECT_CERTIFICATE_GRPC_CRT = env.str("CONNECT_CERTIFICATE_GRPC_CRT") +USE_GRPC = env.bool("USE_GRPC", default=False) + +CONNECT_API_URL = env.str("CONNECT_API_URL", default="https://api.dev.cloud.weni.ai") + # ElasticSearch ELASTICSEARCH_DSL = { "default": {"hosts": env.str("ELASTICSEARCH_DSL", default="es:9200")} diff --git a/bothub/utils.py b/bothub/utils.py index 38949d12..40e36dcd 100644 --- a/bothub/utils.py +++ b/bothub/utils.py @@ -20,6 +20,7 @@ from rest_framework import status from rest_framework.exceptions import APIException, ValidationError + entity_regex = re.compile( r"\[(?P[^\]]+)" r"\]\((?P[^:)]*?)" r"(?:\:(?P[^)]+))?\)" ) @@ -375,6 +376,17 @@ def get_organization(request, organization_id: int): ) +def get_user_and_organization(user_email: str, organization_id: int): + from bothub.authentication.models import User + from bothub.common.models import Organization + + org = Organization.objects.get(pk=organization_id) + user, created = User.objects.get_or_create( + email=user_email, defaults={"nickname": user_email} + ) + return user, org + + class TimeBasedDocument(Document): def save(self, action="create", **kwargs): return super().save(action=action, **kwargs) @@ -462,3 +474,43 @@ def filter_has_invalid_entities(self, queryset, name, value): return filter_validate_entities(queryset, value).exclude( original_entities_count=F("entities_count") ) + + +def check_module_permission(claims, user): + from django.contrib.auth.models import Permission + from django.contrib.contenttypes.models import ContentType + from bothub.common.models import User + + if claims.get("can_communicate_internally", False): + content_type = ContentType.objects.get_for_model(User) + permission, created = Permission.objects.get_or_create( + codename="can_communicate_internally", + name="can communicate internally", + content_type=content_type, + ) + if not user.has_perm("authentication.can_communicate_internally"): + user.user_permissions.add(permission) + return True + return False + + +internal_serializer_fields = [ + "uuid", + "name", + "slug", + "description", + "is_private", + "created_at", + "language", + "owner", + "algorithm", + "use_competing_intents", + "use_name_entities", + "use_analyze_char", + "owner__nickname", + "intents", + "categories", + "available_languages", + "categories_list", + "repository_type", +] diff --git a/poetry.lock b/poetry.lock index d67a0a89..6199b760 100644 --- a/poetry.lock +++ b/poetry.lock @@ -445,6 +445,21 @@ python-versions = ">=3.6" [package.dependencies] Django = ">=2.2" +[[package]] +name = "django-csp" +version = "3.7" +description = "Django Content Security Policy support." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +Django = ">=1.8" + +[package.extras] +jinja2 = ["jinja2 (>=2.9.6)"] +tests = ["pytest (<4.0)", "pytest-django", "pytest-flakes (==1.0.1)", "pytest-pep8 (==1.0.6)", "pep8 (==1.4.6)", "mock (==1.0.1)", "six (==1.12.0)", "jinja2 (>=2.9.6)"] + [[package]] name = "django-elasticsearch-dsl" version = "7.2.2" @@ -1711,7 +1726,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "~=3.6.15" -content-hash = "dd6bfc5c1373deba58e4e85a9cb3114b91f7d5d5a33db1ac48afee53bbe395f8" +content-hash = "31ccdac8c205f34af79715c1d090792b5496b461d94085bc7715f635cfbf4dfa" [metadata.files] amqp = [ @@ -1970,6 +1985,10 @@ django-cors-headers = [ {file = "django-cors-headers-3.7.0.tar.gz", hash = "sha256:96069c4aaacace786a34ee7894ff680780ec2644e4268b31181044410fecd12e"}, {file = "django_cors_headers-3.7.0-py3-none-any.whl", hash = "sha256:1ac2b1213de75a251e2ba04448da15f99bcfcbe164288ae6b5ff929dc49b372f"}, ] +django-csp = [ + {file = "django_csp-3.7-py2.py3-none-any.whl", hash = "sha256:01443a07723f9a479d498bd7bb63571aaa771e690f64bde515db6cdb76e8041a"}, + {file = "django_csp-3.7.tar.gz", hash = "sha256:01eda02ad3f10261c74131cdc0b5a6a62b7c7ad4fd017fbefb7a14776e0a9727"}, +] django-elasticsearch-dsl = [ {file = "django-elasticsearch-dsl-7.2.2.tar.gz", hash = "sha256:811d3909b3387fd55c19d9bbcf0e9a9b234f085df3f8422d59e7519a5f733e0e"}, {file = "django_elasticsearch_dsl-7.2.2-py2.py3-none-any.whl", hash = "sha256:3c58a254a6318b169eb904d41d802924b99ea8e53ddc2c596ebba90506cf47fa"}, diff --git a/pyproject.toml b/pyproject.toml index c6d48f5e..0abed417 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ psycopg2-binary = "~=2.9.1" weni-protobuffers = "~=1.2.1" black = "21.7b0" Pillow = "~=8.4.0" +django-csp = "^3.7" [tool.poetry.dev-dependencies] flake8 = "~=4.0.0" From 8846c98f232521caafb3232c28b1f053a3d47ee2 Mon Sep 17 00:00:00 2001 From: Victor Salles Date: Mon, 15 Aug 2022 15:10:54 -0300 Subject: [PATCH 5/9] Update Python (3.6 => 3.8.13) - Updates Python from version 3.6 to 3.8.13 - Updates pre-commit from version 2.17.0 to 2.20.20 - Update gevent from version 1.4.0 to 21.12.0 --- .travis.yml | 2 +- Dockerfile | 10 +- README.md | 7 +- poetry.lock | 315 +++++++++++++++++++------------------------------ pyproject.toml | 4 +- runtime.txt | 2 +- 6 files changed, 134 insertions(+), 206 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9abc9e64..703d9d9e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ sudo: true language: python python: - - "3.6.8" + - "3.8.13" services: - docker diff --git a/Dockerfile b/Dockerfile index e1cac756..c64c06a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,19 @@ -FROM python:3.6-slim +FROM python:3.8.13-slim ENV WORKDIR /home/app WORKDIR $WORKDIR RUN apt-get update \ - && apt-get install --no-install-recommends --no-install-suggests -y apt-utils \ - && apt-get install --no-install-recommends --no-install-suggests -y gcc bzip2 git curl nginx libpq-dev gettext \ - libgdal-dev python3-cffi python3-gdal vim + && apt-get install --no-install-recommends --no-install-suggests -y apt-utils \ + && apt-get install --no-install-recommends --no-install-suggests -y gcc bzip2 git curl nginx libpq-dev gettext \ + libgdal-dev python3-cffi python3-gdal vim RUN apt-get install make RUN pip install -U pip==21.2.2 setuptools==57.4.0 RUN pip install poetry==1.1.12 RUN pip install gunicorn==19.9.0 -RUN pip install gevent==1.4.0 +RUN pip install gevent==21.12.0 RUN pip install psycopg2-binary RUN apt-get install -y libjpeg-dev libgpgme-dev linux-libc-dev musl-dev libffi-dev libssl-dev ENV LIBRARY_PATH=/lib:/usr/lib diff --git a/README.md b/README.md index 16c97e3c..48118459 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ยท

- +

Bothub @@ -27,7 +27,7 @@ # Requirements -* Python (3.6) +* Python (3.8.13) * Poetry * Docker * Docker-compose @@ -146,7 +146,7 @@ You can set environment variables in your OS, write on ```.env``` file or pass v | APM_SERVICE_ENVIRONMENT | ```string``` | ```''``` | Environment that APM is running on | ENVIRONMENT | ```string``` | ```production``` | Specify the environment you are going to run, it is also used for sentry | SUGGESTION_LANGUAGES | ```string``` | ```en|pt_br``` | Specify the the languages supported by environment for word and intent suggestions -| N_WORDS_TO_GENERATE | ```int``` | ```4``` | Specify the number of suggestions that will be returned for word suggestions +| N_WORDS_TO_GENERATE | ```int``` | ```4``` | Specify the number of suggestions that will be returned for word suggestions | N_SENTENCES_TO_GENERATE | ```int``` | ```10``` | Specify the number of suggestions that will be returned for intent suggestions | REDIS_TIMEOUT | ```int``` | ```3600``` | Specify a systemwide Redis keys life time | SECRET_KEY_CHECK_LEGACY_USER | ```string``` | ```None``` | Enables and specifies the token to use for the legacy user endpoint. @@ -206,4 +206,3 @@ Contributions are what make the open source community such an amazing place to b 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 4. Push to the Branch (`git push origin feature/AmazingFeature`) 5. Open a Pull Request - diff --git a/poetry.lock b/poetry.lock index fb2cec40..1b17a350 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,6 @@ [[package]] name = "amqp" -version = "5.1.0" +version = "5.1.1" description = "Low-level AMQP client for Python (fork of amqplib)." category = "main" optional = false @@ -19,7 +19,7 @@ python-versions = "*" [[package]] name = "appnope" -version = "0.1.2" +version = "0.1.3" description = "Disable App Nap on macOS >= 10.9" category = "dev" optional = false @@ -33,11 +33,8 @@ category = "main" optional = false python-versions = ">=3.6" -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - [package.extras] -tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] +tests = ["mypy (>=0.800)", "pytest-asyncio", "pytest"] [[package]] name = "async-timeout" @@ -47,9 +44,6 @@ category = "main" optional = false python-versions = ">=3.6" -[package.dependencies] -typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} - [[package]] name = "autopep8" version = "1.6.0" @@ -89,19 +83,16 @@ python-versions = ">=3.6.2" [package.dependencies] appdirs = "*" click = ">=7.1.2" -dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} mypy-extensions = ">=0.4.3" pathspec = ">=0.8.1,<1" regex = ">=2020.1.8" tomli = ">=0.2.6,<2.0.0" -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\""} -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] -python2 = ["typed-ast (>=1.4.2)"] uvloop = ["uvloop (>=0.15.2)"] +python2 = ["typed-ast (>=1.4.2)"] +d = ["aiohttp-cors (>=0.4.0)", "aiohttp (>=3.6.0)"] +colorama = ["colorama (>=0.4.3)"] [[package]] name = "bleach" @@ -148,14 +139,6 @@ urllib3 = ">=1.25.4,<1.27" [package.extras] crt = ["awscrt (==0.12.5)"] -[[package]] -name = "cached-property" -version = "1.5.2" -description = "A decorator for caching properties in classes." -category = "main" -optional = false -python-versions = "*" - [[package]] name = "celery" version = "5.1.2" @@ -175,49 +158,49 @@ pytz = ">0.0-dev" vine = ">=5.0.0,<6.0" [package.extras] -arangodb = ["pyArango (>=1.3.2)"] -auth = ["cryptography"] -azureblockblob = ["azure-storage-blob (==12.6.0)"] -brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] -cassandra = ["cassandra-driver (<3.21.0)"] -consul = ["python-consul2"] -cosmosdbsql = ["pydocumentdb (==2.3.2)"] -couchbase = ["couchbase (>=3.0.0)"] -couchdb = ["pycouchdb"] -django = ["Django (>=1.11)"] -dynamodb = ["boto3 (>=1.9.178)"] -elasticsearch = ["elasticsearch"] -eventlet = ["eventlet (>=0.26.1)"] -gevent = ["gevent (>=1.0.0)"] -librabbitmq = ["librabbitmq (>=1.5.0)"] -memcache = ["pylibmc"] -mongodb = ["pymongo[srv] (>=3.3.0)"] -msgpack = ["msgpack"] -pymemcache = ["python-memcached"] -pyro = ["pyro4"] -pytest = ["pytest-celery"] -redis = ["redis (>=3.2.0)"] -s3 = ["boto3 (>=1.9.125)"] -slmq = ["softlayer-messaging (>=1.0.3)"] -solar = ["ephem"] -sqlalchemy = ["sqlalchemy"] -sqs = ["boto3 (>=1.9.125)", "pycurl (==7.43.0.5)"] -tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] -yaml = ["PyYAML (>=3.10)"] -zookeeper = ["kazoo (>=1.3.1)"] zstd = ["zstandard"] +zookeeper = ["kazoo (>=1.3.1)"] +yaml = ["PyYAML (>=3.10)"] +tblib = ["tblib (>=1.5.0)", "tblib (>=1.3.0)"] +sqs = ["pycurl (==7.43.0.5)", "boto3 (>=1.9.125)"] +sqlalchemy = ["sqlalchemy"] +solar = ["ephem"] +slmq = ["softlayer-messaging (>=1.0.3)"] +s3 = ["boto3 (>=1.9.125)"] +redis = ["redis (>=3.2.0)"] +pytest = ["pytest-celery"] +pyro = ["pyro4"] +pymemcache = ["python-memcached"] +msgpack = ["msgpack"] +mongodb = ["pymongo[srv] (>=3.3.0)"] +memcache = ["pylibmc"] +librabbitmq = ["librabbitmq (>=1.5.0)"] +gevent = ["gevent (>=1.0.0)"] +eventlet = ["eventlet (>=0.26.1)"] +elasticsearch = ["elasticsearch"] +dynamodb = ["boto3 (>=1.9.178)"] +django = ["Django (>=1.11)"] +couchdb = ["pycouchdb"] +couchbase = ["couchbase (>=3.0.0)"] +cosmosdbsql = ["pydocumentdb (==2.3.2)"] +consul = ["python-consul2"] +cassandra = ["cassandra-driver (<3.21.0)"] +brotli = ["brotlipy (>=0.7.0)", "brotli (>=1.0.0)"] +azureblockblob = ["azure-storage-blob (==12.6.0)"] +auth = ["cryptography"] +arangodb = ["pyArango (>=1.3.2)"] [[package]] name = "certifi" -version = "2021.10.8" +version = "2022.6.15" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "cffi" -version = "1.15.0" +version = "1.15.1" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -290,7 +273,7 @@ six = "*" [[package]] name = "colorama" -version = "0.4.4" +version = "0.4.5" description = "Cross-platform colored terminal text." category = "main" optional = false @@ -334,7 +317,7 @@ toml = ["toml"] [[package]] name = "cryptography" -version = "36.0.2" +version = "37.0.4" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -349,7 +332,7 @@ docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] sdist = ["setuptools_rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] [[package]] name = "cycler" @@ -359,14 +342,6 @@ category = "main" optional = false python-versions = ">=3.6" -[[package]] -name = "dataclasses" -version = "0.8" -description = "A backport of the dataclasses module for Python 3.6" -category = "main" -optional = false -python-versions = ">=3.6, <3.7" - [[package]] name = "decorator" version = "5.1.1" @@ -387,7 +362,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" wrapt = ">=1.10,<2" [package.extras] -dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] +dev = ["pytest-cov", "pytest", "PyTest-Cov (<2.6)", "PyTest (<5)", "zipp (<2)", "sphinxcontrib-websupport (<2)", "configparser (<5)", "importlib-resources (<4)", "importlib-metadata (<3)", "sphinx (<2)", "bump2version (<1)", "tox"] [[package]] name = "distlib" @@ -399,7 +374,7 @@ python-versions = "*" [[package]] name = "django" -version = "3.2.12" +version = "3.2.15" description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false @@ -411,8 +386,8 @@ pytz = "*" sqlparse = ">=0.2.2" [package.extras] -argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +argon2 = ["argon2-cffi (>=19.1.0)"] [[package]] name = "django-appconf" @@ -473,8 +448,8 @@ python-versions = "*" Django = ">=1.8" [package.extras] +tests = ["jinja2 (>=2.9.6)", "six (==1.12.0)", "mock (==1.0.1)", "pep8 (==1.4.6)", "pytest-pep8 (==1.0.6)", "pytest-flakes (==1.0.1)", "pytest-django", "pytest (<4.0)"] jinja2 = ["jinja2 (>=2.9.6)"] -tests = ["pytest (<4.0)", "pytest-django", "pytest-flakes (==1.0.1)", "pytest-pep8 (==1.0.6)", "pep8 (==1.4.6)", "mock (==1.0.1)", "six (==1.12.0)", "jinja2 (>=2.9.6)"] [[package]] name = "django-elasticsearch-dsl" @@ -490,7 +465,7 @@ six = "*" [[package]] name = "django-elasticsearch-dsl-drf" -version = "0.22.4" +version = "0.22.5" description = "Integrate Elasticsearch DSL with Django REST framework." category = "main" optional = false @@ -645,13 +620,13 @@ six = ">=1.15.0" uritemplate = ">=3.0.1" [package.extras] -dist = ["bump2version", "wheel"] -docs = ["djangorestframework-camel-case (>=0.2.0)", "Pillow (>=4.3.0)", "readme-renderer[md] (>=24.0)", "sphinx-rtd-theme (>=0.2.4)", "Sphinx (>=1.7.0)", "twine (>=1.12.1)"] -format = ["autoflake (>=1.4.0)", "black (>=20.8.0)", "isort (>=4.2.0)"] -heroku = ["dj-database-url (>=0.4.2)", "gunicorn (>=19.7.1)", "psycopg2 (>=2.7.3)", "whitenoise (>=3.3.1)"] -lint = ["flake8 (>=3.5.0)"] -tests = ["dj-database-url (>=0.4.2)", "django-cors-headers (>=3.0.2)", "django-filter (>=1.1.0)", "django-oauth-toolkit (>=1.3.2)", "djangorestframework-camel-case (>=1.0.3)", "djangorestframework-recursive (>=0.1.2)", "Pillow (>=4.3.0)", "pytest (>=4.6.11)", "pytest-django (>=3.10.0)", "tox (>=3.3.0)", "user-agents (>=1.1.0)"] validation = ["swagger-spec-validator (>=2.1.0)"] +tests = ["user-agents (>=1.1.0)", "tox (>=3.3.0)", "pytest-django (>=3.10.0)", "pytest (>=4.6.11)", "Pillow (>=4.3.0)", "djangorestframework-recursive (>=0.1.2)", "djangorestframework-camel-case (>=1.0.3)", "django-oauth-toolkit (>=1.3.2)", "django-filter (>=1.1.0)", "django-cors-headers (>=3.0.2)", "dj-database-url (>=0.4.2)"] +lint = ["flake8 (>=3.5.0)"] +heroku = ["whitenoise (>=3.3.1)", "psycopg2 (>=2.7.3)", "gunicorn (>=19.7.1)", "dj-database-url (>=0.4.2)"] +format = ["isort (>=4.2.0)", "black (>=20.8.0)", "autoflake (>=1.4.0)"] +docs = ["twine (>=1.12.1)", "Sphinx (>=1.7.0)", "sphinx-rtd-theme (>=0.2.4)", "readme-renderer[md] (>=24.0)", "Pillow (>=4.3.0)", "djangorestframework-camel-case (>=0.2.0)"] +dist = ["wheel", "bump2version"] [[package]] name = "elastic-apm" @@ -685,10 +660,10 @@ certifi = "*" urllib3 = ">=1.21.1,<2" [package.extras] -async = ["aiohttp (>=3,<4)"] -develop = ["requests (>=2.0.0,<3.0.0)", "coverage", "mock", "pyyaml", "pytest", "pytest-cov", "sphinx (<1.7)", "sphinx-rtd-theme", "black", "jinja2"] -docs = ["sphinx (<1.7)", "sphinx-rtd-theme"] requests = ["requests (>=2.4.0,<3.0.0)"] +docs = ["sphinx-rtd-theme", "sphinx (<1.7)"] +develop = ["jinja2", "black", "sphinx-rtd-theme", "sphinx (<1.7)", "pytest-cov", "pytest", "pyyaml", "mock", "coverage", "requests (>=2.0.0,<3.0.0)"] +async = ["aiohttp (>=3,<4)"] [[package]] name = "elasticsearch-dsl" @@ -704,7 +679,7 @@ python-dateutil = "*" six = "*" [package.extras] -develop = ["mock", "pytest (>=3.0.0)", "pytest-cov", "pytest-mock (<3.0.0)", "pytz", "coverage (<5.0.0)", "sphinx", "sphinx-rtd-theme"] +develop = ["sphinx-rtd-theme", "sphinx", "coverage (<5.0.0)", "pytz", "pytest-mock (<3.0.0)", "pytest-cov", "pytest (>=3.0.0)", "mock"] [[package]] name = "et-xmlfile" @@ -723,8 +698,8 @@ optional = false python-versions = ">=3.6" [package.extras] -docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] -testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] +testing = ["pytest-timeout (>=1.4.2)", "pytest-cov", "pytest (>=4)", "coverage (>=4)", "covdefaults (>=1.2.0)"] +docs = ["sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4.1)", "furo (>=2021.8.17b43)"] [[package]] name = "flake8" @@ -735,7 +710,6 @@ optional = false python-versions = ">=3.6" [package.dependencies] -importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.8.0,<2.9.0" pyflakes = ">=2.4.0,<2.5.0" @@ -836,27 +810,11 @@ optional = false python-versions = ">=3.6" [package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] - -[[package]] -name = "importlib-resources" -version = "5.2.3" -description = "Read resources from Python packages" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["importlib-resources (>=1.3)", "pytest-mypy", "pytest-black (>=0.3.7)", "flufl.flake8", "pyfakefs", "pep517", "packaging", "pytest-enabler (>=1.0.1)", "pytest-cov", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=4.6)"] +docs = ["rst.linker (>=1.9)", "jaraco.packaging (>=8.2)", "sphinx"] [[package]] name = "inflection" @@ -887,15 +845,15 @@ pygments = "*" traitlets = ">=4.2" [package.extras] -all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.14)", "pygments", "qtconsole", "requests", "testpath"] -doc = ["Sphinx (>=1.3)"] -kernel = ["ipykernel"] -nbconvert = ["nbconvert"] -nbformat = ["nbformat"] -notebook = ["notebook", "ipywidgets"] -parallel = ["ipyparallel"] +test = ["numpy (>=1.14)", "ipykernel", "nbformat", "pygments", "testpath", "requests", "nose (>=0.10.1)"] qtconsole = ["qtconsole"] -test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.14)"] +parallel = ["ipyparallel"] +notebook = ["ipywidgets", "notebook"] +nbformat = ["nbformat"] +nbconvert = ["nbconvert"] +kernel = ["ipykernel"] +doc = ["Sphinx (>=1.3)"] +all = ["testpath", "requests", "qtconsole", "pygments", "numpy (>=1.14)", "notebook", "nose (>=0.10.1)", "nbformat", "nbconvert", "ipywidgets", "ipyparallel", "ipykernel", "Sphinx (>=1.3)"] [[package]] name = "ipython-genutils" @@ -925,8 +883,8 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" parso = ">=0.7.0,<0.8.0" [package.extras] +testing = ["pytest (>=3.9.0,<5.0.0)", "docopt", "colorama", "Django (<3.1)"] qa = ["flake8 (==3.7.9)"] -testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] [[package]] name = "jeepney" @@ -975,9 +933,9 @@ cryptography = ">=1.5" PyOpenSSL = ">=0.13" [package.extras] -dev = ["pytest", "tox"] -docs = ["Sphinx (>=1.0)", "sphinx-rtd-theme (>=1.0)"] -tests = ["coverage (>=4.0)", "flake8 (<4)", "isort", "mypy", "pytest-cov", "pytest-flake8 (>=0.5)", "pytest (>=2.8.0)", "types-pyopenssl", "types-pyrfc3339", "types-requests", "types-setuptools"] +tests = ["types-setuptools", "types-requests", "types-pyrfc3339", "types-pyopenssl", "pytest (>=2.8.0)", "pytest-flake8 (>=0.5)", "pytest-cov", "mypy", "isort", "flake8 (<4)", "coverage (>=4.0)"] +docs = ["sphinx-rtd-theme (>=1.0)", "Sphinx (>=1.0)"] +dev = ["tox", "pytest"] [[package]] name = "keyring" @@ -994,8 +952,8 @@ pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_ SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest-mypy", "pytest-black (>=0.3.7)", "pytest-enabler (>=1.0.1)", "pytest-cov", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=6)"] +docs = ["jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "jaraco.packaging (>=8.2)", "sphinx"] [[package]] name = "kiwisolver" @@ -1015,25 +973,23 @@ python-versions = ">=3.6" [package.dependencies] amqp = ">=5.0.6,<6.0.0" -cached-property = {version = "*", markers = "python_version < \"3.8\""} -importlib-metadata = {version = ">=0.18", markers = "python_version < \"3.8\""} vine = "*" [package.extras] -azureservicebus = ["azure-servicebus (>=7.0.0)"] -azurestoragequeues = ["azure-storage-queue"] -consul = ["python-consul (>=0.6.0)"] -librabbitmq = ["librabbitmq (>=1.5.2)"] -mongodb = ["pymongo (>=3.3.0)"] -msgpack = ["msgpack"] -pyro = ["pyro4"] -qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] -redis = ["redis (>=3.3.11)"] -slmq = ["softlayer-messaging (>=1.0.3)"] -sqlalchemy = ["sqlalchemy"] -sqs = ["boto3 (>=1.4.4)", "pycurl (==7.43.0.2)", "urllib3 (<1.26)"] -yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=1.3.1)"] +yaml = ["PyYAML (>=3.10)"] +sqs = ["urllib3 (<1.26)", "pycurl (==7.43.0.2)", "boto3 (>=1.4.4)"] +sqlalchemy = ["sqlalchemy"] +slmq = ["softlayer-messaging (>=1.0.3)"] +redis = ["redis (>=3.3.11)"] +qpid = ["qpid-tools (>=0.26)", "qpid-python (>=0.26)"] +pyro = ["pyro4"] +msgpack = ["msgpack"] +mongodb = ["pymongo (>=3.3.0)"] +librabbitmq = ["librabbitmq (>=1.5.2)"] +consul = ["python-consul (>=0.6.0)"] +azurestoragequeues = ["azure-storage-queue"] +azureservicebus = ["azure-servicebus (>=7.0.0)"] [[package]] name = "markupsafe" @@ -1119,7 +1075,7 @@ python-versions = ">=3.6" [[package]] name = "openpyxl" -version = "3.0.9" +version = "3.0.10" description = "A Python library to read/write Excel 2010 xlsx/xlsm files" category = "main" optional = false @@ -1187,11 +1143,11 @@ python-versions = ">=3.6" [[package]] name = "pkginfo" -version = "1.8.2" +version = "1.8.3" description = "Query metadatdata from sdists / bdists / installed packages." category = "main" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.extras] testing = ["coverage", "nose"] @@ -1210,17 +1166,15 @@ docs = ["sphinx-autodoc-typehints (>=1.12)", "proselint (>=0.10.2)", "furo (>=20 [[package]] name = "pre-commit" -version = "2.17.0" +version = "2.20.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.7" [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -importlib-resources = {version = "<5.3", markers = "python_version < \"3.7\""} nodeenv = ">=0.11.1" pyyaml = ">=5.1" toml = "*" @@ -1228,7 +1182,7 @@ virtualenv = ">=20.0.8" [[package]] name = "prompt-toolkit" -version = "3.0.28" +version = "3.0.30" description = "Library for building powerful interactive command lines in Python" category = "main" optional = false @@ -1287,11 +1241,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" -version = "2.11.2" +version = "2.12.0" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [[package]] name = "pyloco" @@ -1320,16 +1274,16 @@ python-versions = ">=3.6" cryptography = ">=35.0" [package.extras] -docs = ["sphinx", "sphinx-rtd-theme"] -test = ["flaky", "pretend", "pytest (>=3.0.1)"] +test = ["pytest (>=3.0.1)", "pretend", "flaky"] +docs = ["sphinx-rtd-theme", "sphinx"] [[package]] name = "pyparsing" -version = "3.0.7" -description = "Python parsing module" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.6.8" [package.extras] diagrams = ["jinja2", "railroad-diagrams"] @@ -1416,7 +1370,7 @@ md = ["cmarkgfm (>=0.8.0)"] [[package]] name = "redis" -version = "4.2.0" +version = "4.3.4" description = "Python client for Redis database and key-value store" category = "main" optional = false @@ -1425,17 +1379,15 @@ python-versions = ">=3.6" [package.dependencies] async-timeout = ">=4.0.2" deprecated = ">=1.2.3" -importlib-metadata = {version = ">=1.0", markers = "python_version < \"3.8\""} packaging = ">=20.4" -typing-extensions = "*" [package.extras] +ocsp = ["requests (>=2.26.0)", "pyopenssl (==20.0.1)", "cryptography (>=36.0.1)"] hiredis = ["hiredis (>=1.0.0)"] -ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] [[package]] name = "regex" -version = "2022.3.15" +version = "2022.7.25" description = "Alternative regular expression module, to replace re." category = "main" optional = false @@ -1456,8 +1408,8 @@ idna = ">=2.5,<3" urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" [package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +socks = ["win-inet-pton", "PySocks (>=1.5.6,!=1.5.7)"] +security = ["cryptography (>=1.3.4)", "pyOpenSSL (>=0.14)"] [[package]] name = "requests-mock" @@ -1536,7 +1488,7 @@ crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] [[package]] name = "secretstorage" -version = "3.3.1" +version = "3.3.2" description = "Python bindings to FreeDesktop.org Secret Service API" category = "main" optional = false @@ -1625,7 +1577,7 @@ python-versions = ">=3.6" [[package]] name = "tqdm" -version = "4.63.1" +version = "4.64.0" description = "Fast, Extensible Progress Meter" category = "main" optional = false @@ -1633,12 +1585,12 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-resources = {version = "*", markers = "python_version < \"3.7\""} [package.extras] -dev = ["py-make (>=0.1.0)", "twine", "wheel"] -notebook = ["ipywidgets (>=6)"] telegram = ["requests"] +slack = ["slack-sdk"] +notebook = ["ipywidgets (>=6)"] +dev = ["wheel", "twine", "py-make (>=0.1.0)"] [[package]] name = "traitlets" @@ -1654,7 +1606,7 @@ ipython-genutils = "*" six = "*" [package.extras] -test = ["pytest", "mock"] +test = ["mock", "pytest"] [[package]] name = "twine" @@ -1675,14 +1627,6 @@ requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" rfc3986 = ">=1.4.0" tqdm = ">=4.14" -[[package]] -name = "typed-ast" -version = "1.5.2" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "main" -optional = false -python-versions = ">=3.6" - [[package]] name = "typing" version = "3.7.4.3" @@ -1691,14 +1635,6 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -[[package]] -name = "typing-extensions" -version = "4.1.1" -description = "Backported and Experimental Type Hints for Python 3.6+" -category = "main" -optional = false -python-versions = ">=3.6" - [[package]] name = "uritemplate" version = "4.1.1" @@ -1716,9 +1652,9 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] -brotli = ["brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +secure = ["ipaddress", "certifi", "idna (>=2.0.0)", "cryptography (>=1.3.4)", "pyOpenSSL (>=0.14)"] +brotli = ["brotlipy (>=0.6.0)"] [[package]] name = "ushlex" @@ -1747,13 +1683,11 @@ python-versions = ">=3.6" [package.dependencies] distlib = ">=0.3.1,<1" filelock = ">=3.2,<4" -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} platformdirs = ">=2,<3" [package.extras] -docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] +testing = ["pytest-timeout (>=1)", "pytest-randomly (>=1)", "pytest-mock (>=2)", "pytest-freezegun (>=0.4.1)", "pytest-env (>=0.6.2)", "pytest (>=4)", "packaging (>=20.0)", "flaky (>=3)", "coverage-enable-subprocess (>=1)", "coverage (>=4)"] +docs = ["towncrier (>=21.3)", "sphinx-rtd-theme (>=0.4.3)", "sphinx-argparse (>=0.2.5)", "sphinx (>=3)", "proselint (>=0.10.2)"] [[package]] name = "wcwidth" @@ -1786,7 +1720,7 @@ docs = ["sphinx-rtd-theme (>=0.5)", "Sphinx (>=3.4)"] [[package]] name = "weni-protobuffers" -version = "1.2.10" +version = "1.2.16" description = "Protocol Buffers for Weni Platform" category = "main" optional = false @@ -1809,7 +1743,7 @@ brotli = ["brotli"] [[package]] name = "wrapt" -version = "1.14.0" +version = "1.14.1" description = "Module for decorators, wrappers and monkey patching." category = "main" optional = false @@ -1824,13 +1758,13 @@ optional = false python-versions = ">=3.6" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +testing = ["pytest-mypy", "pytest-black (>=0.3.7)", "func-timeout", "jaraco.itertools", "pytest-enabler (>=1.0.1)", "pytest-cov", "pytest-flake8", "pytest-checkdocs (>=2.4)", "pytest (>=4.6)"] +docs = ["rst.linker (>=1.9)", "jaraco.packaging (>=8.2)", "sphinx"] [metadata] lock-version = "1.1" -python-versions = "~=3.6.15" -content-hash = "25c839bc8231f2bd8e6528102aea0ddff4f20a5b98791f5dc7aa9eb022883189" +python-versions = "~=3.8.13" +content-hash = "414b18406578355170034e5c473d3f62e31bd00f0149de551d70ab76f5dab7ca" [metadata.files] amqp = [] @@ -1845,7 +1779,6 @@ black = [] bleach = [] boto3 = [] botocore = [] -cached-property = [] celery = [] certifi = [] cffi = [] @@ -1861,7 +1794,6 @@ coreschema = [] coverage = [] cryptography = [] cycler = [] -dataclasses = [] decorator = [] deprecated = [] distlib = [] @@ -1898,7 +1830,6 @@ gunicorn = [] identify = [] idna = [] importlib-metadata = [] -importlib-resources = [] inflection = [] ipython = [] ipython-genutils = [] @@ -1967,9 +1898,7 @@ tomli = [] tqdm = [] traitlets = [] twine = [] -typed-ast = [] typing = [] -typing-extensions = [] uritemplate = [] urllib3 = [] ushlex = [] diff --git a/pyproject.toml b/pyproject.toml index a765b803..f034a70e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ authors = ["weni.ai"] license = "MPL-2.0" [tool.poetry.dependencies] -python = "~=3.6.15" +python = "~=3.8.13" django = "~=3.2.6" djangorestframework = "~=3.12.4" django-filter = "~=2.4.0" @@ -61,7 +61,7 @@ coverage = "~=5.5" ipython = "*" black = "==21.7b0" autopep8 = "^1.6.0" -pre-commit = "2.17.0" +pre-commit = "^2.20.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/runtime.txt b/runtime.txt index 82347234..5b3694c1 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.6.8 \ No newline at end of file +python-3.8.13 From 78b7d1c6cf553b65b0a1ac1a760964d3788b1a16 Mon Sep 17 00:00:00 2001 From: Victor Salles Date: Tue, 16 Aug 2022 19:08:34 -0300 Subject: [PATCH 6/9] Hotfix/ Update RepositoryAuthorization.get_role The model property contained an outdated business rule that allowed users to inherit the authorization role from any linked Organizations that had permission to access the repository. The updated property uses the repository's get_user_authorization method, instead of searching for a higher authorization within the users' organizations. --- bothub/common/models.py | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/bothub/common/models.py b/bothub/common/models.py index 05163b18..8ff9e719 100644 --- a/bothub/common/models.py +++ b/bothub/common/models.py @@ -2020,26 +2020,9 @@ def save(self, *args, **kwargs): @property def get_role(self): if self.role < RepositoryAuthorization.ROLE_USER and self.user: - org = ( - self.user.organization_user_authorization.exclude( - role=RepositoryAuthorization.ROLE_NOT_SETTED - ) - .filter( - Q( - organization__in=RepositoryAuthorization.objects.filter( - repository=self.repository, - user__in=self.user.organization_user_authorization.exclude( - role=OrganizationAuthorization.ROLE_NOT_SETTED - ).values_list("organization", flat=True), - ) - .exclude(role=OrganizationAuthorization.ROLE_NOT_SETTED) - .order_by("-role") - .values_list("user") - ) - ) - .order_by("-role") - ).first() - return org.role if org else RepositoryAuthorization.LEVEL_NOTHING + # Get role directly from repository + auth = self.repository.get_user_authorization(self.user) + return auth.role return self.role @property From 4b43b9c15cc5224e93a680272af61b94a191a440 Mon Sep 17 00:00:00 2001 From: Victor Salles Date: Wed, 17 Aug 2022 11:54:23 -0300 Subject: [PATCH 7/9] Update test_authorization_permission_admin_in_organization Update business rule inside the test, checking if an unauthorized user has the corresponding authorization role at the repository. --- bothub/api/v2/tests/test_repository.py | 57 +++++++++++++++----------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/bothub/api/v2/tests/test_repository.py b/bothub/api/v2/tests/test_repository.py index 025753f9..87029a9a 100644 --- a/bothub/api/v2/tests/test_repository.py +++ b/bothub/api/v2/tests/test_repository.py @@ -320,48 +320,57 @@ def test_authorization_with_user(self): ) def test_authorization_permission_admin_in_organization(self): + """Validate that the owner user has full access to the repository + while a common user only has read access""" for repository in self.repositories: - perm = OrganizationAuthorization.objects.create( + + # Create repository and organization authorizations + organization_authorization = OrganizationAuthorization.objects.create( user=self.user, organization=self.organization, role=OrganizationAuthorization.ROLE_ADMIN, ) - - repo_auth = RepositoryAuthorization.objects.create( - user=self.organization, repository=repository, role=3 - ) - user, user_token = ( - (self.owner, self.owner_token) - if repository.is_private - else (self.user, self.user_token) + repository_authorization = RepositoryAuthorization.objects.create( + user=self.organization, + repository=repository, + role=RepositoryAuthorization.ROLE_ADMIN, ) + + user, user_token = (self.owner, self.owner_token) if repository.is_private else (self.user, self.user_token) response, content_data = self.request(repository, user_token) authorization = content_data.get("authorization") self.assertIsNotNone(authorization) - self.assertEqual( - authorization.get("level"), OrganizationAuthorization.ROLE_ADMIN - ) self.assertTrue(authorization.get("can_read")) - self.assertTrue(authorization.get("can_contribute")) - self.assertTrue(authorization.get("can_write")) - self.assertTrue(authorization.get("can_translate")) - self.assertTrue(authorization.get("is_admin")) + + # Assert owner access vs common user access behavior + if user is self.owner: + self.assertEqual(authorization.get("level"), OrganizationAuthorization.ROLE_ADMIN) + self.assertTrue(authorization.get("can_contribute")) + self.assertTrue(authorization.get("can_write")) + self.assertTrue(authorization.get("can_translate")) + self.assertTrue(authorization.get("is_admin")) + else: # is not owner + self.assertEqual(authorization.get("level"), OrganizationAuthorization.ROLE_USER) + self.assertFalse(authorization.get("can_contribute")) + self.assertFalse(authorization.get("can_write")) + self.assertFalse(authorization.get("can_translate")) + self.assertFalse(authorization.get("is_admin")) + self.assertEqual(len(authorization.get("organizations")), 1) - perm.delete() + + # User should have role==ROLE_USER when they lose their authorization in the organization + organization_authorization.delete() response, content_data = self.request(repository, user_token) authorization = content_data.get("authorization") self.assertIsNotNone(authorization) - self.assertEqual( - authorization.get("level"), OrganizationAuthorization.ROLE_USER - ) + self.assertEqual(authorization.get("level"), OrganizationAuthorization.ROLE_USER) - repo_auth.delete() + # User should have role==ROLE_USER when they lose their authorization in the repository + repository_authorization.delete() response, content_data = self.request(repository, user_token) authorization = content_data.get("authorization") self.assertIsNotNone(authorization) - self.assertEqual( - authorization.get("level"), OrganizationAuthorization.ROLE_USER - ) + self.assertEqual(authorization.get("level"), OrganizationAuthorization.ROLE_USER) class RepositoryAvailableRequestAuthorizationTestCase(TestCase): From dfa72ee43942e4f8f29b3c58dd951f132e45a666 Mon Sep 17 00:00:00 2001 From: Victor Salles Date: Fri, 12 Aug 2022 16:40:21 -0300 Subject: [PATCH 8/9] Fix/ Add serializer_class to RepositoryTokenByUser --- bothub/api/v2/repository/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index 386b8ad8..74a6903e 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -1069,6 +1069,8 @@ def destroy(self, request, *args, **kwargs): class RepositoryTokenByUserViewSet(mixins.ListModelMixin, GenericViewSet): + serializer_class = RepositoryAuthorizationSerializer + def get_queryset(self): user = self.request.user if user.is_anonymous: From 60f2d353667f0bc9e13b35fb72724c979bed32ce Mon Sep 17 00:00:00 2001 From: Victor Salles Date: Tue, 26 Jul 2022 14:41:51 -0300 Subject: [PATCH 9/9] Fix ADMINS variable at bothub/settings.py --- bothub/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bothub/settings.py b/bothub/settings.py index 4acaaeea..ff653783 100644 --- a/bothub/settings.py +++ b/bothub/settings.py @@ -277,7 +277,7 @@ envvar_EMAIL_HOST = env.str("EMAIL_HOST") -ADMINS = [("Helder", "helder.souza@weni.ai")] # env.list("ADMINS") +ADMINS = env.list("ADMINS") EMAIL_SUBJECT_PREFIX = "[bothub] " DEFAULT_FROM_EMAIL = env.str("DEFAULT_FROM_EMAIL") SERVER_EMAIL = env.str("SERVER_EMAIL")