diff --git a/backend/build.gradle b/backend/build.gradle index 9744cbf84..180630e68 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -156,7 +156,7 @@ dependencies { runtimeOnly 'org.glassfish.jaxb:jaxb-runtime:2.3.0' compile "com.h2database:h2" - runtimeOnly "org.postgresql:postgresql" + runtimeOnly "org.postgresql:postgresql:42.2.20" runtimeOnly 'org.mariadb.jdbc:mariadb-java-client:2.2.0' runtimeOnly 'mysql:mysql-connector-java:5.1.48' diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JsonSchemaBuilderService.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JsonSchemaBuilderService.groovy index 3a84fce4f..6ebdcf7be 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JsonSchemaBuilderService.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JsonSchemaBuilderService.groovy @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.service import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.IRelyingPartyOverrideProperty import edu.internet2.tier.shibboleth.admin.ui.security.model.User import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import org.springframework.beans.factory.annotation.Autowired @@ -29,15 +30,15 @@ class JsonSchemaBuilderService { def properties = [:] customPropertiesConfiguration.getOverrides().each { def property - if (it['displayType'] == 'list' - || it['displayType'] == 'set') { + if (it['displayType'] == 'list' || it['displayType'] == 'set' || it['displayType'] == 'selection_list') { property = [$ref: '#/definitions/' + it['name']] } else { property = - [title : it['displayName'], - description: it['helpText'], - type : it['displayType'], - examples : it['examples']] + [title : it['displayName'], + description : it['helpText'], + type : ((IRelyingPartyOverrideProperty)it).getTypeForUI(), + default : it['displayType'] == 'boolean' ? Boolean.getBoolean(it['defaultValue']) : it['defaultValue'], + examples : it['examples']] } properties[(String) it['name']] = property } @@ -46,13 +47,12 @@ class JsonSchemaBuilderService { void addRelyingPartyOverridesCollectionDefinitionsToJson(Object json) { customPropertiesConfiguration.getOverrides().stream().filter { - it -> it['displayType'] && (it['displayType'] == 'list' || it['displayType'] == 'set') + it -> it['displayType'] && (it['displayType'] == 'list' || it['displayType'] == 'set' || it['displayType'] == 'selection_list') }.each { def definition = [title : it['displayName'], description: it['helpText'], - type : 'array', - default : null] - if (it['displayType'] == 'set') { + type : 'array'] + if (it['displayType'] == 'set' || it['displayType'] == 'selection_list') { definition['uniqueItems'] = true } else if (it['displayType'] == 'list') { definition['uniqueItems'] = false @@ -61,6 +61,8 @@ class JsonSchemaBuilderService { minLength: 1, // TODO: should this be configurable? maxLength: 255] //TODO: or this? items.examples = it['examples'] + items['default'] = it['defaultValue'] + definition['items'] = items json[(String) it['name']] = definition diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomPropertiesConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomPropertiesConfiguration.java index a6a1db63d..af8aef206 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomPropertiesConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomPropertiesConfiguration.java @@ -1,36 +1,88 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration; +import edu.internet2.tier.shibboleth.admin.ui.domain.IRelyingPartyOverrideProperty; import edu.internet2.tier.shibboleth.admin.ui.domain.RelyingPartyOverrideProperty; +import edu.internet2.tier.shibboleth.admin.ui.service.CustomEntityAttributesDefinitionService; +import edu.internet2.tier.shibboleth.admin.ui.service.events.CustomEntityAttributeDefinitionChangeEvent; + +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Configuration; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; -/** - * @author Bill Smith (wsmith@unicon.net) - */ -@Configuration -@ConfigurationProperties(prefix="custom") -public class CustomPropertiesConfiguration { +import javax.annotation.PostConstruct; +@Configuration +@ConfigurationProperties(prefix = "custom") +public class CustomPropertiesConfiguration implements ApplicationListener { private List> attributes = new ArrayList<>(); - private List overrides = new ArrayList<>(); + + private CustomEntityAttributesDefinitionService ceadService; + + private HashMap overrides = new HashMap<>(); + + private List overridesFromConfigFile = new ArrayList<>(); + + private void buildRelyingPartyOverrides() { + // Start over with a clean map and get the CustomEntityAttributesDefinitions from the DB + HashMap reloaded = new HashMap<>(); + ceadService.getAllDefinitions().forEach(def -> { + def.updateExamplesList(); // totally non-ooo, but @PostLoad wasn't working and JPA/Hibernate is doing some reflection crap + reloaded.put(def.getName(), def); + }); + + // We only want to add to an override from the config file if the incoming override (by name) isn't already in + // the list of overrides (ie DB > file config) + for (RelyingPartyOverrideProperty rpop : this.overridesFromConfigFile) { + if (!reloaded.containsKey(rpop.getName())) { + reloaded.put(rpop.getName(), rpop); + } + } + + this.overrides = reloaded; + } public List> getAttributes() { return attributes; } + public List getOverrides() { + return new ArrayList<>(overrides.values()); + } + + /** + * We don't know what change occurred, so the easiest thing to do is just rebuild our map of overrides. + * (especially since the small occurrence of this and number of items makes doing this ok perf-wise). + */ + @Override + public void onApplicationEvent(CustomEntityAttributeDefinitionChangeEvent arg0) { + buildRelyingPartyOverrides(); + } + + @PostConstruct + public void postConstruct() { + // Make sure we have the right data + buildRelyingPartyOverrides(); + } + public void setAttributes(List> attributes) { this.attributes = attributes; } - public List getOverrides() { - return overrides; + @Autowired + public void setCeadService(CustomEntityAttributesDefinitionService ceadService) { + this.ceadService = ceadService; } - public void setOverrides(List overrides) { - this.overrides = overrides; + /** + * This setter will get used by Spring's property system to create objects from a config file (should the properties exist) + */ + public void setOverrides(List overridesFromConfigFile) { + this.overridesFromConfigFile = overridesFromConfigFile; } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/CustomEntityAttributesDefinitionsController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/CustomEntityAttributesDefinitionsController.java index 56183dfdb..bd683de22 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/CustomEntityAttributesDefinitionsController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/CustomEntityAttributesDefinitionsController.java @@ -1,7 +1,5 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; -import java.util.List; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -18,7 +16,6 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import edu.internet2.tier.shibboleth.admin.ui.domain.CustomEntityAttributeDefinition; -import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; import edu.internet2.tier.shibboleth.admin.ui.service.CustomEntityAttributesDefinitionService; @Controller @@ -31,7 +28,7 @@ public class CustomEntityAttributesDefinitionsController { @Transactional public ResponseEntity create(@RequestBody CustomEntityAttributeDefinition definition) { // If already defined, we can't create a new one, nor will this call update the definition - CustomEntityAttributeDefinition cad = caService.find(definition.getName()); + CustomEntityAttributeDefinition cad = caService.find(definition.getResourceId()); if (cad != null) { HttpHeaders headers = new HttpHeaders(); @@ -39,7 +36,7 @@ public ResponseEntity create(@RequestBody CustomEntityAttributeDefinition def return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers) .body(new ErrorResponse(String.valueOf(HttpStatus.CONFLICT.value()), - String.format("The custom attribute definition with name: [%s] already exists.", definition.getName()))); + String.format("The custom attribute definition already exists - unable to create a new definition"))); } CustomEntityAttributeDefinition result = caService.createOrUpdateDefinition(definition); @@ -49,7 +46,7 @@ public ResponseEntity create(@RequestBody CustomEntityAttributeDefinition def @PutMapping("/attribute") @Transactional public ResponseEntity update(@RequestBody CustomEntityAttributeDefinition definition) { - CustomEntityAttributeDefinition cad = caService.find(definition.getName()); + CustomEntityAttributeDefinition cad = caService.find(definition.getResourceId()); if (cad == null) { HttpHeaders headers = new HttpHeaders(); headers.setLocation(ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/custom/entity/attribute").build().toUri()); @@ -63,40 +60,44 @@ public ResponseEntity update(@RequestBody CustomEntityAttributeDefinition def return ResponseEntity.ok(result); } + /** + * @return List of IRelyingPartyOverrideProperty objects. This will include all of the CustomEntityAttributeDefinition + * and the RelyingPartyOverrideProperties from any configuration file that was read in at startup. + */ @GetMapping("/attributes") @Transactional(readOnly = true) public ResponseEntity getAll() { return ResponseEntity.ok(caService.getAllDefinitions()); } - @GetMapping("/attribute/{name}") + @GetMapping("/attribute/{resourceId}") @Transactional(readOnly = true) - public ResponseEntity getOne(@PathVariable String name) { - CustomEntityAttributeDefinition cad = caService.find(name); + public ResponseEntity getOne(@PathVariable String resourceId) { + CustomEntityAttributeDefinition cad = caService.find(resourceId); if (cad == null) { HttpHeaders headers = new HttpHeaders(); headers.setLocation( - ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/custom/entity/attribute/" + name).build().toUri()); + ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/custom/entity/attribute/" + resourceId).build().toUri()); return ResponseEntity.status(HttpStatus.NOT_FOUND).headers(headers) .body(new ErrorResponse(String.valueOf(HttpStatus.NOT_FOUND.value()), - String.format("The custom attribute definition with name: [%s] does not already exist.", name))); + String.format("The custom attribute definition with resource id: [%s] does not already exist.", resourceId))); } return ResponseEntity.ok(cad); } - @DeleteMapping("/attribute/{name}") + @DeleteMapping("/attribute/{resourceId}") @Transactional - public ResponseEntity delete(@PathVariable String name) { - CustomEntityAttributeDefinition cad = caService.find(name); + public ResponseEntity delete(@PathVariable String resourceId) { + CustomEntityAttributeDefinition cad = caService.find(resourceId); if (cad == null) { HttpHeaders headers = new HttpHeaders(); headers.setLocation( - ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/custom/entity/attribute/" + name).build().toUri()); + ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/custom/entity/attribute/" + resourceId).build().toUri()); return ResponseEntity.status(HttpStatus.NOT_FOUND).headers(headers) .body(new ErrorResponse(String.valueOf(HttpStatus.NOT_FOUND.value()), - String.format("The custom attribute definition with name: [%s] does not already exist.", name))); + String.format("The custom attribute definition with resource id: [%s] does not already exist.", resourceId))); } caService.deleteDefinition(cad); return ResponseEntity.noContent().build(); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomEntityAttributeDefinition.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomEntityAttributeDefinition.java index d852ba5ea..f1d14911a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomEntityAttributeDefinition.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomEntityAttributeDefinition.java @@ -2,16 +2,19 @@ import java.util.HashSet; import java.util.Set; +import java.util.UUID; -import javax.persistence.CascadeType; import javax.persistence.CollectionTable; import javax.persistence.Column; import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.JoinColumn; -import javax.persistence.OneToMany; +import javax.persistence.Transient; +import org.apache.commons.lang3.StringUtils; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; import org.hibernate.envers.Audited; import lombok.Data; @@ -19,28 +22,96 @@ @Entity(name = "custom_entity_attribute_definition") @Audited @Data -public class CustomEntityAttributeDefinition { - @Id - @Column(nullable = false) - String name; - - @Column(name = "help_text", nullable = true) - String helpText; +public class CustomEntityAttributeDefinition implements IRelyingPartyOverrideProperty { + @Column(name = "attribute_friendly_name", nullable = true) + String attributeFriendlyName; + + @Column(name = "attribute_name", nullable = true) + String attributeName; @Column(name = "attribute_type", nullable = false) CustomAttributeType attributeType; - @Column(name = "default_value", nullable = true) - String defaultValue; - @ElementCollection @CollectionTable(name = "custom_entity_attr_list_items", joinColumns = @JoinColumn(name = "name")) + @Fetch(FetchMode.JOIN) @Column(name = "value", nullable = false) Set customAttrListDefinitions = new HashSet<>(); + + @Column(name = "default_value", nullable = true) + String defaultValue; - // @TODO: logic to ensure defaultValue matches an item from the list of values when SELECTION_LIST is the type ?? -} + @Column(name = "display_name", nullable = true) + String displayName; -enum CustomAttributeType { - STRING, BOOLEAN, INTEGER, LONG, DOUBLE, DURATION, SELECTION_LIST, SPRING_BEAN_ID + @Transient + Set examples; + + @Column(name = "help_text", nullable = true) + String helpText; + + @Column(name = "invert", nullable = true) + String invert; + + @Column(nullable = false) + String name; + + @Column(name = "persist_type", nullable = true) + String persistType; + + @Column(name = "persist_value", nullable = true) + String persistValue; + + @Id + @Column(name = "resource_id", nullable = false) + String resourceId = UUID.randomUUID().toString(); + + @Override + public Set getDefaultValues() { + return customAttrListDefinitions; + } + + @Override + public String getDisplayType() { + return attributeType.name().toLowerCase(); + } + + @Override + public Boolean getFromConfigFile() { + return Boolean.FALSE; + } + + public String getTypeForUI() { + switch (attributeType) { + case BOOLEAN: + case INTEGER: + return getDisplayType(); + case SELECTION_LIST: + return "list"; + default: // DOUBLE, DURATION, LONG, SPRING_BEAN_ID, STRING + return "string"; + } + } + + @Override + public void setDefaultValues(Set defaultValues) { + // This is here to comply with the interface only and should not be used to change the set of values in this implementation + } + + @Override + public void setDisplayType(String displayType) { + // This is here to comply with the interface only and should not be used to change the value in this implementation + } + + /** + * Ensure there are no whitespace characters in the name + */ + @Override + public void setName(String name) { + this.name = name.replaceAll("\\s",""); + } + + public void updateExamplesList() { + examples = customAttrListDefinitions; + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IRelyingPartyOverrideProperty.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IRelyingPartyOverrideProperty.java new file mode 100644 index 000000000..076fce7dd --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IRelyingPartyOverrideProperty.java @@ -0,0 +1,64 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import java.util.Set; + +enum CustomAttributeType { + BOOLEAN, DOUBLE, DURATION, INTEGER, LONG, SELECTION_LIST, SPRING_BEAN_ID, STRING +} + +public interface IRelyingPartyOverrideProperty { + public String getAttributeFriendlyName(); + + public String getAttributeName(); + + public CustomAttributeType getAttributeType(); + + public String getDefaultValue(); + + public Set getDefaultValues(); + + public String getDisplayName(); + + public String getDisplayType(); + + public Boolean getFromConfigFile(); + + public String getHelpText(); + + public String getInvert(); + + public String getName(); + + public String getPersistType(); + + public String getPersistValue(); + + /** + * When the override actually is used in the UI, the "type" list is fairly limited, so each implementing class + * should adjust the real value so the UI gets a value it expects. For actual file configured overrides, this + * means doing nothing, but UI defined attributes have to do some work. + */ + public String getTypeForUI(); + + public void setAttributeFriendlyName(String attributeFriendlyName); + + public void setAttributeName(String attributeName); + + public void setDefaultValue(String defaultValue); + + public void setDefaultValues(Set defaultValues); + + public void setDisplayName(String displayName); + + public void setDisplayType(String displayType); + + public void setHelpText(String helpText); + + public void setInvert(String invert); + + public void setName(String name); + + public void setPersistType(String persistType); + + public void setPersistValue(String persistValue); +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RelyingPartyOverrideProperty.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RelyingPartyOverrideProperty.java index c28a290e8..9c75bd382 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RelyingPartyOverrideProperty.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RelyingPartyOverrideProperty.java @@ -1,41 +1,55 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; + import lombok.Getter; import lombok.Setter; - -import java.util.List; +import lombok.ToString; /** * @author Bill Smith (wsmith@unicon.net) */ @Setter @Getter -public class RelyingPartyOverrideProperty { - private String name; +@ToString +public class RelyingPartyOverrideProperty implements IRelyingPartyOverrideProperty { + private String attributeFriendlyName; + private String attributeName; + private String defaultValue; + private Set defaultValues; private String displayName; private String displayType; - private String defaultValue; + private Set examples; private String helpText; - private List examples; + private String invert; + private String name; private String persistType; private String persistValue; - private String attributeName; - private String attributeFriendlyName; - private String invert; + + @Override + public Boolean getFromConfigFile() { + return Boolean.TRUE; + } @Override - public String toString() { - return "RelyingPartyOverrideProperty{" - + "\nname='" + name + '\'' - + ", \ndisplayName='" + displayName + '\'' - + ", \ndisplayType='" + displayType + '\'' - + ", \ndefaultValue='" + defaultValue + '\'' - + ", \nhelpText='" + helpText + '\'' - + ", \npersistType='" + persistType + '\'' - + ", \npersistValue='" + persistValue + '\'' - + ", \nexamples=" + examples - + ", \nattributeName='" + attributeName + '\'' - + ", \nattributeFriendlyName='" + attributeFriendlyName + '\'' - + "\n}"; + public CustomAttributeType getAttributeType() { + switch (displayType) { + case ("set"): + case ("list"): + return CustomAttributeType.SELECTION_LIST; + default: + return CustomAttributeType.valueOf(displayType.toUpperCase()); + } + } + + public String getTypeForUI() { + return getDisplayType(); + } + + public void setDefaultValues(Set defaults) { + defaultValues = defaults; + examples = defaults; } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/CustomEntityAttributeFilterValue.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/CustomEntityAttributeFilterValue.java deleted file mode 100644 index ed6715631..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/CustomEntityAttributeFilterValue.java +++ /dev/null @@ -1,43 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain.filters; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.Table; -import javax.persistence.UniqueConstraint; - -import org.hibernate.envers.Audited; - -import edu.internet2.tier.shibboleth.admin.ui.domain.CustomEntityAttributeDefinition; -import lombok.Getter; -import lombok.Setter; - - -@Entity(name = "custom_entity_attr_filter_value") -@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "filter_id", "custom_entity_attribute_name" }) }) -@Audited -// NOTE: lombok's toString and equals cause an infinite loop somewhere that causes stack overflows, so if we need impls, -// do it manually. Do not replace the Getter and Setter with @Data... -@Getter -@Setter -public class CustomEntityAttributeFilterValue { - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - @Column(name = "generated_id") - private Integer id; - - @ManyToOne - @JoinColumn(name = "filter_id", nullable = false) - EntityAttributesFilter entityAttributesFilter; - - @ManyToOne - @JoinColumn(name = "custom_entity_attribute_name", referencedColumnName = "name", nullable = false) - CustomEntityAttributeDefinition customEntityAttributeDefinition; - - @Column(name = "value", nullable = false) - String value; -} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java index 28926358f..7643f483d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java @@ -53,16 +53,7 @@ public EntityAttributesFilter() { @Transient private List attributeRelease = new ArrayList<>(); - - @JsonIgnore - @OneToMany(cascade = CascadeType.ALL, mappedBy = "entityAttributesFilter", orphanRemoval = true) - private Set customEntityAttributes = new HashSet<>(); - - public void setCustomEntityAttributes (Set newValues) { - customEntityAttributes.clear(); - customEntityAttributes.addAll(newValues); - } - + public void setAttributeRelease(List attributeRelease) { this.attributeRelease = attributeRelease; this.rebuildAttributes(); @@ -95,7 +86,6 @@ private EntityAttributesFilter updateConcreteFilterTypeData(EntityAttributesFilt filterToBeUpdated.setEntityAttributesFilterTarget(getEntityAttributesFilterTarget()); filterToBeUpdated.setRelyingPartyOverrides(getRelyingPartyOverrides()); filterToBeUpdated.setAttributeRelease(getAttributeRelease()); - filterToBeUpdated.setCustomEntityAttributes(customEntityAttributes); return filterToBeUpdated; } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepository.java index 677d7cf87..db3724ea5 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepository.java @@ -15,6 +15,8 @@ public interface CustomEntityAttributeDefinitionRepository extends JpaRepository CustomEntityAttributeDefinition findByName(String name); + CustomEntityAttributeDefinition findByResourceId(String resourceId); + @SuppressWarnings("unchecked") CustomEntityAttributeDefinition save(CustomEntityAttributeDefinition attribute); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeFilterValueRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeFilterValueRepository.java deleted file mode 100644 index a6a7be164..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeFilterValueRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.repository; - -import java.util.List; - -import org.springframework.data.jpa.repository.JpaRepository; - -import edu.internet2.tier.shibboleth.admin.ui.domain.CustomEntityAttributeDefinition; -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.CustomEntityAttributeFilterValue; -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter; - -public interface CustomEntityAttributeFilterValueRepository extends JpaRepository { - // Not entirely sure this is needed for the core, but it did make validation/unit testing a whole lot easier - CustomEntityAttributeFilterValue findByEntityAttributesFilterAndCustomEntityAttributeDefinition(EntityAttributesFilter eaf , CustomEntityAttributeDefinition cead); - - List findAllByCustomEntityAttributeDefinition(CustomEntityAttributeDefinition definition); -} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/CustomEntityAttributesDefinitionService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/CustomEntityAttributesDefinitionService.java index 4087ad221..f4539a15e 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/CustomEntityAttributesDefinitionService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/CustomEntityAttributesDefinitionService.java @@ -10,7 +10,7 @@ public interface CustomEntityAttributesDefinitionService { void deleteDefinition(CustomEntityAttributeDefinition definition); - CustomEntityAttributeDefinition find(String name); + CustomEntityAttributeDefinition find(String resourceId); List getAllDefinitions(); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/CustomEntityAttributesDefinitionServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/CustomEntityAttributesDefinitionServiceImpl.java index 95f654386..6fe0a8c25 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/CustomEntityAttributesDefinitionServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/CustomEntityAttributesDefinitionServiceImpl.java @@ -5,45 +5,44 @@ import javax.persistence.EntityManager; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import edu.internet2.tier.shibboleth.admin.ui.domain.CustomEntityAttributeDefinition; -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.CustomEntityAttributeFilterValue; import edu.internet2.tier.shibboleth.admin.ui.repository.CustomEntityAttributeDefinitionRepository; -import edu.internet2.tier.shibboleth.admin.ui.repository.CustomEntityAttributeFilterValueRepository; +import edu.internet2.tier.shibboleth.admin.ui.service.events.CustomEntityAttributeDefinitionChangeEvent; @Service -public class CustomEntityAttributesDefinitionServiceImpl implements CustomEntityAttributesDefinitionService { +public class CustomEntityAttributesDefinitionServiceImpl implements CustomEntityAttributesDefinitionService { @Autowired - private CustomEntityAttributeDefinitionRepository repository; + private ApplicationEventPublisher applicationEventPublisher; @Autowired - CustomEntityAttributeFilterValueRepository customEntityAttributeFilterValueRepository; + EntityManager entityManager; @Autowired - EntityManager entityManager; - + private CustomEntityAttributeDefinitionRepository repository; + @Override + @Transactional public CustomEntityAttributeDefinition createOrUpdateDefinition(CustomEntityAttributeDefinition definition) { - return repository.save(definition); + CustomEntityAttributeDefinition result = repository.save(definition); + notifyListeners(); + return result; } - + @Override + @Transactional public void deleteDefinition(CustomEntityAttributeDefinition definition) { - // must remove any CustomEntityAttributeFilterValues first to avoid integrity constraint issues - List customEntityValues = customEntityAttributeFilterValueRepository.findAllByCustomEntityAttributeDefinition(definition); - customEntityValues.forEach(value -> { - value.getEntityAttributesFilter().getCustomEntityAttributes().remove(value); - entityManager.remove(value); - customEntityAttributeFilterValueRepository.delete(value); - }); - CustomEntityAttributeDefinition entityToRemove = repository.findByName(definition.getName()); + CustomEntityAttributeDefinition entityToRemove = repository.findByResourceId(definition.getResourceId()); repository.delete(entityToRemove); + notifyListeners(); } @Override - public CustomEntityAttributeDefinition find(String name) { - return repository.findByName(name); + public CustomEntityAttributeDefinition find(String resourceId) { + return repository.findByResourceId(resourceId); } @Override @@ -51,4 +50,7 @@ public List getAllDefinitions() { return repository.findAll(); } + private void notifyListeners() { + applicationEventPublisher.publishEvent(new CustomEntityAttributeDefinitionChangeEvent(this)); + } } 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 743e56981..05477d616 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 @@ -25,7 +25,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationName; import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationURL; import edu.internet2.tier.shibboleth.admin.ui.domain.PrivacyStatementURL; -import edu.internet2.tier.shibboleth.admin.ui.domain.RelyingPartyOverrideProperty; +import edu.internet2.tier.shibboleth.admin.ui.domain.IRelyingPartyOverrideProperty; import edu.internet2.tier.shibboleth.admin.ui.domain.SPSSODescriptor; import edu.internet2.tier.shibboleth.admin.ui.domain.SingleLogoutService; import edu.internet2.tier.shibboleth.admin.ui.domain.UIInfo; @@ -69,6 +69,7 @@ import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getStringListOfAttributeValues; import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getValueFromXMLObject; + /** * Default implementation of {@link EntityDescriptorService} * @@ -645,10 +646,14 @@ public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.ope } else { Optional override = ModelRepresentationConversions.getOverrideByAttributeName(jpaAttribute.getName()); if (override.isPresent()) { - RelyingPartyOverrideProperty overrideProperty = (RelyingPartyOverrideProperty)override.get(); + IRelyingPartyOverrideProperty overrideProperty = (IRelyingPartyOverrideProperty)override.get(); Object attributeValues = null; switch (ModelRepresentationConversions.AttributeTypes.valueOf(overrideProperty.getDisplayType().toUpperCase())) { case STRING: + case LONG: + case DOUBLE: + case DURATION: + case SPRING_BEAN_ID: if (jpaAttribute.getAttributeValues().size() != 1) { throw new RuntimeException("Multiple/No values detected where one is expected!"); } @@ -674,11 +679,12 @@ public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.ope break; case SET: case LIST: + case SELECTION_LIST: attributeValues = jpaAttribute.getAttributeValues().stream() .map(attributeValue -> getValueFromXMLObject(attributeValue)) .collect(Collectors.toList()); } - relyingPartyOverrides.put(((RelyingPartyOverrideProperty) override.get()).getName(), attributeValues); + relyingPartyOverrides.put(((IRelyingPartyOverrideProperty) override.get()).getName(), attributeValues); } } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImpl.java index 111adb394..595ce896b 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImpl.java @@ -1,23 +1,17 @@ package edu.internet2.tier.shibboleth.admin.ui.service; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.opensaml.saml.saml2.core.Attribute; +import org.springframework.beans.factory.annotation.Autowired; + import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration; -import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBuilder; -import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeValue; -import edu.internet2.tier.shibboleth.admin.ui.domain.RelyingPartyOverrideProperty; -import edu.internet2.tier.shibboleth.admin.ui.domain.XSString; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.util.AttributeUtility; -import edu.internet2.tier.shibboleth.admin.util.MDDCConstants; import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; -import org.opensaml.saml.saml2.core.Attribute; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getAttributeFromObjectAndRelyingPartyOverrideProperty; public class JPAEntityServiceImpl implements EntityService { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/events/CustomEntityAttributeDefinitionChangeEvent.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/events/CustomEntityAttributeDefinitionChangeEvent.java new file mode 100644 index 000000000..f0495f615 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/events/CustomEntityAttributeDefinitionChangeEvent.java @@ -0,0 +1,15 @@ +package edu.internet2.tier.shibboleth.admin.ui.service.events; + +import org.springframework.context.ApplicationEvent; + +/** + * The event could be any operation (new, update, delete). + */ +public class CustomEntityAttributeDefinitionChangeEvent extends ApplicationEvent { + private static final long serialVersionUID = 1L; + + public CustomEntityAttributeDefinitionChangeEvent(Object source) { + super(source); + } + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/ModelRepresentationConversions.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/ModelRepresentationConversions.java index 12a1bad82..1cf4273bd 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/ModelRepresentationConversions.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/ModelRepresentationConversions.java @@ -2,7 +2,7 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration; import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; -import edu.internet2.tier.shibboleth.admin.ui.domain.RelyingPartyOverrideProperty; +import edu.internet2.tier.shibboleth.admin.ui.domain.IRelyingPartyOverrideProperty; import edu.internet2.tier.shibboleth.admin.ui.domain.XSAny; import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean; import edu.internet2.tier.shibboleth.admin.ui.domain.XSInteger; @@ -14,11 +14,9 @@ import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; /** @@ -90,7 +88,7 @@ public static Map getRelyingPartyOverridesRepresentationFromAttr Optional override = getOverrideByAttributeName(jpaAttribute.getName()); if (override.isPresent()) { - relyingPartyOverrides.put(((RelyingPartyOverrideProperty) override.get()).getName(), + relyingPartyOverrides.put(((IRelyingPartyOverrideProperty) override.get()).getName(), getOverrideFromAttribute(jpaAttribute)); } } @@ -98,16 +96,8 @@ public static Map getRelyingPartyOverridesRepresentationFromAttr return relyingPartyOverrides; } - private static Object getDefaultValueFromProperty(RelyingPartyOverrideProperty property) { - switch (property.getDisplayType()) { - case "boolean": - return Boolean.getBoolean(property.getDefaultValue()); - } - return null; - } - public static Object getOverrideFromAttribute(Attribute attribute) { - RelyingPartyOverrideProperty relyingPartyOverrideProperty = customPropertiesConfiguration.getOverrides().stream() + IRelyingPartyOverrideProperty relyingPartyOverrideProperty = customPropertiesConfiguration.getOverrides().stream() .filter(it -> it.getAttributeFriendlyName().equals(attribute.getFriendlyName())).findFirst().get(); List attributeValues = attribute.getAttributeValues(); @@ -119,16 +109,21 @@ public static Object getOverrideFromAttribute(Attribute attribute) { } else { return Boolean.valueOf(relyingPartyOverrideProperty.getInvert()) ^ Boolean.valueOf(((XSBoolean) attributeValues.get(0)).getStoredValue()); } - case INTEGER: - return ((XSInteger) attributeValues.get(0)).getValue(); + case INTEGER: + return ((XSInteger) attributeValues.get(0)).getValue(); case STRING: + case LONG: + case DOUBLE: + case DURATION: + case SPRING_BEAN_ID: if (attributeValues.get(0) instanceof XSAny) { return ((XSAny) attributeValues.get(0)).getTextContent(); } else { return ((XSString) attributeValues.get(0)).getValue(); } - case LIST: case SET: + case LIST: + case SELECTION_LIST: return attributeValues.stream().map(it -> ((XSString) it).getValue()).collect(Collectors.toList()); default: throw new UnsupportedOperationException("An unsupported persist type was specified (" + relyingPartyOverrideProperty.getPersistType() + ")!"); @@ -161,13 +156,13 @@ public static List getAttributeListFromA public static List getAttributeListFromRelyingPartyOverridesRepresentation (Map relyingPartyOverridesRepresentation) { - List overridePropertyList = customPropertiesConfiguration.getOverrides(); + List overridePropertyList = customPropertiesConfiguration.getOverrides(); List list = new ArrayList<>(); if (relyingPartyOverridesRepresentation != null) { for (Map.Entry entry : relyingPartyOverridesRepresentation.entrySet()) { String key = (String) entry.getKey(); - RelyingPartyOverrideProperty overrideProperty = overridePropertyList.stream().filter(op -> op.getName().equals(key)).findFirst().get(); + IRelyingPartyOverrideProperty overrideProperty = overridePropertyList.stream().filter(op -> op.getName().equals(key)).findFirst().get(); Attribute attribute = getAttributeFromObjectAndRelyingPartyOverrideProperty(entry.getValue(), overrideProperty); if (attribute != null) { list.add(attribute); @@ -178,7 +173,7 @@ public static List getAttributeListFromA return (List) (List) list; } - public static Attribute getAttributeFromObjectAndRelyingPartyOverrideProperty(Object o, RelyingPartyOverrideProperty overrideProperty) { + public static Attribute getAttributeFromObjectAndRelyingPartyOverrideProperty(Object o, IRelyingPartyOverrideProperty overrideProperty) { switch (ModelRepresentationConversions.AttributeTypes.valueOf(overrideProperty.getDisplayType().toUpperCase())) { case BOOLEAN: if ((o instanceof Boolean && ((Boolean) o)) || @@ -207,15 +202,16 @@ public static Attribute getAttributeFromObjectAndRelyingPartyOverrideProperty(Ob overrideProperty.getAttributeFriendlyName(), Integer.valueOf((String) o)); case STRING: + case LONG: + case DOUBLE: + case DURATION: + case SPRING_BEAN_ID: return ATTRIBUTE_UTILITY.createAttributeWithStringValues(overrideProperty.getAttributeName(), overrideProperty.getAttributeFriendlyName(), (String) o); case SET: - return ATTRIBUTE_UTILITY.createAttributeWithStringValues(overrideProperty.getAttributeName(), - overrideProperty.getAttributeFriendlyName(), - (List) o); - case LIST: + case SELECTION_LIST: return ATTRIBUTE_UTILITY.createAttributeWithStringValues(overrideProperty.getAttributeName(), overrideProperty.getAttributeFriendlyName(), (List) o); @@ -225,11 +221,19 @@ public static Attribute getAttributeFromObjectAndRelyingPartyOverrideProperty(Ob } } + // These are the types for which there are org.opensaml.core.xml.schema.XS[TYPE] definitions that we are supporting + // The ones with comments are the types supported by the Custom Entity Attribute UI and are mapped accordingly + // @see edu.internet2.tier.shibboleth.admin.ui.domain.CustomAttributeType (part of IRelyingPartyOverrideProperty) public enum AttributeTypes { BOOLEAN, INTEGER, STRING, SET, - LIST + LIST, + DOUBLE, // no org.opensaml.core.xml.schema.XSTYPE - will treat as STRING + DURATION, // no org.opensaml.core.xml.schema.XSTYPE - will treat as STRING + LONG, // no org.opensaml.core.xml.schema.XSTYPE - will treat as STRING for generating XML + SELECTION_LIST, // another name for LIST + SPRING_BEAN_ID // treat as STRING } } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index ea5222d05..e9301289a 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -118,7 +118,7 @@ custom: displayName: label.nameid-format-to-send displayType: set helpText: tooltip.nameid-format - examples: + defaultValues: - 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 @@ -129,7 +129,7 @@ custom: displayName: label.authentication-methods-to-use displayType: set helpText: tooltip.authentication-methods-to-use - examples: + defaultValues: - 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 diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 50049f5cb..ee6ab56ed 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -104,9 +104,17 @@ value.template=Template value.string=String value.boolean=Boolean value.list=List +value.long=Long +value.double=Double +value.duration=Duration +value.spring-bean-id=Spring Bean ID value.BOOLEAN=Boolean value.SELECTION_LIST=List value.STRING=String +value.LONG=Long +value.DOUBLE=Double +value.DURATION=Duration +value.SPRING_BEAN_ID=Spring Bean ID brand.header.title=Source Management brand.logo-link-label=Shibboleth @@ -140,11 +148,25 @@ label.entity-attribute-default=Default Value tooltip.entity-attribute-default=Default Value label.entity-attribute-list-options=List options tooltip.entity-attribute-list-options=List options +label.entity-attribute-friendly-name=Friendly name +tooltip.entity-attribute-friendly-name=Friendly name +label.entity-attribute-attr-name=Attribute name +tooltip.entity-attribute-attr-name=This is normally a uri or urn +label.entity-attribute-display-name=Display name +tooltip.entity-attribute-display-name=Display name + +label.entity-attribute-persist-value=Persist Value +label.entity-attribute-persist-type=Persist Type +tooltip.entity-attribute-persist-value=Persist Value +tooltip.entity-attribute-persist-type=Persist Type +label.entity-attribute-invert=Invert +tooltip.entity-attribute-invert=Invert label.entity-attributes=Entity Attributes label.custom-entity-attributes=Custom Entity Attributes label.help-text=Help text label.default-value=Default Value +label.new-attribute=New Custom Entity Attribute label.metadata-source=Metadata Source label.metadata-sources=Metadata Sources @@ -472,6 +494,7 @@ message.uri-valid-format=URI must be valid format. message.id-unique=ID must be unique. message.array-items-must-be-unique=Items in list must be unique. message.valid-duration=Must be a valid duration. +message.valid-name=No special characters or whitespace allowed. message.required=Missing required property. message.org-name-required=Organization Name is required. diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomPropertiesConfigurationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomPropertiesConfigurationTests.groovy new file mode 100644 index 000000000..74ecb9796 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomPropertiesConfigurationTests.groovy @@ -0,0 +1,92 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration + +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.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.CustomEntityAttributeDefinition +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.repository.CustomEntityAttributeDefinitionRepository +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.service.CustomEntityAttributesDefinitionService +import edu.internet2.tier.shibboleth.admin.ui.service.CustomEntityAttributesDefinitionServiceImpl + +import javax.persistence.EntityManager + +import org.opensaml.saml.metadata.resolver.MetadataResolver +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.boot.autoconfigure.domain.EntityScan +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.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration + +import spock.lang.Specification + +/** + * Tests for edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration + */ +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, InternationalizationConfiguration, SearchConfiguration, edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class CustomPropertiesConfigurationTests extends Specification { + + @Autowired + @Qualifier(value="customPropertiesConfiguration") + CustomPropertiesConfiguration configUnderTest + + @Autowired + CustomEntityAttributesDefinitionService ceadService + + @Autowired + CustomEntityAttributeDefinitionRepository repository; + + @Autowired + EntityManager entityManager + + def "Updating Custom Entity Attribute Definitions will update the CustomPropertiesConfiguration automatically"() { + given: 'Test defaults loaded (ie no CEADs in DB)' + + expect: + ceadService.getAllDefinitions().size() == 0 + configUnderTest.getOverrides().size() == 10 + + def ca = new CustomEntityAttributeDefinition().with { + it.name = "newDefName" + it.attributeType = "STRING" + it.defaultValue = "foobar" + it + } + + ceadService.createOrUpdateDefinition(ca) + entityManager.flush() + + ceadService.getAllDefinitions().size() == 1 + configUnderTest.getOverrides().size() == 11 + + def ca2 = new CustomEntityAttributeDefinition().with { + it.name = "newDefName2" + it.attributeType = "STRING" + it.defaultValue = "foobar2" + it + } + + ceadService.createOrUpdateDefinition(ca2) + entityManager.flush() + + ceadService.getAllDefinitions().size() == 2 + configUnderTest.getOverrides().size() == 12 + + ceadService.deleteDefinition(ca) + entityManager.flush() + + ceadService.getAllDefinitions().size() == 1 + configUnderTest.getOverrides().size() == 11 + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy index 5e3c0ad1b..7f12a050d 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy @@ -2,11 +2,16 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.repository.CustomEntityAttributeDefinitionRepository import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository import edu.internet2.tier.shibboleth.admin.ui.security.DefaultAuditorAware +import edu.internet2.tier.shibboleth.admin.ui.service.CustomEntityAttributesDefinitionServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService import net.shibboleth.ext.spring.resource.ResourceHelper import net.shibboleth.utilities.java.support.component.ComponentInitializationException + +import javax.persistence.EntityManager + import org.apache.lucene.document.Document import org.apache.lucene.document.Field import org.apache.lucene.document.StringField @@ -26,6 +31,9 @@ import org.springframework.data.domain.AuditorAware import org.springframework.mail.javamail.JavaMailSender import org.springframework.mail.javamail.JavaMailSenderImpl +/** + * NOT A TEST - this is configuration FOR tests + */ @Configuration class TestConfiguration { @Autowired @@ -35,11 +43,26 @@ class TestConfiguration { final MetadataResolverRepository metadataResolverRepository final Logger logger = LoggerFactory.getLogger(TestConfiguration.class); + @Autowired + private CustomEntityAttributeDefinitionRepository repository; + + @Autowired + EntityManager entityManager + TestConfiguration(final OpenSamlObjects openSamlObjects, final MetadataResolverRepository metadataResolverRepository) { this.openSamlObjects =openSamlObjects this.metadataResolverRepository = metadataResolverRepository } + @Bean + CustomEntityAttributesDefinitionServiceImpl customEntityAttributesDefinitionServiceImpl() { + new CustomEntityAttributesDefinitionServiceImpl().with { + it.entityManager = entityManager + it.repository = repository + return it + } + } + @Bean JavaMailSender javaMailSender() { return new JavaMailSenderImpl().with { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptorTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptorTest.groovy index 5fe53b85b..b0906a68c 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptorTest.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptorTest.groovy @@ -33,7 +33,7 @@ import java.nio.file.Files * @author Bill Smith (wsmith@unicon.net) */ @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, InternationalizationConfiguration, MyConfig, PlaceholderResolverComponentsConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, InternationalizationConfiguration, MyConfig, PlaceholderResolverComponentsConfiguration, edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepositoryTests.groovy index 2601d4d42..5c7f8cf8e 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepositoryTests.groovy @@ -142,11 +142,7 @@ class CustomEntityAttributeDefinitionRepositoryTests extends Specification { // delete tests when: - def delByName = new CustomEntityAttributeDefinition().with { - it.name = "ca-name" - it - } - repo.delete(delByName) + repo.delete(caFromDb1) entityManager.flush() entityManager.clear() @@ -159,24 +155,12 @@ class CustomEntityAttributeDefinitionRepositoryTests extends Specification { def setItems2 = new HashSet(["val2", "val1"]) def setItems3 = new HashSet(["val1", "val2", "val3"]) def setItems4 = new HashSet(["val1", "val2", "val3", "val4"]) - def ca2 = new CustomEntityAttributeDefinition().with { - it.name = "ca-name" - it.attributeType = "SELECTION_LIST" - it.customAttrListDefinitions = setItems2 - it - } def ca3 = new CustomEntityAttributeDefinition().with { it.name = "ca-name" it.attributeType = "SELECTION_LIST" it.customAttrListDefinitions = setItems3 it } - def ca4 = new CustomEntityAttributeDefinition().with { - it.name = "ca-name" - it.attributeType = "SELECTION_LIST" - it.customAttrListDefinitions = setItems4 - it - } when: repo.save(ca3) @@ -186,31 +170,39 @@ class CustomEntityAttributeDefinitionRepositoryTests extends Specification { then: def cas = repo.findAll() cas.size() == 1 - def caFromDb = cas.get(0).asType(CustomEntityAttributeDefinition) - caFromDb.equals(ca3) == true + def ca3FromDb = cas.get(0).asType(CustomEntityAttributeDefinition) + ca3FromDb.equals(ca3) == true // now update the attribute list items - caFromDb.with { + ca3FromDb.with { it.customAttrListDefinitions = setItems4 it } - repo.save(caFromDb) + repo.save(ca3FromDb) entityManager.flush() entityManager.clear() def caFromDb4 = repo.findAll().get(0).asType(CustomEntityAttributeDefinition) + def ca4 = new CustomEntityAttributeDefinition().with { + it.name = "ca-name" + it.attributeType = "SELECTION_LIST" + it.customAttrListDefinitions = setItems4 + it.resourceId = ca3FromDb.resourceId + it + } caFromDb4.equals(ca4) == true // now remove items - caFromDb.with { + ca3FromDb.with { it.customAttrListDefinitions = setItems2 it } - repo.save(caFromDb) + repo.save(ca3FromDb) entityManager.flush() entityManager.clear() def caFromDb2 = repo.findAll().get(0).asType(CustomEntityAttributeDefinition) - caFromDb2.equals(ca2) == true + ca3FromDb.resourceId == caFromDb2.resourceId + ca3FromDb.customAttrListDefinitions.equals(caFromDb2.customAttrListDefinitions) } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy index 86fe1b74e..70331ffe6 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy @@ -8,6 +8,7 @@ import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects 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 edu.internet2.tier.shibboleth.admin.ui.service.CustomEntityAttributesDefinitionServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import org.apache.lucene.analysis.Analyzer @@ -29,7 +30,7 @@ import javax.persistence.EntityManager * A highly unnecessary test so that I can check to make sure that persistence is correct for the model */ @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, InternationalizationConfiguration, Config]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, InternationalizationConfiguration, LocalConfig]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) @@ -37,6 +38,9 @@ class EntityDescriptorRepositoryTest extends Specification { @Autowired EntityDescriptorRepository entityDescriptorRepository + @Autowired + private CustomEntityAttributeDefinitionRepository repository; + @Autowired EntityManager entityManager @@ -86,7 +90,7 @@ class EntityDescriptorRepositoryTest extends Specification { } @TestConfiguration - static class Config { + static class LocalConfig { @Bean MetadataResolver metadataResolver() { new OpenSamlChainingMetadataResolver().with { @@ -100,5 +104,14 @@ class EntityDescriptorRepositoryTest extends Specification { Analyzer analyzer() { return new EnglishAnalyzer() } + + @Bean + CustomEntityAttributesDefinitionServiceImpl customEntityAttributesDefinitionServiceImpl() { + new CustomEntityAttributesDefinitionServiceImpl().with { + it.entityManager = entityManager + it.repository = repository + return it + } + } } } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepositoryTests.groovy index 48a042e5b..f233684fd 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepositoryTests.groovy @@ -5,7 +5,6 @@ import javax.persistence.EntityManager import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.context.annotation.ComponentScan import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.test.context.ContextConfiguration @@ -16,9 +15,7 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.Internationalization import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.CustomEntityAttributeDefinition -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.CustomEntityAttributeFilterValue import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter -import edu.internet2.tier.shibboleth.admin.ui.service.CustomEntityAttributesDefinitionService import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import spock.lang.Specification @@ -26,18 +23,11 @@ import spock.lang.Specification @ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@ComponentScan("edu.internet2.tier.shibboleth.admin.ui.service") class FilterRepositoryTests extends Specification { @Autowired FilterRepository repositoryUnderTest - @Autowired - CustomEntityAttributesDefinitionService ceadService - - @Autowired - CustomEntityAttributeFilterValueRepository ceafvRepo - @Autowired EntityManager entityManager @@ -96,172 +86,4 @@ class FilterRepositoryTests extends Specification { persistedFilter.audId > 0L persistedFilter.formats.size() == 1 } - - def "FilterRepository + EntityAttributesFilter CRUD ops with custom entity attributes correctly"(){ - given: - def ca = new CustomEntityAttributeDefinition().with { - it.name = "ca-name" - it.attributeType = "STRING" - it.defaultValue = "foo" - it - } - ceadService.createOrUpdateDefinition(ca) - entityManager.flush() - entityManager.clear() - - def entityAttributesFilterJson = '''{ - "name": "EntityAttributes", - "resourceId": "29a5d409-562a-41cd-acee-e9b3d7098d05", - "filterEnabled": false, - "entityAttributesFilterTarget": { - "entityAttributesFilterTargetType": "CONDITION_SCRIPT", - "value": [ - "TwUuSOz5O6" - ] - }, - "attributeRelease": [ - "WbkhLQNI3m" - ], - "relyingPartyOverrides": { - "signAssertion": true, - "dontSignResponse": true, - "turnOffEncryption": true, - "useSha": true, - "ignoreAuthenticationMethod": false, - "omitNotBefore": true, - "responderId": null, - "nameIdFormats": [ - "xLenUFmCLn" - ], - "authenticationMethods": [] - }, - "@type": "EntityAttributes" - }''' - def filter = new ObjectMapper().readValue(entityAttributesFilterJson.bytes, EntityAttributesFilter.class) - def persistedFilter = repositoryUnderTest.save(filter) - entityManager.flush() - - def savedFilter = repositoryUnderTest.findByResourceId(persistedFilter.resourceId) - def saveEAD = ceadService.find("ca-name") - - def ceafv = new CustomEntityAttributeFilterValue().with { - it.entityAttributesFilter = savedFilter - it.customEntityAttributeDefinition = saveEAD - it.value = "bar" - it - } - - def customEntityAttributes = new HashSet() - - when: - customEntityAttributes.add(ceafv) // nothing to do yet, just here to let us verify nothing in the CEAFV table in 'then' block - - then: - ((Set)ceafvRepo.findAll()).size() == 0 //nothing yet - ((EntityAttributesFilter)savedFilter).setCustomEntityAttributes(customEntityAttributes) - repositoryUnderTest.save(savedFilter) - entityManager.flush() - - then: - def listOfceafv = ceafvRepo.findAll() - listOfceafv.size() == 1 - - def ceafvFromDb = listOfceafv.get(0).asType(CustomEntityAttributeFilterValue) - ceafvFromDb.getEntityAttributesFilter().getResourceId().equals("29a5d409-562a-41cd-acee-e9b3d7098d05") - - def filterFromDb = (EntityAttributesFilter) repositoryUnderTest.findByResourceId("29a5d409-562a-41cd-acee-e9b3d7098d05") - filterFromDb.getCustomEntityAttributes().size() == 1 - - // now remove all - def emptySet = new HashSet() - filterFromDb.setCustomEntityAttributes(emptySet) - repositoryUnderTest.save(filterFromDb) - entityManager.flush() - - ceafvRepo.findAll().size() == 0 - } - - def "Delete custom entity attributes definition removes entries from filter correctly"(){ - given: - def ca = new CustomEntityAttributeDefinition().with { - it.name = "ca-name" - it.attributeType = "STRING" - it.defaultValue = "foo" - it - } - ceadService.createOrUpdateDefinition(ca) - entityManager.flush() - entityManager.clear() - - def entityAttributesFilterJson = '''{ - "name": "EntityAttributes", - "resourceId": "29a5d409-562a-41cd-acee-e9b3d7098d05", - "filterEnabled": false, - "entityAttributesFilterTarget": { - "entityAttributesFilterTargetType": "CONDITION_SCRIPT", - "value": [ - "TwUuSOz5O6" - ] - }, - "attributeRelease": [ - "WbkhLQNI3m" - ], - "relyingPartyOverrides": { - "signAssertion": true, - "dontSignResponse": true, - "turnOffEncryption": true, - "useSha": true, - "ignoreAuthenticationMethod": false, - "omitNotBefore": true, - "responderId": null, - "nameIdFormats": [ - "xLenUFmCLn" - ], - "authenticationMethods": [] - }, - "@type": "EntityAttributes" - }''' - def filter = new ObjectMapper().readValue(entityAttributesFilterJson.bytes, EntityAttributesFilter.class) - def persistedFilter = repositoryUnderTest.save(filter) - entityManager.flush() - - def savedFilter = repositoryUnderTest.findByResourceId(persistedFilter.resourceId) - def saveEAD = ceadService.find("ca-name"); - - def ceafv = new CustomEntityAttributeFilterValue().with { - it.entityAttributesFilter = savedFilter - it.customEntityAttributeDefinition = saveEAD - it.value = "bar" - it - } - - def customEntityAttributes = new HashSet() - - when: - customEntityAttributes.add(ceafv) // nothing to do yet, just here to let us verify nothing in the CEAFV table in 'then' block - - then: - ((Set)ceafvRepo.findAll()).size() == 0 //nothing yet - ((EntityAttributesFilter)savedFilter).setCustomEntityAttributes(customEntityAttributes) - repositoryUnderTest.save(savedFilter) - entityManager.flush() - - then: - def listOfceafv = ceafvRepo.findAll() - listOfceafv.size() == 1 - - def ceafvFromDb = listOfceafv.get(0).asType(CustomEntityAttributeFilterValue) - ceafvFromDb.getEntityAttributesFilter().getResourceId().equals("29a5d409-562a-41cd-acee-e9b3d7098d05") - - def filterFromDb = (EntityAttributesFilter) repositoryUnderTest.findByResourceId("29a5d409-562a-41cd-acee-e9b3d7098d05") - filterFromDb.getCustomEntityAttributes().size() == 1 - - // now remove the definition - ceadService.deleteDefinition(saveEAD) - entityManager.flush() - entityManager.clear() - - def filterFromDb2 = (EntityAttributesFilter)repositoryUnderTest.findByResourceId("29a5d409-562a-41cd-acee-e9b3d7098d05") - filterFromDb2.getCustomEntityAttributes().size() == 0 - } } 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 110ebffc4..31cec75b2 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 @@ -30,7 +30,7 @@ import spock.lang.Specification import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.* @DataJpaTest -@ContextConfiguration(classes = [CoreShibUiConfiguration, SearchConfiguration, InternationalizationConfiguration, TestConfig]) +@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") class IncommonJPAMetadataResolverServiceImplTests extends Specification { @@ -106,7 +106,7 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { //TODO: check that this configuration is sufficient @TestConfiguration - static class TestConfig { + static class LocalConfig { @Autowired OpenSamlObjects openSamlObjects 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 9470b6046..3ab2154ea 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 @@ -51,7 +51,7 @@ import spock.lang.Unroll import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.generatedXmlIsTheSameAsExpectedXml @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, MetadataResolverConverterConfiguration, SearchConfiguration, InternationalizationConfiguration, PlaceholderResolverComponentsConfiguration, Config]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, MetadataResolverConverterConfiguration, 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/gradle.properties b/gradle.properties index ca31f82be..056038aae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ opensaml.version=3.4.3 spring-boot.version=2.4.2 -hibernate.version=5.3.14.Final +hibernate.version=5.5.0.Final lucene.version=8.1.1 diff --git a/testbed/postgres/.gitignore b/testbed/postgres/.gitignore new file mode 100644 index 000000000..32ed36f28 --- /dev/null +++ b/testbed/postgres/.gitignore @@ -0,0 +1 @@ +/dc-charles.yml diff --git a/testbed/postgres/conf/.gitignore b/testbed/postgres/conf/.gitignore new file mode 100644 index 000000000..f3e10820d --- /dev/null +++ b/testbed/postgres/conf/.gitignore @@ -0,0 +1 @@ +/charles.yml diff --git a/ui/public/assets/schema/attribute/attribute.schema.json b/ui/public/assets/schema/attribute/attribute.schema.json index cc99dc3a6..0514703f0 100644 --- a/ui/public/assets/schema/attribute/attribute.schema.json +++ b/ui/public/assets/schema/attribute/attribute.schema.json @@ -2,7 +2,10 @@ "type": "object", "required": [ "name", - "attributeType" + "attributeType", + "attributeFriendlyName", + "attributeName", + "displayName" ], "properties": { "name": { @@ -10,7 +13,8 @@ "description": "tooltip.entity-attribute-name", "type": "string", "minLength": 1, - "maxLength": 255 + "maxLength": 255, + "pattern": "^[a-zA-Z0-9:]*$" }, "attributeType": { "title": "label.entity-attribute-type", @@ -19,14 +23,43 @@ "enum": [ "STRING", "BOOLEAN", - "SELECTION_LIST" + "SELECTION_LIST", + "LONG", + "DOUBLE", + "DURATION", + "SPRING_BEAN_ID" ], "enumNames": [ "value.string", "value.boolean", - "value.list" + "value.list", + "value.long", + "value.double", + "value.duration", + "value.spring-bean-id" ] }, + "attributeFriendlyName": { + "type": "string", + "title": "label.entity-attribute-friendly-name", + "description": "tooltip.entity-attribute-friendly-name", + "minLength": 1, + "maxLength": 255 + }, + "attributeName": { + "type": "string", + "title": "label.entity-attribute-attr-name", + "description": "tooltip.entity-attribute-attr-name", + "minLength": 1, + "maxLength": 255 + }, + "displayName": { + "type": "string", + "title": "label.entity-attribute-display-name", + "description": "tooltip.entity-attribute-display-name", + "minLength": 1, + "maxLength": 255 + }, "helpText": { "title": "label.entity-attribute-help", "description": "tooltip.entity-attribute-help", @@ -45,7 +78,21 @@ "STRING" ] }, - "defaultValueString": { + "defaultValue": { + "title": "label.entity-attribute-default", + "description": "tooltip.entity-attribute-default", + "type": "string" + } + } + }, + { + "properties": { + "attributeType": { + "enum": [ + "SPRING_BEAN_ID" + ] + }, + "defaultValue": { "title": "label.entity-attribute-default", "description": "tooltip.entity-attribute-default", "type": "string" @@ -53,6 +100,9 @@ } }, { + "required": [ + "persistValue" + ], "properties": { "attributeType": { "enum": [ @@ -65,6 +115,82 @@ "type": "boolean", "default": true, "enumNames": ["True", "False"] + }, + "persistValue": { + "type": "string", + "title": "label.entity-attribute-persist-value", + "description": "tooltip.entity-attribute-persist-value", + "minLength": 1, + "maxLength": 255 + }, + "persistType": { + "type": "string", + "title": "label.entity-attribute-persist-type", + "description": "tooltip.entity-attribute-persist-type", + "default": "string" + }, + "invert": { + "type": "boolean", + "title": "label.entity-attribute-invert", + "description": "tooltip.entity-attribute-invert" + } + } + }, + { + "properties": { + "attributeType": { + "enum": [ + "DURATION" + ] + }, + "defaultValue": { + "title": "label.entity-attribute-default", + "description": "tooltip.entity-attribute-default", + "type": "string", + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + } + } + }, + { + "properties": { + "attributeType": { + "enum": [ + "LONG" + ] + }, + "defaultValue": { + "title": "label.entity-attribute-default", + "description": "tooltip.entity-attribute-default", + "type": "string" + } + } + }, + { + "properties": { + "attributeType": { + "enum": [ + "DOUBLE" + ] + }, + "defaultValue": { + "title": "label.entity-attribute-default", + "description": "tooltip.entity-attribute-default", + "type": "string" + } + } + }, + { + "properties": { + "attributeType": { + "enum": [ + "INTEGER" + ] + }, + "defaultValue": { + "title": "label.entity-attribute-default", + "description": "tooltip.entity-attribute-default", + "type": "string", + "pattern": "^\\d+$" } } }, diff --git a/ui/src/app/App.js b/ui/src/app/App.js index 097c0095d..5d0c4e1e4 100644 --- a/ui/src/app/App.js +++ b/ui/src/app/App.js @@ -34,6 +34,7 @@ function App() { const [showTimeout] = React.useState(); const httpOptions = { + cachePolicy: 'no-cache', redirect: 'manual', interceptors: { request: async ({options, url, path, route}) => { diff --git a/ui/src/app/form/component/fields/StringListWithDefaultField.js b/ui/src/app/form/component/fields/StringListWithDefaultField.js index 3f8ba8b74..8b7a02896 100644 --- a/ui/src/app/form/component/fields/StringListWithDefaultField.js +++ b/ui/src/app/form/component/fields/StringListWithDefaultField.js @@ -95,39 +95,44 @@ const StringListWithDefaultField = ({
-
- {} - - {(props.uiSchema["ui:description"] || schema.description) && ( - +
+ {} + - )} + {(props.uiSchema["ui:description"] || schema.description) && ( + + )} +
+
+ Default +
{items && items.map((p, idx) => -
+
setValue(p, value)}> - setDefault(p) } > diff --git a/ui/src/app/form/component/widgets/CheckboxWidget.js b/ui/src/app/form/component/widgets/CheckboxWidget.js index b1833159c..c95d7e921 100644 --- a/ui/src/app/form/component/widgets/CheckboxWidget.js +++ b/ui/src/app/form/component/widgets/CheckboxWidget.js @@ -41,7 +41,7 @@ const CheckboxWidget = (props) => { {schema.description && } } - checked={typeof value === "undefined" ? false : value} + checked={typeof value === "undefined" ? false : typeof value === 'boolean' ? value : value === 'true' ? true : false} required={required} disabled={disabled || readonly} autoFocus={autofocus} diff --git a/ui/src/app/form/component/widgets/RadioWidget.js b/ui/src/app/form/component/widgets/RadioWidget.js index c92d63e95..ddd248bf4 100644 --- a/ui/src/app/form/component/widgets/RadioWidget.js +++ b/ui/src/app/form/component/widgets/RadioWidget.js @@ -46,7 +46,7 @@ const RadioWidget = ({ const itemDisabled = Array.isArray(enumDisabled) && enumDisabled.indexOf(option.value) !== -1; - const checked = option.value === value; + const checked = option?.value?.toString() === value?.toString(); const radio = ( ({ - value: d, - default: d === defaultValue - })) - } - } - - if (attributeType === 'BOOLEAN') { - formatted = { - ...formatted, - defaultValueBoolean: defaultValue === 'true' ? true : false - } - } - - if (attributeType === 'STRING') { - formatted = { - ...formatted, - defaultValueString: defaultValue - } + switch (attributeType) { + case 'SELECTION_LIST': + formatted = { + ...formatted, + customAttrListDefinitions: formatted.customAttrListDefinitions.map(d => ({ + value: d, + default: d === defaultValue + })) + } + break; + case 'BOOLEAN': + formatted = { + ...formatted, + defaultValueBoolean: defaultValue === 'true' ? true : false, + invert: formatted.invert === 'true' ? true : false + } + break; + default: + formatted = { + ...formatted, + defaultValue + } } return formatted; diff --git a/ui/src/app/metadata/domain/attribute/CustomAttributeDefinition.test.js b/ui/src/app/metadata/domain/attribute/CustomAttributeDefinition.test.js index 9fbbe6e6c..3b6c865ca 100644 --- a/ui/src/app/metadata/domain/attribute/CustomAttributeDefinition.test.js +++ b/ui/src/app/metadata/domain/attribute/CustomAttributeDefinition.test.js @@ -25,10 +25,12 @@ describe('formatter', () => { expect(CustomAttributeDefinition.formatter({ attributeType: 'BOOLEAN', - defaultValue: 'true' + defaultValue: 'true', + invert: 'true' })).toEqual({ attributeType: 'BOOLEAN', - defaultValueBoolean: true + defaultValueBoolean: true, + invert: true }); expect(CustomAttributeDefinition.formatter({ @@ -36,7 +38,7 @@ describe('formatter', () => { defaultValue: 'true' })).toEqual({ attributeType: 'STRING', - defaultValueString: 'true' + defaultValue: 'true' }); }); }); @@ -72,7 +74,7 @@ describe('parser', () => { expect(CustomAttributeDefinition.parser({ attributeType: 'STRING', - defaultValueString: 'true' + defaultValue: 'true' })).toEqual({ attributeType: 'STRING', defaultValue: 'true' diff --git a/ui/src/app/metadata/domain/transform.js b/ui/src/app/metadata/domain/transform.js index 1a3f3f973..83ea61746 100644 --- a/ui/src/app/metadata/domain/transform.js +++ b/ui/src/app/metadata/domain/transform.js @@ -8,8 +8,10 @@ export const transformErrors = (errors) => { if (e.name === 'pattern') { if (e.property.includes('email')) { e.message = 'message.valid-email'; + } else if (e.property.includes('name')) { + e.message = 'message.valid-name'; } else { - e.message = 'message.valid-duration'; + e.message = 'message.duration'; } } diff --git a/ui/src/app/metadata/domain/transform.test.js b/ui/src/app/metadata/domain/transform.test.js index 1ae2c8fa7..1b3109906 100644 --- a/ui/src/app/metadata/domain/transform.test.js +++ b/ui/src/app/metadata/domain/transform.test.js @@ -12,7 +12,7 @@ const errors = [ it('should transform error messages', () => { expect(transformErrors(errors)).toEqual([ { name: 'pattern', property: '/email', message: 'message.valid-email' }, - { name: 'pattern', property: 'foo', message: 'message.valid-duration' }, + { name: 'pattern', property: 'foo', message: 'message.duration' }, { name: 'type', message: 'bar' }, { name: 'type', message: 'message.required' } ]); diff --git a/ui/src/app/metadata/editor/MetadataAttributeEditor.js b/ui/src/app/metadata/editor/MetadataAttributeEditor.js index 9c13e2a5f..126444729 100644 --- a/ui/src/app/metadata/editor/MetadataAttributeEditor.js +++ b/ui/src/app/metadata/editor/MetadataAttributeEditor.js @@ -1,6 +1,7 @@ import React from 'react'; import { MetadataFormContext, setFormDataAction, setFormErrorAction } from '../hoc/MetadataFormContext'; import { MetadataDefinitionContext, MetadataSchemaContext } from '../hoc/MetadataSchema'; +import { transformErrors } from '../domain/transform'; import Form from '@rjsf/bootstrap-4'; @@ -44,7 +45,8 @@ export function MetadataAttributeEditor({ children }) { fields={fields} widgets={widgets} liveValidate={true} - ErrorList={ErrorListTemplate}> + ErrorList={ErrorListTemplate} + transformErrors={transformErrors}> <>
diff --git a/ui/src/app/metadata/editor/MetadataEditor.js b/ui/src/app/metadata/editor/MetadataEditor.js index c38766404..31bc30da0 100644 --- a/ui/src/app/metadata/editor/MetadataEditor.js +++ b/ui/src/app/metadata/editor/MetadataEditor.js @@ -18,6 +18,7 @@ import API_BASE_PATH from '../../App.constant'; import { MetadataObjectContext } from '../hoc/MetadataSelector'; import { FilterableProviders } from '../domain/provider'; import { checkChanges } from '../hooks/utility'; +import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; export function MetadataEditor ({ current }) { @@ -27,6 +28,8 @@ export function MetadataEditor ({ current }) { const { update, loading } = useMetadataUpdater(`${ API_BASE_PATH }${getMetadataPath(type)}`, current); + const notificationDispatch = useNotificationDispatcher(); + const { data } = useMetadataEntities(type, {}, []); const history = useHistory(); const definition = React.useContext(MetadataDefinitionContext); @@ -47,7 +50,8 @@ export function MetadataEditor ({ current }) { gotoDetail({ refresh: true }); }) .catch(err => { - window.location.reload(); + // window.location.reload(); + notificationDispatch(createNotificationAction(`${err.errorCode} - ${translator(err.errorMessage)}`, NotificationTypes.ERROR)) }); }; diff --git a/ui/src/app/metadata/new/NewAttribute.js b/ui/src/app/metadata/new/NewAttribute.js index 41d7ace0e..9c01eb55d 100644 --- a/ui/src/app/metadata/new/NewAttribute.js +++ b/ui/src/app/metadata/new/NewAttribute.js @@ -11,10 +11,13 @@ import { useMetadataAttribute } from '../hooks/api'; import {CustomAttributeDefinition} from '../domain/attribute/CustomAttributeDefinition'; import MetadataSchema from '../hoc/MetadataSchema'; import { MetadataForm } from '../hoc/MetadataFormContext'; +import { createNotificationAction, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; export function NewAttribute() { const history = useHistory(); + const dispatch = useNotificationDispatcher(); + const definition = CustomAttributeDefinition; const { post, response, loading } = useMetadataAttribute({}); @@ -22,9 +25,11 @@ export function NewAttribute() { const [blocking, setBlocking] = React.useState(false); async function save(metadata) { - await post(``, definition.parser(metadata)); + const resp = await post(``, definition.parser(metadata)); if (response.ok) { gotoDetail({ refresh: true }); + } else { + dispatch(createNotificationAction(`${resp.errorCode}: Unable to create attribute ... ${resp.errorMessage}`, 'danger', 5000)); } }; diff --git a/ui/src/app/metadata/view/MetadataAttributeEdit.js b/ui/src/app/metadata/view/MetadataAttributeEdit.js index 58cd6d995..d0c04f287 100644 --- a/ui/src/app/metadata/view/MetadataAttributeEdit.js +++ b/ui/src/app/metadata/view/MetadataAttributeEdit.js @@ -11,6 +11,7 @@ import { useMetadataAttribute } from '../hooks/api'; import { CustomAttributeDefinition, CustomAttributeEditor } from '../domain/attribute/CustomAttributeDefinition'; import MetadataSchema from '../hoc/MetadataSchema'; import { MetadataForm } from '../hoc/MetadataFormContext'; +import { createNotificationAction, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; export function MetadataAttributeEdit() { const { id } = useParams(); @@ -18,6 +19,8 @@ export function MetadataAttributeEdit() { const definition = CustomAttributeDefinition; + const dispatch = useNotificationDispatcher(); + const { get, put, response, loading } = useMetadataAttribute({ cachePolicy: 'no-cache' }); @@ -32,9 +35,11 @@ export function MetadataAttributeEdit() { } async function save(metadata) { - await put(``, definition.parser(metadata)); + const resp = await put(``, definition.parser(metadata)); if (response.ok) { gotoDetail({ refresh: true }); + } else { + dispatch(createNotificationAction(`${resp.errorCode}: Unable to edit attribute ... ${resp.errorMessage}`, 'danger', 5000)); } }; diff --git a/ui/src/app/metadata/view/MetadataAttributeList.js b/ui/src/app/metadata/view/MetadataAttributeList.js index b72c3c0e7..f585f805f 100644 --- a/ui/src/app/metadata/view/MetadataAttributeList.js +++ b/ui/src/app/metadata/view/MetadataAttributeList.js @@ -63,13 +63,13 @@ export function MetadataAttributeList ({entities, onDelete}) { {attr.helpText} {attr.defaultValue?.toString()} - + Edit -