Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ch32786] update attachments permissions #3765

Open
wants to merge 40 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
18745bb
add attachments permission to control files access
roman-karpovich Feb 10, 2023
4c83914
add field monitoring attachments logic to check related third party u…
roman-karpovich Feb 10, 2023
5b857a0
Merge branch 'develop' into ch32786-update-attachments-permissions
roman-karpovich Feb 14, 2023
91b12ef
add psea permissions third party users access
roman-karpovich Feb 14, 2023
30bc937
permit generic fm attachments & tpm partner files
roman-karpovich Feb 14, 2023
04a9043
configure attachments permissions for tpm activity
roman-karpovich Feb 21, 2023
5954d4a
Merge branch 'develop' into ch32786-update-attachments-permissions
roman-karpovich Feb 21, 2023
93ba374
allow unlinked attachments in permissions; add tests for attachmentlink
roman-karpovich Feb 22, 2023
f173547
Merge branch 'develop' into ch32786-update-attachments-permissions
roman-karpovich May 23, 2023
a3608ef
sort permissions
roman-karpovich May 23, 2023
9f52e02
Merge branch 'develop' into ch32786-update-attachments-permissions
roman-karpovich May 29, 2023
91aca25
update code to match realms updates
roman-karpovich May 29, 2023
23318ec
Merge branch 'develop' into ch32786-update-attachments-permissions
roman-karpovich Jun 6, 2023
6fcf3a4
update from develop
robertavram Apr 22, 2024
76ccc0d
update attachments permissions: disallow inactive realms
roman-karpovich Apr 24, 2024
ee3bd64
Merge branch 'develop' into ch32786-update-attachments-permissions
roman-karpovich Apr 24, 2024
1399b6c
add testcases
roman-karpovich Apr 26, 2024
85fb286
Merge remote-tracking branch 'origin/ch32786-update-attachments-permi…
roman-karpovich Apr 26, 2024
adc4cde
Merge branch 'develop' into ch32786-update-attachments-permissions
roman-karpovich Apr 26, 2024
6e5c5e9
Merge branch 'develop' into ch32786-update-attachments-permissions
roman-karpovich Jun 11, 2024
3146044
fix f-string error
roman-karpovich Jun 19, 2024
bc25f8b
Merge remote-tracking branch 'refs/remotes/origin/ch32786-update-atta…
roman-karpovich Jun 19, 2024
503a972
pep8 fix
roman-karpovich Jun 19, 2024
8c10869
update deprecated assertion
roman-karpovich Jun 19, 2024
956e721
switch to git+unicef-attachments 0.14
roman-karpovich Jun 19, 2024
3794615
Merge branch 'refs/heads/robdj4.2' into robdj4.2-ch32786-update-attac…
roman-karpovich Jun 19, 2024
5990c5d
fix tests
roman-karpovich Jun 19, 2024
799a8ce
Merge branch 'develop' into robdj4.2
robertavram Jul 24, 2024
13bfa6f
Merge branch 'develop' of github.com:unicef/etools into robdj4.2
emaciupe Jul 25, 2024
35b7913
Merge branch 'robdj4.2' of github.com:unicef/etools into robdj4.2
emaciupe Jul 25, 2024
b5e4f7a
Merge branch 'refs/heads/robdj4.2' into robdj4.2-ch32786-update-attac…
roman-karpovich Sep 16, 2024
f41a5ce
use unicef-attachments commit
roman-karpovich Sep 16, 2024
982fad4
Merge branch 'refs/heads/develop' into robdj4.2-ch32786-update-attach…
roman-karpovich Sep 16, 2024
118205f
fix imports
roman-karpovich Sep 16, 2024
b7f0540
fix tests
roman-karpovich Sep 20, 2024
781b560
Merge branch 'develop' into new-ch32786-update-attachments-permissions
roman-karpovich Sep 20, 2024
444bef6
Merge branch 'refs/heads/develop' into new-ch32786-update-attachments…
roman-karpovich Oct 28, 2024
496b2fd
update pdm
roman-karpovich Oct 28, 2024
ad07635
update pdm lock
roman-karpovich Oct 28, 2024
bbe9acf
update pdm lock
roman-karpovich Oct 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 120 additions & 110 deletions pdm.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ dependencies = [
'social-auth-app-django==5.4.1',
"social-auth-core[azuread]==4.5.4",
'tenant-schemas-celery==2.2.0',
'unicef-attachments==0.12',
'unicef-attachments @ git+https://github.com/unicef/unicef-attachments.git@b720a3ef26e3320a559f72ed1918c798dd2d7b67',
'unicef-djangolib==0.7',
'unicef-locations==4.2',
"unicef-notification==1.4.1",
Expand Down
36 changes: 35 additions & 1 deletion src/etools/applications/attachments/permissions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,42 @@
from rest_framework.permissions import IsAuthenticated
from rest_framework.permissions import BasePermission, IsAuthenticated

from etools.applications.core.permissions import IsUNICEFUser


class IsInSchema(IsAuthenticated):
def has_permission(self, request, view):
super().has_permission(request, view)
# make sure user has schema/tenant set
return bool(hasattr(request, "tenant") and request.tenant)

def has_object_permission(self, request, view, obj):
return self.has_permission(request, view)


class IsActiveInCurrentSchema(IsInSchema):
def has_permission(self, request, view):
return super().has_permission(request, view) and request.user.realms.filter(
country=request.tenant,
is_active=True,
).exists()

def has_object_permission(self, request, view, obj):
return self.has_permission(request, view)


class IsRelatedThirdPartyUser(BasePermission):
def has_permission(self, request, view):
return True

def has_object_permission(self, request, view, obj):
content_object = obj.content_object
if not content_object:
return True

if hasattr(content_object, 'get_related_third_party_users'):
return content_object.get_related_third_party_users().filter(pk=request.user.pk).exists()

return False


UNICEFAttachmentsPermission = IsActiveInCurrentSchema & (IsUNICEFUser | IsRelatedThirdPartyUser)
561 changes: 561 additions & 0 deletions src/etools/applications/attachments/tests/test_permissions.py

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/etools/applications/audit/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,15 @@ def save(self, *args, **kwargs):
self.reference_number = self.get_reference_number()
self.save()

def get_related_third_party_users(self):
return get_user_model().objects.filter(
models.Q(pk__in=self.authorized_officers.values_list('id')) |
models.Q(pk__in=self.staff_members.values_list('id'))
).filter(
realms__organization=self.agreement.auditor_firm.organization,
realms__is_active=True,
)


class RiskCategory(OrderedModel, models.Model):
"""Group of questions"""
Expand Down
5 changes: 1 addition & 4 deletions src/etools/applications/audit/serializers/engagement.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,7 @@ def to_representation(self, value):
return None

attachment = Attachment.objects.get(pk=value)
if not getattr(attachment.file, "url", None):
return None

url = attachment.file.url
url = attachment.file_link
request = self.context.get('request', None)
if request is not None:
return request.build_absolute_uri(url)
Expand Down
3 changes: 3 additions & 0 deletions src/etools/applications/core/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,6 @@ class IsUNICEFUser(IsAuthenticated):

def has_permission(self, request, view):
return super().has_permission(request, view) and request.user.groups.filter(name='UNICEF User').exists()

def has_object_permission(self, request, view, obj):
return self.has_permission(request, view)
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ class Meta:
def __str__(self):
return '{} - {}'.format(self.started_checklist, self.narrative_finding)

def get_related_third_party_users(self):
return self.started_checklist.monitoring_activity.get_related_third_party_users()


class ActivityOverallFindingQuerySet(models.QuerySet):
def annotate_for_activity_export(self):
Expand Down
13 changes: 13 additions & 0 deletions src/etools/applications/field_monitoring/planning/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,19 @@ def get_export_checklist_findings(self):

yield checklist_dict

def get_related_third_party_users(self):
if not self.tpm_partner:
return get_user_model().objects.none()

return get_user_model().objects.filter(
Q(pk=self.visit_lead_id) |
Q(pk__in=self.team_members.through.objects
.filter(monitoringactivity_id=self.pk).values_list('user_id', flat=True))
).filter(
realms__organization=self.tpm_partner.organization,
realms__is_active=True,
)


class MonitoringActivityActionPointManager(models.Manager):
def get_queryset(self):
Expand Down
8 changes: 5 additions & 3 deletions src/etools/applications/last_mile/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ def import_data(self, workbook):
p_code = poi_dict.get('p_code', None)
if not p_code or p_code == "None":
# add a pcode if it doesn't exist:
p_code = poi_dict.get('p_code', None)
if not p_code or p_code == "None":
poi_dict['p_code'] = generate_hash(poi_dict['partner_org_vendor_no'] + poi_dict['name'] + poi_dict['poi_type'], 12)
long = poi_dict.pop('longitude')
lat = poi_dict.pop('latitude')
Expand Down Expand Up @@ -113,9 +115,9 @@ def import_data(self, workbook):
poi_obj, _ = models.PointOfInterest.all_objects.update_or_create(
p_code=poi_dict['p_code'],
defaults={'private': poi_dict['private'],
'point': poi_dict['point'],
'name': poi_dict['name'],
'poi_type': poi_dict.get('poi_type')}
"point": poi_dict['point'],
"name": poi_dict['name'],
"poi_type": poi_dict.get('poi_type')}
)
poi_obj.partner_organizations.add(partner_org_obj)

