Skip to content
3 changes: 2 additions & 1 deletion gateway/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
22 changes: 22 additions & 0 deletions gateway/api/access_policies/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down
29 changes: 20 additions & 9 deletions gateway/api/use_cases/jobs/get_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
130 changes: 48 additions & 82 deletions gateway/tests/api/test_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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)

Expand Down Expand Up @@ -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."""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
log entry 2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
log entry 1
Loading