Skip to content

Commit e84ea9e

Browse files
committed
Decouple app collaboration from visibility
1 parent 4769746 commit e84ea9e

File tree

8 files changed

+294
-99
lines changed

8 files changed

+294
-99
lines changed

apps/apis.py

+57-46
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,9 @@ def get(self, request, uid=None):
111111
app = get_object_or_404(
112112
App,
113113
Q(uuid=uuid.UUID(uid), owner=request.user) |
114-
Q(uuid=uuid.UUID(uid), accessible_by__contains=[
115-
request.user.email], visibility=AppVisibility.PRIVATE, is_published=True),
114+
Q(uuid=uuid.UUID(uid), read_accessible_by__contains=[
115+
request.user.email], is_published=True) | Q(uuid=uuid.UUID(uid), write_accessible_by__contains=[
116+
request.user.email], is_published=True),
116117
)
117118
serializer = AppSerializer(
118119
instance=app, fields=fields, request_user=request.user,
@@ -131,9 +132,8 @@ def getShared(self, request):
131132
fields = fields.split(',')
132133

133134
queryset = App.objects.all().filter(
134-
accessible_by__contains=[
135-
request.user.email,
136-
], visibility=AppVisibility.PRIVATE, is_published=True,
135+
Q(read_accessible_by__contains=[request.user.email,]) |
136+
Q(write_accessible_by__contains=[request.user.email,]), is_published=True,
137137
).order_by('-last_updated_at')
138138
serializer = AppSerializer(
139139
queryset, many=True, fields=fields, request_user=request.user,
@@ -149,8 +149,8 @@ def versions(self, request, uid=None, version=None):
149149
app = get_object_or_404(
150150
App,
151151
Q(uuid=uuid.UUID(uid), owner=request.user) |
152-
Q(uuid=uuid.UUID(uid), accessible_by__contains=[
153-
request.user.email], visibility=AppVisibility.PRIVATE, is_published=True),
152+
Q(uuid=uuid.UUID(uid), write_accessible_by__contains=[
153+
request.user.email], is_published=True),
154154
)
155155

156156
if version:
@@ -182,8 +182,8 @@ def getByPublishedUUID(self, request, published_uuid):
182182
(app.visibility == AppVisibility.PUBLIC or app.visibility == AppVisibility.UNLISTED) or \
183183
(
184184
request.user.is_authenticated and ((app.visibility == AppVisibility.ORGANIZATION and Profile.objects.get(user=app.owner).organization == Profile.objects.get(user=request.user).organization) or
185-
(app.visibility == AppVisibility.PRIVATE and request.user.email in app.accessible_by))
186-
):
185+
(request.user.email in app.read_accessible_by or request.user.email in app.write_accessible_by))
186+
):
187187
serializer = AppSerializer(
188188
instance=app, request_user=request.user,
189189
)
@@ -279,40 +279,52 @@ def publish(self, request, uid):
279279
app.visibility = AppVisibility.ORGANIZATION
280280
elif request.data['visibility'] == 0 and (flag_enabled('CAN_PUBLISH_PRIVATE_APPS', request=request) or app.visibility == AppVisibility.PRIVATE):
281281
app.visibility = AppVisibility.PRIVATE
282-
if 'accessible_by' in request.data:
283-
# Filter out invalid email addresses from accessible_by
284-
valid_emails = []
285-
for email in request.data['accessible_by']:
286-
try:
287-
validate_email(email)
288-
valid_emails.append(email)
289-
except ValidationError:
290-
pass
291-
292-
# Only allow a maximum of 20 users to be shared with. Trim the list if it is more than 20
293-
if len(valid_emails) > 20:
294-
valid_emails = valid_emails[:20]
295-
296-
new_emails = list(
297-
set(valid_emails) -
298-
set(app.accessible_by),
299-
)
300-
app.accessible_by = valid_emails
301-
app.access_permission = request.data[
302-
'access_permission'
303-
] if 'access_permission' in request.data else AppAccessPermission.READ
304-
305-
# Send email to new users
306-
# TODO: Use multisend to send emails in bulk
307-
for new_email in new_emails:
308-
email_template_cls = EmailTemplateFactory.get_template_by_name(
309-
'app_shared'
310-
)
311-
share_email = email_template_cls(
312-
uuid=app.uuid, published_uuid=app.published_uuid, app_name=app.name, owner_first_name=app.owner.first_name, owner_email=app.owner.email, can_edit=app.access_permission == AppAccessPermission.WRITE, share_to=new_email
313-
)
314-
share_email_sender = EmailSender(share_email)
315-
share_email_sender.send()
282+
283+
if flag_enabled('CAN_PUBLISH_PRIVATE_APPS', request=request) or app.visibility == AppVisibility.PRIVATE:
284+
new_emails = []
285+
old_read_accessible_by = app.read_accessible_by or []
286+
old_write_accessible_by = app.write_accessible_by or []
287+
if 'read_accessible_by' in request.data:
288+
# Filter out invalid email addresses from read_accessible_by
289+
valid_emails = []
290+
for email in request.data['read_accessible_by']:
291+
try:
292+
validate_email(email)
293+
valid_emails.append(email)
294+
except ValidationError:
295+
pass
296+
297+
app.read_accessible_by = valid_emails[:20]
298+
299+
if 'write_accessible_by' in request.data:
300+
# Filter out invalid email addresses from write_accessible_by
301+
valid_emails = []
302+
for email in request.data['write_accessible_by']:
303+
try:
304+
validate_email(email)
305+
valid_emails.append(email)
306+
except ValidationError:
307+
pass
308+
309+
app.write_accessible_by = valid_emails[:20]
310+
311+
new_emails = list(
312+
set(app.read_accessible_by).union(set(app.write_accessible_by)) -
313+
set(old_read_accessible_by).union(
314+
set(old_write_accessible_by)),
315+
)
316+
317+
# Send email to new users
318+
# TODO: Use multisend to send emails in bulk
319+
for new_email in new_emails:
320+
email_template_cls = EmailTemplateFactory.get_template_by_name(
321+
'app_shared'
322+
)
323+
share_email = email_template_cls(
324+
uuid=app.uuid, published_uuid=app.published_uuid, app_name=app.name, owner_first_name=app.owner.first_name, owner_email=app.owner.email, can_edit=app.access_permission == AppAccessPermission.WRITE, share_to=new_email
325+
)
326+
share_email_sender = EmailSender(share_email)
327+
share_email_sender.send()
316328

