diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java new file mode 100644 index 000000000..ebbd057a1 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java @@ -0,0 +1,29 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.service.AttributeBundleService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/custom/entity/bundles") +@Slf4j +public class AttributeBundleController { + @Autowired AttributeBundleService attributeBundleService; + + @GetMapping + @Transactional(readOnly = true) + public ResponseEntity getAll() { + return ResponseEntity.ok(attributeBundleService.findAll()); + } + + //POST + + //DELETE + + //PUT +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeBundle.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeBundle.java new file mode 100644 index 000000000..2df1132ac --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeBundle.java @@ -0,0 +1,31 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import lombok.Data; + +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.Id; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +@Entity(name = "attribute_bundle_definition") +@Data +public class AttributeBundle { + @Column(nullable = false) + @ElementCollection + Set attributes = new HashSet<>(); + + @Column(name = "name", nullable = true) + String name; + + @Id + @Column(name = "resource_id", nullable = false) + String resourceId = UUID.randomUUID().toString(); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BundleableAttributeType.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BundleableAttributeType.java new file mode 100644 index 000000000..9977e90b1 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BundleableAttributeType.java @@ -0,0 +1,38 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import edu.internet2.tier.shibboleth.admin.util.BundleableAttributeTypeValueSerializer; + +@JsonSerialize(using = BundleableAttributeTypeValueSerializer.class) +public enum BundleableAttributeType { + EDUPERSONPRINCIPALNAME("eduPersonPrincipalName"), + UID("uid"), + MAIL("mail"), + SURNAME("surname"), + GIVENNAME("givenName"), + EDUPERSONAFFILIATE("eduPersonAffiliation"), + EDUPERSONSCOPEDAFFILIATION("eduPersonScopedAffiliation"), + EDUPERSONPRIMARYAFFILIATION("eduPersonPrimaryAffiliation"), + EDUPERSONENTITLEMENT("eduPersonEntitlement"), + EDUPERSONASSURANCE("eduPersonAssurance"), + EDUPERSONUNIQUEID("eduPersonUniqueId"), + EMPLOYEENUMBER("employeeNumber"); + + String label; + + BundleableAttributeType(String val) { + label = val; + } + + public String label() {return label;} + + public static BundleableAttributeType valueOfLabel(String label) { + for (BundleableAttributeType e : values()) { + if (e.name().equals(label)) { + return e; + } + } + return null; + } + +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepository.java new file mode 100644 index 000000000..c2d52bb23 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepository.java @@ -0,0 +1,15 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository; + +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +/** + * Repository to manage {@link edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle} instances. + */ +public interface AttributeBundleRepository extends JpaRepository { + List findAll(); + + AttributeBundle save(AttributeBundle target); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java new file mode 100644 index 000000000..413a14386 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java @@ -0,0 +1,18 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle; +import edu.internet2.tier.shibboleth.admin.ui.repository.AttributeBundleRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class AttributeBundleService { + @Autowired + AttributeBundleRepository attributeBundleRepository; + + public List findAll() { + return attributeBundleRepository.findAll(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/BundleableAttributeTypeValueSerializer.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/BundleableAttributeTypeValueSerializer.java new file mode 100644 index 000000000..32a7b7cc8 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/BundleableAttributeTypeValueSerializer.java @@ -0,0 +1,29 @@ +package edu.internet2.tier.shibboleth.admin.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import edu.internet2.tier.shibboleth.admin.ui.domain.BundleableAttributeType; + +import java.io.IOException; + +/** + * This simplifies translation to the front end. We use the ENUM on the backend, but the BundleableAttributeType + * is tagged to serialize using this helper. + * Note: The deserialize is done naturally by setting spring.jackson.mapper.accept-case-insensitive-enums=true in + * the application.properties and by the setup of the ENUM itself + */ +public class BundleableAttributeTypeValueSerializer extends StdSerializer { + public BundleableAttributeTypeValueSerializer() { + this(null); + } + + public BundleableAttributeTypeValueSerializer(Class t) { + super(t); + } + + @Override + public void serialize(BundleableAttributeType value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(value.label()); + } +} \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 5b7b801f1..0556e5b45 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -24,6 +24,7 @@ spring.h2.console.settings.web-allow-others=true # spring.jackson.default-property-inclusion=non_absent spring.jackson.default-property-inclusion=NON_NULL +spring.jackson.mapper.accept-case-insensitive-enums=true # Database Configuration PostgreSQL #spring.datasource.url=jdbc:postgresql://localhost:5432/shibui diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy new file mode 100644 index 000000000..231e83601 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy @@ -0,0 +1,104 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import com.fasterxml.jackson.databind.MapperFeature +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle +import edu.internet2.tier.shibboleth.admin.ui.repository.AttributeBundleRepository +import edu.internet2.tier.shibboleth.admin.ui.service.AttributeBundleService +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.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.transaction.annotation.Transactional +import spock.lang.Specification + +import static org.hamcrest.Matchers.containsInAnyOrder +import static org.springframework.http.MediaType.APPLICATION_JSON +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@DataJpaTest +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@ContextConfiguration(classes = [ShibUIConfiguration, ABCTConfig]) +class AttributeBundleControllerTests extends Specification { + @Autowired + AttributeBundleController controller + + @Autowired + AttributeBundleRepository attributeBundleRepository + + ObjectMapper objectMapper = new ObjectMapper().with { + it.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + it + } + + def MockMvc + + @Transactional + def setup() { + mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + attributeBundleRepository.deleteAll() + } + + def "GET checks" () { + expect: + attributeBundleRepository.findAll().isEmpty() + + when: "fetch for no bundles" + def result = mockMvc.perform(get('/api/custom/entity/bundles')) + + then: + result.andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(content().json('[]')) + + when: "add a bundle" + def json = """ + { + "name": "bundleName", + "resourceId": "randomIDVal", + "attributes": ["eduPersonPrincipalName", "surname", "givenName"] + } + """ + + AttributeBundle bundle = objectMapper.readValue(json, AttributeBundle.class) + attributeBundleRepository.saveAndFlush(bundle) + result = mockMvc.perform(get('/api/custom/entity/bundles')) + + then: + result.andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.[0].name").value("bundleName")) + .andExpect(jsonPath("\$.[0].resourceId").value("randomIDVal")) + .andExpect(jsonPath("\$.[0].attributes", containsInAnyOrder("eduPersonPrincipalName", "surname", "givenName"))) + } + + // can go away with merge to develop... + @TestConfiguration + private static class ABCTConfig { + @Bean + AttributeBundleController attributeBundleController(AttributeBundleService attributeBundleService) { + new AttributeBundleController().with { + it.attributeBundleService = attributeBundleService + it + } + } + + @Bean + AttributeBundleService attributeBundleService(AttributeBundleRepository repo) { + new AttributeBundleService().with { + it.attributeBundleRepository = repo + it + } + } + + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy new file mode 100644 index 000000000..148cceda8 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy @@ -0,0 +1,45 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository + +import com.fasterxml.jackson.databind.MapperFeature +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle +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.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +@DataJpaTest +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@ContextConfiguration(classes = [ShibUIConfiguration]) +class AttributeBundleRepositoryTests extends Specification { + @Autowired + AttributeBundleRepository abRepo + + ObjectMapper objectMapper = new ObjectMapper().with { + it.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + it + } + + def "test create and fetch" () { + given: + def json = """ + { + "name": "bundleName", + "resourceId": "randomIDVal", + "attributes": ["eduPersonPrincipalName", "surname", "givenName"] + } + """ + + AttributeBundle bundle = objectMapper.readValue(json, AttributeBundle.class) + + when: + def result = abRepo.save(bundle) + + then: + result == bundle + } +} \ No newline at end of file