Skip to content

Commit d4ec346

Browse files
authored
Add access_uploader_capability and implement its usage (#705)
1 parent 7183110 commit d4ec346

File tree

10 files changed

+117
-10
lines changed

10 files changed

+117
-10
lines changed

docs/user-guide/9-Sharing-objects.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,12 @@ Each capability has its own name and scope:
173173
*
174174
**sharing_with_all - Can share objects with all groups in system**
175175

176-
Implies the access to the list of all group names, but without access to the membership information and management features. Allows to share object with arbitrary group in MWDB.
176+
Implies the access to the list of all group names, but without access to the membership information and management features. Allows to share object with arbitrary group in MWDB. It also allows the user to view full history of sharing an object (if the user has access to the object).
177+
178+
*
179+
**access_uploader_info - Can view who uploaded object and filter by uploader**
180+
181+
Can view who uploaded object and filter by uploader. Without this capability users can filter by / see only users in their workspaces.
177182

178183
*
179184
**adding_tags - Can add tags**

mwdb/core/capabilities.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ class Capabilities(object):
77
access_all_objects = "access_all_objects"
88
# Can share objects with all groups, have access to complete list of groups
99
sharing_with_all = "sharing_with_all"
10+
# Can view who uploaded object and filter by uploader
11+
access_uploader_info = "access_uploader_info"
1012
# Can add tags
1113
adding_tags = "adding_tags"
1214
# Can remove tags

mwdb/core/search/fields.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,17 @@ def _get_condition(
388388
.join(Member.group)
389389
.filter(Group.name == value)
390390
).all()
391+
elif g.auth_user.has_rights(Capabilities.access_uploader_info):
392+
uploaders = (
393+
db.session.query(User)
394+
.join(User.memberships)
395+
.join(Member.group)
396+
.filter(Group.name == value)
397+
).all()
398+
# Regular users can see only uploads to its own groups
399+
condition = and_(
400+
condition, g.auth_user.is_member(ObjectPermission.group_id)
401+
)
391402
else:
392403
uploaders = (
393404
db.session.query(User)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Add access uploader info capability
2+
3+
Revision ID: 25ea40a798ac
4+
Revises: c7c72fd7fac5
5+
Create Date: 2022-11-03 09:56:45.546628
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
11+
# revision identifiers, used by Alembic.
12+
revision = "25ea40a798ac"
13+
down_revision = "c7c72fd7fac5"
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade():
19+
op.execute(
20+
"""
21+
UPDATE public.group
22+
SET capabilities = array_append(capabilities, 'access_uploader_info')
23+
WHERE name='public' OR array_position(capabilities, 'manage_users') IS NOT NULL;
24+
"""
25+
)
26+
27+
28+
def downgrade():
29+
op.execute(
30+
"""
31+
UPDATE public.group
32+
SET capabilities = array_remove(capabilities, 'access_uploader_info');
33+
"""
34+
)

mwdb/model/object.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -907,7 +907,7 @@ def get_shares(self):
907907
shares = (
908908
db.session.query(ObjectPermission)
909909
.filter(permission_filter)
910-
.order_by(ObjectPermission.access_time.desc())
910+
.order_by(ObjectPermission.access_time.asc())
911911
).all()
912912
return shares
913913

mwdb/resources/share.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from flask import g, request
22
from flask_restful import Resource
3-
from werkzeug.exceptions import NotFound
3+
from werkzeug.exceptions import Forbidden, NotFound
44

55
from mwdb.core.capabilities import Capabilities
66
from mwdb.core.rate_limit import rate_limited_resource
7-
from mwdb.model import Group, User, db
7+
from mwdb.model import Group, Member, User, db
88
from mwdb.model.object import AccessType
99
from mwdb.schema.share import (
1010
ShareGroupListResponseSchema,
@@ -118,14 +118,39 @@ def get(self, type, identifier):
118118
)
119119

120120
group_names = [group[0] for group in groups.all()]
121+
# User cannot share object with themself
122+
if g.auth_user.login in group_names:
123+
group_names.remove(g.auth_user.login)
121124

122125
db_object = access_object(type, identifier)
123126
if db_object is None:
124127
raise NotFound("Object not found")
125128

126129
shares = db_object.get_shares()
127-
schema = ShareInfoResponseSchema()
128-
return schema.dump({"groups": group_names, "shares": shares})
130+
131+
if g.auth_user.has_rights(Capabilities.access_uploader_info):
132+
schema = ShareInfoResponseSchema()
133+
return schema.dump({"groups": group_names, "shares": shares})
134+
else:
135+
# list of user_ids who are in a common workspace with auth_user
136+
users = (
137+
db.session.query(User)
138+
.join(User.memberships)
139+
.join(Member.group)
140+
.filter(g.auth_user.is_member(Group.id))
141+
.filter(Group.workspace.is_(True))
142+
).all()
143+
visible_users = [u.login for u in users]
144+
145+
schema = ShareInfoResponseSchema()
146+
response = schema.dump({"groups": group_names, "shares": shares})
147+
148+
for share in response["shares"]:
149+
if "related_user_login" in share.keys():
150+
if share["related_user_login"] not in visible_users:
151+
share["related_user_login"] = "$hidden"
152+
153+
return response
129154

130155
@requires_authorization
131156
def put(self, type, identifier):
@@ -166,6 +191,9 @@ def put(self, type, identifier):
166191
schema: ShareListResponseSchema
167192
400:
168193
description: When request body is invalid.
194+
403:
195+
description: |
196+
When user tries to share object with themself.
169197
404:
170198
description: |
171199
When object or group doesn't exist
@@ -190,6 +218,9 @@ def put(self, type, identifier):
190218
if group is None or group.pending_group:
191219
raise NotFound(f"Group {group_name} doesn't exist")
192220

221+
if group.name == g.auth_user.login:
222+
raise Forbidden("You cannot share object with yourself")
223+
193224
db_object.give_access(group.id, AccessType.SHARED, db_object, g.auth_user)
194225

195226
shares = db_object.get_shares()

mwdb/web/src/commons/auth/capabilities.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const Capability = {
55
shareQueriedObjects: "share_queried_objects",
66
accessAllObjects: "access_all_objects",
77
sharingWithAll: "sharing_with_all",
8+
accessUploaderInfo: "access_uploader_info",
89
addingTags: "adding_tags",
910
removingTags: "removing_tags",
1011
addingComments: "adding_comments",
@@ -33,6 +34,8 @@ export let capabilitiesList = {
3334
[Capability.accessAllObjects]:
3435
"Has access to all new uploaded objects into system",
3536
[Capability.sharingWithAll]: "Can share objects with all groups in system",
37+
[Capability.accessUploaderInfo]:
38+
"Can view who uploaded object and filter by uploader",
3639
[Capability.addingTags]: "Can add tags",
3740
[Capability.removingTags]: "Can remove tags",
3841
[Capability.addingComments]: "Can add comments",

mwdb/web/src/components/ShowObject/Views/SharesBox.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,18 @@ function ShareGroupItem({ reason, shares }) {
6262
<thead>
6363
<tr>
6464
<th colSpan="2">
65-
<ShareReasonString {...reason} />
65+
{(() => {
66+
if (reason.relatedUserLogin !== "$hidden") {
67+
return <ShareReasonString {...reason} />;
68+
} else {
69+
return (
70+
<span class="text-muted">
71+
You do not have permission to see the
72+
uploader.
73+
</span>
74+
);
75+
}
76+
})()}
6677
</th>
6778
</tr>
6879
</thead>

tests/backend/test_multigroup_groups.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ def test_multigroup_sharing(admin_session):
148148
assert set(shares["groups"]) == {
149149
"public",
150150
"registered",
151-
Alice.identity,
152151
Workgroup.identity,
153152
}
154153
assert set(gr["group_name"] for gr in shares["shares"]) == {
@@ -157,7 +156,7 @@ def test_multigroup_sharing(admin_session):
157156
}
158157

159158
shares = Bob.session.get_shares(File.dhash)
160-
assert set(shares["groups"]) == {"public", "registered", Bob.identity}
159+
assert set(shares["groups"]) == {"public", "registered"}
161160
assert set(gr["group_name"] for gr in shares["shares"]) == {Bob.identity}
162161

163162
shares = Joe.session.get_shares(File.dhash)
@@ -166,7 +165,6 @@ def test_multigroup_sharing(admin_session):
166165
"registered",
167166
Alice.identity,
168167
Bob.identity,
169-
Joe.identity,
170168
Workgroup.identity,
171169
}
172170
assert set(shares["groups"]).intersection(groups) == groups

tests/backend/test_search.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,9 @@ def test_uploader_query(admin_session):
604604

605605
Alice = testCase.new_user("Alice")
606606
Bob = testCase.new_user("Bob")
607+
Charlie = testCase.new_user("Charlie", capabilities=["access_uploader_info"])
608+
# David does not have capability and not in workgroup
609+
David = testCase.new_user("David")
607610

608611
Workgroup = testCase.new_group("Workgroup")
609612

@@ -642,6 +645,15 @@ def test_uploader_query(admin_session):
642645
Bob.session.search(f"uploader:{Alice.identity}")
643646
]
644647
assert sorted(results) == sorted([FileC.dhash])
648+
# Charlie looks for files uploaded by Alice
649+
results = [
650+
result["id"] for result in
651+
Charlie.session.search(f"uploader:{Alice.identity}")
652+
]
653+
assert sorted(results) == sorted([FileC.dhash])
654+
# David looks for files uploaded by Alice
655+
with ShouldRaise(status_code=400):
656+
results = David.session.search(f"uploader:{Alice.identity}")
645657

646658

647659
def test_search_multi(admin_session):

0 commit comments

Comments
 (0)