Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,7 @@ backend/app/tests/*
!clowder-theme.tgz
*clowder2-software-dev.yaml
secrets.yaml

# faker
official.csv
fact.png
3 changes: 2 additions & 1 deletion backend/app/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ class UserLogin(BaseModel):


class UserDoc(Document, UserBase):
admin: bool

class Settings:
name = "users"


class UserDB(UserDoc):
hashed_password: str = Field()
keycloak_id: Optional[str] = None
admin: bool

def verify_password(self, password):
return pwd_context.verify(password, self.hashed_password)
Expand Down
31 changes: 28 additions & 3 deletions backend/app/routers/authentication.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json

from beanie import PydanticObjectId
from fastapi import APIRouter, HTTPException, Depends
from keycloak.exceptions import (
KeycloakAuthenticationError,
Expand All @@ -8,8 +9,6 @@
)
from passlib.hash import bcrypt

from beanie import PydanticObjectId

from app.keycloak_auth import create_user, get_current_user
from app.keycloak_auth import keycloak_openid
from app.models.datasets import DatasetDB
Expand Down Expand Up @@ -113,7 +112,7 @@ async def get_admin(dataset_id: str = None, current_username=Depends(get_current
async def set_admin(
useremail: str, current_username=Depends(get_current_user), admin=Depends(get_admin)
):
if admin:
if admin and current_username.admin:
if (user := await UserDB.find_one(UserDB.email == useremail)) is not None:
user.admin = True
await user.replace()
Expand All @@ -125,3 +124,29 @@ async def set_admin(
status_code=403,
detail=f"User {current_username.email} is not an admin. Only admin can make others admin.",
)


@router.post("/users/revoke_admin/{useremail}", response_model=UserOut)
async def revoke_admin(
useremail: str, current_username=Depends(get_current_user), admin=Depends(get_admin)
):
if admin:
if current_username.email == useremail:
raise HTTPException(
status_code=403,
detail=f"You are currently an admin. Admin cannot revoke their own admin access.",
)
else:
if (user := await UserDB.find_one(UserDB.email == useremail)) is not None:
user.admin = False
await user.replace()
return user.dict()
else:
raise HTTPException(
status_code=404, detail=f"User {useremail} not found"
)
else:
raise HTTPException(
status_code=403,
detail=f"User {current_username.email} is not an admin. Only admin can revoke admin access.",
)
36 changes: 36 additions & 0 deletions frontend/src/actions/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,39 @@ export function fetchUserProfile() {
});
};
}

export const SET_ADMIN = "SET_ADMIN";

export function setAdmin(email) {
return (dispatch) => {
return V2.LoginService.setAdminApiV2UsersSetAdminUseremailPost(email)
.then((json) => {
dispatch({
type: SET_ADMIN,
profile: json,
receivedAt: Date.now(),
});
})
.catch((reason) => {
dispatch(handleErrors(reason, setAdmin(email)));
});
};
}

export const REVOKE_ADMIN = "REVOKE_ADMIN";

export function revokeAdmin(email) {
return (dispatch) => {
return V2.LoginService.revokeAdminApiV2UsersRevokeAdminUseremailPost(email)
.then((json) => {
dispatch({
type: REVOKE_ADMIN,
profile: json,
receivedAt: Date.now(),
});
})
.catch((reason) => {
dispatch(handleErrors(reason, revokeAdmin(email)));
});
};
}
23 changes: 22 additions & 1 deletion frontend/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import { Link, Menu, MenuItem, MenuList, Typography } from "@mui/material";
import { Link as RouterLink, useLocation } from "react-router-dom";
import { useSelector } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../types/data";
import { AddBox, Explore } from "@material-ui/icons";
import HistoryIcon from "@mui/icons-material/History";
Expand All @@ -30,6 +30,8 @@ import { getCurrEmail } from "../utils/common";
import VpnKeyIcon from "@mui/icons-material/VpnKey";
import LogoutIcon from "@mui/icons-material/Logout";
import { EmbeddedSearch } from "./search/EmbeddedSearch";
import { fetchUserProfile } from "../actions/user";
import ManageAccountsIcon from "@mui/icons-material/ManageAccounts";

const drawerWidth = 240;

Expand Down Expand Up @@ -103,6 +105,9 @@ export default function PersistentDrawerLeft(props) {
const [embeddedSearchHidden, setEmbeddedSearchHidden] = React.useState(false);
const [anchorEl, setAnchorEl] = React.useState(null);
const isMenuOpen = Boolean(anchorEl);
const profile = useSelector((state: RootState) => state.user.profile);
const dispatch = useDispatch();
const fetchProfile = () => dispatch(fetchUserProfile());

const handleDrawerOpen = () => {
setOpen(true);
Expand All @@ -128,6 +133,7 @@ export default function PersistentDrawerLeft(props) {
} else {
setEmbeddedSearchHidden(false);
}
fetchProfile();
}, [location]);

const loggedOut = useSelector((state: RootState) => state.error.loggedOut);
Expand Down Expand Up @@ -278,6 +284,21 @@ export default function PersistentDrawerLeft(props) {
</ListItem>
</List>
<Divider />
{profile.admin ? (
<>
<List>
<ListItem key={"manage-user"} disablePadding>
<ListItemButton component={RouterLink} to="/manage-users">
<ListItemIcon>
<ManageAccountsIcon />
</ListItemIcon>
<ListItemText primary={"Manage Users"} />
</ListItemButton>
</ListItem>
</List>
<Divider />
</>
) : null}
<List>
<ListItem key={"groups"} disablePadding>
<ListItemButton component={RouterLink} to="/groups">
Expand Down
Loading