From 577541149da286ca63bca99ece168830924f80af Mon Sep 17 00:00:00 2001 From: Okke Harsta Date: Wed, 23 Aug 2023 10:17:32 +0200 Subject: [PATCH] All invitations for super user --- client/src/api/index.js | 4 ++ client/src/components/Page.jsx | 2 +- client/src/pages/Role.js | 2 +- client/src/pages/System.js | 14 ++++- client/src/tabs/Invitations.js | 59 ++++++++++--------- client/src/tabs/Invitations.scss | 2 + .../java/access/api/InvitationController.java | 7 +++ .../repository/InvitationRepository.java | 2 +- server/src/test/java/access/AbstractTest.java | 5 +- .../access/api/InvitationControllerTest.java | 13 ++++ .../java/access/api/UserControllerTest.java | 15 +++++ 11 files changed, 92 insertions(+), 33 deletions(-) diff --git a/client/src/api/index.js b/client/src/api/index.js index 395e3112..43df6f9d 100644 --- a/client/src/api/index.js +++ b/client/src/api/index.js @@ -136,6 +136,10 @@ export function deleteInvitation(invitationId) { return fetchDelete(`/api/v1/invitations/${invitationId}`); } +export function allInvitations() { + return fetchJson(`/api/v1/invitations/all`, {}, {}, false); +} + //Manage export function allProviders() { return fetchJson("/api/v1/manage/providers"); diff --git a/client/src/components/Page.jsx b/client/src/components/Page.jsx index c704489c..2436f0d1 100644 --- a/client/src/components/Page.jsx +++ b/client/src/components/Page.jsx @@ -1,7 +1,7 @@ import React from "react"; import "./Page.scss"; -export const Page = ({Icon, label, name, children}) => { +export const Page = ({children}) => { return (
diff --git a/client/src/pages/Role.js b/client/src/pages/Role.js index 030d5691..3ae03f40 100644 --- a/client/src/pages/Role.js +++ b/client/src/pages/Role.js @@ -58,7 +58,7 @@ export const Role = () => { label={I18n.t("tabs.invitations")} Icon={InvitationLogo}> + preloadedInvitations={res[2]}/> ]; setTabs(newTabs); diff --git a/client/src/pages/System.js b/client/src/pages/System.js index 8c72a251..4ac612a2 100644 --- a/client/src/pages/System.js +++ b/client/src/pages/System.js @@ -2,14 +2,18 @@ import React, {useEffect, useState} from "react"; import I18n from "../locale/I18n"; import "./System.scss"; import {Loader} from "@surfnet/sds"; -import {useParams} from "react-router-dom"; +import {useNavigate, useParams} from "react-router-dom"; import {useAppStore} from "../stores/AppStore"; import {ReactComponent as CronLogo} from "@surfnet/sds/icons/illustrative-icons/database-check.svg"; import Tabs from "../components/Tabs"; import {Page} from "../components/Page"; import {Cron} from "../tabs/Cron"; +import {Invitations} from "../tabs/Invitations"; +import {ReactComponent as InvitationLogo} from "@surfnet/sds/icons/functional-icons/id-1.svg"; + export const System = () => { + const navigate = useNavigate(); const {tab = "cron"} = useParams(); const [loading, setLoading] = useState(false); const [currentTab, setCurrentTab] = useState(tab); @@ -29,7 +33,14 @@ export const System = () => { label={I18n.t("tabs.cron")} Icon={CronLogo}> + , + + + ]; setTabs(newTabs); setLoading(false); @@ -38,6 +49,7 @@ export const System = () => { const tabChanged = (name) => { setCurrentTab(name); + navigate(`/system/${name}`); } if (loading) { diff --git a/client/src/tabs/Invitations.js b/client/src/tabs/Invitations.js index 126a2f4f..18a2fe7b 100644 --- a/client/src/tabs/Invitations.js +++ b/client/src/tabs/Invitations.js @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from "react"; +import React, {useEffect, useRef, useState} from "react"; import I18n from "../locale/I18n"; import "./Invitations.scss"; import {Button, ButtonSize, ButtonType, Checkbox, Chip, Loader, Tooltip} from "@surfnet/sds"; @@ -8,44 +8,49 @@ import {shortDateFromEpoch} from "../utils/Date"; import {chipTypeForInvitationStatus, chipTypeForUserRole} from "../utils/Authority"; import {useNavigate} from "react-router-dom"; -import {deleteInvitation, resendInvitation} from "../api"; +import {allInvitations, deleteInvitation, resendInvitation} from "../api"; import ConfirmationDialog from "../components/ConfirmationDialog"; import {useAppStore} from "../stores/AppStore"; import {isEmpty, pseudoGuid} from "../utils/Utils"; import {allowedToDeleteInvitation, INVITATION_STATUS} from "../utils/UserRole"; -export const Invitations = ({role, invitations}) => { +export const Invitations = ({role, preloadedInvitations, standAlone = false}) => { const navigate = useNavigate(); const {user, setFlash} = useAppStore(state => state); - + const invitations = useRef(); const [selectedInvitations, setSelectedInvitations] = useState({}); const [allSelected, setAllSelected] = useState(false); - const [resultAfterSearch, setResultAfterSearch] = useState(invitations) + const [resultAfterSearch, setResultAfterSearch] = useState([]) const [loading, setLoading] = useState(true); const [confirmation, setConfirmation] = useState({}); const [confirmationOpen, setConfirmationOpen] = useState(false); useEffect(() => { - invitations.forEach(invitation => { - invitation.intendedRoles = invitation.roles - .sort((r1, r2) => r1.role.name.localeCompare(r2.role.name)) - .map(role => role.role.name).join(", "); - const now = new Date(); - invitation.status = new Date(invitation.expiryDate * 1000) < now ? INVITATION_STATUS.EXPIRED : invitation.status; - }); - setSelectedInvitations(invitations - .reduce((acc, invitation) => { - acc[invitation.id] = { - selected: false, - ref: invitation, - allowed: allowedToDeleteInvitation(user, invitation) - }; - return acc; - }, {})); - setLoading(false); + const promise = standAlone ? allInvitations() : Promise.resolve(preloadedInvitations); + promise.then(res => { + res.forEach(invitation => { + invitation.intendedRoles = invitation.roles + .sort((r1, r2) => r1.role.name.localeCompare(r2.role.name)) + .map(role => role.role.name).join(", "); + const now = new Date(); + invitation.status = new Date(invitation.expiryDate * 1000) < now ? INVITATION_STATUS.EXPIRED : invitation.status; + }); + setSelectedInvitations(res + .reduce((acc, invitation) => { + acc[invitation.id] = { + selected: false, + ref: invitation, + allowed: allowedToDeleteInvitation(user, invitation) + }; + return acc; + }, {})); + invitations.current = res; + setResultAfterSearch(res); + setLoading(false); + }) }, - [invitations, user]) + [invitations, user]) // eslint-disable-line react-hooks/exhaustive-deps const onCheck = invitation => e => { const checked = e.target.checked; @@ -219,7 +224,7 @@ export const Invitations = ({role, invitations}) => { mapper: invitation => shortDateFromEpoch(invitation.roleExpiryDate) }]; - const countInvitations = invitations.length; + const countInvitations = invitations.current.length; const hasEntities = countInvitations > 0; let title = ""; @@ -237,14 +242,14 @@ export const Invitations = ({role, invitations}) => { confirmationTxt={confirmation.confirmationTxt} question={confirmation.question}/>} - navigate("/invitation/new", {state: role.id})} + showNew={!!role} + newEntityFunc={role ? () => navigate("/invitation/new", {state: role.id}) : null} hideTitle={true} customNoEntities={I18n.t(`invitations.noResults`)} loading={false} diff --git a/client/src/tabs/Invitations.scss b/client/src/tabs/Invitations.scss index d3111464..0616eac2 100644 --- a/client/src/tabs/Invitations.scss +++ b/client/src/tabs/Invitations.scss @@ -1,4 +1,6 @@ .mod-invitations { + background-color: white; + table.invitations { thead { diff --git a/server/src/main/java/access/api/InvitationController.java b/server/src/main/java/access/api/InvitationController.java index 6219c6bb..5ba85ba8 100644 --- a/server/src/main/java/access/api/InvitationController.java +++ b/server/src/main/java/access/api/InvitationController.java @@ -162,6 +162,13 @@ public ResponseEntity getInvitation(@RequestParam("hash") String return ResponseEntity.ok(new MetaInvitation(invitation, providers)); } + @GetMapping("all") + public ResponseEntity> all(@Parameter(hidden = true) User user) { + LOG.debug("/all invitations"); + UserPermissions.assertSuperUser(user); + return ResponseEntity.ok(invitationRepository.findByStatus(Status.OPEN)); + } + @PostMapping("accept") public ResponseEntity> accept(@Validated @RequestBody AcceptInvitation acceptInvitation, diff --git a/server/src/main/java/access/repository/InvitationRepository.java b/server/src/main/java/access/repository/InvitationRepository.java index 351fe4be..944dd285 100644 --- a/server/src/main/java/access/repository/InvitationRepository.java +++ b/server/src/main/java/access/repository/InvitationRepository.java @@ -17,7 +17,7 @@ public interface InvitationRepository extends JpaRepository { attributePaths = {"inviter", "roles", "roles.role"}) Optional findByHash(String hash); - Optional findByIdAndStatus(Long id, Status status); + List findByStatus(Status status); List findByStatusAndRoles_role(Status status, Role role); } diff --git a/server/src/test/java/access/AbstractTest.java b/server/src/test/java/access/AbstractTest.java index c1a65b19..3a0effbb 100644 --- a/server/src/test/java/access/AbstractTest.java +++ b/server/src/test/java/access/AbstractTest.java @@ -37,6 +37,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponentsBuilder; import java.io.IOException; @@ -216,7 +217,7 @@ protected AccessCookieFilter openIDConnectFlow(String path, String sub, Consumer Map userInfo = objectMapper.readValue(new ClassPathResource("user-info.json").getInputStream(), new TypeReference<>() { }); - userInfo.put("sub", sub); + userInfo.put("sub", StringUtils.hasText(sub) ? sub : "sub"); userInfo.put("email", sub); userInfo.put("eduperson_principal_name", sub); String userInfoResult = objectMapper.writeValueAsString(userInfo); @@ -288,7 +289,7 @@ protected JWTClaimsSet getJwtClaimsSet(String clientId, String sub, String redir .jwtID(UUID.randomUUID().toString()) .issuer(clientId) .issueTime(Date.from(instant)) - .subject(sub) + .subject(StringUtils.hasText(sub) ? sub : "sub") .notBeforeTime(new Date(System.currentTimeMillis())) .claim("redirect_uri", redirectURI) .claim("eduperson_principal_name", sub) diff --git a/server/src/test/java/access/api/InvitationControllerTest.java b/server/src/test/java/access/api/InvitationControllerTest.java index 22dad6d1..1e31b6b1 100644 --- a/server/src/test/java/access/api/InvitationControllerTest.java +++ b/server/src/test/java/access/api/InvitationControllerTest.java @@ -316,4 +316,17 @@ void byRole() throws Exception { assertEquals(2, invitations.size()); } + @Test + void all() throws Exception { + AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", SUPER_SUB); + List invitations = given() + .when() + .filter(accessCookieFilter.cookieFilter()) + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .get("/api/v1/invitations/all") + .as(new TypeRef<>() { + }); + assertEquals(5, invitations.size()); + } } \ No newline at end of file diff --git a/server/src/test/java/access/api/UserControllerTest.java b/server/src/test/java/access/api/UserControllerTest.java index 98de15fb..6a6d426b 100644 --- a/server/src/test/java/access/api/UserControllerTest.java +++ b/server/src/test/java/access/api/UserControllerTest.java @@ -53,6 +53,21 @@ void configNewUserWithOauth2Login() throws Exception { assertEquals("John Doe", res.get("name")); } + @Test + void configMissingAttributes() throws Exception { + AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", ""); + + Map res = given() + .when() + .filter(accessCookieFilter.cookieFilter()) + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .get("/api/v1/users/config") + .as(Map.class); + assertFalse((Boolean) res.get("authenticated")); + assertEquals(2, ((List)res.get("missingAttributes")).size()); + } + @Test void meWithOauth2Login() throws Exception { AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/me", "urn:collab:person:example.com:admin");