317329
app_newly_published = not app.is_published
318330
app.is_published = True
@@ -379,9 +391,8 @@ def patch(self, request, uid):
379391
app = get_object_or_404(App, uuid=uuid.UUID(uid))
380392
app_owner_profile = get_object_or_404(Profile, user=app.owner)
381393
if app.owner != request.user and not (
382-
app.visibility == AppVisibility.PRIVATE
383-
and app.access_permission == AppAccessPermission.WRITE
384-
and request.user.email in app.accessible_by
394+
app.is_published == True
395+
and request.user.email in app.write_accessible_by
385396
):
386397
return DRFResponse(status=403)
387398

apps/handlers/app_runnner.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def _is_app_accessible(self):
109109
if self.app.visibility == AppVisibility.ORGANIZATION and self.app_owner_profile.organization != Profile.objects.get(user=self.app_run_request_user).organization:
110110
raise Exception('App not found')
111111

112-
if self.app.visibility == AppVisibility.PRIVATE and self.app_run_request_user != self.app.owner and self.app_run_request_user.email not in self.app.accessible_by:
112+
if self.app.visibility == AppVisibility.PRIVATE and self.app_run_request_user != self.app.owner and self.app_run_request_user.email not in self.app.read_accessible_by and self.app_run_request_user.email not in self.app.write_accessible_by:
113113
raise Exception('App not found')
114114

