diff --git a/gateway/.gitignore b/gateway/.gitignore index 2dfb26dee..16b4924ac 100644 --- a/gateway/.gitignore +++ b/gateway/.gitignore @@ -137,4 +137,5 @@ GitHub.sublime-settings !.vscode/extensions.json .history -tests/resources/fake_media/* +tests/resources/fake_media/**/arguments/* +!tests/resources/fake_media/**/logs/*.log diff --git a/gateway/api/access_policies/jobs.py b/gateway/api/access_policies/jobs.py index f9a2227a0..7bf4ef47b 100644 --- a/gateway/api/access_policies/jobs.py +++ b/gateway/api/access_policies/jobs.py @@ -68,6 +68,28 @@ def can_read_result(user: type[AbstractUser], job: Job) -> bool: ) return has_access + @staticmethod + def can_read_logs(user: type[AbstractUser], job: Job) -> bool: + """ + Checks if the user has permissions to read the result of a job: + + Args: + user: Django user from the request + job: Job instance against to check the permission + + Returns: + bool: True or False in case the user has permissions + """ + + has_access = user.id == job.author.id + if not has_access: + logger.warning( + "User [%s] has no access to read the result of the job [%s].", + user.username, + job.author, + ) + return has_access + @staticmethod def can_save_result(user: type[AbstractUser], job: Job) -> bool: """ diff --git a/gateway/api/use_cases/jobs/get_logs.py b/gateway/api/use_cases/jobs/get_logs.py index cf3cbee5a..21cccc0a7 100644 --- a/gateway/api/use_cases/jobs/get_logs.py +++ b/gateway/api/use_cases/jobs/get_logs.py @@ -6,10 +6,13 @@ from django.contrib.auth.models import AbstractUser +from api.access_policies.jobs import JobAccessPolicies from api.domain.exceptions.not_found_error import NotFoundError from api.domain.exceptions.forbidden_error import ForbiddenError from api.repositories.jobs import JobsRepository from api.access_policies.providers import ProviderAccessPolicy +from api.services.storage.enums.working_dir import WorkingDir +from api.services.storage.logs_storage import LogsStorage NO_LOGS_MSG: Final[str] = "No available logs" @@ -37,14 +40,22 @@ def execute(self, job_id: UUID, user: AbstractUser) -> str: if job is None: raise NotFoundError(f"Job [{job_id}] not found") - # Case 1: Provider function - check provider access policy - if job.program and job.program.provider: - if ProviderAccessPolicy.can_access(user, job.program.provider): - return job.logs + if not JobAccessPolicies.can_read_logs(user, job): + raise ForbiddenError(f"You don't have access to job [{job_id}]") - # Case 2: User is the author of the job - elif user == job.author: - return job.logs + logs_storage = LogsStorage( + username=user.username, + working_dir=WorkingDir.USER_STORAGE, + function_title=job.program.title, + provider_name=job.program.provider.name if job.program.provider else None, + ) - # Access denied for all other cases - raise ForbiddenError(f"You don't have access to job [{job_id}]") + logs = logs_storage.get(job_id) + + if logs is None: + raise NotFoundError(f"Logs for job[{job_id}] are not found") + + if len(logs) == 0: + return "No logs available" + + return logs diff --git a/gateway/tests/api/test_job.py b/gateway/tests/api/test_job.py index b981a65df..75b47b93f 100644 --- a/gateway/tests/api/test_job.py +++ b/gateway/tests/api/test_job.py @@ -22,6 +22,16 @@ def _authorize(self, username="test_user"): user = models.User.objects.get(username=username) self.client.force_authenticate(user=user) + def _fake_media_root(self): + media_root = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "..", + "resources", + "fake_media", + ) + media_root = os.path.normpath(os.path.join(os.getcwd(), media_root)) + return media_root + def test_job_non_auth_user(self): """Tests job list non-authorized.""" url = reverse("v1:jobs-list") @@ -256,15 +266,7 @@ def test_job_provider_list_pagination(self): def test_job_detail(self): """Tests job detail authorized.""" - media_root = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "..", - "resources", - "fake_media", - ) - media_root = os.path.normpath(os.path.join(os.getcwd(), media_root)) - - with self.settings(MEDIA_ROOT=media_root): + with self.settings(MEDIA_ROOT=self._fake_media_root()): self._authorize() jobs_response = self.client.get( @@ -276,15 +278,7 @@ def test_job_detail(self): def test_job_detail_without_result_param(self): """Tests job detail authorized.""" - media_root = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "..", - "resources", - "fake_media", - ) - media_root = os.path.normpath(os.path.join(os.getcwd(), media_root)) - - with self.settings(MEDIA_ROOT=media_root): + with self.settings(MEDIA_ROOT=self._fake_media_root()): self._authorize() jobs_response = self.client.get( @@ -297,15 +291,7 @@ def test_job_detail_without_result_param(self): def test_job_detail_without_result_file(self): """Tests job detail authorized.""" - media_root = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "..", - "resources", - "fake_media", - ) - media_root = os.path.normpath(os.path.join(os.getcwd(), media_root)) - - with self.settings(MEDIA_ROOT=media_root): + with self.settings(MEDIA_ROOT=self._fake_media_root()): self._authorize() jobs_response = self.client.get( @@ -340,15 +326,7 @@ def test_not_authorized_job_detail(self): def test_job_save_result(self): """Tests job results save.""" - media_root = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "..", - "resources", - "fake_media", - ) - media_root = os.path.normpath(os.path.join(os.getcwd(), media_root)) - - with self.settings(MEDIA_ROOT=media_root): + with self.settings(MEDIA_ROOT=self._fake_media_root()): self._authorize() job_id = "57fc2e4d-267f-40c6-91a3-38153272e764" jobs_response = self.client.post( @@ -465,15 +443,7 @@ def test_user_has_access_to_job_result_from_provider_function(self): User has access to job result from a function provider as the authot of the job """ - media_root = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "..", - "resources", - "fake_media", - ) - media_root = os.path.normpath(os.path.join(os.getcwd(), media_root)) - - with self.settings(MEDIA_ROOT=media_root): + with self.settings(MEDIA_ROOT=self._fake_media_root()): self._authorize() jobs_response = self.client.get( @@ -488,15 +458,7 @@ def test_provider_admin_has_no_access_to_job_result_from_provider_function(self) A provider admin has no access to job result from a function provider if it's not the author of the job """ - media_root = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "..", - "resources", - "fake_media", - ) - media_root = os.path.normpath(os.path.join(os.getcwd(), media_root)) - - with self.settings(MEDIA_ROOT=media_root): + with self.settings(MEDIA_ROOT=self._fake_media_root()): user = models.User.objects.get(username="test_user_3") self.client.force_authenticate(user=user) @@ -529,47 +491,51 @@ def test_stop_job(self): def test_job_logs_by_author_for_function_without_provider(self): """Tests job log by job author.""" - self._authorize() + with self.settings(MEDIA_ROOT=self._fake_media_root()): + self._authorize() - jobs_response = self.client.get( - reverse("v1:jobs-logs", args=["57fc2e4d-267f-40c6-91a3-38153272e764"]), - format="json", - ) - self.assertEqual(jobs_response.status_code, status.HTTP_200_OK) - self.assertEqual(jobs_response.data.get("logs"), "log entry 2") + jobs_response = self.client.get( + reverse("v1:jobs-logs", args=["57fc2e4d-267f-40c6-91a3-38153272e764"]), + format="json", + ) + self.assertEqual(jobs_response.status_code, status.HTTP_200_OK) + self.assertEqual(jobs_response.data.get("logs"), "log entry 2") def test_job_logs_by_author_for_function_with_provider(self): """Tests job log by job author.""" - self._authorize() + with self.settings(MEDIA_ROOT=self._fake_media_root()): + self._authorize() - jobs_response = self.client.get( - reverse("v1:jobs-logs", args=["1a7947f9-6ae8-4e3d-ac1e-e7d608deec85"]), - format="json", - ) - self.assertEqual(jobs_response.status_code, status.HTTP_403_FORBIDDEN) + jobs_response = self.client.get( + reverse("v1:jobs-logs", args=["1a7947f9-6ae8-4e3d-ac1e-e7d608deec85"]), + format="json", + ) + self.assertEqual(jobs_response.status_code, status.HTTP_403_FORBIDDEN) def test_job_logs_by_function_provider(self): """Tests job log by fuction provider.""" - user = models.User.objects.get(username="test_user_2") - self.client.force_authenticate(user=user) + with self.settings(MEDIA_ROOT=self._fake_media_root()): + user = models.User.objects.get(username="test_user_2") + self.client.force_authenticate(user=user) - jobs_response = self.client.get( - reverse("v1:jobs-logs", args=["1a7947f9-6ae8-4e3d-ac1e-e7d608deec85"]), - format="json", - ) - self.assertEqual(jobs_response.status_code, status.HTTP_200_OK) - self.assertEqual(jobs_response.data.get("logs"), "log entry 1") + jobs_response = self.client.get( + reverse("v1:jobs-logs", args=["1a7947f9-6ae8-4e3d-ac1e-e7d608deec85"]), + format="json", + ) + self.assertEqual(jobs_response.status_code, status.HTTP_200_OK) + self.assertEqual(jobs_response.data.get("logs"), "log entry 1") def test_job_logs(self): """Tests job log non-authorized.""" - user = models.User.objects.get(username="test_user_3") - self.client.force_authenticate(user=user) + with self.settings(MEDIA_ROOT=self._fake_media_root()): + user = models.User.objects.get(username="test_user_3") + self.client.force_authenticate(user=user) - jobs_response = self.client.get( - reverse("v1:jobs-logs", args=["1a7947f9-6ae8-4e3d-ac1e-e7d608deec85"]), - format="json", - ) - self.assertEqual(jobs_response.status_code, status.HTTP_403_FORBIDDEN) + jobs_response = self.client.get( + reverse("v1:jobs-logs", args=["1a7947f9-6ae8-4e3d-ac1e-e7d608deec85"]), + format="json", + ) + self.assertEqual(jobs_response.status_code, status.HTTP_403_FORBIDDEN) def test_runtime_jobs_post(self): """Tests runtime jobs POST endpoint.""" diff --git a/gateway/tests/resources/fake_media/test_user/logs/57fc2e4d-267f-40c6-91a3-38153272e764.log b/gateway/tests/resources/fake_media/test_user/logs/57fc2e4d-267f-40c6-91a3-38153272e764.log new file mode 100644 index 000000000..6949f9ab1 --- /dev/null +++ b/gateway/tests/resources/fake_media/test_user/logs/57fc2e4d-267f-40c6-91a3-38153272e764.log @@ -0,0 +1 @@ +log entry 2 \ No newline at end of file diff --git a/gateway/tests/resources/fake_media/test_user_2/default/Docker-Image-Program/logs/1a7947f9-6ae8-4e3d-ac1e-e7d608deec85.log b/gateway/tests/resources/fake_media/test_user_2/default/Docker-Image-Program/logs/1a7947f9-6ae8-4e3d-ac1e-e7d608deec85.log new file mode 100644 index 000000000..a78b9817a --- /dev/null +++ b/gateway/tests/resources/fake_media/test_user_2/default/Docker-Image-Program/logs/1a7947f9-6ae8-4e3d-ac1e-e7d608deec85.log @@ -0,0 +1 @@ +log entry 1 \ No newline at end of file