Expand Down
14 changes: 7 additions & 7 deletions src/etools/applications/last_mile/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ def test_partial_checkin_with_short(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.incoming.refresh_from_db()
self.assertEqual(self.incoming.status, models.Transfer.COMPLETED)
self.assertIn(response.data['proof_file'], self.attachment.file.path)
self.assertIn(response.data['proof_file'], self.attachment.file_link)

self.assertIn(f'DW @ {checkin_data["destination_check_in_at"].strftime("%y-%m-%d")}', self.incoming.name)
self.assertEqual(self.incoming.items.count(), len(response.data['items']))
Expand Down Expand Up @@ -334,7 +334,7 @@ def test_partial_checkin_with_short_surplus(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.incoming.refresh_from_db()
self.assertEqual(self.incoming.status, models.Transfer.COMPLETED)
self.assertIn(response.data['proof_file'], self.attachment.file.path)
self.assertIn(response.data['proof_file'], self.attachment.file_link)
self.assertEqual(self.incoming.name, checkin_data['name'])
self.assertEqual(self.incoming.items.count(), len(response.data['items']))
self.assertEqual(self.incoming.items.get(pk=item_1.pk).quantity, 11)
Expand Down Expand Up @@ -382,7 +382,7 @@ def test_partial_checkin_RUFT_material(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.incoming.refresh_from_db()
self.assertEqual(self.incoming.status, models.Transfer.COMPLETED)
self.assertIn(response.data['proof_file'], self.attachment.file.path)
self.assertIn(response.data['proof_file'], self.attachment.file_link)
self.assertEqual(self.incoming.name, checkin_data['name'])
self.assertEqual(self.incoming.items.count(), len(response.data['items']))
self.assertEqual(self.incoming.items.count(), 1) # only 1 checked-in item is visible, non RUFT
Expand Down Expand Up @@ -482,7 +482,7 @@ def test_checkout_distribution(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['status'], models.Transfer.PENDING)
self.assertEqual(response.data['transfer_type'], models.Transfer.DISTRIBUTION)
self.assertIn(response.data['proof_file'], self.attachment.file.path)
self.assertIn(response.data['proof_file'], self.attachment.file_link)

checkout_transfer = models.Transfer.objects.get(pk=response.data['id'])
self.assertEqual(checkout_transfer.destination_point, self.hospital)
Expand Down Expand Up @@ -519,7 +519,7 @@ def test_checkout_wastage(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['status'], models.Transfer.COMPLETED)
self.assertEqual(response.data['transfer_type'], models.Transfer.WASTAGE)
self.assertIn(response.data['proof_file'], self.attachment.file.path)
self.assertIn(response.data['proof_file'], self.attachment.file_link)

wastage_transfer = models.Transfer.objects.get(pk=response.data['id'])
self.assertEqual(wastage_transfer.destination_point, destination)
Expand Down Expand Up @@ -554,7 +554,7 @@ def test_checkout_handover(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['status'], models.Transfer.PENDING)
self.assertEqual(response.data['transfer_type'], models.Transfer.HANDOVER)
self.assertIn(response.data['proof_file'], self.attachment.file.path)
self.assertIn(response.data['proof_file'], self.attachment.file_link)

handover_transfer = models.Transfer.objects.get(pk=response.data['id'])
self.assertEqual(handover_transfer.partner_organization, agreement.partner)
Expand Down Expand Up @@ -613,7 +613,7 @@ def test_checkout_wastage_without_location(self):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['status'], models.Transfer.COMPLETED)
self.assertEqual(response.data['transfer_type'], models.Transfer.WASTAGE)
self.assertIn(response.data['proof_file'], self.attachment.file.path)
self.assertIn(response.data['proof_file'], self.attachment.file_link)

wastage_transfer = models.Transfer.objects.get(pk=response.data['id'])
self.assertEqual(wastage_transfer.destination_point, None)
Expand Down
29 changes: 24 additions & 5 deletions src/etools/applications/last_mile/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.utils.functional import cached_property
from django.utils.translation import gettext as _

import requests
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import mixins, status
from rest_framework.decorators import action
Expand Down Expand Up @@ -411,18 +412,36 @@ def get_pbi_access_token():
def pbi_headers(self):
return {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + self.get_pbi_access_token()}

def get_embed_url(self, workspace_id, report_id):
url_to_call = f'https://api.powerbi.com/v1.0/myorg/groups/{workspace_id}/reports/{report_id}'
api_response = requests.get(url_to_call, headers=self.pbi_headers)
if api_response.status_code == 200:
r = api_response.json()
return r["embedUrl"], r["datasetId"]

def get_embed_token(self, dataset_id, workspace_id, report_id):
embed_token_api = 'https://api.powerbi.com/v1.0/myorg/GenerateToken'
request_body = {
"datasets": [{'id': dataset_id}],
"reports": [{'id': report_id}],
"targetWorkspaces": [{'id': workspace_id}]
}
api_response = requests.post(embed_token_api, data=request_body, headers=self.pbi_headers)
if api_response.status_code == 200:
return api_response.json()["token"]
return None

def get(self, request, *args, **kwargs):
try:
embed_url, dataset_id = get_embed_url(self.pbi_headers)
print(embed_url, 'embedurl')
embed_token = get_embed_token(dataset_id, self.pbi_headers)
print(embed_token)
except TokenRetrieveException:
return Response("Temporary unavailable, PowerBI information cannot be retrieved from Microsoft Servers",
status=status.HTTP_503_SERVICE_UNAVAILABLE)

raise PermissionDenied('Token cannot be retrieved')
resp_data = {
"report_id": settings.PBI_CONFIG["REPORT_ID"],
"embed_url": embed_url,
"access_token": embed_token,
"vendor_number": request.user.profile.organization.vendor_number
"access_token": embed_token
}
return Response(resp_data, status=status.HTTP_200_OK)
10 changes: 10 additions & 0 deletions src/etools/applications/partners/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import decimal

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import ArrayField
from django.core.validators import MinValueValidator
Expand Down Expand Up @@ -2571,6 +2572,15 @@ def was_active_before(self):
change__status__after__in=[self.SIGNED, self.ACTIVE],
).exists()

def get_related_third_party_users(self):
qs_filter = Q(pk__in=self.partner_focal_points.values_list('user_id'))
if self.partner_authorized_officer_signatory:
qs_filter = qs_filter | Q(pk=self.partner_authorized_officer_signatory.user_id)
return get_user_model().objects.filter(qs_filter).filter(
realms__organization=self.agreement.partner,
realms__is_active=True,
)


class InterventionAmendment(TimeStampedModel):
"""
Expand Down
8 changes: 4 additions & 4 deletions src/etools/applications/partners/tests/test_api_agreements.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def test_agreement_detail_attachment(self):
status_code, response = self.run_request(self.agreement1.pk)

self.assertEqual(status_code, status.HTTP_200_OK)
self.assertTrue(response["attachment"].endswith(attachment.file.url))
self.assertTrue(response["attachment"].endswith(attachment.file_link))

def test_add_new_PCA(self):
self.assertFalse(Activity.objects.exists())
Expand Down Expand Up @@ -399,7 +399,7 @@ def test_patch_agreement_with_attachment_as_pk(self):
)

self.assertEqual(status_code, status.HTTP_200_OK)
self.assertTrue(response["attachment"].endswith(attachment.file.url))
self.assertTrue(response["attachment"].endswith(attachment.file_link))
attachment_update = Attachment.objects.get(pk=attachment.pk)
self.assertEqual(attachment_update.content_object, agreement)
self.assertEqual(attachment_update.file_type, self.file_type_agreement)
Expand Down Expand Up @@ -450,7 +450,7 @@ def test_patch_agreement_replace_attachment(self):
status_code, response = self.run_request(agreement.pk)
self.assertEqual(status_code, status.HTTP_200_OK)
self.assertTrue(
response["attachment"].endswith(attachment_current.file.url)
response["attachment"].endswith(attachment_current.file_link)
)

data = {
Expand All @@ -464,7 +464,7 @@ def test_patch_agreement_replace_attachment(self):

self.assertEqual(status_code, status.HTTP_200_OK)
self.assertTrue(
response["attachment"].endswith(attachment_new.file.url)
response["attachment"].endswith(attachment_new.file_link)
)
agreement_updated = Agreement.objects.get(pk=agreement.pk)
self.assertEqual(agreement_updated.attachment.last(), attachment_new)
Expand Down
4 changes: 2 additions & 2 deletions src/etools/applications/partners/tests/test_api_partners.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ def test_patch_with_core_values_assessment_attachment(self):
self.assertEqual(len(data["core_values_assessments"]), 1)
self.assertEqual(
data["core_values_assessments"][0]["attachment"],
attachment.file.url
attachment.file_link
)

def test_patch_with_assessment_attachment(self):
Expand Down Expand Up @@ -234,7 +234,7 @@ def test_patch_with_assessment_attachment(self):
self.assertEqual(len(data["assessments"]), 1)
self.assertEqual(
data["assessments"][0]["report_attachment"],
attachment.file.url
attachment.file_link
)

def test_add_planned_visits(self):
Expand Down
16 changes: 16 additions & 0 deletions src/etools/applications/psea/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import connection, models
from django.db.models import Sum
from django.urls import reverse
Expand Down Expand Up @@ -425,6 +426,18 @@ def transition_to_rejected_invalid(self):
def transition_to_cancelled_invalid(self):
"""Allowed to move to cancelled status, except from submitted/final"""

def get_related_third_party_users(self):
if self.assessor.assessor_type == Assessor.TYPE_EXTERNAL:
return get_user_model().objects.filter(pk=self.assessor.user_id)
elif self.assessor.assessor_type == Assessor.TYPE_VENDOR:
if self.assessor.auditor_firm:
return self.assessor.auditor_firm_staff.filter(
realms__organization=self.assessor.auditor_firm.organization,
realms__is_active=True,
)

return get_user_model().objects.none()


class AssessmentStatusHistory(TimeStampedModel):
assessment = models.ForeignKey(
Expand Down Expand Up @@ -513,6 +526,9 @@ def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.assessment.update_rating()

def get_related_third_party_users(self):
return self.assessment.get_related_third_party_users()


class AnswerEvidence(TimeStampedModel):
answer = models.ForeignKey(
Expand Down
13 changes: 13 additions & 0 deletions src/etools/applications/tpm/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import itertools

from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import connection, models
from django.utils import timezone
from django.utils.encoding import force_str
Expand Down Expand Up @@ -355,6 +356,15 @@ def approve(self, mark_as_programmatic_visit=None, approval_comment=None, notify
def get_object_url(self, **kwargs):
return build_frontend_url('tpm', 'visits', self.id, 'details', **kwargs)

def get_related_third_party_users(self):
return get_user_model().objects.filter(
models.Q(pk__in=self.tpm_partner.staff_members.values_list('id')) |
models.Q(pk__in=self.tpm_partner_focal_points.values_list('id'))
).filter(
realms__organization=self.tpm_partner.organization,
realms__is_active=True,
)


class TPMVisitReportRejectComment(models.Model):
rejected_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Rejected At'))
Expand Down Expand Up @@ -457,6 +467,9 @@ def get_mail_context(self, user=None, include_visit=True):

return context

def get_related_third_party_users(self):
return self.tpm_visit.get_related_third_party_users()


class TPMActionPointManager(models.Manager):
def get_queryset(self):
Expand Down
Loading