From 2b0511f684a495585d369e2c57eac3ff9fd7ab01 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Wed, 14 Jul 2021 12:11:30 -0700 Subject: [PATCH 01/24] SHIBUI-1844 Added controller for roles --- .../ui/exception/EntityNotFoundException.java | 7 ++ .../security/controller/RolesController.java | 65 +++++++++++++++++++ .../controller/RolesExceptionHandler.java | 38 +++++++++++ .../exception/RoleDeleteException.java | 7 ++ .../RoleExistsConflictException.java | 9 +++ .../admin/ui/security/model/Role.java | 39 ++++++----- .../security/repository/RoleRepository.java | 4 ++ .../ui/security/service/IRolesService.java | 22 +++++++ .../ui/security/service/RolesServiceImpl.java | 62 ++++++++++++++++++ 9 files changed, 236 insertions(+), 17 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesExceptionHandler.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleDeleteException.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleExistsConflictException.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IRolesService.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/RolesServiceImpl.java diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java new file mode 100644 index 000000000..4d0009523 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class EntityNotFoundException extends Exception { + public EntityNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java new file mode 100644 index 000000000..12cfe7025 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java @@ -0,0 +1,65 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IRolesService; + +@RestController +@RequestMapping("/api/admin/roles") +public class RolesController { + @Autowired + private IRolesService rolesService; + + @Secured("ROLE_ADMIN") + @PostMapping + @Transactional + public ResponseEntity create(@RequestBody Role role) throws RoleExistsConflictException { + Role result = rolesService.createRole(role); + return ResponseEntity.status(HttpStatus.CREATED).body(result); + } + + @Secured("ROLE_ADMIN") + @DeleteMapping("/{resourceId}") + @Transactional + public ResponseEntity delete(@PathVariable String resourceId) throws EntityNotFoundException, RoleDeleteException { + rolesService.deleteDefinition(resourceId); + return ResponseEntity.noContent().build(); + } + + @GetMapping + @Transactional(readOnly = true) + public ResponseEntity getAll() { + return ResponseEntity.ok(rolesService.findAll()); + } + + @GetMapping("/{resourceId}") + @Transactional(readOnly = true) + public ResponseEntity getOne(@PathVariable String resourceId) throws EntityNotFoundException { + Role role = rolesService.findByResourceId(resourceId); + return ResponseEntity.ok(role); + } + + @Secured("ROLE_ADMIN") + @PutMapping + @Transactional + public ResponseEntity update(@RequestBody Role role) throws EntityNotFoundException { + Role result = rolesService.updateRole(role); + return ResponseEntity.ok(result); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesExceptionHandler.java new file mode 100644 index 000000000..e4b840f1a --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesExceptionHandler.java @@ -0,0 +1,38 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.controller; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import edu.internet2.tier.shibboleth.admin.ui.controller.ErrorResponse; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; + +@ControllerAdvice(assignableTypes = {RolesController.class}) +public class RolesExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler({ EntityNotFoundException.class }) + public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(HttpStatus.NOT_FOUND, e.getMessage())); + } + + @ExceptionHandler({ RoleDeleteException.class }) + public ResponseEntity handleForbiddenAccess(RoleDeleteException e, WebRequest request) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/admin/role/{resourceId}").build().toUri()); + return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers) + .body(new ErrorResponse(String.valueOf(HttpStatus.CONFLICT.value()), e.getMessage())); + + } + + @ExceptionHandler({RoleExistsConflictException.class}) + public ResponseEntity handleRoleExistsConflictException(RoleExistsConflictException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponse(HttpStatus.CONFLICT, e.getMessage())); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleDeleteException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleDeleteException.java new file mode 100644 index 000000000..a7a35ab4f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleDeleteException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.exception; + +public class RoleDeleteException extends Exception { + public RoleDeleteException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleExistsConflictException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleExistsConflictException.java new file mode 100644 index 000000000..f5364c6ff --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleExistsConflictException.java @@ -0,0 +1,9 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.exception; + +public class RoleExistsConflictException extends Exception { + + public RoleExistsConflictException(String message) { + super(message); + } + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java index 64792774d..63484618d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java @@ -1,6 +1,16 @@ package edu.internet2.tier.shibboleth.admin.ui.security.model; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ManyToMany; + import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -8,14 +18,6 @@ import lombok.Setter; import lombok.ToString; -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.ManyToMany; -import java.util.HashSet; -import java.util.Set; - /** * Models a basic administrative role concept in the system. * @@ -29,24 +31,27 @@ @ToString(exclude = "users") public class Role extends AbstractAuditable { - public Role(String name) { - this.name = name; - } - - public Role(String name, int rank) { - this.name = name; - this.rank = rank; - } - @Column(unique = true) private String name; @Column(name = "ROLE_RANK") private int rank; + @Column(name = "resource_id") + String resourceId = UUID.randomUUID().toString(); + //Ignore properties annotation here is to prevent stack overflow recursive error during JSON serialization @JsonIgnoreProperties("roles") @ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY) private Set users = new HashSet<>(); + public Role(String name) { + this.name = name; + } + + public Role(String name, int rank) { + this.name = name; + this.rank = rank; + } + } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java index 120f2938e..fb77d0e9d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java @@ -12,5 +12,9 @@ */ public interface RoleRepository extends JpaRepository { + void deleteByResourceId(String resourceId); + Optional findByName(final String name); + + Optional findByResourceId(String resourceId); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IRolesService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IRolesService.java new file mode 100644 index 000000000..26653d241 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IRolesService.java @@ -0,0 +1,22 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service; + +import java.util.List; + +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; + +public interface IRolesService { + + Role createRole(Role role) throws RoleExistsConflictException; + + Role updateRole(Role role) throws EntityNotFoundException; + + List findAll(); + + Role findByResourceId(String resourceId) throws EntityNotFoundException; + + void deleteDefinition(String resourceId) throws EntityNotFoundException, RoleDeleteException; + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/RolesServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/RolesServiceImpl.java new file mode 100644 index 000000000..cfbe57fb0 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/RolesServiceImpl.java @@ -0,0 +1,62 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service; + +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository; + +@Service +public class RolesServiceImpl implements IRolesService { + @Autowired + private RoleRepository roleRepository; + + @Override + public Role createRole(Role role) throws RoleExistsConflictException { + Optional found = roleRepository.findByName(role.getName()); + // If already defined, we don't want to create a new one, nor do we want this call update the definition + if (found.isPresent()) { + throw new RoleExistsConflictException( + String.format("Call update (PUT) to modify the role with name: [%s]", role.getName())); + } + return roleRepository.save(role); + } + + @Override + public void deleteDefinition(String resourceId) throws EntityNotFoundException, RoleDeleteException { + Optional found = roleRepository.findByResourceId(resourceId); + if (found.isPresent() && !found.get().getUsers().isEmpty()) { + throw new RoleDeleteException(String.format("Unable to delete role with resource id: [%s] - remove role from all users first", resourceId)); + } + roleRepository.deleteByResourceId(resourceId); + } + + @Override + public List findAll() { + return roleRepository.findAll(); + } + + @Override + public Role findByResourceId(String resourceId) throws EntityNotFoundException { + Optional found = roleRepository.findByResourceId(resourceId); + if (found.isEmpty()) { + throw new EntityNotFoundException(String.format("Unable to find role with resource id: [%s]", resourceId)); + } + return found.get(); + } + + @Override + public Role updateRole(Role role) throws EntityNotFoundException { + Optional found = roleRepository.findByName(role.getName()); + if (found.isEmpty()) { + throw new EntityNotFoundException(String.format("Unable to find role with name: [%s]", role.getName())); + } + return roleRepository.save(role); + } +} From 5b043981651f8f88d832da956f5524a17752daad Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 22 Jul 2021 15:27:53 -0700 Subject: [PATCH 02/24] Separated enabled functionality --- .../main/resources/i18n/messages.properties | 2 + ui/src/app/admin/container/MetadataActions.js | 45 +++++++++++ ui/src/app/admin/container/SourcesActions.js | 31 -------- ui/src/app/dashboard/view/ActionsTab.js | 9 ++- ui/src/app/dashboard/view/ProvidersTab.js | 23 ++++-- ui/src/app/dashboard/view/SourcesTab.js | 14 +++- ui/src/app/metadata/Metadata.js | 4 +- .../app/metadata/component/MetadataHeader.js | 2 +- .../EntityAttributesFilterDefinition.js | 1 - .../definition/NameIdFilterDefinition.js | 1 - .../domain/provider/component/ProviderList.js | 31 ++++++-- .../DynamicHttpMetadataProviderDefinition.js | 5 +- ...ileBackedHttpMetadataProviderDefinition.js | 11 +-- .../FileSystemMetadataProviderDefinition.js | 12 +-- .../LocalDynamicMetadataProviderDefinition.js | 12 +-- .../domain/source/component/SourceList.js | 74 ++++++++++--------- .../source/definition/SourceDefinition.js | 10 +-- ui/src/app/metadata/hoc/MetadataSelector.js | 5 +- ui/src/app/metadata/view/MetadataOptions.js | 57 ++++++++++---- ui/src/app/metadata/view/MetadataWizard.js | 37 +++++++++- .../metadata/wizard/MetadataProviderWizard.js | 48 ++---------- .../metadata/wizard/MetadataSourceWizard.js | 33 +-------- .../app/metadata/wizard/MetadataWizardForm.js | 2 +- 23 files changed, 248 insertions(+), 221 deletions(-) create mode 100644 ui/src/app/admin/container/MetadataActions.js delete mode 100644 ui/src/app/admin/container/SourcesActions.js diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index ee6ab56ed..1c103c695 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -63,6 +63,8 @@ action.advanced=Advanced action.add-new-attribute=Add new attribute action.add-attribute=Add Attribute action.custom-entity-attributes=Custom Entity Attributes +action.enable=Enable +action.disable=Disable value.enabled=Enabled value.disabled=Disabled diff --git a/ui/src/app/admin/container/MetadataActions.js b/ui/src/app/admin/container/MetadataActions.js new file mode 100644 index 000000000..80f43d56a --- /dev/null +++ b/ui/src/app/admin/container/MetadataActions.js @@ -0,0 +1,45 @@ +import React from 'react'; +import { DeleteConfirmation } from '../../metadata/component/DeleteConfirmation'; +import { useMetadataEntity } from '../../metadata/hooks/api'; + +import { NotificationContext, createNotificationAction } from '../../notifications/hoc/Notifications'; + +export function MetadataActions ({type, children}) { + + const { dispatch } = React.useContext(NotificationContext); + + const { del, put, response } = useMetadataEntity(type, { + cachePolicy: 'no-cache' + }); + + async function enableEntity(entity, enabled, cb = () => {}) { + await put(`/${type === 'source' ? entity.id : entity.resourceId}`, { + ...entity, + [type === 'source' ? 'serviceEnabled' : 'enabled']: enabled + }); + if (response.ok) { + dispatch(createNotificationAction( + `Metadata ${type} has been ${enabled ? 'enabled' : 'disabled'}.` + )); + cb(); + } + } + + async function deleteEntity(id, cb = () => {}) { + await del(`/${id}`); + if (response.ok) { + dispatch(createNotificationAction( + `Metadata ${type} has been deleted.` + )); + cb(); + } + } + + return ( + + {(block) => + <>{children(enableEntity, (id, cb) => block(() => deleteEntity(id, cb)))} + } + + ); +} \ No newline at end of file diff --git a/ui/src/app/admin/container/SourcesActions.js b/ui/src/app/admin/container/SourcesActions.js deleted file mode 100644 index 1d7316735..000000000 --- a/ui/src/app/admin/container/SourcesActions.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import SourceList from '../../metadata/domain/source/component/SourceList'; -import { useMetadataEntity } from '../../metadata/hooks/api'; - -import { NotificationContext, createNotificationAction } from '../../notifications/hoc/Notifications'; - -export function SourcesActions ({sources, reloadSources}) { - - const { dispatch } = React.useContext(NotificationContext); - - const { put, response } = useMetadataEntity('source', { - cachePolicy: 'no-cache' - }); - - async function enableSource(source) { - await put(`/${source.id}`, { - ...source, - serviceEnabled: true - }); - if (response.ok) { - dispatch(createNotificationAction( - `Metadata Source has been enabled.` - )); - reloadSources(); - } - } - - return ( - - ); -} \ No newline at end of file diff --git a/ui/src/app/dashboard/view/ActionsTab.js b/ui/src/app/dashboard/view/ActionsTab.js index 76d1ff592..30bc9e371 100644 --- a/ui/src/app/dashboard/view/ActionsTab.js +++ b/ui/src/app/dashboard/view/ActionsTab.js @@ -1,8 +1,9 @@ import React from 'react'; -import { SourcesActions } from '../../admin/container/SourcesActions'; +import { MetadataActions } from '../../admin/container/MetadataActions'; import UserActions from '../../admin/container/UserActions'; import Translate from '../../i18n/components/translate'; +import SourceList from '../../metadata/domain/source/component/SourceList'; export function ActionsTab({ sources, users, reloadSources, reloadUsers }) { @@ -18,7 +19,11 @@ export function ActionsTab({ sources, users, reloadSources, reloadUsers }) {
- + + {(enable) => + enable(s, e, reloadSources)} /> + } +
diff --git a/ui/src/app/dashboard/view/ProvidersTab.js b/ui/src/app/dashboard/view/ProvidersTab.js index 123e54f41..023a7f78a 100644 --- a/ui/src/app/dashboard/view/ProvidersTab.js +++ b/ui/src/app/dashboard/view/ProvidersTab.js @@ -7,7 +7,7 @@ import {Search} from '../component/Search'; import { Ordered } from '../component/Ordered'; import { useIsAdmin } from '../../core/user/UserContext'; import Alert from 'react-bootstrap/Alert'; - +import { MetadataActions } from '../../admin/container/MetadataActions'; const searchProps = ['name', '@type', 'createdBy']; export function ProvidersTab () { @@ -44,13 +44,20 @@ export function ProvidersTab () { {(ordered, first, last, onOrderUp, onOrderDown) => - {(searched) => } + {(searched) => + + {(enable) => + enable(p, e, loadProviders)} + onOrderUp={onOrderUp} + onOrderDown={onOrderDown}> + } + + } } diff --git a/ui/src/app/dashboard/view/SourcesTab.js b/ui/src/app/dashboard/view/SourcesTab.js index 852ec221a..b40a94aa9 100644 --- a/ui/src/app/dashboard/view/SourcesTab.js +++ b/ui/src/app/dashboard/view/SourcesTab.js @@ -1,4 +1,5 @@ import React from 'react'; +import { MetadataActions } from '../../admin/container/MetadataActions'; import Translate from '../../i18n/components/translate'; import SourceList from '../../metadata/domain/source/component/SourceList'; @@ -22,8 +23,6 @@ export function SourcesTab () { } } - const updateSources = () => loadSources(); - /*eslint-disable react-hooks/exhaustive-deps*/ React.useEffect(() => { loadSources() }, []); @@ -37,7 +36,16 @@ export function SourcesTab () {
- {(searched) => } + {(searched) => + + {(enable, remove) => + remove(id, loadSources)} + onEnable={(s, e) => enable(s, e, loadSources) } /> + } + + }
diff --git a/ui/src/app/metadata/Metadata.js b/ui/src/app/metadata/Metadata.js index 0d1e38b07..9fb70ad44 100644 --- a/ui/src/app/metadata/Metadata.js +++ b/ui/src/app/metadata/Metadata.js @@ -20,13 +20,13 @@ export function Metadata () { return ( <> - {(entity) => + {(entity, reload) => - + } /> diff --git a/ui/src/app/metadata/component/MetadataHeader.js b/ui/src/app/metadata/component/MetadataHeader.js index d8a2baa85..e9cd62aea 100644 --- a/ui/src/app/metadata/component/MetadataHeader.js +++ b/ui/src/app/metadata/component/MetadataHeader.js @@ -8,7 +8,7 @@ export function MetadataHeader ({ model, current = true, enabled = true, childre
-
+
Saved diff --git a/ui/src/app/metadata/domain/filter/definition/EntityAttributesFilterDefinition.js b/ui/src/app/metadata/domain/filter/definition/EntityAttributesFilterDefinition.js index aab3adb4d..3260f79a1 100644 --- a/ui/src/app/metadata/domain/filter/definition/EntityAttributesFilterDefinition.js +++ b/ui/src/app/metadata/domain/filter/definition/EntityAttributesFilterDefinition.js @@ -100,7 +100,6 @@ export const EntityAttributesFilterEditor= { 'name', '@type', 'resourceId', - 'filterEnabled', 'entityAttributesFilterTarget' ] }, diff --git a/ui/src/app/metadata/domain/filter/definition/NameIdFilterDefinition.js b/ui/src/app/metadata/domain/filter/definition/NameIdFilterDefinition.js index e4abd17eb..aa575cefd 100644 --- a/ui/src/app/metadata/domain/filter/definition/NameIdFilterDefinition.js +++ b/ui/src/app/metadata/domain/filter/definition/NameIdFilterDefinition.js @@ -66,7 +66,6 @@ export const NameIDFilterEditor = { index: 1, fields: [ 'name', - 'filterEnabled', '@type', 'resourceId', 'nameIdFormatFilterTarget' diff --git a/ui/src/app/metadata/domain/provider/component/ProviderList.js b/ui/src/app/metadata/domain/provider/component/ProviderList.js index 86e0d83ea..4cd26c2b9 100644 --- a/ui/src/app/metadata/domain/provider/component/ProviderList.js +++ b/ui/src/app/metadata/domain/provider/component/ProviderList.js @@ -2,14 +2,21 @@ import React from 'react'; import { Link } from 'react-router-dom'; import Badge from 'react-bootstrap/Badge'; import Button from 'react-bootstrap/Button'; +import Form from 'react-bootstrap/Form'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faChevronCircleDown, faChevronCircleUp } from '@fortawesome/free-solid-svg-icons'; import FormattedDate from '../../../../core/components/FormattedDate'; import Translate from '../../../../i18n/components/translate'; import { Scroller } from '../../../../dashboard/component/Scroller'; +import { useIsAdmin } from '../../../../core/user/UserContext'; +import { useTranslator } from '../../../../i18n/hooks'; + +export function ProviderList({ entities, reorder = true, first, last, onEnable, onOrderUp, onOrderDown }) { + + const isAdmin = useIsAdmin(); + const translator = useTranslator(); -export function ProviderList({ entities, reorder = true, first, last, onOrderUp, onOrderDown }) { return ( {(limited) =>
@@ -61,10 +68,24 @@ export function ProviderList({ entities, reorder = true, first, last, onOrderUp, { provider['@type'] } { provider.createdBy } - - - - + + + {onEnable && isAdmin ? + onEnable(provider, checked)} + checked={provider.enabled} + > + + : + + + + } + )} diff --git a/ui/src/app/metadata/domain/provider/definition/DynamicHttpMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/definition/DynamicHttpMetadataProviderDefinition.js index 87ed3bb31..58bb0002b 100644 --- a/ui/src/app/metadata/domain/provider/definition/DynamicHttpMetadataProviderDefinition.js +++ b/ui/src/app/metadata/domain/provider/definition/DynamicHttpMetadataProviderDefinition.js @@ -61,9 +61,7 @@ export const DynamicHttpMetadataProviderWizard = { label: 'label.finished', index: 5, initialValues: [], - fields: [ - 'enabled' - ] + fields: [] } ], uiSchema: defaultsDeep({ @@ -191,7 +189,6 @@ export const DynamicHttpMetadataProviderEditor = { '@type', 'xmlId', 'metadataRequestURLConstructionScheme', - 'enabled', 'requireValidMetadata', 'failFastInitialization' ] diff --git a/ui/src/app/metadata/domain/provider/definition/FileBackedHttpMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/definition/FileBackedHttpMetadataProviderDefinition.js index 5d2d8e009..4d7146c98 100644 --- a/ui/src/app/metadata/domain/provider/definition/FileBackedHttpMetadataProviderDefinition.js +++ b/ui/src/app/metadata/domain/provider/definition/FileBackedHttpMetadataProviderDefinition.js @@ -52,9 +52,7 @@ export const FileBackedHttpMetadataProviderWizard = { label: 'label.finished', index: 5, initialValues: [], - fields: [ - 'enabled' - ] + fields: [] } ], uiSchema: defaultsDeep({ @@ -68,12 +66,6 @@ export const FileBackedHttpMetadataProviderWizard = { '@type' ] }, - { - size: 8, - fields: [ - 'enabled' - ] - }, { size: 8, fields: [ @@ -181,7 +173,6 @@ export const FileBackedHttpMetadataProviderEditor = { fields: [ 'name', '@type', - 'enabled', 'xmlId', 'metadataURL', 'initializeFromBackupFile', diff --git a/ui/src/app/metadata/domain/provider/definition/FileSystemMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/definition/FileSystemMetadataProviderDefinition.js index 8738ffad2..57d3447af 100644 --- a/ui/src/app/metadata/domain/provider/definition/FileSystemMetadataProviderDefinition.js +++ b/ui/src/app/metadata/domain/provider/definition/FileSystemMetadataProviderDefinition.js @@ -35,9 +35,7 @@ export const FileSystemMetadataProviderWizard = { label: 'label.finished', index: 4, initialValues: [], - fields: [ - 'enabled' - ] + fields: [] } ], uiSchema: defaultsDeep({ @@ -51,12 +49,6 @@ export const FileSystemMetadataProviderWizard = { '@type' ] }, - { - size: 8, - fields: [ - 'enabled' - ] - }, { size: 8, fields: [ @@ -115,7 +107,6 @@ export const FileSystemMetadataProviderEditor = { 'xmlId', '@type', 'metadataFile', - 'enabled', 'doInitialization' ], override: { @@ -140,7 +131,6 @@ export const FileSystemMetadataProviderEditor = { type: 'group-lg', class: ['col-12'], fields: [ - 'enabled', 'xmlId', 'metadataFile', 'doInitialization' diff --git a/ui/src/app/metadata/domain/provider/definition/LocalDynamicMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/definition/LocalDynamicMetadataProviderDefinition.js index 339c0c606..eea5d3541 100644 --- a/ui/src/app/metadata/domain/provider/definition/LocalDynamicMetadataProviderDefinition.js +++ b/ui/src/app/metadata/domain/provider/definition/LocalDynamicMetadataProviderDefinition.js @@ -35,9 +35,7 @@ export const LocalDynamicMetadataProviderWizard = { label: 'label.finished', index: 4, initialValues: [], - fields: [ - 'enabled' - ] + fields: [] } ], uiSchema: defaultsDeep({ @@ -51,12 +49,6 @@ export const LocalDynamicMetadataProviderWizard = { '@type' ] }, - { - size: 8, - fields: [ - 'enabled' - ] - }, { size: 8, fields: [ @@ -125,7 +117,6 @@ export const LocalDynamicMetadataProviderEditor = { fields: [ 'name', '@type', - 'enabled', 'xmlId', 'sourceDirectory', ], @@ -150,7 +141,6 @@ export const LocalDynamicMetadataProviderEditor = { type: 'group-lg', class: ['col-12'], fields: [ - 'enabled', 'xmlId', 'sourceDirectory', ] diff --git a/ui/src/app/metadata/domain/source/component/SourceList.js b/ui/src/app/metadata/domain/source/component/SourceList.js index 41e2c7adb..04ea8f983 100644 --- a/ui/src/app/metadata/domain/source/component/SourceList.js +++ b/ui/src/app/metadata/domain/source/component/SourceList.js @@ -3,24 +3,27 @@ import { Link } from 'react-router-dom'; import Badge from 'react-bootstrap/Badge'; import Popover from 'react-bootstrap/Popover'; import Button from 'react-bootstrap/Button'; +import Form from 'react-bootstrap/Form'; import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTrash, faCheck } from '@fortawesome/free-solid-svg-icons'; +import { faTrash } from '@fortawesome/free-solid-svg-icons'; import FormattedDate from '../../../../core/components/FormattedDate'; import Translate from '../../../../i18n/components/translate'; import { Scroller } from '../../../../dashboard/component/Scroller'; -import { DeleteSourceConfirmation } from './DeleteSourceConfirmation'; +import { useTranslator } from '../../../../i18n/hooks'; +import { useIsAdmin } from '../../../../core/user/UserContext'; export default function SourceList({ entities, onDelete, onEnable }) { + + const translator = useTranslator(); + const isAdmin = useIsAdmin(); + return ( - - {(onDeleteSource) => - + {(limited) =>
- @@ -29,7 +32,7 @@ export default function SourceList({ entities, onDelete, onEnable }) { - {onDeleteSource && } + {onDelete && } @@ -46,39 +49,40 @@ export default function SourceList({ entities, onDelete, onEnable }) { - - - {onDeleteSource && } @@ -87,8 +91,6 @@ export default function SourceList({ entities, onDelete, onEnable }) {
Author Created Date Enabled
- {onEnable ? - - : - - - - } + + {onEnable && isAdmin ? + onEnable(source, checked)} + checked={source.serviceEnabled} + > + + : + + + + } + + {onDelete && A metadata source must be disabled before it can be deleted. }> - - - + + +
} -
- } -
+ ); } diff --git a/ui/src/app/metadata/domain/source/definition/SourceDefinition.js b/ui/src/app/metadata/domain/source/definition/SourceDefinition.js index bc11108ee..6f7276255 100644 --- a/ui/src/app/metadata/domain/source/definition/SourceDefinition.js +++ b/ui/src/app/metadata/domain/source/definition/SourceDefinition.js @@ -89,7 +89,6 @@ export const SourceBase = { fields: [ 'serviceProviderName', 'entityId', - 'serviceEnabled', 'organization' ] }, @@ -291,7 +290,6 @@ export const SourceEditor = { fields: [ 'serviceProviderName', 'entityId', - 'serviceEnabled', 'organization', 'contacts' ] @@ -424,9 +422,7 @@ export const SourceWizard = { }, { size: 6, - fields: [ - 'serviceEnabled' - ] + fields: [] } ] } @@ -510,9 +506,7 @@ export const SourceWizard = { index: 10, id: 'summary', label: 'label.finished', - fields: [ - 'serviceEnabled' - ] + fields: [] } ] } \ No newline at end of file diff --git a/ui/src/app/metadata/hoc/MetadataSelector.js b/ui/src/app/metadata/hoc/MetadataSelector.js index 7d7155066..fd37ed2c3 100644 --- a/ui/src/app/metadata/hoc/MetadataSelector.js +++ b/ui/src/app/metadata/hoc/MetadataSelector.js @@ -31,6 +31,9 @@ export function MetadataSelector({ children, ...props }) { setMetadata(source); } } + + const reload = () => loadMetadata(id); + React.useEffect(() => { loadMetadata(id) }, [id]); return ( @@ -38,7 +41,7 @@ export function MetadataSelector({ children, ...props }) { {type && {metadata && metadata.version && - {children(metadata)} + {children(metadata, reload)} } } diff --git a/ui/src/app/metadata/view/MetadataOptions.js b/ui/src/app/metadata/view/MetadataOptions.js index 93011229a..04242016d 100644 --- a/ui/src/app/metadata/view/MetadataOptions.js +++ b/ui/src/app/metadata/view/MetadataOptions.js @@ -1,5 +1,5 @@ import React from 'react'; -import { faArrowDown, faArrowUp, faHistory, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { faArrowDown, faArrowUp, faHistory, faPlus, faToggleOff, faToggleOn, faTrash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Link, useHistory, useParams } from 'react-router-dom'; import Button from 'react-bootstrap/Button'; @@ -14,14 +14,14 @@ import { MetadataDefinitionContext, MetadataSchemaContext } from '../hoc/Metadat import { useMetadataConfiguration } from '../hooks/configuration'; import { MetadataViewToggle } from '../component/MetadataViewToggle'; -import { DeleteSourceConfirmation } from '../domain/source/component/DeleteSourceConfirmation'; +import { MetadataActions } from '../../admin/container/MetadataActions'; import { MetadataFilters } from '../domain/filter/component/MetadataFilters'; import { MetadataFilterConfigurationList } from '../domain/filter/component/MetadataFilterConfigurationList'; import { MetadataFilterTypes } from '../domain/filter'; import { useMetadataSchema } from '../hooks/schema'; import { FilterableProviders } from '../domain/provider'; -export function MetadataOptions () { +export function MetadataOptions ({reload}) { const metadata = React.useContext(MetadataObjectContext); const definition = React.useContext(MetadataDefinitionContext); @@ -49,9 +49,11 @@ export function MetadataOptions () { const canFilter = FilterableProviders.indexOf(definition.type) > -1; + const enabled = type === 'source' ? metadata.serviceEnabled : metadata.enabled; + return ( - - {(onDeleteSource) => + + {(enable, remove) => <>
diff --git a/ui/src/app/metadata/wizard/MetadataSourceWizard.js b/ui/src/app/metadata/wizard/MetadataSourceWizard.js index 0d23b34af..57369b0dc 100644 --- a/ui/src/app/metadata/wizard/MetadataSourceWizard.js +++ b/ui/src/app/metadata/wizard/MetadataSourceWizard.js @@ -12,18 +12,13 @@ import { useMetadataDefinitionContext, useMetadataSchemaContext } from '../hoc/M import { useMetadataFormDispatcher, setFormDataAction, setFormErrorAction, useMetadataFormData, useMetadataFormErrors } from '../hoc/MetadataFormContext'; import { MetadataConfiguration } from '../component/MetadataConfiguration'; import { Configuration } from '../hoc/Configuration'; -import { useMetadataEntity, useMetadataSources } from '../hooks/api'; -import { Prompt, useHistory } from 'react-router'; -import { removeNull } from '../../core/utility/remove_null'; +import { useMetadataSources } from '../hooks/api'; import Translate from '../../i18n/components/translate'; import { checkChanges } from '../hooks/utility'; -export function MetadataSourceWizard ({ onShowNav }) { - - const { post, loading, response } = useMetadataEntity('source'); - const history = useHistory(); +export function MetadataSourceWizard ({ onShowNav, onSave, block, loading }) { const { data } = useMetadataSources({ cachePolicy: 'no-cache' @@ -50,39 +45,20 @@ export function MetadataSourceWizard ({ onShowNav }) { const onChange = (changes) => { formDispatch(setFormDataAction(changes.formData)); formDispatch(setFormErrorAction(changes.errors)); - setBlocking(checkChanges(metadata, changes.formData)); + block(checkChanges(metadata, changes.formData)); }; const onEditFromSummary = (idx) => { wizardDispatch(setWizardIndexAction(idx)); }; - const onBlur = (form) => { - // console.log(form); - } - - async function save () { - const body = removeNull(metadata, true); - await post('', body); - if (response.ok) { - setBlocking(false); - history.push('/'); - } - } - - const [blocking, setBlocking] = React.useState(false); + const save = () => onSave(definition.parser(metadata)); const validator = definition.validator(data); const warnings = definition.warnings && definition.warnings(metadata); return ( <> - - `message.unsaved-editor` - } - />
0 || loading } saving={loading} /> @@ -109,7 +85,6 @@ export function MetadataSourceWizard ({ onShowNav }) { schema={schema || {}} current={current} onChange={onChange} - onBlur={onBlur} validator={validator} />
diff --git a/ui/src/app/metadata/wizard/MetadataWizardForm.js b/ui/src/app/metadata/wizard/MetadataWizardForm.js index c508bd267..58000c605 100644 --- a/ui/src/app/metadata/wizard/MetadataWizardForm.js +++ b/ui/src/app/metadata/wizard/MetadataWizardForm.js @@ -12,7 +12,7 @@ function ErrorListTemplate () { return (<>); } -export function MetadataWizardForm ({ metadata, definition, schema, current, onChange, onBlur = false, validator }) { +export function MetadataWizardForm ({ metadata, definition, schema, current, onChange, onBlur = () => {}, validator }) { const {uiSchema} = useUiSchema(definition, schema, current); From 99381396ea5608266fdf65edfbdf10d55c36b5e9 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Fri, 23 Jul 2021 14:00:52 -0700 Subject: [PATCH 03/24] Fixed issues with provider loading, updated filter enable functionality --- ui/src/app/metadata/Metadata.js | 24 +++---- .../MetadataFilterConfigurationList.js | 69 ++++++++++--------- .../MetadataFilterConfigurationListItem.js | 16 +++-- .../component/MetadataFilterEditorList.js | 4 +- .../filter/component/MetadataFilters.js | 24 ++++++- .../app/metadata/editor/MetadataFilterList.js | 3 +- ui/src/app/metadata/hoc/MetadataSchema.js | 4 +- ui/src/app/metadata/hoc/MetadataSelector.js | 4 +- ui/src/app/metadata/hooks/api.js | 3 +- 9 files changed, 94 insertions(+), 57 deletions(-) diff --git a/ui/src/app/metadata/Metadata.js b/ui/src/app/metadata/Metadata.js index 9fb70ad44..81cd1e35a 100644 --- a/ui/src/app/metadata/Metadata.js +++ b/ui/src/app/metadata/Metadata.js @@ -18,13 +18,12 @@ export function Metadata () { let { path } = useRouteMatch(); return ( - <> - - {(entity, reload) => + + {(entity, reload) => - + @@ -34,36 +33,35 @@ export function Metadata () { } /> - + } /> - + } /> - + } /> - + } /> - + } /> - + } /> - } - - + } + ); } \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationList.js b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationList.js index 521055c12..04c33c10f 100644 --- a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationList.js +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationList.js @@ -2,43 +2,50 @@ import React from 'react'; import { Ordered } from '../../../../dashboard/component/Ordered'; import { Translate } from '../../../../i18n/components/translate'; -import { MetadataFiltersContext } from './MetadataFilters'; +import { MetadataFilters, MetadataFiltersContext } from './MetadataFilters'; import { MetadataFilterConfigurationListItem } from './MetadataFilterConfigurationListItem'; +import { MetadataFilterTypes } from '..'; export function MetadataFilterConfigurationList ({provider, onDelete, editable = true}) { - const filters = React.useContext(MetadataFiltersContext); + // const filters = React.useContext(MetadataFiltersContext); return ( - - {(ordered, first, last, onOrderUp, onOrderDown) => - <> - {ordered.length > 0 && -
    - {ordered.map((filter, i) => -
  • - onDelete(filter.resourceId)} - /> -
  • - )} -
- } - { filters && filters.length < 1 && -
-

No Filters

-

No filters have been added to this Metadata Provider

-
- } - + + {(filters, onUpdate, onDelete, onEnable, loading) => + + {(ordered, first, last, onOrderUp, onOrderDown) => + <> + {ordered.length > 0 && +
    + {ordered.map((filter, i) => +
  • + onDelete(filter.resourceId)} + /> +
  • + )} +
+ } + { filters && filters.length < 1 && +
+

No Filters

+

No filters have been added to this Metadata Provider

+
+ } + + } +
} -
+ ); } \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js index 4601db323..3c336a27e 100644 --- a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js @@ -3,6 +3,7 @@ import React from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faArrowCircleDown, faArrowCircleUp, faChevronUp, faEdit, faTrash } from '@fortawesome/free-solid-svg-icons'; import Button from 'react-bootstrap/Button'; +import Form from 'react-bootstrap/Form'; import { Translate } from '../../../../i18n/components/translate'; import { Link } from 'react-router-dom'; @@ -11,7 +12,7 @@ import { MetadataConfiguration } from '../../../component/MetadataConfiguration' import { useMetadataConfiguration } from '../../../hooks/configuration'; import useFetch from 'use-http'; -export function MetadataFilterConfigurationListItem ({ filter, isLast, isFirst, onOrderUp, onOrderDown, editable, onRemove, index }) { +export function MetadataFilterConfigurationListItem ({ filter, isLast, isFirst, onOrderUp, onOrderDown, onEnable, editable, onRemove, loading, index }) { const [open, setOpen] = React.useState(false); const definition = React.useMemo(() => getDefinition(filter['@type'], ), [filter]); @@ -49,10 +50,15 @@ export function MetadataFilterConfigurationListItem ({ filter, isLast, isFirst, } { filter['@type'] } - - - - + + } + checked={filter.filterEnabled} + disabled={loading} + onChange={({ target: { checked } }) => onEnable(filter, checked)} /> + {filter.disabled && } +
{open && diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilterEditorList.js b/ui/src/app/metadata/domain/filter/component/MetadataFilterEditorList.js index f5b23aa23..befeef819 100644 --- a/ui/src/app/metadata/domain/filter/component/MetadataFilterEditorList.js +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilterEditorList.js @@ -9,7 +9,7 @@ import { faArrowCircleDown, faArrowCircleUp, faEdit, faTrash } from '@fortawesom import { Ordered } from '../../../../dashboard/component/Ordered'; import { Translate } from '../../../../i18n/components/translate'; -export function MetadataFilterEditorList ({provider, filters, onDelete, onUpdate, loading}) { +export function MetadataFilterEditorList ({provider, filters, onDelete, onUpdate, onEnable, loading}) { return ( @@ -51,7 +51,7 @@ export function MetadataFilterEditorList ({provider, filters, onDelete, onUpdate label={Toggle this switch element} checked={filter.filterEnabled} disabled={loading} - onChange={() => onUpdate({ ...filter, filterEnabled: !filter.filterEnabled })} /> + onChange={({target: { checked }}) => onEnable(filter, checked)} /> {filter.disabled && }
diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilters.js b/ui/src/app/metadata/domain/filter/component/MetadataFilters.js index 13a22b53c..d38c97013 100644 --- a/ui/src/app/metadata/domain/filter/component/MetadataFilters.js +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilters.js @@ -1,11 +1,14 @@ import React from 'react'; import { useMetadataFilters } from '../../../hooks/api'; import { DeleteConfirmation } from '../../../component/DeleteConfirmation'; +import { NotificationContext, createNotificationAction } from '../../../../notifications/hoc/Notifications'; export const MetadataFiltersContext = React.createContext(); export function MetadataFilters ({ providerId, types = [], filters, children }) { + const { dispatch } = React.useContext(NotificationContext); + const { put, del, get, response, loading } = useMetadataFilters(providerId, { cachePolicy: 'no-cache' }); @@ -23,12 +26,31 @@ export function MetadataFilters ({ providerId, types = [], filters, children }) await put(`/${filter.resourceId}`, filter); if (response.ok) { loadFilters(providerId); + dispatch(createNotificationAction( + `Metadata Filter has been updated.` + )); + } + } + + async function enableFilter(filter, enabled) { + await put(`/${filter.resourceId}`, { + ...filter, + filterEnabled: enabled + }); + if (response.ok) { + dispatch(createNotificationAction( + `Metadata Filter has been ${enabled ? 'enabled' : 'disabled'}.` + )); + loadFilters(providerId); } } async function deleteFilter(filterId) { await del(`/${filterId}`); if (response.ok) { + dispatch(createNotificationAction( + `Metadata Filter has been deleted.` + )); loadFilters(); } } @@ -50,7 +72,7 @@ export function MetadataFilters ({ providerId, types = [], filters, children }) {(block) => - {children(filterData, onUpdate, (id) => block(() => onDelete(id)), loading)} + {children(filterData, onUpdate, (id) => block(() => onDelete(id)), enableFilter, loading)} } diff --git a/ui/src/app/metadata/editor/MetadataFilterList.js b/ui/src/app/metadata/editor/MetadataFilterList.js index 9b71f6ee5..699b1c400 100644 --- a/ui/src/app/metadata/editor/MetadataFilterList.js +++ b/ui/src/app/metadata/editor/MetadataFilterList.js @@ -90,12 +90,13 @@ export function MetadataFilterList() {
{definition && schema && current && - {(filters, onUpdate, onDelete, loading) => + {(filters, onUpdate, onDelete, onEnable, loading) => } diff --git a/ui/src/app/metadata/hoc/MetadataSchema.js b/ui/src/app/metadata/hoc/MetadataSchema.js index ec6dd49f0..c8a8365ee 100644 --- a/ui/src/app/metadata/hoc/MetadataSchema.js +++ b/ui/src/app/metadata/hoc/MetadataSchema.js @@ -9,7 +9,9 @@ export function MetadataSchema({ type, children, wizard = false }) { const definition = React.useMemo(() => wizard ? getWizard(type) : getDefinition(type), [type, wizard]); - const { get, response } = useFetch(``, {}, []); + const { get, response } = useFetch(``, { + cachePolicy: 'no-cache' + }); const [schema, setSchema] = React.useState(); diff --git a/ui/src/app/metadata/hoc/MetadataSelector.js b/ui/src/app/metadata/hoc/MetadataSelector.js index fd37ed2c3..6174d2e3a 100644 --- a/ui/src/app/metadata/hoc/MetadataSelector.js +++ b/ui/src/app/metadata/hoc/MetadataSelector.js @@ -23,7 +23,7 @@ export function MetadataSelector({ children, ...props }) { const { get, response } = useMetadataEntity(type); - const [metadata, setMetadata] = React.useState([]); + const [metadata, setMetadata] = React.useState(); async function loadMetadata(id) { const source = await get(`/${id}`); @@ -34,7 +34,7 @@ export function MetadataSelector({ children, ...props }) { const reload = () => loadMetadata(id); - React.useEffect(() => { loadMetadata(id) }, [id]); + React.useEffect(reload, [id]); return ( <> diff --git a/ui/src/app/metadata/hooks/api.js b/ui/src/app/metadata/hooks/api.js index d0f30ef38..cc2afa9c7 100644 --- a/ui/src/app/metadata/hooks/api.js +++ b/ui/src/app/metadata/hooks/api.js @@ -61,7 +61,8 @@ export const xmlRequestInterceptor = ({ options }) => { export function useMetadataEntityXml(type = 'source', opts = { interceptors: { request: xmlRequestInterceptor - } + }, + cachePolicy: 'no-cache' }) { return useFetch(`${API_BASE_PATH}${getMetadataPath(type)}`, opts); } From 066318b0a1f3540cd41948034436184c1196fb59 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Wed, 28 Jul 2021 15:28:03 -0700 Subject: [PATCH 04/24] SHIBUI-1844 correcting API for role --- .../ui/controller/ActivateController.java | 23 +++++++++++++++++++ .../security/controller/RolesController.java | 15 +++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java new file mode 100644 index 000000000..10b0a742b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java @@ -0,0 +1,23 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; + +@RestController +@RequestMapping("/api/activate") +public class ActivateController { + @Autowired + private UserService userService; + + @Autowired + private EntityDescriptorRepository entityDescriptorRepo; + + + + +// Enable/disable for : entity descriptor, provider, filter +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java index 12cfe7025..d576e0630 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java @@ -1,5 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.security.controller; +import java.util.Optional; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -56,10 +58,17 @@ public ResponseEntity getOne(@PathVariable String resourceId) throws EntityNo } @Secured("ROLE_ADMIN") - @PutMapping + @PutMapping(path = {"/", "/{resourceId}" }) @Transactional - public ResponseEntity update(@RequestBody Role role) throws EntityNotFoundException { - Role result = rolesService.updateRole(role); + public ResponseEntity update(@RequestBody Role incomingRoleDetail, @PathVariable Optional resourceId) throws EntityNotFoundException { + Role updateRole; + if (resourceId.isPresent()) { + updateRole = rolesService.findByResourceId(resourceId.get()); + } else { + updateRole = rolesService.findByResourceId(incomingRoleDetail.getResourceId()); + } + updateRole.setName(incomingRoleDetail.getName()); + Role result = rolesService.updateRole(updateRole); return ResponseEntity.ok(result); } } From 9445355b20ea87ffef1794801672ede0de9898af Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 29 Jul 2021 08:49:44 -0700 Subject: [PATCH 05/24] Added roles management --- .../main/resources/i18n/messages.properties | 19 +++- ui/public/assets/schema/roles/role.json | 15 +++ ui/src/app/App.js | 2 + ui/src/app/admin/Roles.js | 32 ++++++ ui/src/app/admin/component/RoleForm.js | 68 ++++++++++++ ui/src/app/admin/container/EditRole.js | 91 +++++++++++++++ ui/src/app/admin/container/MetadataActions.js | 2 +- ui/src/app/admin/container/NewRole.js | 79 +++++++++++++ ui/src/app/admin/container/RoleList.js | 78 +++++++++++++ ui/src/app/admin/hoc/RoleProvider.js | 20 ++++ ui/src/app/admin/hoc/RolesProvider.js | 42 +++++++ ui/src/app/admin/hooks.js | 20 ++++ .../components}/DeleteConfirmation.js | 2 +- .../components}/DeleteConfirmation.test.js | 2 +- ui/src/app/core/components/Header.js | 6 +- .../core/components/UserConfirmation.test.js | 2 +- ui/src/app/form/FormManager.js | 105 ++++++++++++++++++ ui/src/app/form/Schema.js | 24 ++++ .../filter/component/MetadataFilters.js | 2 +- .../metadata/view/MetadataAttributeList.js | 2 +- 20 files changed, 605 insertions(+), 8 deletions(-) create mode 100644 ui/public/assets/schema/roles/role.json create mode 100644 ui/src/app/admin/Roles.js create mode 100644 ui/src/app/admin/component/RoleForm.js create mode 100644 ui/src/app/admin/container/EditRole.js create mode 100644 ui/src/app/admin/container/NewRole.js create mode 100644 ui/src/app/admin/container/RoleList.js create mode 100644 ui/src/app/admin/hoc/RoleProvider.js create mode 100644 ui/src/app/admin/hoc/RolesProvider.js create mode 100644 ui/src/app/admin/hooks.js rename ui/src/app/{metadata/component => core/components}/DeleteConfirmation.js (93%) rename ui/src/app/{metadata/component => core/components}/DeleteConfirmation.test.js (97%) create mode 100644 ui/src/app/form/FormManager.js create mode 100644 ui/src/app/form/Schema.js diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 1c103c695..749267367 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -66,6 +66,10 @@ action.custom-entity-attributes=Custom Entity Attributes action.enable=Enable action.disable=Disable +action.add-new-role=Add new role +action.roles=Roles +action.source-role=Role + value.enabled=Enabled value.disabled=Disabled value.current=Current @@ -483,6 +487,16 @@ label.by=By label.source=Metadata Source label.provider=Metadata Provider +label.roles-management=Role Management +label.new-role=New Role +label.role-name=Role Name +label.role-description=Role Description +label.role=Role + +message.delete-role-title=Delete Role? + +message.delete-role-body=You are requesting to delete a role. If you complete this process the role will be removed. This cannot be undone. Do you wish to continue? + message.delete-user-title=Delete User? message.delete-user-body=You are requesting to delete a user. If you complete this process the user will be removed. This cannot be undone. Do you wish to continue? @@ -669,4 +683,7 @@ tooltip.match=A regular expression against which the entityID is evaluated. tooltip.remove-existing-formats=Whether to remove any existing formats from a role if any are added by the filter (unmodified roles will be untouched regardless of this setting) tooltip.nameid-formats-format=Format tooltip.nameid-formats-value=Value -tooltip.nameid-formats-type=Type \ No newline at end of file +tooltip.nameid-formats-type=Type + +tooltip.role-name=Role Name +tooltip.role-description=Role Description \ No newline at end of file diff --git a/ui/public/assets/schema/roles/role.json b/ui/public/assets/schema/roles/role.json new file mode 100644 index 000000000..8145fae88 --- /dev/null +++ b/ui/public/assets/schema/roles/role.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "title": "label.role-name", + "description": "tooltip.role-name", + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } +} \ No newline at end of file diff --git a/ui/src/app/App.js b/ui/src/app/App.js index 5d0c4e1e4..c65324bb9 100644 --- a/ui/src/app/App.js +++ b/ui/src/app/App.js @@ -26,6 +26,7 @@ import { NewProvider } from './metadata/new/NewProvider'; import { Filter } from './metadata/Filter'; import { Contention } from './metadata/contention/ContentionContext'; import { SessionModal } from './core/user/SessionModal'; +import { Roles } from './admin/Roles'; import Button from 'react-bootstrap/Button'; @@ -79,6 +80,7 @@ function App() { + diff --git a/ui/src/app/admin/Roles.js b/ui/src/app/admin/Roles.js new file mode 100644 index 000000000..08daed90d --- /dev/null +++ b/ui/src/app/admin/Roles.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { Switch, Route, useRouteMatch, Redirect } from 'react-router-dom'; +import { RolesProvider } from './hoc/RolesProvider'; +import { NewRole } from './container/NewRole'; +import { EditRole } from './container/EditRole'; +import { RoleList } from './container/RoleList'; + +export function Roles() { + + let { path } = useRouteMatch(); + + return ( + <> + + + + {(roles, onDelete) => + + } + + } /> + + + } /> + + + } /> + + + + ); +} \ No newline at end of file diff --git a/ui/src/app/admin/component/RoleForm.js b/ui/src/app/admin/component/RoleForm.js new file mode 100644 index 000000000..074cb5b09 --- /dev/null +++ b/ui/src/app/admin/component/RoleForm.js @@ -0,0 +1,68 @@ +import React from 'react'; +import Button from 'react-bootstrap/Button'; +import Form from '@rjsf/bootstrap-4'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSpinner, faSave } from '@fortawesome/free-solid-svg-icons'; +import Translate from '../../i18n/components/translate'; + +import { useRoleUiSchema } from '../hooks'; +import { fields, widgets } from '../../form/component'; +import { templates } from '../../form/component'; +import { FormContext, setFormDataAction, setFormErrorAction } from '../../form/FormManager'; + +function ErrorListTemplate() { + return (<>); +} + +export function RoleForm({ role = {}, errors = [], loading = false, schema, onSave, onCancel }) { + + const { dispatch } = React.useContext(FormContext); + const onChange = ({ formData, errors }) => { + dispatch(setFormDataAction(formData)); + dispatch(setFormErrorAction(errors)); + }; + + const uiSchema = useRoleUiSchema(); + + return (<> +
+
+ + + + +
+
+
+
+
onChange(form)} + schema={schema} + uiSchema={uiSchema} + FieldTemplate={templates.FieldTemplate} + ObjectFieldTemplate={templates.ObjectFieldTemplate} + ArrayFieldTemplate={templates.ArrayFieldTemplate} + fields={fields} + widgets={widgets} + liveValidate={true} + ErrorList={ErrorListTemplate}> + <> +
+
+
+
+ ) +} +/**/ \ No newline at end of file diff --git a/ui/src/app/admin/container/EditRole.js b/ui/src/app/admin/container/EditRole.js new file mode 100644 index 000000000..cc8d802ed --- /dev/null +++ b/ui/src/app/admin/container/EditRole.js @@ -0,0 +1,91 @@ +import React from 'react'; + +import { Prompt, useHistory } from 'react-router'; +import { useParams } from 'react-router-dom'; +import Translate from '../../i18n/components/translate'; +import { useRoles } from '../hooks'; +import { Schema } from '../../form/Schema'; +import { FormManager } from '../../form/FormManager'; + +import { RoleForm } from '../component/RoleForm'; +import { RoleProvider } from '../hoc/RoleProvider'; +import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; +import { useTranslator } from '../../i18n/hooks'; + +export function EditRole() { + + const { id } = useParams(); + + const notifier = useNotificationDispatcher(); + const translator = useTranslator(); + + const history = useHistory(); + + const { put, response, loading } = useRoles(); + + const [blocking, setBlocking] = React.useState(false); + + async function save(role) { + let toast; + const resp = await put(``, role); + if (response.ok) { + gotoDetail({ refresh: true }); + toast = createNotificationAction(`Updated role successfully.`, NotificationTypes.SUCCESS); + } else { + toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); + } + if (toast) { + notifier(toast); + } + }; + + const cancel = () => { + gotoDetail(); + }; + + const gotoDetail = (state = null) => { + setBlocking(false); + history.push(`/roles`, state); + }; + + return ( +
+ + `message.unsaved-editor` + } + /> +
+
+
+
+ Edit role +
+
+
+
+ + {(role) => + + {(schema) => + <>{role && + + {(data, errors) => + save(data)} + onCancel={() => cancel()} />} + + }} + + } + +
+
+
+ ); +} \ No newline at end of file diff --git a/ui/src/app/admin/container/MetadataActions.js b/ui/src/app/admin/container/MetadataActions.js index 80f43d56a..4500354f4 100644 --- a/ui/src/app/admin/container/MetadataActions.js +++ b/ui/src/app/admin/container/MetadataActions.js @@ -1,5 +1,5 @@ import React from 'react'; -import { DeleteConfirmation } from '../../metadata/component/DeleteConfirmation'; +import { DeleteConfirmation } from '../../core/components/DeleteConfirmation'; import { useMetadataEntity } from '../../metadata/hooks/api'; import { NotificationContext, createNotificationAction } from '../../notifications/hoc/Notifications'; diff --git a/ui/src/app/admin/container/NewRole.js b/ui/src/app/admin/container/NewRole.js new file mode 100644 index 000000000..6d1abfc56 --- /dev/null +++ b/ui/src/app/admin/container/NewRole.js @@ -0,0 +1,79 @@ +import React from 'react'; + +import { Prompt, useHistory } from 'react-router'; +import Translate from '../../i18n/components/translate'; +import { useRoles } from '../hooks'; +import { Schema } from '../../form/Schema'; +import { FormManager } from '../../form/FormManager'; +import { RoleForm } from '../component/RoleForm'; + +import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; +import { useTranslator } from '../../i18n/hooks'; + +export function NewRole() { + const history = useHistory(); + const notifier = useNotificationDispatcher(); + const translator = useTranslator(); + + const { post, response, loading } = useRoles({}); + + const [blocking, setBlocking] = React.useState(false); + + async function save(role) { + let toast; + const resp = await post(``, role); + if (response.ok) { + gotoDetail({ refresh: true }); + toast = createNotificationAction(`Added role successfully.`, NotificationTypes.SUCCESS); + } else { + toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); + } + if (toast) { + notifier(toast); + } + }; + + const cancel = () => { + gotoDetail(); + }; + + const gotoDetail = (state = null) => { + setBlocking(false); + history.push(`/roles`, state); + }; + + return ( +
+ + `message.unsaved-editor` + } + /> +
+
+
+
+ Add a new role +
+
+
+
+ + {(schema) => + + {(data, errors) => + save(data)} + onCancel={() => cancel()} />} + } + +
+
+
+ ); +} \ No newline at end of file diff --git a/ui/src/app/admin/container/RoleList.js b/ui/src/app/admin/container/RoleList.js new file mode 100644 index 000000000..e48851f90 --- /dev/null +++ b/ui/src/app/admin/container/RoleList.js @@ -0,0 +1,78 @@ +import React from 'react'; +import { faEdit, faPlusCircle, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import Button from 'react-bootstrap/Button'; +import { Link } from 'react-router-dom'; + +import { Translate } from '../../i18n/components/translate'; + +import { DeleteConfirmation } from '../../core/components/DeleteConfirmation'; + +export function RoleList({ roles, onDelete }) { + + const remove = (id) => { + onDelete(id); + } + + return ( + + {(block) => +
+
+
+
+ + Roles Management + +
+
+
+ +   + Add new role + +
+
+ + + + + + + + + {(roles?.length > 0) ? roles.map((role, i) => + + + + + ) : + + } + +
+ Role Name + Actions
{role.name} + + + + Edit + + + +
No roles defined.
+
+
+
+
+
+ } +
+ ); +} \ No newline at end of file diff --git a/ui/src/app/admin/hoc/RoleProvider.js b/ui/src/app/admin/hoc/RoleProvider.js new file mode 100644 index 000000000..ce4000cf8 --- /dev/null +++ b/ui/src/app/admin/hoc/RoleProvider.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { useRole } from '../hooks'; + +export function RoleProvider({ id, children }) { + + const [role, setRole] = React.useState(); + const { get, response } = useRole(id); + + async function loadRole() { + const role = await get(``); + if (response.ok) { + setRole(role); + } + } + + /*eslint-disable react-hooks/exhaustive-deps*/ + React.useEffect(() => { loadRole() }, []); + + return (<>{children(role)}); +} \ No newline at end of file diff --git a/ui/src/app/admin/hoc/RolesProvider.js b/ui/src/app/admin/hoc/RolesProvider.js new file mode 100644 index 000000000..e87d96539 --- /dev/null +++ b/ui/src/app/admin/hoc/RolesProvider.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { useRoles } from '../hooks'; +import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; +import { useTranslator } from '../../i18n/hooks'; + +export function RolesProvider({ children, cache = 'no-cache' }) { + + const [roles, setRoles] = React.useState([]); + + const notifier = useNotificationDispatcher(); + const translator = useTranslator(); + + const { get, del, response, loading } = useRoles({ + cachePolicy: cache + }); + + async function loadRoles() { + const list = await get(``); + if (response.ok) { + setRoles(list); + } + } + + async function removeRole(id) { + let toast; + const resp = await del(`/${id}`); + if (response.ok) { + loadRoles(); + toast = createNotificationAction(`Deleted role successfully.`, NotificationTypes.SUCCESS); + } else { + toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); + } + if (toast) { + notifier(toast); + } + } + + /*eslint-disable react-hooks/exhaustive-deps*/ + React.useEffect(() => { loadRoles() }, []); + + return (<>{children(roles, removeRole, loading)}); +} \ No newline at end of file diff --git a/ui/src/app/admin/hooks.js b/ui/src/app/admin/hooks.js new file mode 100644 index 000000000..919be8e4e --- /dev/null +++ b/ui/src/app/admin/hooks.js @@ -0,0 +1,20 @@ +import useFetch from 'use-http'; +import API_BASE_PATH from '../App.constant'; + +export function useRoles(opts = { cachePolicy: 'no-cache' }) { + return useFetch(`${API_BASE_PATH}/admin/roles`, opts); +} + +export function useRole(id) { + return useFetch(`${API_BASE_PATH}/admin/roles/${id}`, { + cachePolicy: 'no-cache' + }); +} + +export function useRoleUiSchema() { + return { + description: { + 'ui:widget': 'textarea' + } + }; +} \ No newline at end of file diff --git a/ui/src/app/metadata/component/DeleteConfirmation.js b/ui/src/app/core/components/DeleteConfirmation.js similarity index 93% rename from ui/src/app/metadata/component/DeleteConfirmation.js rename to ui/src/app/core/components/DeleteConfirmation.js index a0f2f8c09..ac699063e 100644 --- a/ui/src/app/metadata/component/DeleteConfirmation.js +++ b/ui/src/app/core/components/DeleteConfirmation.js @@ -37,7 +37,7 @@ export function DeleteConfirmation({ children, body, title }) {

- You are deleting an entity. This cannot be undone. Continue? + You are deleting an entity. This cannot be undone. Continue?

diff --git a/ui/src/app/metadata/component/DeleteConfirmation.test.js b/ui/src/app/core/components/DeleteConfirmation.test.js similarity index 97% rename from ui/src/app/metadata/component/DeleteConfirmation.test.js rename to ui/src/app/core/components/DeleteConfirmation.test.js index 8aa4a57ee..76abf8dd6 100644 --- a/ui/src/app/metadata/component/DeleteConfirmation.test.js +++ b/ui/src/app/core/components/DeleteConfirmation.test.js @@ -28,7 +28,7 @@ test('Delete confirmation', () => { {(block) => } , - container); + container); }); const initiator = container.querySelector('button'); diff --git a/ui/src/app/core/components/Header.js b/ui/src/app/core/components/Header.js index 54d0c402c..b464dffe6 100644 --- a/ui/src/app/core/components/Header.js +++ b/ui/src/app/core/components/Header.js @@ -6,7 +6,7 @@ import Navbar from 'react-bootstrap/Navbar'; import Dropdown from 'react-bootstrap/Dropdown'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTh, faSignOutAlt, faPlusCircle, faCube, faCubes } from '@fortawesome/free-solid-svg-icons'; +import { faTh, faSignOutAlt, faPlusCircle, faCube, faCubes, faUserTag } from '@fortawesome/free-solid-svg-icons'; import Translate from '../../i18n/components/translate'; import { useTranslator } from '../../i18n/hooks'; @@ -41,6 +41,10 @@ export function Header () { + + + + } diff --git a/ui/src/app/core/components/UserConfirmation.test.js b/ui/src/app/core/components/UserConfirmation.test.js index 9c2835d9b..58f73c3b8 100644 --- a/ui/src/app/core/components/UserConfirmation.test.js +++ b/ui/src/app/core/components/UserConfirmation.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { act, render, fireEvent, waitFor, screen } from '@testing-library/react'; +import { render, fireEvent, screen } from '@testing-library/react'; import { UserConfirmation, ConfirmWindow } from './UserConfirmation'; jest.mock('../../i18n/hooks', () => ({ diff --git a/ui/src/app/form/FormManager.js b/ui/src/app/form/FormManager.js new file mode 100644 index 000000000..41b39d520 --- /dev/null +++ b/ui/src/app/form/FormManager.js @@ -0,0 +1,105 @@ +import React from 'react'; + +const initialState = { + data: {}, + errors: [] +}; + +const FormContext = React.createContext(); + +const { Provider, Consumer } = FormContext; + +export const FormActions = { + SET_FORM_ERROR: 'set form error', + SET_FORM_DATA: 'set form data' +}; + +export const setFormDataAction = (payload) => { + return { + type: FormActions.SET_FORM_DATA, + payload + } +} + +export const setFormErrorAction = (errors) => { + return { + type: FormActions.SET_FORM_ERROR, + payload: errors + } +} + +function reducer(state, action) { + switch (action.type) { + + case FormActions.SET_FORM_ERROR: + return { + ...state, + errors: action.payload + }; + case FormActions.SET_FORM_DATA: + return { + ...state, + data: action.payload + }; + default: + return state; + } +} + +/*eslint-disable react-hooks/exhaustive-deps*/ +function FormManager({ children, initial = {} }) { + + const data = { + ...initial + }; + + const [state, dispatch] = React.useReducer(reducer, { + ...initialState, + data + }); + const contextValue = React.useMemo(() => ({ state, dispatch }), [state, dispatch]); + + return ( + + {children(state.data, state.errors)} + + ); +} + +function useFormErrors() { + const { state } = React.useContext(FormContext); + const { errors } = state; + + return errors; +} + +function useFormContext() { + return React.useContext(FormContext); +} + +function useFormDispatcher() { + const { dispatch } = useFormContext(); + return dispatch; +} + +function useFormState() { + const { state } = useFormContext(); + return state; +} + +function useFormData() { + const { data } = useFormContext(); + return data; +} + +export { + useFormErrors, + useFormContext, + useFormDispatcher, + useFormState, + useFormData, + FormManager, + FormContext, + Provider as FormProvider, + Consumer as FormConsumer +}; \ No newline at end of file diff --git a/ui/src/app/form/Schema.js b/ui/src/app/form/Schema.js new file mode 100644 index 000000000..92aefe325 --- /dev/null +++ b/ui/src/app/form/Schema.js @@ -0,0 +1,24 @@ +import React from 'react'; +import useFetch from 'use-http'; + +export function Schema({ path, children }) { + + const [schema, setSchema] = React.useState({}); + + + const { get, response } = useFetch(path, { + cachePolicy: 'no-cache' + }); + + async function loadSchema() { + const list = await get(``); + if (response.ok) { + setSchema(list); + } + } + + /*eslint-disable react-hooks/exhaustive-deps*/ + React.useEffect(() => { loadSchema() }, []); + + return (<>{children(schema)}); +} \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilters.js b/ui/src/app/metadata/domain/filter/component/MetadataFilters.js index d38c97013..26c63f131 100644 --- a/ui/src/app/metadata/domain/filter/component/MetadataFilters.js +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilters.js @@ -1,6 +1,6 @@ import React from 'react'; import { useMetadataFilters } from '../../../hooks/api'; -import { DeleteConfirmation } from '../../../component/DeleteConfirmation'; +import { DeleteConfirmation } from '../../../../core/components/DeleteConfirmation'; import { NotificationContext, createNotificationAction } from '../../../../notifications/hoc/Notifications'; export const MetadataFiltersContext = React.createContext(); diff --git a/ui/src/app/metadata/view/MetadataAttributeList.js b/ui/src/app/metadata/view/MetadataAttributeList.js index f585f805f..69e6754f6 100644 --- a/ui/src/app/metadata/view/MetadataAttributeList.js +++ b/ui/src/app/metadata/view/MetadataAttributeList.js @@ -8,7 +8,7 @@ import { Link } from 'react-router-dom'; import { Translate } from '../../i18n/components/translate'; import { useTranslator } from '../../i18n/hooks'; -import { DeleteConfirmation } from '../component/DeleteConfirmation'; +import { DeleteConfirmation } from '../../core/components/DeleteConfirmation'; export function MetadataAttributeList ({entities, onDelete}) { From 7f4eca829925cb2cd8b268f7e6e59dbdffd7e9bf Mon Sep 17 00:00:00 2001 From: chasegawa Date: Thu, 29 Jul 2021 11:56:35 -0700 Subject: [PATCH 06/24] SHIBUI-2001 mid-work save --- .../ui/controller/ActivateController.java | 37 +++++++-- .../controller/ActivateExceptionHandler.java | 32 ++++++++ .../ui/exception/ForbiddenException.java | 11 +++ .../ui/security/service/UserService.java | 22 +++-- .../ui/service/EntityDescriptorService.java | 5 ++ .../admin/ui/service/FilterService.java | 7 ++ .../JPAEntityDescriptorServiceImpl.java | 22 ++++- .../ui/service/JPAFilterServiceImpl.java | 80 +++++++++++++++++-- 8 files changed, 195 insertions(+), 21 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/ForbiddenException.java diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java index 10b0a742b..1a3ecf0a4 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java @@ -1,23 +1,46 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; +import javax.script.ScriptException; + import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; -import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; +import edu.internet2.tier.shibboleth.admin.ui.service.FilterService; @RestController @RequestMapping("/api/activate") public class ActivateController { + @Autowired - private UserService userService; + private EntityDescriptorService entityDescriptorService; @Autowired - private EntityDescriptorRepository entityDescriptorRepo; - - + private FilterService filterService; + @PatchMapping(path = "/entityDescriptor/{resourceId}/{mode}") + @Transactional + public ResponseEntity enableEntityDescriptor(@PathVariable String resourceId, @PathVariable String mode) throws EntityNotFoundException, ForbiddenException { + boolean status = "enable".equalsIgnoreCase(mode); + EntityDescriptorRepresentation edr = entityDescriptorService.updateEntityDescriptorEnabledStatus(resourceId, status); + return ResponseEntity.ok(edr); + } -// Enable/disable for : entity descriptor, provider, filter + @PatchMapping(path = "/MetadataResolvers/{metadataResolverId}/Filter/{resourceId}/{mode}") + @Transactional + public ResponseEntity enableFilter(@PathVariable String metadataResolverId, @PathVariable String resourceId, @PathVariable String mode) throws EntityNotFoundException, ForbiddenException, ScriptException { + boolean status = "enable".equalsIgnoreCase(mode); + MetadataFilter persistedFilter = filterService.updateFilterEnabledStatus(metadataResolverId, resourceId, status); + return ResponseEntity.ok(persistedFilter); + } +// Enable/disable for : , provider } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java new file mode 100644 index 000000000..85264b582 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java @@ -0,0 +1,32 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import javax.script.ScriptException; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; + +@ControllerAdvice(assignableTypes = {ActivateController.class}) +public class ActivateExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler({ EntityNotFoundException.class }) + public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(HttpStatus.NOT_FOUND, e.getMessage())); + } + + @ExceptionHandler({ ForbiddenException.class }) + public ResponseEntity handleForbiddenAccess(ForbiddenException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(String.valueOf(HttpStatus.FORBIDDEN.value()), e.getMessage())); + } + + @ExceptionHandler({ ScriptException.class }) + public ResponseEntity handleScriptException(ScriptException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), e.getMessage())); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/ForbiddenException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/ForbiddenException.java new file mode 100644 index 000000000..66f2e61c5 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/ForbiddenException.java @@ -0,0 +1,11 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class ForbiddenException extends Exception { + public ForbiddenException() { + super("You are not authorized to perform the requested operation."); + } + + public ForbiddenException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java index a2d11a1ab..898e99e00 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java @@ -1,15 +1,17 @@ package edu.internet2.tier.shibboleth.admin.ui.security.service; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.apache.commons.lang.StringUtils; +import org.springframework.security.core.context.SecurityContextHolder; + import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; import edu.internet2.tier.shibboleth.admin.ui.security.model.User; import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository; import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; -import org.apache.commons.lang.StringUtils; -import org.springframework.security.core.context.SecurityContextHolder; - -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; /** * @author Bill Smith (wsmith@unicon.net) @@ -70,4 +72,12 @@ public Set getUserRoles(String username) { } return result; } + + /** + * Current logic is pretty dumb, this will need to change/expand once a user can have more than one role. + */ + public boolean currentUserHasExpectedRole(List acceptedRoles) { + User user = getCurrentUser(); + return acceptedRoles.contains(user.getRole()); + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java index ea57c4d06..5d635d493 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java @@ -2,6 +2,9 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; + import org.opensaml.saml.saml2.metadata.EntityDescriptor; import java.util.List; @@ -54,4 +57,6 @@ public interface EntityDescriptorService { */ Map getRelyingPartyOverridesRepresentationFromAttributeList(List attributeList); + EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String resourceId, boolean status) throws EntityNotFoundException, ForbiddenException; + } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java index 2ef9ab08e..6d752928b 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java @@ -1,7 +1,12 @@ package edu.internet2.tier.shibboleth.admin.ui.service; +import javax.script.ScriptException; + import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; /** * Main backend facade API that defines operations pertaining to manipulating {@link EntityAttributesFilter} objects. @@ -25,4 +30,6 @@ public interface FilterService { * @return FilterRepresentation front end representation */ FilterRepresentation createRepresentationFromFilter(final EntityAttributesFilter entityAttributesFilter); + + MetadataFilter updateFilterEnabledStatus(String metadataResolverId, String resourceId, boolean status) throws EntityNotFoundException, ForbiddenException, ScriptException; } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java index 05477d616..ab93343c8 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java @@ -42,7 +42,10 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSsoDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import edu.internet2.tier.shibboleth.admin.util.MDDCConstants; import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; @@ -76,8 +79,8 @@ * @since 1.0 */ public class JPAEntityDescriptorServiceImpl implements EntityDescriptorService { - - private static final Logger LOGGER = LoggerFactory.getLogger(JPAEntityDescriptorServiceImpl.class); + @Autowired + private EntityDescriptorRepository entityDescriptorRepository; @Autowired private OpenSamlObjects openSamlObjects; @@ -709,4 +712,19 @@ public List getAttributeReleaseListFromAttributeList(List att public Map getRelyingPartyOverridesRepresentationFromAttributeList(List attributeList) { return ModelRepresentationConversions.getRelyingPartyOverridesRepresentationFromAttributeList(attributeList); } + + @Override + public EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String resourceId, boolean status) throws EntityNotFoundException, ForbiddenException { + EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); + if (ed == null) { + throw new EntityNotFoundException("Entity with resourceid[" + resourceId + "] was not found for update"); + } + // @TODO: when merged with groups, this should maybe be merged with group check as they have to have the role in the right group + if (!userService.currentUserHasExpectedRole(Arrays.asList(new String[] { "ROLE_ADMIN", "ROLE_ENABLE" }))) { + throw new ForbiddenException("You do not have the permissions necessary to change the enable status of this entity descriptor."); + } + ed.setServiceEnabled(status); + ed = entityDescriptorRepository.save(ed); + return createRepresentationFromDescriptor(ed); + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java index 0693b538a..6810fa243 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java @@ -1,13 +1,24 @@ package edu.internet2.tier.shibboleth.admin.ui.service; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; + import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.interceptor.TransactionAspectSupport; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Optional; + +import javax.script.ScriptException; /** * Default implementation of {@link FilterService} @@ -16,18 +27,27 @@ * @author Bill Smith (wsmith@unicon.net) */ public class JPAFilterServiceImpl implements FilterService { - - private static final Logger LOGGER = LoggerFactory.getLogger(JPAFilterServiceImpl.class); - @Autowired EntityDescriptorService entityDescriptorService; - + @Autowired EntityService entityService; + @Autowired + FilterRepository filterRepository; + @Autowired FilterTargetService filterTargetService; + + @Autowired + private MetadataResolverRepository metadataResolverRepository; + + @Autowired + private MetadataResolverService metadataResolverService; + @Autowired + private UserService userService; + @Override public EntityAttributesFilter createFilterFromRepresentation(FilterRepresentation representation) { //TODO? use OpenSamlObjects.buildDefaultInstanceOfType(EntityAttributesFilter.class)? @@ -66,4 +86,52 @@ public FilterRepresentation createRepresentationFromFilter(EntityAttributesFilte representation.setVersion(entityAttributesFilter.hashCode()); return representation; } + + private void reloadFiltersAndHandleScriptException(String resolverResourceId) throws ScriptException { + try { + metadataResolverService.reloadFilters(resolverResourceId); + } catch (Throwable ex) { + //explicitly mark transaction for rollback when we get ScriptException as we call reloadFilters + //after persistence call. Then re-throw the exception with pertinent message + if (ex instanceof ScriptException) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + throw new ScriptException("Caught invalid script parsing error when reloading filters. Please fix the script data"); + } + } + } + + /** + * Logic taken directly from the MetadataFiltersController and then modified slightly. + */ + @Override + public MetadataFilter updateFilterEnabledStatus(String metadataResolverId, String resourceId, boolean status) + throws EntityNotFoundException, ForbiddenException, ScriptException { + + MetadataResolver metadataResolver = metadataResolverRepository.findByResourceId(metadataResolverId); + // Now we operate directly on the filter attached to MetadataResolver, + // Instead of fetching filter separately, to accommodate correct envers versioning with uni-directional one-to-many + Optional filterTobeUpdatedOptional = metadataResolver.getMetadataFilters().stream() + .filter(it -> it.getResourceId().equals(resourceId)).findFirst(); + if (filterTobeUpdatedOptional.isEmpty()) { + throw new EntityNotFoundException("Filter with resource id[" + resourceId + "] not found"); + } + + // @TODO: when merged with groups, this should maybe be merged with group check as they have to have the role in the right group + if (!userService.currentUserHasExpectedRole(Arrays.asList(new String[] { "ROLE_ADMIN", "ROLE_ENABLE" }))) { + throw new ForbiddenException("You do not have the permissions necessary to change the enable status of this filter."); + } + + MetadataFilter filterTobeUpdated = filterTobeUpdatedOptional.get(); + filterTobeUpdated.setFilterEnabled(status); + MetadataFilter persistedFilter = filterRepository.save(filterTobeUpdated); + + // To support envers versioning from MetadataResolver side + metadataResolver.markAsModified(); + metadataResolverRepository.save(metadataResolver); + + // TODO: do we need to reload filters here? + reloadFiltersAndHandleScriptException(metadataResolver.getResourceId()); + + return persistedFilter; + } } From 43ea0b8a5cb7d3d8c609d9a4d59a724e2dcb6d1c Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 29 Jul 2021 12:42:44 -0700 Subject: [PATCH 07/24] Added tests --- ui/src/app/admin/hoc/RoleProvider.js | 2 +- ui/src/app/admin/hoc/RoleProvider.test.js | 35 ++++++++++++++ ui/src/app/admin/hoc/RolesProvider.test.js | 43 +++++++++++++++++ ui/src/app/admin/hooks.js | 6 +-- ui/src/app/admin/hooks.test.js | 55 ++++++++++++++++++++++ ui/src/app/metadata/hooks/schema.js | 17 +------ ui/src/testing/sourceSchema.js | 2 +- ui/src/testing/uiSchema.js | 2 - 8 files changed, 137 insertions(+), 25 deletions(-) create mode 100644 ui/src/app/admin/hoc/RoleProvider.test.js create mode 100644 ui/src/app/admin/hoc/RolesProvider.test.js create mode 100644 ui/src/app/admin/hooks.test.js diff --git a/ui/src/app/admin/hoc/RoleProvider.js b/ui/src/app/admin/hoc/RoleProvider.js index ce4000cf8..091ac1bd6 100644 --- a/ui/src/app/admin/hoc/RoleProvider.js +++ b/ui/src/app/admin/hoc/RoleProvider.js @@ -3,7 +3,7 @@ import { useRole } from '../hooks'; export function RoleProvider({ id, children }) { - const [role, setRole] = React.useState(); + const [role, setRole] = React.useState({id: 'foo'}); const { get, response } = useRole(id); async function loadRole() { diff --git a/ui/src/app/admin/hoc/RoleProvider.test.js b/ui/src/app/admin/hoc/RoleProvider.test.js new file mode 100644 index 000000000..9c063a41a --- /dev/null +++ b/ui/src/app/admin/hoc/RoleProvider.test.js @@ -0,0 +1,35 @@ +import { render, screen } from "@testing-library/react"; +import { act } from 'react-dom/test-utils'; +import React from 'react'; +import {RoleProvider} from './RoleProvider'; + +import {useRole} from '../hooks'; + +jest.mock('../hooks'); + +describe('RoleProvider component', () => { + + beforeEach(() => { + useRole.mockImplementation(() => { + return { + get: jest.fn().mockResolvedValue({ id: 'foo' }), + response: { + ok: false, + status: 200 + } + }; + }); + }) + + test('should provide the role context', async () => { + act(() => { + render( + {(role) =>
{role?.id}
} +
); + }); + + expect(useRole).toHaveBeenCalled(); + expect(screen.getByText('foo')).toBeInTheDocument(); + }); + +}) \ No newline at end of file diff --git a/ui/src/app/admin/hoc/RolesProvider.test.js b/ui/src/app/admin/hoc/RolesProvider.test.js new file mode 100644 index 000000000..11c01481e --- /dev/null +++ b/ui/src/app/admin/hoc/RolesProvider.test.js @@ -0,0 +1,43 @@ +import { render, screen } from "@testing-library/react"; +import { act } from 'react-dom/test-utils'; +import React from 'react'; +import { RolesProvider } from './RolesProvider'; + +import { useRoles } from '../hooks'; +import { useNotificationDispatcher } from "../../notifications/hoc/Notifications"; + +jest.mock('../hooks'); + +jest.mock('../../notifications/hoc/Notifications'); + +describe('RolesProvider component', () => { + + beforeEach(() => { + + useNotificationDispatcher.mockImplementation(() => { + return {}; + }); + + useRoles.mockImplementation(() => { + return { + get: jest.fn().mockResolvedValue([]), + response: { + ok: false, + status: 200 + } + }; + }); + }) + + test('should provide the roles context', async () => { + act(() => { + render( + {(roles) =>
{roles.length}
} +
); + }); + + expect(useRoles).toHaveBeenCalled(); + expect(screen.getByText('0')).toBeInTheDocument(); + }); + +}) \ No newline at end of file diff --git a/ui/src/app/admin/hooks.js b/ui/src/app/admin/hooks.js index 919be8e4e..30e840dc8 100644 --- a/ui/src/app/admin/hooks.js +++ b/ui/src/app/admin/hooks.js @@ -12,9 +12,5 @@ export function useRole(id) { } export function useRoleUiSchema() { - return { - description: { - 'ui:widget': 'textarea' - } - }; + return {}; } \ No newline at end of file diff --git a/ui/src/app/admin/hooks.test.js b/ui/src/app/admin/hooks.test.js new file mode 100644 index 000000000..d739c9aa1 --- /dev/null +++ b/ui/src/app/admin/hooks.test.js @@ -0,0 +1,55 @@ +import { + useRoles, + useRole, +} from './hooks'; + +import useFetch from 'use-http'; +import API_BASE_PATH from '../App.constant'; + +jest.mock('use-http'); + +describe('api hooks', () => { + + let mockPut; + let mockGet; + + beforeEach(() => { + + mockPut = jest.fn().mockResolvedValue({ response: { ok: true } }); + mockGet = jest.fn().mockResolvedValue({ response: { ok: true } }); + + useFetch.mockImplementation(() => { + return { + request: { + ok: true + }, + put: mockPut, + get: mockGet, + error: null, + response: { + status: 409 + } + }; + }); + }) + + describe('useRoles', () => { + it('should call useFetch', () => { + const opts = {}; + const roles = useRoles(opts); + + expect(useFetch).toHaveBeenCalledWith(`${API_BASE_PATH}/admin/roles`, opts) + }); + }); + + describe('useRole', () => { + it('should call useFetch', () => { + const opts = { + cachePolicy: 'no-cache' + }; + const role = useRole('foo'); + + expect(useFetch).toHaveBeenCalledWith(`${API_BASE_PATH}/admin/roles/foo`, opts) + }); + }); +}); \ No newline at end of file diff --git a/ui/src/app/metadata/hooks/schema.js b/ui/src/app/metadata/hooks/schema.js index ecde47f05..5083559b1 100644 --- a/ui/src/app/metadata/hooks/schema.js +++ b/ui/src/app/metadata/hooks/schema.js @@ -1,5 +1,4 @@ import React from 'react'; -import { useIsAdmin } from '../../core/user/UserContext'; export const fillInRootProperties = (keys, ui) => keys.reduce((sch, key, idx) => { if (!sch.hasOwnProperty(key)) { @@ -31,21 +30,7 @@ export function useUiSchema(definition, schema, current, locked = true) { } ), [mapped, step.locked, locked]); - const isAdmin = useIsAdmin(); - - const hideEnableFromNonAdmins = React.useMemo(() => { - if (!isAdmin) { - return { - ...isLocked, - serviceEnabled: { - 'ui:widget': 'hidden' - } - }; - } - return isLocked; - }, [isAdmin, isLocked]); - - return { uiSchema: hideEnableFromNonAdmins, step}; + return { uiSchema: isLocked, step}; } diff --git a/ui/src/testing/sourceSchema.js b/ui/src/testing/sourceSchema.js index 93262c5f4..c723fd548 100644 --- a/ui/src/testing/sourceSchema.js +++ b/ui/src/testing/sourceSchema.js @@ -1,3 +1,3 @@ -const SCHEMA = { "type": "object", "required": ["serviceProviderName", "entityId"], "properties": { "serviceProviderName": { "title": "label.service-provider-name", "description": "tooltip.service-provider-name", "type": "string", "minLength": 1, "maxLength": 255 }, "entityId": { "title": "label.entity-id", "description": "tooltip.entity-id", "type": "string", "minLength": 1, "maxLength": 255 }, "serviceEnabled": { "title": "label.enable-this-service", "description": "tooltip.enable-this-service-upon-saving", "type": "boolean", "default": false }, "organization": { "$ref": "#/definitions/Organization" }, "contacts": { "title": "label.contact-information", "description": "tooltip.contact-information", "type": "array", "items": { "$ref": "#/definitions/Contact" } }, "mdui": { "$ref": "#/definitions/MDUI" }, "securityInfo": { "type": "object", "widget": { "id": "fieldset" }, "dependencies": { "authenticationRequestsSigned": { "oneOf": [{ "properties": { "authenticationRequestsSigned": { "enum": [true] }, "x509Certificates": { "minItems": 1 } } }, { "properties": { "authenticationRequestsSigned": { "enum": [false] }, "x509Certificates": { "minItems": 0 } } }] } }, "properties": { "x509CertificateAvailable": { "type": "boolean", "default": true }, "authenticationRequestsSigned": { "title": "label.authentication-requests-signed", "description": "tooltip.authentication-requests-signed", "type": "boolean", "enumNames": ["value.true", "value.false"] }, "wantAssertionsSigned": { "title": "label.want-assertions-signed", "description": "tooltip.want-assertions-signed", "type": "boolean", "enumNames": ["value.true", "value.false"] }, "x509Certificates": { "title": "label.x509-certificates", "type": "array", "items": { "$ref": "#/definitions/Certificate" } } } }, "assertionConsumerServices": { "title": "label.assertion-consumer-service-endpoints", "description": "", "type": "array", "items": { "$ref": "#/definitions/AssertionConsumerService" } }, "serviceProviderSsoDescriptor": { "type": "object", "properties": { "protocolSupportEnum": { "title": "label.protocol-support-enumeration", "description": "tooltip.protocol-support-enumeration", "type": "string", "widget": { "id": "select" }, "oneOf": [{ "enum": ["SAML 2"], "description": "SAML 2" }, { "enum": ["SAML 1.1"], "description": "SAML 1.1" }] }, "nameIdFormats": { "$ref": "#/definitions/nameIdFormats" } }, "dependencies": { "nameIdFormats": ["protocolSupportEnum"] } }, "logoutEndpoints": { "title": "label.logout-endpoints", "description": "tooltip.logout-endpoints", "type": "array", "items": { "$ref": "#/definitions/LogoutEndpoint" } }, "relyingPartyOverrides": { "type": "object", "properties": { "signAssertion": { "title": "label.sign-the-assertion", "description": "tooltip.sign-assertion", "type": "boolean", "default": false }, "dontSignResponse": { "title": "label.dont-sign-the-response", "description": "tooltip.dont-sign-response", "type": "boolean", "default": false }, "turnOffEncryption": { "title": "label.turn-off-encryption-of-response", "description": "tooltip.turn-off-encryption", "type": "boolean", "default": false }, "useSha": { "title": "label.use-sha1-signing-algorithm", "description": "tooltip.usa-sha-algorithm", "type": "boolean", "default": false }, "ignoreAuthenticationMethod": { "title": "label.ignore-any-sp-requested-authentication-method", "description": "tooltip.ignore-auth-method", "type": "boolean", "default": false }, "omitNotBefore": { "title": "label.omit-not-before-condition", "description": "tooltip.omit-not-before-condition", "type": "boolean", "default": false }, "responderId": { "title": "label.responder-id", "description": "tooltip.responder-id", "type": "string", "default": "" }, "nameIdFormats": { "$ref": "#/definitions/nameIdFormats" }, "authenticationMethods": { "$ref": "#/definitions/authenticationMethods" }, "forceAuthn": { "title": "label.force-authn", "description": "tooltip.force-authn", "type": "boolean", "default": false } } }, "attributeRelease": { "type": "array", "title": "label.attribute-release", "description": "Attribute release table - select the attributes you want to release (default unchecked)", "items": { "type": "string", "enum": ["eduPersonPrincipalName", "uid", "mail", "surname", "givenName", "eduPersonAffiliation", "eduPersonScopedAffiliation", "eduPersonPrimaryAffiliation", "eduPersonEntitlement", "eduPersonAssurance", "eduPersonUniqueId", "employeeNumber"] }, "uniqueItems": true } }, "definitions": { "Contact": { "type": "object", "required": ["name", "type", "emailAddress"], "properties": { "name": { "title": "label.contact-name", "description": "tooltip.contact-name", "type": "string", "minLength": 1, "maxLength": 255 }, "type": { "title": "label.contact-type", "description": "tooltip.contact-type", "type": "string", "widget": "select", "minLength": 1, "oneOf": [{ "enum": ["support"], "description": "value.support" }, { "enum": ["technical"], "description": "value.technical" }, { "enum": ["administrative"], "description": "value.administrative" }, { "enum": ["other"], "description": "value.other" }] }, "emailAddress": { "title": "label.contact-email-address", "description": "tooltip.contact-email", "type": "string", "pattern": "^(mailto:)?(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$", "minLength": 1, "maxLength": 255 } } }, "Certificate": { "type": "object", "required": ["type", "value"], "properties": { "name": { "title": "label.certificate-name-display-only", "description": "tooltip.certificate-name", "type": "string", "maxLength": 255 }, "type": { "title": "label.certificate-type", "type": "string", "widget": { "id": "radio", "class": "form-check-inline" }, "oneOf": [{ "enum": ["signing"], "description": "value.signing" }, { "enum": ["encryption"], "description": "value.encryption" }, { "enum": ["both"], "description": "value.both" }] }, "value": { "title": "label.certificate", "description": "tooltip.certificate", "type": "string", "widget": "textarea", "minLength": 1 } } }, "AssertionConsumerService": { "type": "object", "required": ["locationUrl", "binding"], "properties": { "locationUrl": { "title": "label.assertion-consumer-service-location", "description": "tooltip.assertion-consumer-service-location", "type": "string", "widget": { "id": "string", "help": "message.valid-url" }, "minLength": 1, "maxLength": 255 }, "binding": { "title": "label.assertion-consumer-service-location-binding", "description": "tooltip.assertion-consumer-service-location-binding", "type": "string", "widget": "select", "oneOf": [{ "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:PAOS"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:PAOS" }, { "enum": ["urn:oasis:names:tc:SAML:1.0:profiles:browser-post"], "description": "urn:oasis:names:tc:SAML:1.0:profiles:browser-post" }, { "enum": ["urn:oasis:names:tc:SAML:1.0:profiles:artifact-01"], "description": "urn:oasis:names:tc:SAML:1.0:profiles:artifact-01" }] }, "makeDefault": { "title": "label.mark-as-default", "description": "tooltip.mark-as-default", "type": "boolean" } } }, "LogoutEndpoint": { "description": "tooltip.new-endpoint", "type": "object", "fieldsets": [{ "fields": ["url", "bindingType"] }], "required": ["url", "bindingType"], "properties": { "url": { "title": "label.url", "description": "tooltip.url", "type": "string", "minLength": 1, "maxLength": 255 }, "bindingType": { "title": "label.binding-type", "description": "tooltip.binding-type", "type": "string", "widget": "select", "oneOf": [{ "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:SOAP"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" }] } } }, "MDUI": { "type": "object", "widget": { "id": "fieldset" }, "fieldsets": [{ "type": "group", "fields": ["displayName", "informationUrl", "description"] }, { "type": "group", "fields": ["privacyStatementUrl", "logoUrl", "logoWidth", "logoHeight"] }], "properties": { "displayName": { "title": "label.display-name", "description": "tooltip.mdui-display-name", "type": "string", "minLength": 1, "maxLength": 255 }, "informationUrl": { "title": "label.information-url", "description": "tooltip.mdui-information-url", "type": "string", "minLength": 1, "maxLength": 255 }, "privacyStatementUrl": { "title": "label.privacy-statement-url", "description": "tooltip.mdui-privacy-statement-url", "type": "string", "minLength": 1, "maxLength": 255 }, "description": { "title": "label.description", "description": "tooltip.mdui-description", "type": "string", "widget": { "id": "textarea" }, "minLength": 1, "maxLength": 255 }, "logoUrl": { "title": "label.logo-url", "description": "tooltip.mdui-logo-url", "type": "string", "minLength": 1, "maxLength": 255 }, "logoHeight": { "title": "label.logo-height", "description": "tooltip.mdui-logo-height", "minimum": 0, "type": "integer" }, "logoWidth": { "title": "label.logo-width", "description": "tooltip.mdui-logo-width", "minimum": 0, "type": "integer" } } }, "Organization": { "type": "object", "properties": { "name": { "title": "label.organization-name", "description": "tooltip.organization-name", "type": "string", "minLength": 1, "maxLength": 255 }, "displayName": { "title": "label.organization-display-name", "description": "tooltip.organization-display-name", "type": "string", "minLength": 1, "maxLength": 255 }, "url": { "title": "label.organization-url", "description": "tooltip.organization-url", "type": "string", "minLength": 1, "maxLength": 255 } }, "dependencies": { "name": { "required": ["displayName", "url"] }, "displayName": { "required": ["name", "url"] }, "url": { "required": ["name", "displayName"] } } }, "nameIdFormats": { "title": "label.nameid-format-to-send", "description": "tooltip.nameid-format", "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 255, "examples": ["urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"] } }, "authenticationMethods": { "title": "label.authentication-methods-to-use", "description": "tooltip.authentication-methods-to-use", "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 255, "examples": ["https://refeds.org/profile/mfa", "urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken", "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"] } } } }; +const SCHEMA = { "type": "object", "required": ["serviceProviderName", "entityId"], "properties": { "serviceProviderName": { "title": "label.service-provider-name", "description": "tooltip.service-provider-name", "type": "string", "minLength": 1, "maxLength": 255 }, "entityId": { "title": "label.entity-id", "description": "tooltip.entity-id", "type": "string", "minLength": 1, "maxLength": 255 }, "organization": { "$ref": "#/definitions/Organization" }, "contacts": { "title": "label.contact-information", "description": "tooltip.contact-information", "type": "array", "items": { "$ref": "#/definitions/Contact" } }, "mdui": { "$ref": "#/definitions/MDUI" }, "securityInfo": { "type": "object", "widget": { "id": "fieldset" }, "dependencies": { "authenticationRequestsSigned": { "oneOf": [{ "properties": { "authenticationRequestsSigned": { "enum": [true] }, "x509Certificates": { "minItems": 1 } } }, { "properties": { "authenticationRequestsSigned": { "enum": [false] }, "x509Certificates": { "minItems": 0 } } }] } }, "properties": { "x509CertificateAvailable": { "type": "boolean", "default": true }, "authenticationRequestsSigned": { "title": "label.authentication-requests-signed", "description": "tooltip.authentication-requests-signed", "type": "boolean", "enumNames": ["value.true", "value.false"] }, "wantAssertionsSigned": { "title": "label.want-assertions-signed", "description": "tooltip.want-assertions-signed", "type": "boolean", "enumNames": ["value.true", "value.false"] }, "x509Certificates": { "title": "label.x509-certificates", "type": "array", "items": { "$ref": "#/definitions/Certificate" } } } }, "assertionConsumerServices": { "title": "label.assertion-consumer-service-endpoints", "description": "", "type": "array", "items": { "$ref": "#/definitions/AssertionConsumerService" } }, "serviceProviderSsoDescriptor": { "type": "object", "properties": { "protocolSupportEnum": { "title": "label.protocol-support-enumeration", "description": "tooltip.protocol-support-enumeration", "type": "string", "widget": { "id": "select" }, "oneOf": [{ "enum": ["SAML 2"], "description": "SAML 2" }, { "enum": ["SAML 1.1"], "description": "SAML 1.1" }] }, "nameIdFormats": { "$ref": "#/definitions/nameIdFormats" } }, "dependencies": { "nameIdFormats": ["protocolSupportEnum"] } }, "logoutEndpoints": { "title": "label.logout-endpoints", "description": "tooltip.logout-endpoints", "type": "array", "items": { "$ref": "#/definitions/LogoutEndpoint" } }, "relyingPartyOverrides": { "type": "object", "properties": { "signAssertion": { "title": "label.sign-the-assertion", "description": "tooltip.sign-assertion", "type": "boolean", "default": false }, "dontSignResponse": { "title": "label.dont-sign-the-response", "description": "tooltip.dont-sign-response", "type": "boolean", "default": false }, "turnOffEncryption": { "title": "label.turn-off-encryption-of-response", "description": "tooltip.turn-off-encryption", "type": "boolean", "default": false }, "useSha": { "title": "label.use-sha1-signing-algorithm", "description": "tooltip.usa-sha-algorithm", "type": "boolean", "default": false }, "ignoreAuthenticationMethod": { "title": "label.ignore-any-sp-requested-authentication-method", "description": "tooltip.ignore-auth-method", "type": "boolean", "default": false }, "omitNotBefore": { "title": "label.omit-not-before-condition", "description": "tooltip.omit-not-before-condition", "type": "boolean", "default": false }, "responderId": { "title": "label.responder-id", "description": "tooltip.responder-id", "type": "string", "default": "" }, "nameIdFormats": { "$ref": "#/definitions/nameIdFormats" }, "authenticationMethods": { "$ref": "#/definitions/authenticationMethods" }, "forceAuthn": { "title": "label.force-authn", "description": "tooltip.force-authn", "type": "boolean", "default": false } } }, "attributeRelease": { "type": "array", "title": "label.attribute-release", "description": "Attribute release table - select the attributes you want to release (default unchecked)", "items": { "type": "string", "enum": ["eduPersonPrincipalName", "uid", "mail", "surname", "givenName", "eduPersonAffiliation", "eduPersonScopedAffiliation", "eduPersonPrimaryAffiliation", "eduPersonEntitlement", "eduPersonAssurance", "eduPersonUniqueId", "employeeNumber"] }, "uniqueItems": true } }, "definitions": { "Contact": { "type": "object", "required": ["name", "type", "emailAddress"], "properties": { "name": { "title": "label.contact-name", "description": "tooltip.contact-name", "type": "string", "minLength": 1, "maxLength": 255 }, "type": { "title": "label.contact-type", "description": "tooltip.contact-type", "type": "string", "widget": "select", "minLength": 1, "oneOf": [{ "enum": ["support"], "description": "value.support" }, { "enum": ["technical"], "description": "value.technical" }, { "enum": ["administrative"], "description": "value.administrative" }, { "enum": ["other"], "description": "value.other" }] }, "emailAddress": { "title": "label.contact-email-address", "description": "tooltip.contact-email", "type": "string", "pattern": "^(mailto:)?(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$", "minLength": 1, "maxLength": 255 } } }, "Certificate": { "type": "object", "required": ["type", "value"], "properties": { "name": { "title": "label.certificate-name-display-only", "description": "tooltip.certificate-name", "type": "string", "maxLength": 255 }, "type": { "title": "label.certificate-type", "type": "string", "widget": { "id": "radio", "class": "form-check-inline" }, "oneOf": [{ "enum": ["signing"], "description": "value.signing" }, { "enum": ["encryption"], "description": "value.encryption" }, { "enum": ["both"], "description": "value.both" }] }, "value": { "title": "label.certificate", "description": "tooltip.certificate", "type": "string", "widget": "textarea", "minLength": 1 } } }, "AssertionConsumerService": { "type": "object", "required": ["locationUrl", "binding"], "properties": { "locationUrl": { "title": "label.assertion-consumer-service-location", "description": "tooltip.assertion-consumer-service-location", "type": "string", "widget": { "id": "string", "help": "message.valid-url" }, "minLength": 1, "maxLength": 255 }, "binding": { "title": "label.assertion-consumer-service-location-binding", "description": "tooltip.assertion-consumer-service-location-binding", "type": "string", "widget": "select", "oneOf": [{ "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:PAOS"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:PAOS" }, { "enum": ["urn:oasis:names:tc:SAML:1.0:profiles:browser-post"], "description": "urn:oasis:names:tc:SAML:1.0:profiles:browser-post" }, { "enum": ["urn:oasis:names:tc:SAML:1.0:profiles:artifact-01"], "description": "urn:oasis:names:tc:SAML:1.0:profiles:artifact-01" }] }, "makeDefault": { "title": "label.mark-as-default", "description": "tooltip.mark-as-default", "type": "boolean" } } }, "LogoutEndpoint": { "description": "tooltip.new-endpoint", "type": "object", "fieldsets": [{ "fields": ["url", "bindingType"] }], "required": ["url", "bindingType"], "properties": { "url": { "title": "label.url", "description": "tooltip.url", "type": "string", "minLength": 1, "maxLength": 255 }, "bindingType": { "title": "label.binding-type", "description": "tooltip.binding-type", "type": "string", "widget": "select", "oneOf": [{ "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:SOAP"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" }] } } }, "MDUI": { "type": "object", "widget": { "id": "fieldset" }, "fieldsets": [{ "type": "group", "fields": ["displayName", "informationUrl", "description"] }, { "type": "group", "fields": ["privacyStatementUrl", "logoUrl", "logoWidth", "logoHeight"] }], "properties": { "displayName": { "title": "label.display-name", "description": "tooltip.mdui-display-name", "type": "string", "minLength": 1, "maxLength": 255 }, "informationUrl": { "title": "label.information-url", "description": "tooltip.mdui-information-url", "type": "string", "minLength": 1, "maxLength": 255 }, "privacyStatementUrl": { "title": "label.privacy-statement-url", "description": "tooltip.mdui-privacy-statement-url", "type": "string", "minLength": 1, "maxLength": 255 }, "description": { "title": "label.description", "description": "tooltip.mdui-description", "type": "string", "widget": { "id": "textarea" }, "minLength": 1, "maxLength": 255 }, "logoUrl": { "title": "label.logo-url", "description": "tooltip.mdui-logo-url", "type": "string", "minLength": 1, "maxLength": 255 }, "logoHeight": { "title": "label.logo-height", "description": "tooltip.mdui-logo-height", "minimum": 0, "type": "integer" }, "logoWidth": { "title": "label.logo-width", "description": "tooltip.mdui-logo-width", "minimum": 0, "type": "integer" } } }, "Organization": { "type": "object", "properties": { "name": { "title": "label.organization-name", "description": "tooltip.organization-name", "type": "string", "minLength": 1, "maxLength": 255 }, "displayName": { "title": "label.organization-display-name", "description": "tooltip.organization-display-name", "type": "string", "minLength": 1, "maxLength": 255 }, "url": { "title": "label.organization-url", "description": "tooltip.organization-url", "type": "string", "minLength": 1, "maxLength": 255 } }, "dependencies": { "name": { "required": ["displayName", "url"] }, "displayName": { "required": ["name", "url"] }, "url": { "required": ["name", "displayName"] } } }, "nameIdFormats": { "title": "label.nameid-format-to-send", "description": "tooltip.nameid-format", "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 255, "examples": ["urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"] } }, "authenticationMethods": { "title": "label.authentication-methods-to-use", "description": "tooltip.authentication-methods-to-use", "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 255, "examples": ["https://refeds.org/profile/mfa", "urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken", "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"] } } } }; export default SCHEMA; \ No newline at end of file diff --git a/ui/src/testing/uiSchema.js b/ui/src/testing/uiSchema.js index eb557bcf2..c1af6f4fa 100644 --- a/ui/src/testing/uiSchema.js +++ b/ui/src/testing/uiSchema.js @@ -11,7 +11,6 @@ const schema = { "fields": [ "serviceProviderName", "entityId", - "serviceEnabled", "organization" ] }, @@ -208,7 +207,6 @@ const schema = { }, "serviceProviderName": {}, "entityId": {}, - "serviceEnabled": {}, "organization": {}, "ui:disabled": false }; From afbe10e51dc508b7fda3fb751e1afb48b69fff62 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Thu, 29 Jul 2021 21:47:40 -0700 Subject: [PATCH 08/24] SHIBUI-2001 Enable endpoint --- .../JPAMetadataResolverServiceImpl.groovy | 454 ++++++++++-------- .../admin/ui/service/UserBootstrap.groovy | 7 +- .../CoreShibUiConfiguration.java | 73 ++- ...etadataResolverConverterConfiguration.java | 17 - .../ui/controller/ActivateController.java | 21 +- .../controller/ActivateExceptionHandler.java | 16 + .../ui/exception/InitializationException.java | 7 + .../ui/security/service/UserService.java | 8 + .../JPAEntityDescriptorServiceImpl.java | 3 + .../ui/service/JPAFilterServiceImpl.java | 2 + .../MetadataResolverConverterServiceImpl.java | 2 + .../ui/service/MetadataResolverService.java | 12 +- .../MetadataFiltersControllerTests.groovy | 16 + ...JPAMetadataResolverServiceImplTests.groovy | 3 +- ...taResolverConverterServiceImplTests.groovy | 4 + .../ui/service/UserBootstrapTests.groovy | 12 +- 16 files changed, 392 insertions(+), 265 deletions(-) delete mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConverterConfiguration.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InitializationException.java diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy index 5dfbf65eb..f8f2118cb 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy @@ -2,6 +2,7 @@ package edu.internet2.tier.shibboleth.admin.ui.service import com.google.common.base.Predicate import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityRoleWhiteListFilter @@ -20,8 +21,13 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMet import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.TemplateScheme import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.Refilterable +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import edu.internet2.tier.shibboleth.admin.util.OpenSamlChainingMetadataResolverUtil import groovy.util.logging.Slf4j import groovy.xml.DOMBuilder import groovy.xml.MarkupBuilder @@ -34,9 +40,11 @@ import org.opensaml.saml.metadata.resolver.filter.impl.NameIDFormatFilter import org.opensaml.saml.saml2.core.Attribute import org.opensaml.saml.saml2.metadata.EntityDescriptor import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service import org.w3c.dom.Document import javax.annotation.Nonnull +import javax.transaction.Transactional import static edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilterTarget.NameIdFormatFilterTargetType.ENTITY import static edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilterTarget.NameIdFormatFilterTargetType.CONDITION_SCRIPT @@ -45,171 +53,32 @@ import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBa import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver.ResourceType.SVN @Slf4j +@Service class JPAMetadataResolverServiceImpl implements MetadataResolverService { + + @Autowired + org.opensaml.saml.metadata.resolver.MetadataResolver chainingMetadataResolver @Autowired private MetadataResolver metadataResolver @Autowired - private MetadataResolverRepository metadataResolverRepository + MetadataResolverConverterService metadataResolverConverterService + @Autowired + private MetadataResolverRepository metadataResolverRepository + @Autowired private OpenSamlObjects openSamlObjects - + @Autowired private MetadataResolversPositionOrderContainerService resolversPositionOrderContainerService - + @Autowired private ShibUIConfiguration shibUIConfiguration - - // TODO: enhance - @Override - void reloadFilters(String metadataResolverResourceId) { - OpenSamlChainingMetadataResolver chainingMetadataResolver = (OpenSamlChainingMetadataResolver) metadataResolver - MetadataResolver targetMetadataResolver = chainingMetadataResolver.getResolvers().find { - it.id == metadataResolverResourceId - } - edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver jpaMetadataResolver = metadataResolverRepository.findByResourceId(metadataResolverResourceId) - - if (targetMetadataResolver && targetMetadataResolver.getMetadataFilter() instanceof MetadataFilterChain) { - MetadataFilterChain metadataFilterChain = (MetadataFilterChain) targetMetadataResolver.getMetadataFilter() - - List metadataFilters = new ArrayList<>() - - // set up namespace protection - if (shibUIConfiguration.protectedAttributeNamespaces && shibUIConfiguration.protectedAttributeNamespaces.size() > 0 && targetMetadataResolver && jpaMetadataResolver.type in ['FileBackedHttpMetadataResolver', 'DynamicHttpMetadataResolver']) { - def target = new org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter() - target.attributeFilter = new ScriptedPredicate(new EvaluableScript(protectedNamespaceScript())) - metadataFilters.add(target) - } - - for (edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter metadataFilter : jpaMetadataResolver.getMetadataFilters()) { - if (metadataFilter instanceof EntityAttributesFilter) { - EntityAttributesFilter entityAttributesFilter = (EntityAttributesFilter) metadataFilter - - org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter target = new org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter() - Map, Collection> rules = new HashMap<>() - switch (entityAttributesFilter.getEntityAttributesFilterTarget().getEntityAttributesFilterTargetType()) { - case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY: - rules.put( - new EntityIdPredicate(entityAttributesFilter.getEntityAttributesFilterTarget().getValue()), - (List) (List) entityAttributesFilter.getAttributes() - ) - break - case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.CONDITION_SCRIPT: - rules.put(new ScriptedPredicate(new EvaluableScript(entityAttributesFilter.entityAttributesFilterTarget.value[0])), - (List) (List) entityAttributesFilter.getAttributes()) - break - case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.REGEX: - rules.put(new ScriptedPredicate(new EvaluableScript(generateJavaScriptRegexScript(entityAttributesFilter.entityAttributesFilterTarget.value[0]))), - (List) (List) entityAttributesFilter.getAttributes()) - break - default: - // do nothing, we'd have exploded elsewhere previously. - break - } - target.setRules(rules) - metadataFilters.add(target) - } - if (metadataFilter instanceof NameIdFormatFilter) { - NameIdFormatFilter nameIdFormatFilter = NameIdFormatFilter.cast(metadataFilter) - NameIDFormatFilter openSamlTargetFilter = new OpenSamlNameIdFormatFilter() - openSamlTargetFilter.removeExistingFormats = nameIdFormatFilter.removeExistingFormats - Map, Collection> predicateRules = [:] - def type = nameIdFormatFilter.nameIdFormatFilterTarget.nameIdFormatFilterTargetType - def values = nameIdFormatFilter.nameIdFormatFilterTarget.value - switch (type) { - case ENTITY: - predicateRules[new EntityIdPredicate(values)] = nameIdFormatFilter.formats - break - case CONDITION_SCRIPT: - predicateRules[new ScriptedPredicate(new EvaluableScript(values[0]))] = nameIdFormatFilter.formats - break - case REGEX: - predicateRules[new ScriptedPredicate(new EvaluableScript(generateJavaScriptRegexScript(values[0])))] = nameIdFormatFilter.formats - break - default: - // do nothing, we'd have exploded elsewhere previously. - break - } - openSamlTargetFilter.rules = predicateRules - metadataFilters << openSamlTargetFilter - } - } - metadataFilterChain.setFilters(metadataFilters) - } - - if (targetMetadataResolver != null && targetMetadataResolver instanceof Refilterable) { - (Refilterable) targetMetadataResolver.refilter() - } else { - //TODO: Do something here if we need to refilter other non-Batch resolvers - log.warn("Target resolver is not a Refilterable resolver. Skipping refilter()") - } - } - - private String protectedNamespaceScript() { - return """(function (attribute) { - "use strict"; - var namespaces = [${shibUIConfiguration.protectedAttributeNamespaces.collect({"\"${it}\""}).join(', ')}]; - // check the parameter - if (attribute === null) { return true; } - for (var i in namespaces) { - if (attribute.getName().startsWith(namespaces[i])) { - return false; - } - } - return true; - }(input));""" - } - - private class ScriptedPredicate extends net.shibboleth.utilities.java.support.logic.ScriptedPredicate { - protected ScriptedPredicate(@Nonnull EvaluableScript theScript) { - super(theScript) - } - } - - // TODO: enhance - @Override - Document generateConfiguration() { - // TODO: this can probably be a better writer - new StringWriter().withCloseable { writer -> - def xml = new MarkupBuilder(writer) - xml.omitEmptyAttributes = true - xml.omitNullAttributes = true - - xml.MetadataProvider(id: 'ShibbolethMetadata', - xmlns: 'urn:mace:shibboleth:2.0:metadata', - 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'xsi:type': 'ChainingMetadataProvider', - 'xsi:schemaLocation': 'urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd urn:mace:shibboleth:2.0:resource http://shibboleth.net/schema/idp/shibboleth-resource.xsd urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd' - ) { - - - resolversPositionOrderContainerService.allMetadataResolversInDefinedOrderOrUnordered.each { - edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr -> - //TODO: We do not currently marshall the internal incommon chaining resolver (with BaseMetadataResolver type) - if ((mr.type != 'BaseMetadataResolver') && (mr.enabled)) { - constructXmlNodeForResolver(mr, delegate) { - //TODO: enhance - def didNamespaceProtectionFilter = !(shibUIConfiguration.protectedAttributeNamespaces && shibUIConfiguration.protectedAttributeNamespaces.size() > 0) - def doNamespaceProtectionFilter = { def filter -> - if (mr.type in ['FileBackedMetadataResolver', 'DynamicHttpMetadataResolver'] && (filter == null || filter instanceof EntityAttributesFilter) && !didNamespaceProtectionFilter) { - constructXmlNodeForEntityAttributeNamespaceProtection(delegate) - didNamespaceProtectionFilter = true - } - } - mr.metadataFilters.each { edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter filter -> - doNamespaceProtectionFilter() - constructXmlNodeForFilter(filter, delegate) - } - doNamespaceProtectionFilter() - } - } - } - } - return DOMBuilder.newInstance().parseText(writer.toString()) - } - } + + @Autowired + private UserService userService void constructXmlNodeForEntityAttributeNamespaceProtection(def markupBuilderDelegate) { markupBuilderDelegate.MetadataFilter('xsi:type': 'EntityAttributes') { @@ -221,21 +90,6 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } - void constructXmlNodeForFilter(SignatureValidationFilter filter, def markupBuilderDelegate) { - if (filter.xmlShouldBeGenerated()) { - markupBuilderDelegate.MetadataFilter(id: filter.name, - 'xsi:type': 'SignatureValidation', - 'xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata', - 'requireSignedRoot': !filter.requireSignedRoot ?: null, - 'certificateFile': filter.certificateFile, - 'defaultCriteriaRef': filter.defaultCriteriaRef, - 'signaturePrevalidatorRef': filter.signaturePrevalidatorRef, - 'dynamicTrustedNamesStrategyRef': filter.dynamicTrustedNamesStrategyRef, - 'trustEngineRef': filter.trustEngineRef, - 'publicKey': filter.publicKey) - } - } - void constructXmlNodeForFilter(EntityAttributesFilter filter, def markupBuilderDelegate) { markupBuilderDelegate.MetadataFilter('xsi:type': 'EntityAttributes') { // TODO: enhance. currently this does weird things with namespaces @@ -274,12 +128,6 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } - private String generateJavaScriptRegexScript(String regex) { - return """ - "use strict"; - ${regex.startsWith('/') ? '' : '/'}${regex}${regex.endsWith('/') ? '' : '/'}.test(input.getEntityID());\n""" - } - void constructXmlNodeForFilter(EntityRoleWhiteListFilter filter, def markupBuilderDelegate) { if (!filter.retainedRoles?.isEmpty()) { markupBuilderDelegate.MetadataFilter( @@ -294,15 +142,6 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } - void constructXmlNodeForFilter(RequiredValidUntilFilter filter, def markupBuilderDelegate) { - if (filter.xmlShouldBeGenerated()) { - markupBuilderDelegate.MetadataFilter( - 'xsi:type': 'RequiredValidUntil', - maxValidityInterval: filter.maxValidityInterval - ) - } - } - void constructXmlNodeForFilter(NameIdFormatFilter filter, def markupBuilderDelegate) { def type = filter.nameIdFormatFilterTarget.nameIdFormatFilterTargetType markupBuilderDelegate.MetadataFilter( @@ -341,27 +180,27 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } - void constructXmlNodeForResolver(FilesystemMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { - markupBuilderDelegate.MetadataProvider(id: resolver.xmlId, - 'xsi:type': 'FilesystemMetadataProvider', - metadataFile: resolver.metadataFile, - - requireValidMetadata: !resolver.requireValidMetadata ?: null, - failFastInitialization: !resolver.failFastInitialization ?: null, - sortKey: resolver.sortKey, - criterionPredicateRegistryRef: resolver.criterionPredicateRegistryRef, - useDefaultPredicateRegistry: !resolver.useDefaultPredicateRegistry ?: null, - satisfyAnyPredicates: resolver.satisfyAnyPredicates ?: null, - - parserPoolRef: resolver.reloadableMetadataResolverAttributes?.parserPoolRef, - minRefreshDelay: resolver.reloadableMetadataResolverAttributes?.minRefreshDelay, - maxRefreshDelay: resolver.reloadableMetadataResolverAttributes?.maxRefreshDelay, - refreshDelayFactor: resolver.reloadableMetadataResolverAttributes?.refreshDelayFactor, - indexesRef: resolver.reloadableMetadataResolverAttributes?.indexesRef, - resolveViaPredicatesOnly: resolver.reloadableMetadataResolverAttributes?.resolveViaPredicatesOnly ?: null, - expirationWarningThreshold: resolver.reloadableMetadataResolverAttributes?.expirationWarningThreshold) { + void constructXmlNodeForFilter(RequiredValidUntilFilter filter, def markupBuilderDelegate) { + if (filter.xmlShouldBeGenerated()) { + markupBuilderDelegate.MetadataFilter( + 'xsi:type': 'RequiredValidUntil', + maxValidityInterval: filter.maxValidityInterval + ) + } + } - childNodes() + void constructXmlNodeForFilter(SignatureValidationFilter filter, def markupBuilderDelegate) { + if (filter.xmlShouldBeGenerated()) { + markupBuilderDelegate.MetadataFilter(id: filter.name, + 'xsi:type': 'SignatureValidation', + 'xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata', + 'requireSignedRoot': !filter.requireSignedRoot ?: null, + 'certificateFile': filter.certificateFile, + 'defaultCriteriaRef': filter.defaultCriteriaRef, + 'signaturePrevalidatorRef': filter.signaturePrevalidatorRef, + 'dynamicTrustedNamesStrategyRef': filter.dynamicTrustedNamesStrategyRef, + 'trustEngineRef': filter.trustEngineRef, + 'publicKey': filter.publicKey) } } @@ -483,6 +322,30 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } + void constructXmlNodeForResolver(FilesystemMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { + markupBuilderDelegate.MetadataProvider(id: resolver.xmlId, + 'xsi:type': 'FilesystemMetadataProvider', + metadataFile: resolver.metadataFile, + + requireValidMetadata: !resolver.requireValidMetadata ?: null, + failFastInitialization: !resolver.failFastInitialization ?: null, + sortKey: resolver.sortKey, + criterionPredicateRegistryRef: resolver.criterionPredicateRegistryRef, + useDefaultPredicateRegistry: !resolver.useDefaultPredicateRegistry ?: null, + satisfyAnyPredicates: resolver.satisfyAnyPredicates ?: null, + + parserPoolRef: resolver.reloadableMetadataResolverAttributes?.parserPoolRef, + minRefreshDelay: resolver.reloadableMetadataResolverAttributes?.minRefreshDelay, + maxRefreshDelay: resolver.reloadableMetadataResolverAttributes?.maxRefreshDelay, + refreshDelayFactor: resolver.reloadableMetadataResolverAttributes?.refreshDelayFactor, + indexesRef: resolver.reloadableMetadataResolverAttributes?.indexesRef, + resolveViaPredicatesOnly: resolver.reloadableMetadataResolverAttributes?.resolveViaPredicatesOnly ?: null, + expirationWarningThreshold: resolver.reloadableMetadataResolverAttributes?.expirationWarningThreshold) { + + childNodes() + } + } + void constructXmlNodeForResolver(LocalDynamicMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { markupBuilderDelegate.MetadataProvider(sourceDirectory: resolver.sourceDirectory, sourceManagerRef: resolver.sourceManagerRef, @@ -556,7 +419,194 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { childNodes() } + } + public edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver findByResourceId(String resourceId) throws EntityNotFoundException { + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver result = metadataResolverRepository.findByResourceId(resourceId) + if (result == null ) { + throw new EntityNotFoundException("No Provider with resourceId[" + resourceId + "] was found") + } + return result + } + + // TODO: enhance + @Override + Document generateConfiguration() { + // TODO: this can probably be a better writer + new StringWriter().withCloseable { writer -> + def xml = new MarkupBuilder(writer) + xml.omitEmptyAttributes = true + xml.omitNullAttributes = true + + xml.MetadataProvider(id: 'ShibbolethMetadata', + xmlns: 'urn:mace:shibboleth:2.0:metadata', + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:type': 'ChainingMetadataProvider', + 'xsi:schemaLocation': 'urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd urn:mace:shibboleth:2.0:resource http://shibboleth.net/schema/idp/shibboleth-resource.xsd urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd' + ) { + + + resolversPositionOrderContainerService.allMetadataResolversInDefinedOrderOrUnordered.each { + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr -> + //TODO: We do not currently marshall the internal incommon chaining resolver (with BaseMetadataResolver type) + if ((mr.type != 'BaseMetadataResolver') && (mr.enabled)) { + constructXmlNodeForResolver(mr, delegate) { + //TODO: enhance + def didNamespaceProtectionFilter = !(shibUIConfiguration.protectedAttributeNamespaces && shibUIConfiguration.protectedAttributeNamespaces.size() > 0) + def doNamespaceProtectionFilter = { def filter -> + if (mr.type in ['FileBackedMetadataResolver', 'DynamicHttpMetadataResolver'] && (filter == null || filter instanceof EntityAttributesFilter) && !didNamespaceProtectionFilter) { + constructXmlNodeForEntityAttributeNamespaceProtection(delegate) + didNamespaceProtectionFilter = true + } + } + mr.metadataFilters.each { edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter filter -> + doNamespaceProtectionFilter() + constructXmlNodeForFilter(filter, delegate) + } + doNamespaceProtectionFilter() + } + } + } + } + return DOMBuilder.newInstance().parseText(writer.toString()) + } + } + + private String generateJavaScriptRegexScript(String regex) { + return """ + "use strict"; + ${regex.startsWith('/') ? '' : '/'}${regex}${regex.endsWith('/') ? '' : '/'}.test(input.getEntityID());\n""" + } + + private String protectedNamespaceScript() { + return """(function (attribute) { + "use strict"; + var namespaces = [${shibUIConfiguration.protectedAttributeNamespaces.collect({"\"${it}\""}).join(', ')}]; + // check the parameter + if (attribute === null) { return true; } + for (var i in namespaces) { + if (attribute.getName().startsWith(namespaces[i])) { + return false; + } + } + return true; + }(input));""" + } + + // TODO: enhance + @Override + void reloadFilters(String metadataResolverResourceId) { + OpenSamlChainingMetadataResolver chainingMetadataResolver = (OpenSamlChainingMetadataResolver) metadataResolver + MetadataResolver targetMetadataResolver = chainingMetadataResolver.getResolvers().find { + it.id == metadataResolverResourceId + } + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver jpaMetadataResolver = metadataResolverRepository.findByResourceId(metadataResolverResourceId) + + if (targetMetadataResolver && targetMetadataResolver.getMetadataFilter() instanceof MetadataFilterChain) { + MetadataFilterChain metadataFilterChain = (MetadataFilterChain) targetMetadataResolver.getMetadataFilter() + + List metadataFilters = new ArrayList<>() + + // set up namespace protection + if (shibUIConfiguration.protectedAttributeNamespaces && shibUIConfiguration.protectedAttributeNamespaces.size() > 0 && targetMetadataResolver && jpaMetadataResolver.type in ['FileBackedHttpMetadataResolver', 'DynamicHttpMetadataResolver']) { + def target = new org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter() + target.attributeFilter = new ScriptedPredicate(new EvaluableScript(protectedNamespaceScript())) + metadataFilters.add(target) + } + + for (edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter metadataFilter : jpaMetadataResolver.getMetadataFilters()) { + if (metadataFilter instanceof EntityAttributesFilter) { + EntityAttributesFilter entityAttributesFilter = (EntityAttributesFilter) metadataFilter + + org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter target = new org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter() + Map, Collection> rules = new HashMap<>() + switch (entityAttributesFilter.getEntityAttributesFilterTarget().getEntityAttributesFilterTargetType()) { + case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY: + rules.put( + new EntityIdPredicate(entityAttributesFilter.getEntityAttributesFilterTarget().getValue()), + (List) (List) entityAttributesFilter.getAttributes() + ) + break + case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.CONDITION_SCRIPT: + rules.put(new ScriptedPredicate(new EvaluableScript(entityAttributesFilter.entityAttributesFilterTarget.value[0])), + (List) (List) entityAttributesFilter.getAttributes()) + break + case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.REGEX: + rules.put(new ScriptedPredicate(new EvaluableScript(generateJavaScriptRegexScript(entityAttributesFilter.entityAttributesFilterTarget.value[0]))), + (List) (List) entityAttributesFilter.getAttributes()) + break + default: + // do nothing, we'd have exploded elsewhere previously. + break + } + target.setRules(rules) + metadataFilters.add(target) + } + if (metadataFilter instanceof NameIdFormatFilter) { + NameIdFormatFilter nameIdFormatFilter = NameIdFormatFilter.cast(metadataFilter) + NameIDFormatFilter openSamlTargetFilter = new OpenSamlNameIdFormatFilter() + openSamlTargetFilter.removeExistingFormats = nameIdFormatFilter.removeExistingFormats + Map, Collection> predicateRules = [:] + def type = nameIdFormatFilter.nameIdFormatFilterTarget.nameIdFormatFilterTargetType + def values = nameIdFormatFilter.nameIdFormatFilterTarget.value + switch (type) { + case ENTITY: + predicateRules[new EntityIdPredicate(values)] = nameIdFormatFilter.formats + break + case CONDITION_SCRIPT: + predicateRules[new ScriptedPredicate(new EvaluableScript(values[0]))] = nameIdFormatFilter.formats + break + case REGEX: + predicateRules[new ScriptedPredicate(new EvaluableScript(generateJavaScriptRegexScript(values[0])))] = nameIdFormatFilter.formats + break + default: + // do nothing, we'd have exploded elsewhere previously. + break + } + openSamlTargetFilter.rules = predicateRules + metadataFilters << openSamlTargetFilter + } + } + metadataFilterChain.setFilters(metadataFilters) + } + + if (targetMetadataResolver != null && targetMetadataResolver instanceof Refilterable) { + (Refilterable) targetMetadataResolver.refilter() + } else { + //TODO: Do something here if we need to refilter other non-Batch resolvers + log.warn("Target resolver is not a Refilterable resolver. Skipping refilter()") + } + } + + public edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver updateMetadataResolverEnabledStatus(edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver updatedResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException { + // @TODO: when merged with groups, this should maybe be merged with group check as they have to have the role in the right group + if (!userService.currentUserHasExpectedRole(["ROLE_ADMIN", "ROLE_ENABLE"])) { + throw new ForbiddenException("You do not have the permissions necessary to change the enable status of this filter.") + } + + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver persistedResolver = metadataResolverRepository.save(updatedResolver) + + if (persistedResolver.getDoInitialization()) { + MetadataResolver openSamlRepresentation = null + try { + openSamlRepresentation = metadataResolverConverterService.convertToOpenSamlRepresentation(persistedResolver) + } catch (FileNotFoundException e) { + throw new MetadataFileNotFoundException("message.file-doesnt-exist") + } + try { + OpenSamlChainingMetadataResolverUtil.updateChainingMetadataResolver((OpenSamlChainingMetadataResolver) chainingMetadataResolver, openSamlRepresentation); + } + catch (Throwable e) { + throw new InitializationException(e); + } + } + + } + + private class ScriptedPredicate extends net.shibboleth.utilities.java.support.logic.ScriptedPredicate { + protected ScriptedPredicate(@Nonnull EvaluableScript theScript) { + super(theScript) + } } } diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy index 4ca5b435b..2635f908c 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy @@ -6,6 +6,7 @@ import edu.internet2.tier.shibboleth.admin.ui.security.model.Role import edu.internet2.tier.shibboleth.admin.ui.security.model.User import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import groovy.util.logging.Slf4j import org.springframework.boot.context.event.ApplicationStartedEvent import org.springframework.context.event.EventListener @@ -19,11 +20,13 @@ class UserBootstrap { private final ShibUIConfiguration shibUIConfiguration private final UserRepository userRepository private final RoleRepository roleRepository + private final UserService userService - UserBootstrap(ShibUIConfiguration shibUIConfiguration, UserRepository userRepository, RoleRepository roleRepository) { + UserBootstrap(ShibUIConfiguration shibUIConfiguration, UserRepository userRepository, RoleRepository roleRepository, UserService userService) { this.shibUIConfiguration = shibUIConfiguration this.userRepository = userRepository this.roleRepository = roleRepository + this.userService = userService } @Transactional @@ -50,7 +53,7 @@ class UserBootstrap { it.emailAddress = email it } - userRepository.saveAndFlush(user) + userService.save(user) } } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java index da33c53f8..06299172a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java @@ -1,6 +1,27 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration; +import javax.servlet.http.HttpServletRequest; + +import org.apache.lucene.analysis.Analyzer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.core.io.Resource; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; +import org.springframework.web.util.UrlPathHelper; + import com.fasterxml.jackson.databind.Module; + import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; @@ -13,49 +34,25 @@ import edu.internet2.tier.shibboleth.admin.ui.service.DefaultMetadataResolversPositionOrderContainerService; import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryService; import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryServiceImpl; -import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; import edu.internet2.tier.shibboleth.admin.ui.service.EntityIdsSearchService; import edu.internet2.tier.shibboleth.admin.ui.service.EntityIdsSearchServiceImpl; import edu.internet2.tier.shibboleth.admin.ui.service.EntityService; import edu.internet2.tier.shibboleth.admin.ui.service.FileCheckingFileWritingService; import edu.internet2.tier.shibboleth.admin.ui.service.FileWritingService; -import edu.internet2.tier.shibboleth.admin.ui.service.FilterService; import edu.internet2.tier.shibboleth.admin.ui.service.FilterTargetService; -import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl; import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl; -import edu.internet2.tier.shibboleth.admin.ui.service.JPAFilterServiceImpl; import edu.internet2.tier.shibboleth.admin.ui.service.JPAFilterTargetServiceImpl; -import edu.internet2.tier.shibboleth.admin.ui.service.JPAMetadataResolverServiceImpl; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolversPositionOrderContainerService; import edu.internet2.tier.shibboleth.admin.util.AttributeUtility; import edu.internet2.tier.shibboleth.admin.util.LuceneUtility; import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; -import org.apache.lucene.analysis.Analyzer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.support.ResourceBundleMessageSource; -import org.springframework.core.io.Resource; -import org.springframework.web.servlet.LocaleResolver; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; -import org.springframework.web.util.UrlPathHelper; - -import javax.servlet.http.HttpServletRequest; @Configuration +@Import(SearchConfiguration.class) +@ComponentScan(basePackages="{ edu.internet2.tier.shibboleth.admin.ui.service }") @EnableConfigurationProperties({CustomPropertiesConfiguration.class, ShibUIConfiguration.class}) public class CoreShibUiConfiguration { - private static final Logger logger = LoggerFactory.getLogger(CoreShibUiConfiguration.class); - @Bean public OpenSamlObjects openSamlObjects() { return new OpenSamlObjects(); @@ -66,25 +63,25 @@ public EntityService jpaEntityService() { return new JPAEntityServiceImpl(openSamlObjects()); } - @Bean - public EntityDescriptorService jpaEntityDescriptorService(UserService userService) { - return new JPAEntityDescriptorServiceImpl(openSamlObjects(), jpaEntityService(), userService); - } +// @Bean +// public EntityDescriptorService JPAEntityDescriptorServiceImpl(UserService userService) { +// return new JPAEntityDescriptorServiceImpl(openSamlObjects(), jpaEntityService(), userService); +// } - @Bean - public FilterService jpaFilterService() { - return new JPAFilterServiceImpl(); - } +// @Bean +// public FilterService jpaFilterService() { +// return new JPAFilterServiceImpl(); +// } @Bean public FilterTargetService jpaFilterTargetService() { return new JPAFilterTargetServiceImpl(); } - @Bean - public MetadataResolverService metadataResolverService() { - return new JPAMetadataResolverServiceImpl(); - } +// @Bean +// public MetadataResolverService metadataResolverService() { +// return new JPAMetadataResolverServiceImpl(); +// } @Bean public AttributeUtility attributeUtility() { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConverterConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConverterConfiguration.java deleted file mode 100644 index 6380e0018..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConverterConfiguration.java +++ /dev/null @@ -1,17 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.configuration; - -import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverConverterService; -import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverConverterServiceImpl; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * @author Bill Smith (wsmith@unicon.net) - */ -@Configuration -public class MetadataResolverConverterConfiguration { - @Bean - public MetadataResolverConverterService metadataResolverConverterService() { - return new MetadataResolverConverterServiceImpl(); - } -} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java index 1a3ecf0a4..60705cd15 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java @@ -3,19 +3,25 @@ import javax.script.ScriptException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException; import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; import edu.internet2.tier.shibboleth.admin.ui.service.FilterService; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; @RestController @RequestMapping("/api/activate") @@ -27,6 +33,9 @@ public class ActivateController { @Autowired private FilterService filterService; + @Autowired + private MetadataResolverService metadataResolverService; + @PatchMapping(path = "/entityDescriptor/{resourceId}/{mode}") @Transactional public ResponseEntity enableEntityDescriptor(@PathVariable String resourceId, @PathVariable String mode) throws EntityNotFoundException, ForbiddenException { @@ -42,5 +51,15 @@ public ResponseEntity enableFilter(@PathVariable String metadataResolverId, @ MetadataFilter persistedFilter = filterService.updateFilterEnabledStatus(metadataResolverId, resourceId, status); return ResponseEntity.ok(persistedFilter); } -// Enable/disable for : , provider + + @PatchMapping("/MetadataResolvers/{resourceId}/{mode}") + @Transactional + public ResponseEntity enableProvider(@PathVariable String resourceId, @PathVariable String mode) throws EntityNotFoundException, ForbiddenException, MetadataFileNotFoundException, InitializationException { + boolean status = "enable".equalsIgnoreCase(mode); + MetadataResolver existingResolver = metadataResolverService.findByResourceId(resourceId); + existingResolver.setEnabled(status); + existingResolver = metadataResolverService.updateMetadataResolverEnabledStatus(existingResolver); + + return ResponseEntity.ok(existingResolver); + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java index 85264b582..0c766c53c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java @@ -1,5 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + import javax.script.ScriptException; import org.springframework.http.HttpStatus; @@ -9,8 +11,10 @@ import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException; @ControllerAdvice(assignableTypes = {ActivateController.class}) public class ActivateExceptionHandler extends ResponseEntityExceptionHandler { @@ -25,8 +29,20 @@ public ResponseEntity handleForbiddenAccess(ForbiddenException e, WebRequest return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(String.valueOf(HttpStatus.FORBIDDEN.value()), e.getMessage())); } + @ExceptionHandler({ InitializationException.class }) + public ResponseEntity handleInitializationException(InitializationException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), e.getMessage())); + } + + @ExceptionHandler({ MetadataFileNotFoundException.class }) + public ResponseEntity handleMetadataFileNotFoundException(MetadataFileNotFoundException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse(INTERNAL_SERVER_ERROR.toString(), e.getLocalizedMessage())); + } + @ExceptionHandler({ ScriptException.class }) public ResponseEntity handleScriptException(ScriptException e, WebRequest request) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), e.getMessage())); } + + } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InitializationException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InitializationException.java new file mode 100644 index 000000000..f18483176 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InitializationException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class InitializationException extends Exception { + public InitializationException(Exception e) { + super(e); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java index 898e99e00..c3bac03de 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java @@ -5,6 +5,8 @@ import java.util.Optional; import java.util.Set; +import javax.transaction.Transactional; + import org.apache.commons.lang.StringUtils; import org.springframework.security.core.context.SecurityContextHolder; @@ -80,4 +82,10 @@ public boolean currentUserHasExpectedRole(List acceptedRoles) { User user = getCurrentUser(); return acceptedRoles.contains(user.getRole()); } + + @Transactional + public User save(User user) { + // NOTE: REPLACE ENTIRELY WITH 1740 code + return userRepository.save(user); + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java index ab93343c8..291e300c2 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java @@ -58,6 +58,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.ArrayList; @@ -78,6 +79,7 @@ * * @since 1.0 */ +@Service public class JPAEntityDescriptorServiceImpl implements EntityDescriptorService { @Autowired private EntityDescriptorRepository entityDescriptorRepository; @@ -88,6 +90,7 @@ public class JPAEntityDescriptorServiceImpl implements EntityDescriptorService { @Autowired private EntityService entityService; + @Autowired private UserService userService; public JPAEntityDescriptorServiceImpl(OpenSamlObjects openSamlObjects, EntityService entityService, UserService userService) { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java index 6810fa243..23a70268a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java @@ -11,6 +11,7 @@ import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; import org.springframework.transaction.interceptor.TransactionAspectSupport; import java.util.ArrayList; @@ -26,6 +27,7 @@ * @since 1.0 * @author Bill Smith (wsmith@unicon.net) */ +@Service public class JPAFilterServiceImpl implements FilterService { @Autowired EntityDescriptorService entityDescriptorService; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java index ee5f8cec3..2343206a7 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java @@ -22,6 +22,7 @@ import org.opensaml.saml.metadata.resolver.MetadataResolver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; import java.io.File; import java.io.FileNotFoundException; @@ -32,6 +33,7 @@ /** * @author Bill Smith (wsmith@unicon.net) */ +@Service public class MetadataResolverConverterServiceImpl implements MetadataResolverConverterService { @Autowired diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java index 376c34aa5..5fd205c20 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java @@ -2,8 +2,18 @@ import org.w3c.dom.Document; +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException; + public interface MetadataResolverService { - public void reloadFilters(String metadataResolverName); + public MetadataResolver findByResourceId(String resourceId) throws EntityNotFoundException; public Document generateConfiguration(); + + public void reloadFilters(String metadataResolverName); + + public MetadataResolver updateMetadataResolverEnabledStatus(MetadataResolver existingResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException; } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy index 280294aa1..b915d6fac 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy @@ -7,10 +7,14 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.Internationalization import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository import edu.internet2.tier.shibboleth.admin.ui.service.FilterService @@ -86,6 +90,18 @@ class MetadataFiltersControllerTests extends Specification { Document generateConfiguration() { return null } + + @Override + public MetadataResolver updateMetadataResolverEnabledStatus(MetadataResolver existingResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException { + // This won't get called + return null + } + + @Override + public MetadataResolver findByResourceId(String resourceId) throws EntityNotFoundException { + // This won't get called + return null + } }, chainingMetadataResolver: new OpenSamlChainingMetadataResolver().with { it.id = 'chain' diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy index 3ab2154ea..1f3c7962a 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy @@ -2,7 +2,6 @@ package edu.internet2.tier.shibboleth.admin.ui.service import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConverterConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.PlaceholderResolverComponentsConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration @@ -51,7 +50,7 @@ import spock.lang.Unroll import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.generatedXmlIsTheSameAsExpectedXml @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, MetadataResolverConverterConfiguration, SearchConfiguration, InternationalizationConfiguration, PlaceholderResolverComponentsConfiguration, edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration ,Config]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, InternationalizationConfiguration, PlaceholderResolverComponentsConfiguration, edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration ,Config]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImplTests.groovy index 8a261565e..f5d0f72e3 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImplTests.groovy @@ -1,9 +1,13 @@ package edu.internet2.tier.shibboleth.admin.ui.service +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicMetadataResolverAttributes import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ContextConfiguration + import spock.lang.Specification @SpringBootTest diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy index 0a5db2bcf..93857449e 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy @@ -7,6 +7,7 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest @@ -33,12 +34,19 @@ class UserBootstrapTests extends Specification { @Autowired RoleRepository roleRepository + + @Autowired + UserService userService + def setup() { + roleRepository.deleteAll(); + } + def "simple test"() { setup: shibUIConfiguration.roles = [] shibUIConfiguration.userBootstrapResource = new ClassPathResource('/conf/1044.csv') - def userBootstrap = new UserBootstrap(shibUIConfiguration, userRepository, roleRepository) + def userBootstrap = new UserBootstrap(shibUIConfiguration, userRepository, roleRepository, userService) when: userBootstrap.bootstrapUsersAndRoles(null) @@ -52,7 +60,7 @@ class UserBootstrapTests extends Specification { def "bootstrap roles"() { setup: shibUIConfiguration.roles = ['ROLE_ADMIN', 'ROLE_USER'] - def userbootstrap = new UserBootstrap(shibUIConfiguration, userRepository, roleRepository) + def userbootstrap = new UserBootstrap(shibUIConfiguration, userRepository, roleRepository, userService) when: userbootstrap.bootstrapUsersAndRoles(null) From e4a60cab84a93d5f0f06308d9d3b5242edbcfa07 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Tue, 10 Aug 2021 18:42:36 -0700 Subject: [PATCH 09/24] SHIBUI-1742 Fixing tests after merging master changes in --- .../JPAMetadataResolverServiceImpl.groovy | 52 ++++++------------- ...JPAMetadataResolverServiceImplTests.groovy | 4 ++ 2 files changed, 20 insertions(+), 36 deletions(-) diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy index 83e35a4c6..365125395 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy @@ -3,22 +3,9 @@ package edu.internet2.tier.shibboleth.admin.ui.service import com.google.common.base.Predicate import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityRoleWhiteListFilter -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilter -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilFilter -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.SignatureValidationFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.* import edu.internet2.tier.shibboleth.admin.ui.domain.filters.opensaml.OpenSamlNameIdFormatFilter -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FilesystemMetadataResolver -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataQueryProtocolScheme -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataRequestURLConstructionScheme -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.RegexScheme -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.TemplateScheme +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.* import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.Refilterable import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException @@ -44,20 +31,14 @@ import org.springframework.stereotype.Service import org.w3c.dom.Document import javax.annotation.Nonnull -import javax.transaction.Transactional -import static edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilterTarget.NameIdFormatFilterTargetType.ENTITY -import static edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilterTarget.NameIdFormatFilterTargetType.CONDITION_SCRIPT -import static edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilterTarget.NameIdFormatFilterTargetType.REGEX +import static edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilterTarget.NameIdFormatFilterTargetType.* import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver.ResourceType.CLASSPATH import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver.ResourceType.SVN @Slf4j @Service class JPAMetadataResolverServiceImpl implements MetadataResolverService { - - @Autowired - org.opensaml.saml.metadata.resolver.MetadataResolver chainingMetadataResolver @Autowired private MetadataResolver metadataResolver @@ -67,16 +48,16 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { @Autowired private MetadataResolverRepository metadataResolverRepository - + @Autowired private OpenSamlObjects openSamlObjects - + @Autowired private MetadataResolversPositionOrderContainerService resolversPositionOrderContainerService - + @Autowired private ShibUIConfiguration shibUIConfiguration - + @Autowired private UserService userService @@ -142,6 +123,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } + void constructXmlNodeForFilter(NameIdFormatFilter filter, def markupBuilderDelegate) { def type = filter.nameIdFormatFilterTarget.nameIdFormatFilterTargetType markupBuilderDelegate.MetadataFilter( @@ -429,7 +411,6 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { return result } - // TODO: enhance @Override Document generateConfiguration() { // TODO: this can probably be a better writer @@ -481,7 +462,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { private String protectedNamespaceScript() { return """(function (attribute) { "use strict"; - var namespaces = [${shibUIConfiguration.protectedAttributeNamespaces.collect({"\"${it}\""}).join(', ')}]; + var namespaces = [${shibUIConfiguration.protectedAttributeNamespaces.collect({ "\"${it}\"" }).join(', ')}]; // check the parameter if (attribute === null) { return true; } for (var i in namespaces) { @@ -492,8 +473,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { return true; }(input));""" } - - // TODO: enhance + @Override void reloadFilters(String metadataResolverResourceId) { OpenSamlChainingMetadataResolver chainingMetadataResolver = (OpenSamlChainingMetadataResolver) metadataResolver @@ -577,15 +557,15 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { log.warn("Target resolver is not a Refilterable resolver. Skipping refilter()") } } - + public edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver updateMetadataResolverEnabledStatus(edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver updatedResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException { // @TODO: when merged with groups, this should maybe be merged with group check as they have to have the role in the right group if (!userService.currentUserHasExpectedRole(["ROLE_ADMIN", "ROLE_ENABLE"])) { throw new ForbiddenException("You do not have the permissions necessary to change the enable status of this filter.") } - + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver persistedResolver = metadataResolverRepository.save(updatedResolver) - + if (persistedResolver.getDoInitialization()) { MetadataResolver openSamlRepresentation = null try { @@ -600,13 +580,13 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { throw new InitializationException(e); } } - + } - + private class ScriptedPredicate extends net.shibboleth.utilities.java.support.logic.ScriptedPredicate { protected ScriptedPredicate(@Nonnull EvaluableScript theScript) { super(theScript) } } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy index 31cec75b2..5ac4a7a02 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy @@ -22,7 +22,9 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Profile import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration import spock.lang.Specification @@ -33,6 +35,7 @@ import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.* @ContextConfiguration(classes = [CoreShibUiConfiguration, SearchConfiguration, InternationalizationConfiguration, edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration ,LocalConfig]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@ActiveProfiles(["local"]) class IncommonJPAMetadataResolverServiceImplTests extends Specification { @Autowired MetadataResolverService metadataResolverService @@ -106,6 +109,7 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { //TODO: check that this configuration is sufficient @TestConfiguration + @Profile("local") static class LocalConfig { @Autowired OpenSamlObjects openSamlObjects From e9e80fffa82091c5256af1b1221d7e12b9a57525 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 12 Aug 2021 08:58:38 -0700 Subject: [PATCH 10/24] Fixed issues with merge --- ui/src/app/admin/component/UserMaintenance.js | 18 ++++----- .../domain/provider/component/ProviderList.js | 2 +- .../domain/source/component/SourceList.js | 40 +++++++++---------- ui/src/app/metadata/view/MetadataOptions.js | 8 ---- 4 files changed, 30 insertions(+), 38 deletions(-) diff --git a/ui/src/app/admin/component/UserMaintenance.js b/ui/src/app/admin/component/UserMaintenance.js index c4542438f..e9a7b8c33 100644 --- a/ui/src/app/admin/component/UserMaintenance.js +++ b/ui/src/app/admin/component/UserMaintenance.js @@ -23,10 +23,10 @@ export default function UserMaintenance({ users, roles, loading, onDeleteUser, o UserId - Name + Name Email - Role - Group + Role + Group Delete? @@ -36,10 +36,10 @@ export default function UserMaintenance({ users, roles, loading, onDeleteUser, o {users.map((user, idx) => - {user.username} - {user.firstName} {user.lastName} - {user.emailAddress} - + {user.username} + {user.firstName} {user.lastName} + {user.emailAddress} +