115115
def _get_processor_actor_configs(self):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 4.2.1 on 2023-09-07 03:18
2+
3+
import django.contrib.postgres.fields
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('apps', '0005_appdata_version'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='app',
16+
name='read_accessible_by',
17+
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=320), blank=True, default=list, help_text='List of user emails or domains who can access the app', size=None),
18+
),
19+
migrations.AddField(
20+
model_name='app',
21+
name='write_accessible_by',
22+
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=320), blank=True, default=list, help_text='List of user emails or domains who can modify the app', size=None),
23+
),
24+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Generated by Django 4.2.1 on 2023-09-07 06:57
2+
3+
from django.db import migrations
4+
5+
from apps.models import AppAccessPermission
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
def create_permissions_from_accessible_by(apps, schema_editor):
11+
App = apps.get_model('apps', 'App')
12+
for app in App.objects.all():
13+
if app.accessible_by and len(app.accessible_by) > 0:
14+
if app.access_permission == AppAccessPermission.READ and not app.read_accessible_by:
15+
app.read_accessible_by = app.accessible_by
16+
elif app.access_permission == AppAccessPermission.WRITE and not app.write_accessible_by:
17+
app.write_accessible_by = app.accessible_by
18+
app.save()
19+
20+
dependencies = [
21+
('apps', '0006_app_read_accessible_by_app_write_accessible_by'),
22+
]
23+
24+
operations = [
25+
migrations.RunPython(
26+
create_permissions_from_accessible_by, migrations.RunPython.noop),
27+
]

apps/models.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,12 @@ class App(models.Model):
187187
accessible_by = ArrayField(
188188
models.CharField(max_length=320), default=list, help_text='List of user emails or domains who can access the app', blank=True,
189189
)
190+
read_accessible_by = ArrayField(
191+
models.CharField(max_length=320), default=list, help_text='List of user emails or domains who can access the app', blank=True,
192+
)
193+
write_accessible_by = ArrayField(
194+
models.CharField(max_length=320), default=list, help_text='List of user emails or domains who can modify the app', blank=True,
195+
)
190196
access_permission = models.PositiveSmallIntegerField(
191197
default=AppAccessPermission.READ, choices=AppAccessPermission.choices, help_text='Permission for users who can access the app',
192198
)
@@ -235,7 +241,7 @@ def has_write_permission(self, user):
235241
if not user or not user.is_authenticated:
236242
return False
237243

238-
return self.owner == user or (self.access_permission == AppAccessPermission.WRITE and user.email in self.accessible_by)
244+
return self.owner == user or (self.is_published == True and user.email in self.write_accessible_by)
239245

240246
def __str__(self) -> str:
241247
return self.name + ' - ' + self.owner.username

apps/serializers.py

+9
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ class Meta:
109109
web_config = serializers.SerializerMethodField()
110110
access_permission = serializers.SerializerMethodField()
111111
accessible_by = serializers.SerializerMethodField()
112+
read_accessible_by = serializers.SerializerMethodField()
113+
write_accessible_by = serializers.SerializerMethodField()
112114
last_modified_by_email = serializers.SerializerMethodField()
113115
template = serializers.SerializerMethodField()
114116
visibility = serializers.SerializerMethodField()
@@ -202,6 +204,12 @@ def get_access_permission(self, obj):
202204
def get_accessible_by(self, obj):
203205
return obj.accessible_by if obj.has_write_permission(self._request_user) else None
204206

207+
def get_read_accessible_by(self, obj):
208+
return obj.read_accessible_by if obj.has_write_permission(self._request_user) else None
209+
210+
def get_write_accessible_by(self, obj):
211+
return obj.write_accessible_by if obj.has_write_permission(self._request_user) else None
212+
205213
def get_last_modified_by_email(self, obj):
206214
return obj.last_modified_by.email if (obj.last_modified_by and obj.has_write_permission(self._request_user)) else None
207215

@@ -229,6 +237,7 @@ class Meta:
229237
'logo', 'is_shareable', 'has_footer', 'domain', 'visibility', 'accessible_by',
230238
'access_permission', 'last_modified_by_email', 'owner_email', 'web_config',
231239
'slack_config', 'discord_config', 'app_type_name', 'processors', 'template',
240+
'read_accessible_by', 'write_accessible_by'
232241
]
233242

234243

0 commit comments

Comments
 (0)