Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ca2a1a7
add endpoint to assign user role to manage group
longshuicy Apr 3, 2023
bccb40d
should be or logic?
longshuicy Apr 3, 2023
f2c7385
codegen
longshuicy Apr 3, 2023
3e95b22
fix allow dependencies
longshuicy Apr 3, 2023
1be5ffe
revert
longshuicy Apr 3, 2023
f7efac9
role chip
longshuicy Apr 3, 2023
aaec35b
backend endpoints ready
longshuicy Apr 3, 2023
a6a0a51
assign group member
longshuicy Apr 4, 2023
aedb725
add edit button
longshuicy Apr 4, 2023
4938efc
ui features are there
longshuicy Apr 5, 2023
d94fa43
endpoint logic
longshuicy Apr 5, 2023
ff8ce3b
enforce the correct response model
longshuicy Apr 5, 2023
c9177e3
add an auth wrapper
longshuicy Apr 5, 2023
66b413b
fix css
longshuicy Apr 5, 2023
319792d
Merge branch 'main' into 406-group-management-ui-improvements
longshuicy Apr 5, 2023
b95f341
fix bug
longshuicy Apr 5, 2023
7867f5b
add pagination back
longshuicy Apr 5, 2023
7c64b8f
add delete
longshuicy Apr 5, 2023
fa3c2fb
half way through adding delete group
longshuicy Apr 6, 2023
c8e4b18
css of delete group works
longshuicy Apr 6, 2023
0627d85
fix more typo
longshuicy Apr 6, 2023
9ecd047
wire in search group redux
longshuicy Apr 6, 2023
ae09109
frontend search done; backend doesn't work
longshuicy Apr 6, 2023
2940bb1
modify search endpoint to search both name and description
longshuicy Apr 6, 2023
b000f33
match search term as a path parameter
longshuicy Apr 6, 2023
ba93291
properly wire in search
longshuicy Apr 6, 2023
886c42d
fix search; will need to do the same for listener search
longshuicy Apr 6, 2023
29e8c2a
align the pagination button
longshuicy Apr 6, 2023
b52d753
linting
longshuicy Apr 6, 2023
b14aded
add pytests
longshuicy Apr 6, 2023
3cf86c3
add pytest and narrow down the search result to reflect role
longshuicy Apr 6, 2023
46d6bad
tightening the loose end
longshuicy Apr 6, 2023
b9f4840
Merge branch 'main' into 406-group-management-ui-improvements
longshuicy Apr 6, 2023
ef3a5cb
revert back to and
longshuicy Apr 6, 2023
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
2 changes: 1 addition & 1 deletion backend/app/deps/authorization_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ async def get_role_by_group(
if group_out.creator == current_user:
# Creator can do everything
return RoleType.OWNER
for u in group.users:
for u in group_out.users:
if u.user.email == current_user:
if u.editor:
return RoleType.EDITOR
Expand Down
125 changes: 99 additions & 26 deletions backend/app/routers/groups.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import re
from datetime import datetime
from http.client import HTTPException
from fastapi import HTTPException, Depends, APIRouter
from bson.objectid import ObjectId
from fastapi.params import Depends
from fastapi.routing import APIRouter
from pymongo import DESCENDING
from pymongo.mongo_client import MongoClient
from typing import List
from typing import List, Optional

from app import dependencies
from app.deps.authorization_deps import AuthorizationDB, GroupAuthorization
from app.keycloak_auth import get_current_user, get_user
from app.models.authorization import RoleType
from app.models.groups import GroupOut, GroupIn, GroupDB, GroupBase, Member
from app.models.users import UserOut

Expand Down Expand Up @@ -68,6 +68,48 @@ async def get_groups(
return groups


@router.get("/search/{search_term}", response_model=List[GroupOut])
async def search_group(
search_term: str,
user_id=Depends(get_user),
db: MongoClient = Depends(dependencies.get_db),
skip: int = 0,
limit: int = 10,
):
"""Search all groups in the db based on text.

Arguments:
text -- any text matching name or description
skip -- number of initial records to skip (i.e. for pagination)
limit -- restrict number of records to be returned (i.e. for pagination)
"""

# Check all connection and abort if any one of them is not available
if db is None:
raise HTTPException(status_code=503, detail="Service not available")
return

groups = []
query_regx = re.compile(search_term, re.IGNORECASE)
for doc in (
# user has to be the creator or member first; then apply search
await db["groups"]
.find(
{
"$and": [
{"$or": [{"creator": user_id}, {"users.user.email": user_id}]},
{"$or": [{"name": query_regx}, {"description": query_regx}]},
]
}
)
.skip(skip)
.limit(limit)
.to_list(length=limit)
):
groups.append(GroupOut.from_mongo(doc))
return groups


@router.get("/{group_id}", response_model=GroupOut)
async def get_group(
group_id: str,
Expand Down Expand Up @@ -113,45 +155,30 @@ async def edit_group(
raise HTTPException(status_code=404, detail=f"Group {group_id} not found")


@router.delete("/{group_id}")
@router.delete("/{group_id}", response_model=GroupOut)
async def delete_group(
group_id: str,
db: MongoClient = Depends(dependencies.get_db),
allow: bool = Depends(GroupAuthorization("owner")),
):
if (group := await db["groups"].find_one({"_id": ObjectId(group_id)})) is not None:
if (
group_q := await db["groups"].find_one({"_id": ObjectId(group_id)})
) is not None:
await db["groups"].delete_one({"_id": ObjectId(group_id)})
return {"deleted": group_id}
return GroupDB.from_mongo(group_q)
else:
raise HTTPException(status_code=404, detail=f"Dataset {group_id} not found")


@router.get("/search/{search_term}")
async def search_group(
search_term: str, db: MongoClient = Depends(dependencies.get_db)
):
# Check all connection and abort if any one of them is not available
if db is None:
raise HTTPException(status_code=503, detail="Service not available")
return

groups = []
pattern = ".*" + search_term + ".*"
async for f in db["groups"].find({"name": {"$regex": pattern, "$options": "i"}}):
groups.append(GroupDB.from_mongo(f))

return groups


@router.post("/{group_id}/add/{username}")
@router.post("/{group_id}/add/{username}", response_model=GroupOut)
async def add_member(
group_id: str,
username: str,
role: Optional[str] = None,
db: MongoClient = Depends(dependencies.get_db),
allow: bool = Depends(GroupAuthorization("editor")),
):
"""Add a new user to a group."""

if (user_q := await db["users"].find_one({"email": username})) is not None:
new_member = Member(user=UserOut.from_mongo(user_q))
if (
Expand All @@ -165,6 +192,12 @@ async def add_member(
break
if not found_already:
# If user is already in the group, skip directly to returning the group
# else add role and attach this member

if role is not None and role == RoleType.EDITOR:
new_member.editor = True
else:
new_member.editor = False
group.users.append(new_member)
await db["groups"].replace_one(
{"_id": ObjectId(group_id)}, group.to_mongo()
Expand Down Expand Up @@ -213,3 +246,43 @@ async def remove_member(

return group
raise HTTPException(status_code=404, detail=f"Group {group_id} not found")


@router.put("/{group_id}/update/{username}", response_model=GroupOut)
async def update_member(
group_id: str,
username: str,
role: str,
db: MongoClient = Depends(dependencies.get_db),
allow: bool = Depends(GroupAuthorization("editor")),
):
"""Update user role."""
if (user_q := await db["users"].find_one({"email": username})) is not None:
if (
group_q := await db["groups"].find_one({"_id": ObjectId(group_id)})
) is not None:
group = GroupDB.from_mongo(group_q)
found_user = None
found_user_index = -1
for i, u in enumerate(group.users):
if u.user.email == username:
found_user = u.user
found_user_index = i
break
if found_user and found_user_index >= 0:
if role == RoleType.EDITOR:
updated_member = Member(user=found_user, editor=True)
else:
updated_member = Member(user=found_user, editor=False)
group.users[found_user_index] = updated_member
await db["groups"].replace_one(
{"_id": ObjectId(group_id)}, group.to_mongo()
)
else:
raise HTTPException(
status_code=404,
detail=f"User {username} does not belong to this group!",
)
return group
raise HTTPException(status_code=404, detail=f"Group {group_id} not found")
raise HTTPException(status_code=404, detail=f"User {username} not found")
48 changes: 45 additions & 3 deletions backend/app/tests/test_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@
create_user,
get_user_token,
)
from app.models.pyobjectid import PyObjectId
from app.models.authorization import AuthorizationDB


member_alt = {"user": user_alt, "editor": False}

Expand Down Expand Up @@ -42,6 +39,51 @@ def test_search_group(client: TestClient, headers: dict):
assert response.status_code == 200


def test_add_member_with_editor_role(client: TestClient, headers: dict):
new_group = create_group(client, headers)
group_id = new_group.get("id")

create_user(client, headers)
new_group["users"].append(member_alt)

response = client.post(
f"{settings.API_V2_STR}/groups/{group_id}/add/{member_alt['user']['email']}?role=editor",
headers=headers,
)

assert response.status_code == 200
assert response.json().get("id") is not None
for user in response.json().get("users"):
if user.get("user").get("email") == member_alt["user"]["email"]:
assert user.get("editor") is True


def test_assign_member_role(client: TestClient, headers: dict):
new_group = create_group(client, headers)
group_id = new_group.get("id")

create_user(client, headers)
new_group["users"].append(member_alt)

# create user as viewer
client.post(
f"{settings.API_V2_STR}/groups/{group_id}/add/{member_alt['user']['email']}",
headers=headers,
)

# assign as editor
response = client.put(
f"{settings.API_V2_STR}/groups/{group_id}/update/{member_alt['user']['email']}?role=editor",
headers=headers,
)

assert response.status_code == 200
assert response.json().get("id") is not None
for user in response.json().get("users"):
if user.get("user").get("email") == member_alt["user"]["email"]:
assert user.get("editor") is True


def test_member_permissions(client: TestClient, headers: dict):
new_group = create_group(client, headers)
group_id = new_group.get("id")
Expand Down
59 changes: 56 additions & 3 deletions frontend/src/actions/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,42 @@ export function fetchGroups(skip=0, limit=21){
};
}

export const DELETE_GROUP = "DELETE_GROUP";
export function deleteGroup(groupId){
return (dispatch) => {
return V2.GroupsService.deleteGroupApiV2GroupsGroupIdDelete(groupId)
.then(json => {
dispatch({
type: DELETE_GROUP,
about: json,
receivedAt: Date.now(),
});
})
.catch(reason => {
dispatch(handleErrors(reason, deleteGroup(groupId)));
});

};
}

export const SEARCH_GROUPS = "SEARCH_GROUPS";
export function searchGroups(searchTerm, skip=0, limit=21) {
return (dispatch) => {
return V2.GroupsService.searchGroupApiV2GroupsSearchSearchTermGet(searchTerm, skip, limit)
.then(json => {
dispatch({
type: SEARCH_GROUPS,
groups: json,
receivedAt: Date.now(),
});
})
.catch(reason => {
dispatch(handleErrors(reason, searchGroups(searchTerm, skip, limit)));
});

};
}

export const RECEIVE_GROUP_ABOUT = "RECEIVE_GROUP_ABOUT";
export function fetchGroupAbout(id){
return (dispatch) => {
Expand Down Expand Up @@ -54,9 +90,9 @@ export function deleteGroupMember(groupId, username){
}

export const ADD_GROUP_MEMBER = "ADD_GROUP_MEMBER";
export function addGroupMember(groupId, username){
export function addGroupMember(groupId, username, role="viewer"){
return (dispatch) => {
return V2.GroupsService.addMemberApiV2GroupsGroupIdAddUsernamePost(groupId, username)
return V2.GroupsService.addMemberApiV2GroupsGroupIdAddUsernamePost(groupId, username, role)
.then(json => {
dispatch({
type: ADD_GROUP_MEMBER,
Expand All @@ -65,7 +101,24 @@ export function addGroupMember(groupId, username){
});
})
.catch(reason => {
dispatch(handleErrors(reason, addGroupMember(groupId, username)));
dispatch(handleErrors(reason, addGroupMember(groupId, username, role)));
});
};
}

export const ASSIGN_GROUP_MEMBER_ROLE = "ASSIGN_GROUP_MEMBER_ROLE";
export function assignGroupMemberRole(groupId, username, role="viewer"){
return (dispatch) => {
return V2.GroupsService.updateMemberApiV2GroupsGroupIdUpdateUsernamePut(groupId, username, role)
.then(json => {
dispatch({
type: ASSIGN_GROUP_MEMBER_ROLE,
about: json,
receivedAt: Date.now(),
});
})
.catch(reason => {
dispatch(handleErrors(reason, assignGroupMemberRole(groupId, username, role)));
});
};
}
2 changes: 1 addition & 1 deletion frontend/src/components/groups/AddMemberModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type AddMemberModalProps = {
open: boolean,
handleClose: any,
groupName: string,
groupId: string,
groupId: string|undefined,
}

export default function AddMemberModal(props: AddMemberModalProps) {
Expand Down
Loading