Skip to content

Commit 3c673b6

Browse files
committed
add group types and clean up creation
1 parent a07f794 commit 3c673b6

File tree

4 files changed

+115
-154
lines changed

4 files changed

+115
-154
lines changed

backend/app/deps/authorization_deps.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from app.models.project import ProjectDB
21
from beanie import PydanticObjectId
32
from beanie.operators import Or
43
from fastapi import Depends, HTTPException
@@ -11,6 +10,7 @@
1110
from app.models.groups import GroupDB
1211
from app.models.listeners import EventListenerDB
1312
from app.models.metadata import MetadataDB
13+
from app.models.projects import ProjectDB
1414
from app.routers.authentication import get_admin, get_admin_mode
1515

1616

@@ -447,15 +447,14 @@ async def __call__(
447447
for u in group.users:
448448
if u.user.email == current_user:
449449
if group.project_id == project.id and u.editor and self.role == RoleType.EDITOR:
450-
# Editors of the
451450
return True
452451
elif self.role == RoleType.VIEWER:
453452
return True
454453
raise HTTPException(
455454
status_code=403,
456-
detail=f"User `{current_user} does not have `{self.role}` permission on group {group_id}",
455+
detail=f"User `{current_user} does not have `{self.role}` permission on project {project_id}",
457456
)
458-
raise HTTPException(status_code=404, detail=f"Group {group_id} not found")
457+
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
459458

460459

461460
class ListenerAuthorization:

backend/app/models/groups.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from enum import Enum
12
from typing import List, Optional
23

34
from beanie import Document
@@ -12,10 +13,20 @@ class Member(BaseModel):
1213
editor: bool = False
1314

1415

16+
class GroupType(str, Enum):
17+
"""Certain group types will be hidden from common lists. For example, 'project' type groups are associated with
18+
specific projects and used to track their membership; those groups are managed using the project interface, not
19+
the groups interface."""
20+
21+
STANDARD = "standard"
22+
PROJECT = "project"
23+
24+
1525
class GroupBase(BaseModel):
1626
name: str
1727
description: Optional[str]
1828
users: List[Member] = []
29+
type: GroupType = GroupType.STANDARD
1930
project_id: Optional[str] = None
2031

2132

backend/app/models/projects.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ class ProjectBase(BaseModel):
2424
created: datetime = Field(default_factory=datetime.utcnow)
2525
name: str
2626
description: Optional[str] = None
27-
# Individual users are added to the project's primary group
27+
# Individual users are added to one of the project's hidden groups (viewers or editors)
28+
viewers_group_id: Optional[str] = None
29+
editors_group_id: Optional[str] = None
2830
groups: List[ProjectMember] = []
2931
dataset_ids: List[PydanticObjectId] = []
3032

backend/app/routers/projects.py

Lines changed: 98 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@
66
from fastapi import APIRouter, Depends, HTTPException
77
from fastapi.security import HTTPBearer
88

9-
from app.deps.authorization_deps import Authorization
9+
from app.deps.authorization_deps import Authorization, ProjectAuthorization
1010
from app.keycloak_auth import get_current_user, get_user
1111
from app.models.datasets import DatasetDB
12-
from app.models.files import FileDB
13-
from app.models.folders import FolderDB
14-
from app.models.groups import GroupDB
12+
from app.models.groups import GroupDB, Member, GroupType
1513
from app.models.pages import Paged, _construct_page_metadata, _get_page_query
16-
from app.models.projects import ProjectMember, ProjectDB, ProjectIn, ProjectOut
14+
from app.models.projects import ProjectDB, ProjectIn, ProjectOut
1715
from app.models.users import UserDB, UserOut
1816

1917
router = APIRouter()
@@ -30,18 +28,29 @@ async def save_project(
3028
project = ProjectDB(**project_in.dict())
3129
await project.insert()
3230

33-
# Automatically create a group to go with this project
34-
group = GroupDB({
35-
"name": project.name,
36-
"description": f"Automatically created for members of {project.name} project.",
31+
# Automatically create viewer and editor groups to go with this project
32+
viewer_group = GroupDB({
33+
"name": project.name + " (Viewers)",
34+
"description": f"Automatically created for viewers of {project.name} project.",
35+
"users": [],
36+
"project_id": project.id,
37+
"type": GroupType.PROJECT
38+
}, creator=user.email)
39+
await viewer_group.insert()
40+
41+
editor_group = GroupDB({
42+
"name": project.name + " (Editors)",
43+
"description": f"Automatically created for editors of {project.name} project.",
3744
"users": [
3845
{"user": user, "editor": True}
3946
],
40-
"project_id": project.id
47+
"project_id": project.id,
48+
"type": GroupType.PROJECT
4149
}, creator=user.email)
42-
await group.insert()
50+
await editor_group.insert()
4351

44-
project.group_id = group.id
52+
project.viewers_group = viewer_group.id
53+
project.editors_group = editor_group.id
4554
await project.save()
4655

4756
return project.dict()
@@ -51,8 +60,8 @@ async def save_project(
5160
async def add_dataset(
5261
project_id: str,
5362
dataset_id: str,
54-
# allow_proj: bool = Depends(ProjectAuthorization("editor")),
55-
allow_ds: bool = Depends(Authorization("editor")),
63+
allow_proj: bool = Depends(ProjectAuthorization("editor")),
64+
allow_ds: bool = Depends(Authorization("viewer")),
5665
):
5766
if (project := await ProjectDB.get(PydanticObjectId(project_id))) is not None:
5867
if (dataset := await DatasetDB.get(PydanticObjectId(dataset_id))) is not None:
@@ -93,118 +102,6 @@ async def remove_dataset(
93102
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
94103

95104

96-
@router.post("/{project_id}/add_folder/{folder_id}", response_model=ProjectOut)
97-
async def add_folder(
98-
project_id: str,
99-
folder_id: str,
100-
):
101-
if (
102-
project := await ProjectDB.find_one(
103-
Or(
104-
ProjectDB.id == PydanticObjectId(project_id),
105-
)
106-
)
107-
) is not None:
108-
if (
109-
folder := await FolderDB.find_one(
110-
Or(
111-
FolderDB.id == FolderDB(PydanticObjectId(folder_id)),
112-
)
113-
)
114-
) is not None:
115-
if folder_id not in project.folder_ids:
116-
project.folder_ids.append(PydanticObjectId(folder_id))
117-
await project.replace()
118-
return project.dict()
119-
raise HTTPException(status_code=404, detail=f"Folder {folder_id} not found")
120-
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
121-
122-
123-
@router.post("/{project_id}/remove_folder/{folder_id}", response_model=ProjectOut)
124-
async def remove_folder(
125-
project_id: str,
126-
folder_id: str,
127-
):
128-
if (
129-
project := await ProjectDB.find_one(
130-
Or(
131-
ProjectDB.id == PydanticObjectId(project_id),
132-
)
133-
)
134-
) is not None:
135-
if (
136-
folder := await FolderDB.find_one(
137-
Or(
138-
FolderDB.id == FolderDB(PydanticObjectId(folder_id)),
139-
)
140-
)
141-
) is not None:
142-
if folder_id in project.folder_ids:
143-
project.folder_ids.remove(PydanticObjectId(folder_id))
144-
await project.replace()
145-
return project.dict()
146-
else:
147-
return project.dict()
148-
raise HTTPException(status_code=404, detail=f"Folder {folder_id} not found")
149-
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
150-
151-
152-
@router.post("/{project_id}/add_file/{file_id}", response_model=ProjectOut)
153-
async def add_file(
154-
project_id: str,
155-
file_id: str,
156-
):
157-
if (
158-
project := await ProjectDB.find_one(
159-
Or(
160-
ProjectDB.id == PydanticObjectId(project_id),
161-
)
162-
)
163-
) is not None:
164-
if (
165-
file := await FolderDB.find_one(
166-
Or(
167-
FileDB.id == FileDB(PydanticObjectId(file_id)),
168-
)
169-
)
170-
) is not None:
171-
if file_id not in project.file_ids:
172-
project.file_ids.append(PydanticObjectId(file_id))
173-
await project.replace()
174-
return project.dict()
175-
raise HTTPException(status_code=404, detail=f"File {file_id} not found")
176-
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
177-
178-
179-
@router.post("/{project_id}/remove_file/{file_id}", response_model=ProjectOut)
180-
async def remove_file(
181-
project_id: str,
182-
file_id: str,
183-
):
184-
if (
185-
project := await ProjectDB.find_one(
186-
Or(
187-
ProjectDB.id == PydanticObjectId(project_id),
188-
)
189-
)
190-
) is not None:
191-
if (
192-
file := await FolderDB.find_one(
193-
Or(
194-
FileDB.id == FileDB(PydanticObjectId(file_id)),
195-
)
196-
)
197-
) is not None:
198-
if file_id in project.file_ids:
199-
project.file_ids.remove(PydanticObjectId(file_id))
200-
await project.replace()
201-
return project.dict()
202-
else:
203-
return project.dict()
204-
raise HTTPException(status_code=404, detail=f"File {file_id} not found")
205-
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
206-
207-
208105
@router.get("", response_model=Paged)
209106
async def get_projects(
210107
user_id=Depends(get_user),
@@ -263,22 +160,59 @@ async def delete_project(
263160
async def add_member(
264161
project_id: str,
265162
username: str,
266-
role: Optional[str] = None,
163+
role: Optional[str] = "viewer",
164+
allow: bool = Depends(ProjectAuthorization("editor")),
267165
):
268-
"""Add a new user to a group."""
166+
"""Add a new user to the project individually - this is routed to one of the project's hidden groups."""
269167
if (user := await UserDB.find_one(UserDB.email == username)) is not None:
270-
new_member = ProjectMember(user=UserOut(**user.dict()))
168+
# Add to viewers group if role is none, otherwise add to appropriate group
169+
new_member = Member(user=UserOut(**user.dict()), editor=(role == "editor"))
271170
if (project := await ProjectDB.get(PydanticObjectId(project_id))) is not None:
272-
found_already = False
273-
for u in project.users:
274-
if u.user.email == username:
275-
found_already = True
276-
break
277-
if not found_already:
278-
# If user is already in the group, skip directly to returning the group
279-
# else add role and attach this member
280-
project.users.append(new_member)
281-
await project.replace()
171+
viewers_group = await GroupDB.get(PydanticObjectId(project.viewers_group_id))
172+
editors_group = await GroupDB.get(PydanticObjectId(project.editors_group_id))
173+
174+
if role == "viewer":
175+
found_in_viewers = False
176+
for u in viewers_group.users:
177+
if u.user.email == username:
178+
found_in_viewers = True
179+
break
180+
if not found_in_viewers:
181+
viewers_group.users.append(new_member)
182+
await viewers_group.save()
183+
184+
found_in_editors = False
185+
clean_users = []
186+
for u in editors_group.users:
187+
if u.user.email == username:
188+
found_in_editors = True
189+
else:
190+
clean_users.append(u)
191+
if found_in_editors:
192+
editors_group.users = clean_users
193+
await editors_group.save()
194+
195+
elif role == "editor":
196+
found_in_editors = False
197+
for u in editors_group.users:
198+
if u.user.email == username:
199+
found_in_editors = True
200+
break
201+
if not found_in_editors:
202+
editors_group.users.append(new_member)
203+
await editors_group.save()
204+
205+
found_in_viewers = False
206+
clean_users = []
207+
for u in viewers_group.users:
208+
if u.user.email == username:
209+
found_in_viewers = True
210+
else:
211+
clean_users.append(u)
212+
if found_in_viewers:
213+
viewers_group.users = clean_users
214+
await viewers_group.save()
215+
282216
return project.dict()
283217
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
284218
raise HTTPException(status_code=404, detail=f"User {username} not found")
@@ -288,20 +222,35 @@ async def add_member(
288222
async def remove_member(
289223
project_id: str,
290224
username: str,
225+
allow: bool = Depends(ProjectAuthorization("editor")),
291226
):
292227
"""Remove a user from a group."""
293228

294229
if (project := await ProjectDB.get(PydanticObjectId(project_id))) is not None:
295-
# Is the user actually in the group already?
296-
found_user = None
297-
for u in project.users:
230+
viewers_group = await GroupDB.get(PydanticObjectId(project.viewers_group_id))
231+
editors_group = await GroupDB.get(PydanticObjectId(project.editors_group_id))
232+
233+
found_in_editors = False
234+
clean_users = []
235+
for u in editors_group.users:
236+
if u.user.email == username:
237+
found_in_editors = True
238+
else:
239+
clean_users.append(u)
240+
if found_in_editors:
241+
editors_group.users = clean_users
242+
await editors_group.save()
243+
244+
found_in_viewers = False
245+
clean_users = []
246+
for u in viewers_group.users:
298247
if u.user.email == username:
299-
found_user = u
300-
if not found_user:
301-
# TODO: User wasn't in group, should this throw an error instead? Either way, the user is removed...
302-
return project
303-
# Update group itself
304-
project.users.remove(found_user)
305-
await project.replace()
248+
found_in_viewers = True
249+
else:
250+
clean_users.append(u)
251+
if found_in_viewers:
252+
viewers_group.users = clean_users
253+
await viewers_group.save()
254+
306255
return project.dict()
307256
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")

0 commit comments

Comments
 (0)