diff --git a/client/src/api/index.js b/client/src/api/index.js index 3cfd11d5..adcc60e3 100644 --- a/client/src/api/index.js +++ b/client/src/api/index.js @@ -214,3 +214,7 @@ export function validate(type, value) { export function cron() { return fetchJson("/api/v1/system/cron") } + +export function rolesUnknownInManage() { + return fetchJson("/api/v1/system/unknown-roles") +} diff --git a/client/src/locale/en.js b/client/src/locale/en.js index e9ff9504..234ddbf2 100644 --- a/client/src/locale/en.js +++ b/client/src/locale/en.js @@ -48,7 +48,8 @@ const en = { guestRoles: "Guests with this role", cron: "Cron", invite: "Invite", - tokens: "API tokens" + tokens: "API tokens", + unknownRoles: "Missing applications" }, home: { access: "SURFconext Invite", @@ -156,6 +157,7 @@ const en = { deleteConfirmation: "Are you sure you want to delete this role?", createFlash: "Role {{name}} has been created", updateFlash: "Role {{name}} has been updated", + unknownInManage: "Unknown in Manage" }, applications: { searchPlaceHolder: "Search for roles" @@ -425,6 +427,10 @@ const en = { trigger: "Trigger", clear: "Clear", cronInfo: "Trigger the cron job to cleanup resources like expired user-roles, orphaned users and in-active users" + }, + unknownRoles: { + title: "Roles linked to applications unknown in Manage", + searchPlaceHolder: "Search..." } } diff --git a/client/src/locale/nl.js b/client/src/locale/nl.js index 104c622e..1cc99ab4 100644 --- a/client/src/locale/nl.js +++ b/client/src/locale/nl.js @@ -156,6 +156,7 @@ const nl = { deleteConfirmation: "Weet je zeker dat je deze rol wil verwijderen?", createFlash: "Rol {{name}} is aangemaakt", updateFlash: "Rol {{name}} is bijgewerkt", + unknownInManage: "Onbekend in Manage" }, applications: { searchPlaceHolder: "Zoek rollen" diff --git a/client/src/pages/Role.js b/client/src/pages/Role.js index 180211d2..70bf5d0f 100644 --- a/client/src/pages/Role.js +++ b/client/src/pages/Role.js @@ -7,6 +7,7 @@ import {useNavigate, useParams} from "react-router-dom"; import {useAppStore} from "../stores/AppStore"; import {UnitHeader} from "../components/UnitHeader"; import {ReactComponent as UserLogo} from "@surfnet/sds/icons/functional-icons/id-2.svg"; +import {ReactComponent as AlertLogo} from "@surfnet/sds/icons/functional-icons/alert-circle.svg"; import {ReactComponent as WebsiteIcon} from "../icons/network-information.svg"; import {ReactComponent as PersonIcon} from "../icons/persons.svg"; import {ReactComponent as GuestLogo} from "@surfnet/sds/icons/illustrative-icons/hr.svg"; @@ -159,6 +160,7 @@ export const Role = () => { })) }}/> + {!role.unknownInManage &&
{ target="_blank"> {`${role.applicationNames}`} {role.applicationOrganizationName && {` (${role.applicationOrganizationName})`}} -
- + } + {role.unknownInManage && +
+ + {I18n.t("roles.unknownInManage")} +
} }
diff --git a/client/src/pages/Role.scss b/client/src/pages/Role.scss index e6f2b2a3..4ea99bb8 100644 --- a/client/src/pages/Role.scss +++ b/client/src/pages/Role.scss @@ -17,8 +17,14 @@ height: auto; margin-right: 15px; } + + &.unknown-in-manage { + color: var(--sds--color--red--500); + font-weight: 600; + } } } + .application-name { margin-right: 5px; } diff --git a/client/src/pages/System.js b/client/src/pages/System.js index 4ac612a2..97552afd 100644 --- a/client/src/pages/System.js +++ b/client/src/pages/System.js @@ -5,9 +5,11 @@ import {Loader} from "@surfnet/sds"; 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 {ReactComponent as RoleLogo} from "@surfnet/sds/icons/illustrative-icons/hierarchy-2.svg"; import Tabs from "../components/Tabs"; import {Page} from "../components/Page"; import {Cron} from "../tabs/Cron"; +import {RolesUnknownInManage} from "../tabs/RolesUnknownInManage"; import {Invitations} from "../tabs/Invitations"; import {ReactComponent as InvitationLogo} from "@surfnet/sds/icons/functional-icons/id-1.svg"; @@ -39,6 +41,12 @@ export const System = () => { label={I18n.t("tabs.invitations")} Icon={InvitationLogo}> + , + + ]; diff --git a/client/src/tabs/Roles.js b/client/src/tabs/Roles.js index 3589251f..a7b8da4d 100644 --- a/client/src/tabs/Roles.js +++ b/client/src/tabs/Roles.js @@ -12,6 +12,7 @@ import debounce from "lodash.debounce"; import {chipTypeForUserRole} from "../utils/Authority"; import {ReactComponent as VoidImage} from "../icons/undraw_void_-3-ggu.svg"; import Select from "react-select"; +import {ReactComponent as AlertLogo} from "@surfnet/sds/icons/functional-icons/alert-circle.svg"; const allValue = "all"; @@ -158,14 +159,15 @@ export const Roles = () => { key: "logo", header: "", - mapper: role =>
+ mapper: role => role.unknownInManage ?
:
{typeof role.logo === "string" ? logo : role.logo}
}, { key: "applicationName", header: I18n.t("roles.applicationName"), - mapper: role => {role.applicationName} + mapper: role => role.unknownInManage ? {I18n.t("roles.unknownInManage")} : + {role.applicationName} }, { key: "name", diff --git a/client/src/tabs/Roles.scss b/client/src/tabs/Roles.scss index 4e46036f..1cba92c6 100644 --- a/client/src/tabs/Roles.scss +++ b/client/src/tabs/Roles.scss @@ -66,6 +66,15 @@ div.mod-roles { td.authority, td.defaultExpiryDays, td.userRoleCount { text-align: center; } + + .unknown-in-manage { + color: var(--sds--color--red--500); + font-weight: 600; + svg { + margin-left: 15px; + } + } + } } diff --git a/client/src/tabs/RolesUnknownInManage.js b/client/src/tabs/RolesUnknownInManage.js new file mode 100644 index 00000000..b4f60973 --- /dev/null +++ b/client/src/tabs/RolesUnknownInManage.js @@ -0,0 +1,105 @@ +import "./RolesUnknownInManage.scss"; +import {useAppStore} from "../stores/AppStore"; +import React, {useEffect, useState} from "react"; +import {Entities} from "../components/Entities"; +import I18n from "../locale/I18n"; +import {Chip, Loader} from "@surfnet/sds"; +import {useNavigate} from "react-router-dom"; +import {AUTHORITIES, isUserAllowed} from "../utils/UserRole"; +import {rolesUnknownInManage} from "../api"; +import {stopEvent} from "../utils/Utils"; +import {chipTypeForUserRole} from "../utils/Authority"; +import {ReactComponent as AlertLogo} from "@surfnet/sds/icons/functional-icons/alert-circle.svg"; +import {deriveApplicationAttributes} from "../utils/Manage"; + +export const RolesUnknownInManage = () => { + const navigate = useNavigate(); + const user = useAppStore(state => state.user); + + const [loading, setLoading] = useState(true); + const [roles, setRoles] = useState([]); + + useEffect(() => { + if (isUserAllowed(AUTHORITIES.SUPER_USER, user)) { + rolesUnknownInManage() + .then(res => { + deriveApplicationAttributes(res, I18n.locale, I18n.t("roles.multiple"), I18n.t("forms.and")) + setRoles(res); + setLoading(false); + }) + } else { + navigate("/404") + } + }, [user]);// eslint-disable-line react-hooks/exhaustive-deps + + + const openRole = (e, role) => { + const path = `/roles/${role.id}` + if (e.metaKey || e.ctrlKey) { + window.open(path, '_blank'); + } else { + stopEvent(e); + navigate(path); + } + }; + + const columns = [ + { + nonSortable: true, + key: "logo", + header: "", + mapper: () =>
+ }, + { + key: "applicationName", + header: I18n.t("roles.applicationName"), + mapper: role => {I18n.t("roles.unknownInManage")} + }, + { + key: "name", + header: I18n.t("roles.accessRole"), + mapper: role => {role.name} + }, + { + key: "description", + header: I18n.t("roles.description"), + mapper: role => {role.description} + }, + { + key: "authority", + header: I18n.t("roles.authority"), + mapper: role => + }, + { + key: "userRoleCount", + header: I18n.t("roles.userRoleCount"), + mapper: role => role.userRoleCount + } + + ]; + + if (loading) { + return + } + + return ( +
+ (entity.applications || []).length > 1 ? "multi-role" : ""}/> +
+ ); + +} diff --git a/client/src/tabs/RolesUnknownInManage.scss b/client/src/tabs/RolesUnknownInManage.scss new file mode 100644 index 00000000..f887a06d --- /dev/null +++ b/client/src/tabs/RolesUnknownInManage.scss @@ -0,0 +1,50 @@ +@import "../styles/vars.scss"; + +div.mod-unknown-roles { + + table.unknown-roles { + + thead { + th { + &.applicationName { + width: 15%; + } + + &.name { + width: 15%; + } + + &.description { + width: 25%; + } + + &.authority { + width: 20%; + text-align: center; + } + + &.userRoleCount { + width: 15%; + text-align: center; + } + + } + } + + tbody { + td.authority, td.defaultExpiryDays, td.userRoleCount { + text-align: center; + } + + .unknown-in-manage { + color: var(--sds--color--red--500); + font-weight: 600; + svg { + margin-left: 15px; + } + } + + } + } + +} \ No newline at end of file diff --git a/client/src/utils/Manage.js b/client/src/utils/Manage.js index b38adcea..bc4aa129 100644 --- a/client/src/utils/Manage.js +++ b/client/src/utils/Manage.js @@ -33,12 +33,19 @@ export const deriveApplicationAttributes = (role, locale, multiple, separator) = const applications = role.applicationMaps; if (!isEmpty(applications)) { if (applications.length === 1) { - role.applicationName = applications[0][`name:${locale}`] || applications[0]["name:en"]; + const firstApplication = applications[0]; + if (firstApplication.unknown) { + role.unknownInManage = true; + } + role.applicationName = firstApplication[`name:${locale}`] || firstApplication["name:en"]; role.applicationNames = role.applicationName; - role.applicationOrganizationName = applications[0][`OrganizationName:${locale}`] || applications[0]["OrganizationName:en"]; - role.logo = applications[0].logo; + role.applicationOrganizationName = firstApplication[`OrganizationName:${locale}`] || firstApplication["OrganizationName:en"]; + role.logo = firstApplication.logo; } else { role.applicationName = multiple; + if (applications.every(app => app.unknown)) { + role.unknownInManage = true; + } const appNames = new Set(applications .map(app => app[`name:${locale}`] || app["name:en"])); role.applicationNames = splitListSemantically([...appNames], separator); diff --git a/server/src/main/java/access/aggregation/AttributeAggregatorController.java b/server/src/main/java/access/aggregation/AttributeAggregatorController.java index 74cab12e..98a959b9 100644 --- a/server/src/main/java/access/aggregation/AttributeAggregatorController.java +++ b/server/src/main/java/access/aggregation/AttributeAggregatorController.java @@ -7,6 +7,8 @@ import access.provision.scim.GroupURN; import access.repository.UserRepository; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -26,6 +28,8 @@ @SecurityRequirement(name = ATTRIBUTE_AGGREGATION_SCHEME_NAME) public class AttributeAggregatorController { + private static final Log LOG = LogFactory.getLog(AttributeAggregatorController.class); + private final UserRepository userRepository; private final Manage manage; private final String groupUrnPrefix; @@ -42,9 +46,16 @@ public AttributeAggregatorController(UserRepository userRepository, @PreAuthorize("hasRole('ATTRIBUTE_AGGREGATION')") public ResponseEntity>> getGroupMemberships(@PathVariable("unspecified_id") String unspecifiedId, @RequestParam("SPentityID") String spEntityId) { - Optional> optionalProvider = manage - .providerByEntityID(EntityType.SAML20_SP, spEntityId) - .or(() -> manage.providerByEntityID(EntityType.OIDC10_RP, spEntityId)); + Optional> optionalProvider; + try { + optionalProvider = manage + .providerByEntityID(EntityType.SAML20_SP, spEntityId) + .or(() -> manage.providerByEntityID(EntityType.OIDC10_RP, spEntityId)); + } catch (RuntimeException e) { + LOG.error("Error in communication with Manage", e); + optionalProvider = Optional.empty(); + } + if (optionalProvider.isEmpty()) { return ResponseEntity.ok(Collections.emptyList()); } diff --git a/server/src/main/java/access/api/SystemController.java b/server/src/main/java/access/api/SystemController.java index 01face10..84461b71 100644 --- a/server/src/main/java/access/api/SystemController.java +++ b/server/src/main/java/access/api/SystemController.java @@ -2,7 +2,10 @@ import access.config.Config; import access.cron.ResourceCleaner; +import access.manage.Manage; +import access.model.Role; import access.model.User; +import access.repository.RoleRepository; import access.security.UserPermissions; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -30,9 +33,13 @@ public class SystemController { private static final Log LOG = LogFactory.getLog(SystemController.class); private final ResourceCleaner resourceCleaner; + private final RoleRepository roleRepository; + private final Manage manage; - public SystemController(ResourceCleaner resourceCleaner) { + public SystemController(ResourceCleaner resourceCleaner, RoleRepository roleRepository, Manage manage) { this.resourceCleaner = resourceCleaner; + this.roleRepository = roleRepository; + this.manage = manage; } @GetMapping("/cron") @@ -43,4 +50,13 @@ public ResponseEntity>> cron(@Parameter return ResponseEntity.ok(body); } + @GetMapping("/unknown-roles") + public ResponseEntity> unknownRoles(@Parameter(hidden = true) User user) { + LOG.debug("/unknown-roles"); + UserPermissions.assertSuperUser(user); + List roles = manage.addManageMetaData(roleRepository.findAll()); + List unknownManageRoles = roles.stream().filter(role -> role.getApplicationMaps().stream().anyMatch(applicationMap -> applicationMap.containsKey("unknown"))).toList(); + return ResponseEntity.ok(unknownManageRoles); + } + } diff --git a/server/src/main/java/access/manage/Manage.java b/server/src/main/java/access/manage/Manage.java index d73342a2..f7130928 100644 --- a/server/src/main/java/access/manage/Manage.java +++ b/server/src/main/java/access/manage/Manage.java @@ -105,6 +105,7 @@ default List addManageMetaData(List roles) { if (applicationMap == null) { //If remote manage is not behaving applicationMap = new HashMap<>(); + applicationMap.put("unknown", true); } applicationMap.put("landingPage", applicationUsage.getLandingPage()); return applicationMap;