diff --git a/backend/build.gradle b/backend/build.gradle index a6797b52c..914ccc374 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -150,6 +150,7 @@ dependencies { compile 'io.springfox:springfox-swagger-ui:2.9.2' testCompile "org.springframework.boot:spring-boot-starter-test" + testCompile "org.springframework.security:spring-security-test" testCompile "org.spockframework:spock-core:1.1-groovy-2.4" testCompile "org.spockframework:spock-spring:1.1-groovy-2.4" testCompile "org.xmlunit:xmlunit-core:2.5.1" diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/EndpointSecurityConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/EndpointSecurityConfiguration.java new file mode 100644 index 000000000..81187a17f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/EndpointSecurityConfiguration.java @@ -0,0 +1,16 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@Configuration +@EnableGlobalMethodSecurity( + prePostEnabled = true, + securedEnabled = true, + jsr250Enabled = true) +public class EndpointSecurityConfiguration extends GlobalMethodSecurityConfiguration { +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java index 3f507a929..97d88f5a7 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java @@ -57,9 +57,6 @@ public class JsonSchemaComponentsConfiguration { @Setter private String nameIdFormatFilterUiSchemaLocation = "classpath:nameid-filter.schema.json"; - @Autowired - UserRepository userRepository; - @Bean public JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry(ResourceLoader resourceLoader, ObjectMapper jacksonMapper) { return JsonSchemaResourceLocationRegistry.inMemory() diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java index e76749c06..03cdf92b5 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java @@ -74,6 +74,11 @@ public void initRestTemplate() { public ResponseEntity create(@RequestBody EntityDescriptorRepresentation edRepresentation) { final String entityId = edRepresentation.getEntityId(); + ResponseEntity entityDescriptorEnablingDeniedResponse = entityDescriptorEnablePermissionsCheck(edRepresentation.isServiceEnabled()); + if (entityDescriptorEnablingDeniedResponse != null) { + return entityDescriptorEnablingDeniedResponse; + } + ResponseEntity existingEntityDescriptorConflictResponse = existingEntityDescriptorCheck(entityId); if (existingEntityDescriptorConflictResponse != null) { return existingEntityDescriptorConflictResponse; @@ -113,7 +118,12 @@ public ResponseEntity update(@RequestBody EntityDescriptorRepresentation edRe if (existingEd == null) { return ResponseEntity.notFound().build(); } else { - if (currentUser.getRole().equals("ROLE_ADMIN") || currentUser.getUsername().equals(existingEd.getCreatedBy())) { + if (currentUser != null && (currentUser.getRole().equals("ROLE_ADMIN") || currentUser.getUsername().equals(existingEd.getCreatedBy()))) { + ResponseEntity entityDescriptorEnablingDeniedResponse = entityDescriptorEnablePermissionsCheck(edRepresentation.isServiceEnabled()); + if (entityDescriptorEnablingDeniedResponse != null) { + return entityDescriptorEnablingDeniedResponse; + } + // Verify we're the only one attempting to update the EntityDescriptor if (edRepresentation.getVersion() != existingEd.hashCode()) { return new ResponseEntity(HttpStatus.CONFLICT); @@ -142,11 +152,11 @@ public ResponseEntity getAll() { User currentUser = userService.getCurrentUser(); if (currentUser != null) { if (currentUser.getRole().equals("ROLE_ADMIN")) { - return ResponseEntity.ok(entityDescriptorRepository.findAllByCustomQueryAndStream() + return ResponseEntity.ok(entityDescriptorRepository.findAllStreamByCustomQuery() .map(ed -> entityDescriptorService.createRepresentationFromDescriptor(ed)) .collect(Collectors.toList())); } else { - return ResponseEntity.ok(entityDescriptorRepository.findAllByCreatedBy(currentUser.getUsername()) + return ResponseEntity.ok(entityDescriptorRepository.findAllStreamByCreatedBy(currentUser.getUsername()) .map(ed -> entityDescriptorService.createRepresentationFromDescriptor(ed)) .collect(Collectors.toList())); } @@ -211,6 +221,17 @@ private ResponseEntity existingEntityDescriptorCheck(String entityId) { return null; } + private ResponseEntity entityDescriptorEnablePermissionsCheck(boolean serviceEnabled) { + User user = userService.getCurrentUser(); + if (user != null) { + if (serviceEnabled && !user.getRole().equals("ROLE_ADMIN")) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(new ErrorResponse(HttpStatus.FORBIDDEN, "You do not have the permissions necessary to enable this service.")); + } + } + return null; + } + private ResponseEntity handleUploadingEntityDescriptorXml(byte[] rawXmlBytes, String spName) throws Exception { final EntityDescriptor ed = EntityDescriptor.class.cast(openSamlObjects.unmarshalFromXml(rawXmlBytes)); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java index 2ba4f419d..d87bf1367 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java @@ -16,10 +16,10 @@ public interface EntityDescriptorRepository extends CrudRepository findAllByServiceEnabled(boolean serviceEnabled); + Stream findAllStreamByServiceEnabled(boolean serviceEnabled); @Query("select e from EntityDescriptor e") - Stream findAllByCustomQueryAndStream(); + Stream findAllStreamByCustomQuery(); - Stream findAllByCreatedBy(String createdBy); + Stream findAllStreamByCreatedBy(String createdBy); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasks.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasks.java index 6e93a7d99..d7bb02282 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasks.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasks.java @@ -60,7 +60,7 @@ public EntityDescriptorFilesScheduledTasks(String metadataDirName, @Scheduled(fixedRateString = "${shibui.taskRunRate:30000}") @Transactional(readOnly = true) public void generateEntityDescriptorFiles() throws MarshallingException { - this.entityDescriptorRepository.findAllByServiceEnabled(true) + this.entityDescriptorRepository.findAllStreamByServiceEnabled(true) .forEach(ed -> { Path targetFilePath = targetFilePathFor(toSha1HexString(ed.getEntityID())); if (Files.exists(targetFilePath)) { @@ -91,7 +91,7 @@ public void removeDanglingEntityDescriptorFiles() { .map(it -> it.substring(0, it.indexOf("."))) .collect(toSet()); - Set enabledEidsSha1Hashes = this.entityDescriptorRepository.findAllByServiceEnabled(true) + Set enabledEidsSha1Hashes = this.entityDescriptorRepository.findAllStreamByServiceEnabled(true) .map(EntityDescriptor::getEntityID) .map(EntityDescriptorFilesScheduledTasks::toSha1HexString) .collect(toSet()); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java index 8532d3d26..3303a7e66 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java @@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.crypto.bcrypt.BCrypt; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.DeleteMapping; @@ -22,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.HttpClientErrorException; +import java.security.Principal; import java.util.List; import java.util.Optional; @@ -48,6 +50,7 @@ public UsersController(UserRepository userRepository, RoleRepository roleReposit this.userService = userService; } + @PreAuthorize("hasRole('ADMIN')") @Transactional(readOnly = true) @GetMapping public List getAll() { @@ -56,21 +59,19 @@ public List getAll() { @Transactional(readOnly = true) @GetMapping("/current") - public ResponseEntity getCurrentUser() { - User user = userService.getCurrentUser(); - if (user != null) { - return ResponseEntity.ok(user); - } else { - return ResponseEntity.notFound().build(); - } + public User getCurrentUser(Principal principal) { + // TODO: fix this + return userService.getCurrentUser(); } + @PreAuthorize("hasRole('ADMIN')") @Transactional(readOnly = true) @GetMapping("/{username}") public ResponseEntity getOne(@PathVariable String username) { return ResponseEntity.ok(findUserOrThrowHttp404(username)); } + @PreAuthorize("hasRole('ADMIN')") @Transactional @DeleteMapping("/{username}") public ResponseEntity deleteOne(@PathVariable String username) { @@ -79,6 +80,7 @@ public ResponseEntity deleteOne(@PathVariable String username) { return ResponseEntity.noContent().build(); } + @PreAuthorize("hasRole('ADMIN')") @Transactional @PostMapping ResponseEntity saveOne(@RequestBody User user) { @@ -96,6 +98,7 @@ ResponseEntity saveOne(@RequestBody User user) { return ResponseEntity.ok(savedUser); } + @PreAuthorize("hasRole('ADMIN')") @Transactional @PatchMapping("/{username}") ResponseEntity updateOne(@PathVariable(value = "username") String username, @RequestBody User user) { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java index ced3fe133..d34e83e6f 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java @@ -48,6 +48,7 @@ public void updateUserRole(User user) { } public User getCurrentUser() { + //TODO: Consider returning an Optional here User user = null; if (SecurityContextHolder.getContext() != null && SecurityContextHolder.getContext().getAuthentication() != null) { String principal = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EmailServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EmailServiceImpl.java index 6e8d20f28..e21051abd 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EmailServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EmailServiceImpl.java @@ -2,6 +2,7 @@ import edu.internet2.tier.shibboleth.admin.ui.security.model.User; import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; +import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.support.ResourceBundleMessageSource; @@ -72,6 +73,6 @@ public String[] getSystemAdminEmailAddresses() { logger.warn("No users with ROLE_ADMIN were found! Check your configuration!"); systemAdmins = new HashSet<>(); } - return systemAdmins.stream().map(User::getEmailAddress).distinct().toArray(String[]::new); + return systemAdmins.stream().filter(user -> StringUtils.isNotBlank(user.getEmailAddress())).map(User::getEmailAddress).distinct().toArray(String[]::new); } } diff --git a/backend/src/main/resources/dynamic-http-metadata-provider.schema.json b/backend/src/main/resources/dynamic-http-metadata-provider.schema.json index 167e79ab1..68fdf6fd7 100644 --- a/backend/src/main/resources/dynamic-http-metadata-provider.schema.json +++ b/backend/src/main/resources/dynamic-http-metadata-provider.schema.json @@ -119,14 +119,12 @@ "description": "tooltip.refresh-delay-factor", "type": "string", "widget": { - "id": "number", - "step": 0.01 + "id": "string", + "help": "message.real-number" }, "placeholder": "label.real-number", - "minimum": 0, - "maximum": 1, - "default": null, - "pattern": "^([0]*(\\.[0-9]+)?|[0]*\\.[0-9]*[1-9][0-9]*)$" + "default": "", + "pattern": "^(?:([0]*(\\.[0-9]+)?|[0]*\\.[0-9]*[1-9][0-9]*)|)$" }, "minCacheDuration": { "title": "label.min-cache-duration", @@ -599,8 +597,7 @@ "certificateFile": { "title": "label.certificate-file", "description": "tooltip.certificate-file", - "type": "string", - "default": "" + "type": "string" } }, "anyOf": [ diff --git a/backend/src/main/resources/file-system-metadata-provider.schema.json b/backend/src/main/resources/file-system-metadata-provider.schema.json index b4722fc1d..cbfec6b8c 100644 --- a/backend/src/main/resources/file-system-metadata-provider.schema.json +++ b/backend/src/main/resources/file-system-metadata-provider.schema.json @@ -130,14 +130,12 @@ "description": "tooltip.refresh-delay-factor", "type": "string", "widget": { - "id": "number", - "step": 0.01 + "id": "string", + "help": "message.real-number" }, "placeholder": "label.real-number", - "minimum": 0, - "maximum": 1, - "default": null, - "pattern": "^([0]*(\\.[0-9]+)?|[0]*\\.[0-9]*[1-9][0-9]*)$" + "default": "", + "pattern": "^(?:([0]*(\\.[0-9]+)?|[0]*\\.[0-9]*[1-9][0-9]*)|)$" } } } diff --git a/backend/src/main/resources/i18n/messages_en.properties b/backend/src/main/resources/i18n/messages_en.properties index f7c238fb5..e6f30bfe3 100644 --- a/backend/src/main/resources/i18n/messages_en.properties +++ b/backend/src/main/resources/i18n/messages_en.properties @@ -392,6 +392,7 @@ message.name-must-be-unique=Name must be unique. 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.real-number=Optional. If using a value, must be a real number between 0-1. message.org-name-required=Organization Name is required. message.org-displayName-required=Organization Name is required. @@ -401,6 +402,7 @@ message.org-incomplete=These three fields must all be entered if any single fiel message.type-required=Missing required property: Type message.match-required=Missing required property: Match message.value-required=Missing required property: Value +message.required=Missing required property. message.conflict=Conflict message.data-version-contention=Data Version Contention diff --git a/backend/src/main/resources/local-dynamic-metadata-provider.schema.json b/backend/src/main/resources/local-dynamic-metadata-provider.schema.json index b3c193dcf..d683db316 100644 --- a/backend/src/main/resources/local-dynamic-metadata-provider.schema.json +++ b/backend/src/main/resources/local-dynamic-metadata-provider.schema.json @@ -63,14 +63,12 @@ "description": "tooltip.refresh-delay-factor", "type": "string", "widget": { - "id": "number", - "step": 0.01 + "id": "string", + "help": "message.real-number" }, "placeholder": "label.real-number", - "minimum": 0, - "maximum": 1, - "default": null, - "pattern": "^([0]*(\\.[0-9]+)?|[0]*\\.[0-9]*[1-9][0-9]*)$" + "default": "", + "pattern": "^(?:([0]*(\\.[0-9]+)?|[0]*\\.[0-9]*[1-9][0-9]*)|)$" }, "minCacheDuration": { "title": "label.min-cache-duration", @@ -87,6 +85,7 @@ "PT30M", "PT1H", "PT4H", + "PT8H", "PT12H", "PT24H" ] @@ -109,6 +108,7 @@ "PT30M", "PT1H", "PT4H", + "PT8H", "PT12H", "PT24H" ] @@ -131,6 +131,7 @@ "PT30M", "PT1H", "PT4H", + "PT8H", "PT12H", "PT24H" ] @@ -176,6 +177,7 @@ "PT30M", "PT1H", "PT4H", + "PT8H", "PT12H", "PT24H" ] diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy index 99a8a0fd4..30117bc14 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy @@ -1,12 +1,22 @@ package edu.internet2.tier.shibboleth.admin.ui.controller +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.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import net.shibboleth.ext.spring.resource.ResourceHelper import org.opensaml.saml.metadata.resolver.impl.ResourceBackedMetadataResolver +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.core.io.ClassPathResource +import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.http.MediaType +import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.servlet.setup.MockMvcBuilders import spock.lang.Specification import spock.lang.Subject @@ -15,6 +25,10 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") class EntitiesControllerTests extends Specification { def openSamlObjects = new OpenSamlObjects().with { init() @@ -30,10 +44,13 @@ class EntitiesControllerTests extends Specification { it } + @Autowired + UserService userService + @Subject def controller = new EntitiesController( openSamlObjects: openSamlObjects, - entityDescriptorService: new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects)), + entityDescriptorService: new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects), userService), metadataResolver: metadataResolver ) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy index 58173bd33..d3079fa3c 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy @@ -8,13 +8,13 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository -import edu.internet2.tier.shibboleth.admin.ui.security.model.User import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator +import edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import groovy.json.JsonOutput import groovy.json.JsonSlurper @@ -24,16 +24,12 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.security.core.Authentication import org.springframework.security.core.context.SecurityContext import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.security.web.context.HttpSessionSecurityContextRepository import org.springframework.test.context.ContextConfiguration -import org.springframework.test.web.servlet.result.MockMvcResultHandlers import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.web.client.RestTemplate import spock.lang.Specification import spock.lang.Subject -import javax.servlet.http.HttpSession -import java.security.Principal import java.time.LocalDateTime import static org.hamcrest.CoreMatchers.containsString @@ -71,26 +67,36 @@ class EntityDescriptorControllerTests extends Specification { UserRepository userRepository = Mock() RoleRepository roleRepository = Mock() + UserService userService + def setup() { generator = new TestObjectGenerator() randomGenerator = new RandomGenerator() mapper = new ObjectMapper() - service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects)) - controller = new EntityDescriptorController(userRepository, roleRepository, new UserService(roleRepository, userRepository)) + userService = new UserService(roleRepository, userRepository) + service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects), userService) + + controller = new EntityDescriptorController(userRepository, roleRepository, userService) controller.entityDescriptorRepository = entityDescriptorRepository controller.openSamlObjects = openSamlObjects controller.entityDescriptorService = service controller.restTemplate = mockRestTemplate + mockMvc = MockMvcBuilders.standaloneSetup(controller).build() securityContext.getAuthentication() >> authentication + SecurityContextHolder.setContext(securityContext) + } def 'GET /EntityDescriptors with empty repository as admin'() { given: - prepareAdminUser() + def username = 'admin' + def role = 'ROLE_ADMIN' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def emptyRecordsFromRepository = [].stream() def expectedEmptyListResponseBody = '[]' def expectedResponseContentType = APPLICATION_JSON_UTF8 @@ -101,7 +107,7 @@ class EntityDescriptorControllerTests extends Specification { then: //One call to the repo expected - 1 * entityDescriptorRepository.findAllByCustomQueryAndStream() >> emptyRecordsFromRepository + 1 * entityDescriptorRepository.findAllStreamByCustomQuery() >> emptyRecordsFromRepository result.andExpect(expectedHttpResponseStatus) .andExpect(content().contentType(expectedResponseContentType)) .andExpect(content().json(expectedEmptyListResponseBody)) @@ -110,7 +116,10 @@ class EntityDescriptorControllerTests extends Specification { def 'GET /EntityDescriptors with 1 record in repository as admin'() { given: - prepareAdminUser() + def username = 'admin' + def role = 'ROLE_ADMIN' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def expectedCreationDate = '2017-10-23T11:11:11' def entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, createdDate: LocalDateTime.parse(expectedCreationDate)) @@ -148,7 +157,7 @@ class EntityDescriptorControllerTests extends Specification { then: //One call to the repo expected - 1 * entityDescriptorRepository.findAllByCustomQueryAndStream() >> oneRecordFromRepository + 1 * entityDescriptorRepository.findAllStreamByCustomQuery() >> oneRecordFromRepository result.andExpect(expectedHttpResponseStatus) .andExpect(content().contentType(expectedResponseContentType)) .andExpect(content().json(expectedOneRecordListResponseBody, true)) @@ -157,7 +166,10 @@ class EntityDescriptorControllerTests extends Specification { def 'GET /EntityDescriptors with 2 records in repository as admin'() { given: - prepareAdminUser() + def username = 'admin' + def role = 'ROLE_ADMIN' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def expectedCreationDate = '2017-10-23T11:11:11' def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, @@ -219,7 +231,7 @@ class EntityDescriptorControllerTests extends Specification { then: //One call to the repo expected - 1 * entityDescriptorRepository.findAllByCustomQueryAndStream() >> twoRecordsFromRepository + 1 * entityDescriptorRepository.findAllStreamByCustomQuery() >> twoRecordsFromRepository result.andExpect(expectedHttpResponseStatus) .andExpect(content().contentType(expectedResponseContentType)) .andExpect(content().json(expectedTwoRecordsListResponseBody, true)) @@ -228,7 +240,10 @@ class EntityDescriptorControllerTests extends Specification { def 'GET /EntityDescriptors with 1 record in repository as user returns only that user\'s records'() { given: - prepareUser('someUser', 'ROLE_USER') + def username = 'someUser' + def role = 'ROLE_USER' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def expectedCreationDate = '2017-10-23T11:11:11' def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, @@ -268,7 +283,7 @@ class EntityDescriptorControllerTests extends Specification { then: //One call to the repo expected - 1 * entityDescriptorRepository.findAllByCreatedBy('someUser') >> oneRecordFromRepository + 1 * entityDescriptorRepository.findAllStreamByCreatedBy('someUser') >> oneRecordFromRepository result.andExpect(expectedHttpResponseStatus) .andExpect(content().contentType(expectedResponseContentType)) .andExpect(content().json(expectedOneRecordListResponseBody, true)) @@ -276,6 +291,10 @@ class EntityDescriptorControllerTests extends Specification { def 'POST /EntityDescriptor and successfully create new record'() { given: + def username = 'admin' + def role = 'ROLE_ADMIN' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def expectedCreationDate = '2017-10-23T11:11:11' def expectedEntityId = 'https://shib' def expectedSpName = 'sp1' @@ -353,6 +372,48 @@ class EntityDescriptorControllerTests extends Specification { } + def 'POST /EntityDescriptor as user disallows enabling'() { + given: + def username = 'someUser' + def role = 'ROLE_USER' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) + def expectedEntityId = 'https://shib' + def expectedSpName = 'sp1' + + def postedJsonBody = """ + { + "serviceProviderName": "$expectedSpName", + "entityId": "$expectedEntityId", + "organization": null, + "serviceEnabled": true, + "createdDate": null, + "modifiedDate": null, + "organization": null, + "contacts": null, + "mdui": null, + "serviceProviderSsoDescriptor": null, + "logoutEndpoints": null, + "securityInfo": null, + "assertionConsumerServices": null, + "relyingPartyOverrides": null, + "attributeRelease": null + } + """ + + when: + def result = mockMvc.perform( + post('/api/EntityDescriptor') + .contentType(APPLICATION_JSON_UTF8) + .content(postedJsonBody)) + + then: + 0 * entityDescriptorRepository.findByEntityID(_) + 0 * entityDescriptorRepository.save(_) + + result.andExpect(status().isForbidden()) + } + def 'POST /EntityDescriptor record already exists'() { given: def expectedEntityId = 'eid1' @@ -390,6 +451,10 @@ class EntityDescriptorControllerTests extends Specification { def 'GET /EntityDescriptor/{resourceId} non-existent'() { given: + def username = 'admin' + def role = 'ROLE_ADMIN' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def providedResourceId = 'uuid-1' when: @@ -403,7 +468,10 @@ class EntityDescriptorControllerTests extends Specification { def 'GET /EntityDescriptor/{resourceId} existing'() { given: - prepareAdminUser() + def username = 'admin' + def role = 'ROLE_ADMIN' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def expectedCreationDate = '2017-10-23T11:11:11' def providedResourceId = 'uuid-1' def expectedSpName = 'sp1' @@ -451,7 +519,10 @@ class EntityDescriptorControllerTests extends Specification { def 'GET /EntityDescriptor/{resourceId} existing, owned by non-admin'() { given: - prepareUser('someUser', 'ROLE_USER') + def username = 'someUser' + def role = 'ROLE_USER' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def expectedCreationDate = '2017-10-23T11:11:11' def providedResourceId = 'uuid-1' def expectedSpName = 'sp1' @@ -500,7 +571,10 @@ class EntityDescriptorControllerTests extends Specification { def 'GET /EntityDescriptor/{resourceId} existing, owned by some other user'() { given: - prepareUser('someUser', 'ROLE_USER') + def username = 'someUser' + def role = 'ROLE_USER' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def expectedCreationDate = '2017-10-23T11:11:11' def providedResourceId = 'uuid-1' def expectedSpName = 'sp1' @@ -523,7 +597,10 @@ class EntityDescriptorControllerTests extends Specification { def 'GET /EntityDescriptor/{resourceId} existing (xml)'() { given: - prepareAdminUser() + def username = 'admin' + def role = 'ROLE_ADMIN' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def expectedCreationDate = '2017-10-23T11:11:11' def providedResourceId = 'uuid-1' def expectedSpName = 'sp1' @@ -555,7 +632,10 @@ class EntityDescriptorControllerTests extends Specification { def 'GET /EntityDescriptor/{resourceId} existing (xml), user-owned'() { given: - prepareUser('someUser', 'ROLE_USER') + def username = 'someUser' + def role = 'ROLE_USER' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def expectedCreationDate = '2017-10-23T11:11:11' def providedResourceId = 'uuid-1' def expectedSpName = 'sp1' @@ -588,7 +668,10 @@ class EntityDescriptorControllerTests extends Specification { def 'GET /EntityDescriptor/{resourceId} existing (xml), other user-owned'() { given: - prepareUser('someUser', 'ROLE_USER') + def username = 'someUser' + def role = 'ROLE_USER' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def expectedCreationDate = '2017-10-23T11:11:11' def providedResourceId = 'uuid-1' def expectedSpName = 'sp1' @@ -615,7 +698,10 @@ class EntityDescriptorControllerTests extends Specification { def "POST /EntityDescriptor handles XML happily"() { given: - prepareAdminUser() + def username = 'admin' + def role = 'ROLE_ADMIN' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def postedBody = ''' @@ -732,7 +818,10 @@ class EntityDescriptorControllerTests extends Specification { def "POST /EntityDescriptor handles x-www-form-urlencoded happily"() { given: - prepareAdminUser() + def username = 'admin' + def role = 'ROLE_ADMIN' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def postedMetadataUrl = "http://test.scaldingspoon.org/test1" def restXml = ''' @@ -812,7 +901,10 @@ class EntityDescriptorControllerTests extends Specification { def "PUT /EntityDescriptor updates entity descriptors properly as admin"() { given: - prepareAdminUser() + def username = 'admin' + def role = 'ROLE_ADMIN' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def entityDescriptor = generator.buildEntityDescriptor() def updatedEntityDescriptor = generator.buildEntityDescriptor() updatedEntityDescriptor.resourceId = entityDescriptor.resourceId @@ -838,9 +930,42 @@ class EntityDescriptorControllerTests extends Specification { .andExpect(content().json(JsonOutput.toJson(expectedJson), true)) } + def "PUT /EntityDescriptor disallows user from enabling"() { + given: + def username = 'someUser' + def role = 'ROLE_USER' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) + def entityDescriptor = generator.buildEntityDescriptor() + entityDescriptor.serviceEnabled = false + def updatedEntityDescriptor = generator.buildEntityDescriptor() + updatedEntityDescriptor.serviceEnabled = true + updatedEntityDescriptor.resourceId = entityDescriptor.resourceId + def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(updatedEntityDescriptor) + updatedEntityDescriptorRepresentation.version = entityDescriptor.hashCode() + def postedJsonBody = mapper.writeValueAsString(updatedEntityDescriptorRepresentation) + + def resourceId = entityDescriptor.resourceId + + 1 * entityDescriptorRepository.findByResourceId(resourceId) >> entityDescriptor + 0 * entityDescriptorRepository.save(_) >> updatedEntityDescriptor + + when: + def result = mockMvc.perform( + put("/api/EntityDescriptor/$resourceId") + .contentType(APPLICATION_JSON_UTF8) + .content(postedJsonBody)) + + then: + result.andExpect(status().isForbidden()) + } + def "PUT /EntityDescriptor denies the request if the PUTing user is not an ADMIN and not the createdBy user"() { given: - prepareUser('randomUser', 'ROLE_USER') + def username = 'someUser' + def role = 'ROLE_USERN' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def entityDescriptor = generator.buildEntityDescriptor() entityDescriptor.createdBy = 'someoneElse' def updatedEntityDescriptor = generator.buildEntityDescriptor() @@ -863,7 +988,10 @@ class EntityDescriptorControllerTests extends Specification { def "PUT /EntityDescriptor 409's if the version numbers don't match"() { given: - prepareAdminUser() + def username = 'admin' + def role = 'ROLE_ADMIN' + authentication.getPrincipal() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def entityDescriptor = generator.buildEntityDescriptor() def updatedEntityDescriptor = generator.buildEntityDescriptor() updatedEntityDescriptor.resourceId = entityDescriptor.resourceId @@ -883,16 +1011,4 @@ class EntityDescriptorControllerTests extends Specification { then: result.andExpect(status().is(409)) } - - def prepareAdminUser() { - prepareUser('foo', 'ROLE_ADMIN') - } - - def prepareUser(String username, String rolename) { - authentication.getPrincipal() >> username - SecurityContextHolder.setContext(securityContext) - def user = new User(username: username, role: rolename) - Optional currentUser = Optional.of(user) - userRepository.findByUsername(username) >> currentUser - } } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EnityDescriptorRepositoryTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy similarity index 82% rename from backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EnityDescriptorRepositoryTest.groovy rename to backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy index eb29bf550..4ecb6e758 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EnityDescriptorRepositoryTest.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy @@ -6,6 +6,9 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfigurat import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor 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.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import org.springframework.beans.factory.annotation.Autowired @@ -25,19 +28,25 @@ import javax.persistence.EntityManager @ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") -class EnityDescriptorRepositoryTest extends Specification { +class EntityDescriptorRepositoryTest extends Specification { @Autowired EntityDescriptorRepository entityDescriptorRepository @Autowired EntityManager entityManager + @Autowired + RoleRepository roleRepository + + @Autowired + UserRepository userRepository + OpenSamlObjects openSamlObjects = new OpenSamlObjects().with { init() it } - def service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects)) + def service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects), new UserService(roleRepository, userRepository)) def "SHIBUI-553.2"() { when: diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy index 506c2b488..cec7fe8c5 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy @@ -11,6 +11,9 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMet import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver 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.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import org.springframework.beans.factory.annotation.Autowired @@ -39,7 +42,13 @@ class MetadataResolverRepositoryTests extends Specification { @Autowired OpenSamlObjects openSamlObjects - def service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects)) + @Autowired + RoleRepository roleRepository + + @Autowired + UserRepository userRepository + + def service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects), new UserService(roleRepository, userRepository)) def "test persisting a metadata resolver"() { when: diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy index b687e5c1c..117c0fbd4 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy @@ -8,6 +8,9 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRe import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepresentation import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository +import edu.internet2.tier.shibboleth.admin.ui.security.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.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator @@ -44,10 +47,16 @@ class EntityDescriptorFilesScheduledTasksTests extends Specification { def randomGenerator + @Autowired + RoleRepository roleRepository + + @Autowired + UserRepository userRepository + def setup() { randomGenerator = new RandomGenerator() tempPath = tempPath + randomGenerator.randomRangeInt(10000, 20000) - service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects)) + service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects), new UserService(roleRepository, userRepository)) entityDescriptorFilesScheduledTasks = new EntityDescriptorFilesScheduledTasks(tempPath, entityDescriptorRepository, openSamlObjects) directory = new File(tempPath) directory.mkdir() @@ -80,7 +89,7 @@ class EntityDescriptorFilesScheduledTasksTests extends Specification { } it }) - 1 * entityDescriptorRepository.findAllByServiceEnabled(true) >> [entityDescriptor].stream() + 1 * entityDescriptorRepository.findAllStreamByServiceEnabled(true) >> [entityDescriptor].stream() when: if (directory.exists()) { @@ -128,7 +137,7 @@ class EntityDescriptorFilesScheduledTasksTests extends Specification { def file = new File(directory, randomGenerator.randomId() + ".xml") file.text = "Delete me!" - 1 * entityDescriptorRepository.findAllByServiceEnabled(true) >> [entityDescriptor].stream() + 1 * entityDescriptorRepository.findAllStreamByServiceEnabled(true) >> [entityDescriptor].stream() when: entityDescriptorFilesScheduledTasks.removeDanglingEntityDescriptorFiles() diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersControllerIntegrationTests.groovy index 04aa033e9..0e96fc1b7 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersControllerIntegrationTests.groovy @@ -1,83 +1,141 @@ package edu.internet2.tier.shibboleth.admin.ui.security.controller -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.SerializationFeature + import groovy.json.JsonOutput import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.web.client.TestRestTemplate -import org.springframework.http.HttpEntity -import org.springframework.http.HttpHeaders -import org.springframework.http.HttpMethod +import org.springframework.http.MediaType +import org.springframework.security.test.context.support.WithMockUser import org.springframework.test.annotation.DirtiesContext import org.springframework.test.context.ActiveProfiles +import org.springframework.test.web.servlet.MockMvc +import spock.lang.Ignore import spock.lang.Specification +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + /** * @author Dmitriy Kopylenko */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@SpringBootTest +@AutoConfigureMockMvc @ActiveProfiles(["no-auth", "dev"]) class UsersControllerIntegrationTests extends Specification { @Autowired - private TestRestTemplate restTemplate + private MockMvc mockMvc static RESOURCE_URI = '/api/admin/users' - ObjectMapper mapper - - def setup() { - mapper = new ObjectMapper() - mapper.enable(SerializationFeature.INDENT_OUTPUT) - } - + @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'GET ALL users (when there are existing users)'() { + given: + def expectedJson = """ +[ + { + "modifiedBy" : null, + "firstName" : "Joe", + "emailAddress" : "joe@institution.edu", + "role" : "ROLE_ADMIN", + "username" : "admin", + "createdBy" : null, + "lastName" : "Doe" + }, + { + "modifiedBy" : null, + "firstName" : "Peter", + "emailAddress" : "peter@institution.edu", + "role" : "ROLE_USER", + "username" : "nonadmin", + "createdBy" : null, + "lastName" : "Vandelay" + }, + { + "modifiedBy" : null, + "firstName" : "Anon", + "emailAddress" : "anon@institution.edu", + "role" : "ROLE_ADMIN", + "username" : "anonymousUser", + "createdBy" : null, + "lastName" : "Ymous" + } +]""" when: 'GET request is made for ALL users in the system, and system has users in it' - def result = this.restTemplate.getForEntity(RESOURCE_URI, Object) + def result = mockMvc.perform(get(RESOURCE_URI)) + then: 'Request completed with HTTP 200 and returned a list of users' - result.statusCodeValue == 200 - result.body[0].username == 'admin' - result.body[0].role == 'ROLE_ADMIN' + result + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(content().json(expectedJson, false)) + } + @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'GET ONE existing user'() { + given: + def expectedJson = """ +{ + "modifiedBy" : null, + "firstName" : "Joe", + "emailAddress" : "joe@institution.edu", + "role" : "ROLE_ADMIN", + "username" : "admin", + "createdBy" : null, + "lastName" : "Doe" +}""" when: 'GET request is made for one existing user' - def result = this.restTemplate.getForEntity("$RESOURCE_URI/admin", Map) + def result = mockMvc.perform(get("$RESOURCE_URI/admin")) then: 'Request completed with HTTP 200 and returned one user' - result.statusCodeValue == 200 - result.body.username == 'admin' - result.body.role == 'ROLE_ADMIN' + result + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(content().json(expectedJson, false)) } + @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'GET ONE NON-existing user'() { when: 'GET request is made for one NON-existing user' - def result = this.restTemplate.getForEntity("$RESOURCE_URI/bogus", Map) + def result = mockMvc.perform(get("$RESOURCE_URI/bogus")) then: 'Request completed with HTTP 404' - result.statusCodeValue == 404 - result.body.errorCode == '404' - result.body.errorMessage == 'User with username [bogus] not found' + result.andExpect(status().isNotFound()) } + //TODO: These are broken due to a bug in Spring Boot. Unignore these after we update to spring boot 2.0.8+. + @Ignore @DirtiesContext + @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'DELETE ONE existing user'() { when: 'GET request is made for one existing user' - def result = this.restTemplate.getForEntity("$RESOURCE_URI/admin", Map) + def result = mockMvc.perform(get("$RESOURCE_URI/nonadmin")) then: 'Request completed with HTTP 200' - result.statusCodeValue == 200 + result.andExpect(status().isOk()) when: 'DELETE request is made' - this.restTemplate.delete("$RESOURCE_URI/admin") - result = this.restTemplate.getForEntity("$RESOURCE_URI/admin", Map) + result = mockMvc.perform(delete("$RESOURCE_URI/nonadmin")) + + then: 'DELETE was successful' + result.andExpect(status().isNoContent()) + + when: 'GET request is made for the deleted user' + result = mockMvc.perform(get("$RESOURCE_URI/nonadmin")) then: 'The deleted user is gone' - result.statusCodeValue == 404 + result.andExpect(status().isNotFound()) } + @Ignore + @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'POST new user persists properly'() { given: def newUser = [firstName: 'Foo', @@ -88,12 +146,17 @@ class UsersControllerIntegrationTests extends Specification { role: 'ROLE_USER'] when: - def result = this.restTemplate.postForEntity("$RESOURCE_URI", createRequestHttpEntityFor { JsonOutput.toJson(newUser) }, Map) + def result = mockMvc.perform(post(RESOURCE_URI) + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(JsonOutput.toJson(newUser)) + .accept(MediaType.APPLICATION_JSON)) then: - result.statusCodeValue == 200 + result.andExpect(status().isOk()) } + @Ignore + @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'POST new duplicate username returns 409'() { given: def newUser = [firstName: 'Foo', @@ -104,14 +167,22 @@ class UsersControllerIntegrationTests extends Specification { role: 'ROLE_USER'] when: - this.restTemplate.postForEntity("$RESOURCE_URI", createRequestHttpEntityFor { JsonOutput.toJson(newUser) }, Map) - def result = this.restTemplate.postForEntity("$RESOURCE_URI", createRequestHttpEntityFor { JsonOutput.toJson(newUser) }, Map) + mockMvc.perform(post(RESOURCE_URI) + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(JsonOutput.toJson(newUser)) + .accept(MediaType.APPLICATION_JSON)) + def result = mockMvc.perform(post(RESOURCE_URI) + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(JsonOutput.toJson(newUser)) + .accept(MediaType.APPLICATION_JSON)) then: - result.statusCodeValue == 409 + result.andExpect(status().isConflict()) } - def 'PUT updates user properly'() { + @Ignore + @WithMockUser(value = "admin", roles = ["ADMIN"]) + def 'PATCH updates user properly'() { given: def newUser = [firstName: 'Foo', lastName: 'Bar', @@ -121,14 +192,26 @@ class UsersControllerIntegrationTests extends Specification { role: 'ROLE_USER'] when: - this.restTemplate.postForEntity("$RESOURCE_URI", createRequestHttpEntityFor { JsonOutput.toJson(newUser) }, Map) + def result = mockMvc.perform(post(RESOURCE_URI) + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(JsonOutput.toJson(newUser)) + .accept(MediaType.APPLICATION_JSON)) + + then: + result.andExpect(status().isOk()) + + when: newUser['firstName'] = 'Bob' - def result = this.restTemplate.exchange("$RESOURCE_URI/$newUser.username", HttpMethod.PATCH, createRequestHttpEntityFor { JsonOutput.toJson(newUser) }, Map) + result = mockMvc.perform(patch("$RESOURCE_URI/$newUser.username") + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(JsonOutput.toJson(newUser)) + .accept(MediaType.APPLICATION_JSON)) then: - result.statusCodeValue == 200 + result.andExpect(status().isOk()) } + @Ignore def 'PATCH detects unknown username'() { given: def newUser = [firstName: 'Foo', @@ -139,13 +222,11 @@ class UsersControllerIntegrationTests extends Specification { role: 'ROLE_USER'] when: - def result = this.restTemplate.exchange("$RESOURCE_URI/$newUser.username", HttpMethod.PATCH, createRequestHttpEntityFor { mapper.writeValueAsString(newUser) }, Map) + def result = mockMvc.perform(patch("$RESOURCE_URI/$newUser.username") + .contentType(MediaType.APPLICATION_JSON_UTF8) + .content(JsonOutput.toJson(newUser))) then: - result.statusCodeValue == 404 - } - - private HttpEntity createRequestHttpEntityFor(Closure jsonBodySupplier) { - new HttpEntity(jsonBodySupplier(), ['Content-Type': 'application/json'] as HttpHeaders) + result.andExpect(status().isNotFound()) } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy index e69ff9ad5..8bec07d33 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy @@ -16,6 +16,9 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepres import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSsoDescriptorRepresentation import edu.internet2.tier.shibboleth.admin.ui.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.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility @@ -51,9 +54,15 @@ class JPAEntityDescriptorServiceImplTests extends Specification { RandomGenerator generator + @Autowired + RoleRepository roleRepository + + @Autowired + UserRepository userRepository + def setup() { service = new JPAEntityDescriptorServiceImpl(openSamlObjects, - new JPAEntityServiceImpl(openSamlObjects, new AttributeUtility(openSamlObjects), customPropertiesConfiguration)) + new JPAEntityServiceImpl(openSamlObjects, new AttributeUtility(openSamlObjects), customPropertiesConfiguration), new UserService(roleRepository, userRepository)) JacksonTester.initFields(this, new ObjectMapper()) generator = new RandomGenerator() testObjectGenerator = new TestObjectGenerator() diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy index 9311fde2d..672618b30 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy @@ -1,7 +1,9 @@ package edu.internet2.tier.shibboleth.admin.ui.util +import edu.internet2.tier.shibboleth.admin.ui.security.model.User import groovy.xml.XmlUtil import org.apache.commons.lang.StringUtils +import org.springframework.security.core.context.SecurityContextHolder import org.w3c.dom.Document import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input @@ -39,4 +41,9 @@ class TestHelpers { static String XmlDocumentToString(Document document) { return XmlUtil.serialize(document.documentElement) } + + static Optional generateOptionalUser(String username, String rolename) { + def user = new User(username: username, role: rolename) + Optional.of(user) + } } diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java index 5c5bf6e12..6098e346a 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java @@ -7,11 +7,10 @@ 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.EmailService; -import org.apache.commons.lang.RandomStringUtils; -import org.apache.http.entity.ContentType; +import org.apache.commons.lang3.RandomStringUtils; +import org.pac4j.saml.profile.SAML2Profile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.bcrypt.BCrypt; @@ -25,6 +24,7 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.List; import java.util.Optional; /** @@ -40,32 +40,51 @@ public class AddNewUserFilter implements Filter { private RoleRepository roleRepository; private EmailService emailService; - public AddNewUserFilter(UserRepository userRepository, RoleRepository roleRepository, EmailService emailService) { + private Pac4jConfigurationProperties pac4jConfigurationProperties; + + private Pac4jConfigurationProperties.SAML2ProfileMapping saml2ProfileMapping; + + public AddNewUserFilter(Pac4jConfigurationProperties pac4jConfigurationProperties, UserRepository userRepository, RoleRepository roleRepository, EmailService emailService) { this.userRepository = userRepository; this.roleRepository = roleRepository; this.emailService = emailService; + this.pac4jConfigurationProperties = pac4jConfigurationProperties; + saml2ProfileMapping = this.pac4jConfigurationProperties.getSaml2ProfileMapping(); } @Override public void init(FilterConfig filterConfig) throws ServletException { } + private User buildAndPersistNewUserFromProfile(SAML2Profile profile) { + Role noRole = roleRepository.findByName(ROLE_NONE).orElse(new Role(ROLE_NONE)); + roleRepository.save(noRole); + + User user = new User(); + user.getRoles().add(noRole); + user.setUsername(getAttributeFromProfile(profile, "username")); + user.setPassword(BCrypt.hashpw(RandomStringUtils.randomAlphanumeric(20), BCrypt.gensalt())); + user.setFirstName(getAttributeFromProfile(profile, "firstName")); + user.setLastName(getAttributeFromProfile(profile, "lastName")); + user.setEmailAddress(getAttributeFromProfile(profile, "email")); + User persistedUser = userRepository.save(user); + if (logger.isDebugEnabled()) { + logger.debug("Persisted new user:\n" + user); + } + return persistedUser; + } + @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication != null) { - String username = authentication.getName(); + SAML2Profile profile = (SAML2Profile) authentication.getPrincipal(); + if (profile != null) { + String username = getAttributeFromProfile(profile, "username"); if (username != null) { Optional persistedUser = userRepository.findByUsername(username); User user; if (!persistedUser.isPresent()) { - user = new User(); - user.setUsername(username); - user.setPassword(BCrypt.hashpw(RandomStringUtils.randomAlphanumeric(20), BCrypt.gensalt())); - Role noRole = roleRepository.findByName(ROLE_NONE).orElse(new Role(ROLE_NONE)); - roleRepository.save(noRole); - user.getRoles().add(noRole); - userRepository.save(user); + user = buildAndPersistNewUserFromProfile(profile); try { emailService.sendNewUserMail(username); } catch (MessagingException e) { @@ -75,7 +94,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha user = persistedUser.get(); } if (user.getRole().equals(ROLE_NONE)) { - ((HttpServletResponse) response).sendRedirect("/static.html"); + ((HttpServletResponse) response).sendRedirect("/unsecured/error.html"); } else { chain.doFilter(request, response); // else, user is in the system already, carry on } @@ -87,6 +106,28 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha public void destroy() { } + private String getAttributeFromProfile(SAML2Profile profile, String stringKey) { + String attribute = null; + switch (stringKey) { + case "username": + attribute = saml2ProfileMapping.getUsername(); + break; + case "firstName": + attribute = saml2ProfileMapping.getFirstName(); + break; + case "lastName": + attribute = saml2ProfileMapping.getLastName(); + break; + case "email": + attribute = saml2ProfileMapping.getEmail(); + break; + default: + // do we care? Not yet. + } + List attributeList = (List) profile.getAttribute(attribute); + return attributeList.size() < 1 ? null : attributeList.get(0); + } + private byte[] getJsonResponseBytes(ErrorResponse eErrorResponse) throws IOException { String errorResponseJson = new ObjectMapper().writeValueAsString(eErrorResponse); return errorResponseJson.getBytes(); diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/BetterSAML2Profile.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/BetterSAML2Profile.java new file mode 100644 index 000000000..58d16a9ec --- /dev/null +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/BetterSAML2Profile.java @@ -0,0 +1,23 @@ +package net.unicon.shibui.pac4j; + +import org.pac4j.saml.profile.SAML2Profile; + +import java.util.Collection; + +public class BetterSAML2Profile extends SAML2Profile { + private final String usernameAttribute; + + public BetterSAML2Profile(final String usernameAttribute) { + this.usernameAttribute = usernameAttribute; + } + + @Override + public String getUsername() { + Object username = getAttribute(usernameAttribute); + if (username instanceof Collection) { + return (String) ((Collection)username).toArray()[0]; + } else { + return (String) username; + } + } +} diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java index 1c28b1ee1..884873881 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java @@ -1,17 +1,24 @@ package net.unicon.shibui.pac4j; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; import org.pac4j.core.client.Clients; import org.pac4j.core.config.Config; +import org.pac4j.core.profile.definition.CommonProfileDefinition; import org.pac4j.saml.client.SAML2Client; import org.pac4j.saml.client.SAML2ClientConfiguration; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.pac4j.saml.credentials.authenticator.SAML2Authenticator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class Pac4jConfiguration { @Bean - public Config config(final Pac4jConfigurationProperties pac4jConfigurationProperties) { + public SAML2ModelAuthorizationGenerator saml2ModelAuthorizationGenerator(UserRepository userRepository) { + return new SAML2ModelAuthorizationGenerator(userRepository); + } + + @Bean + public Config config(final Pac4jConfigurationProperties pac4jConfigurationProperties, final SAML2ModelAuthorizationGenerator saml2ModelAuthorizationGenerator) { final SAML2ClientConfiguration saml2ClientConfiguration = new SAML2ClientConfiguration(); saml2ClientConfiguration.setKeystorePath(pac4jConfigurationProperties.getKeystorePath()); saml2ClientConfiguration.setKeystorePassword(pac4jConfigurationProperties.getKeystorePassword()); @@ -23,8 +30,15 @@ public Config config(final Pac4jConfigurationProperties pac4jConfigurationProper saml2ClientConfiguration.setForceServiceProviderMetadataGeneration(pac4jConfigurationProperties.isForceServiceProviderMetadataGeneration()); saml2ClientConfiguration.setWantsAssertionsSigned(pac4jConfigurationProperties.isWantAssertionsSigned()); + saml2ClientConfiguration.setAttributeAsId(pac4jConfigurationProperties.getSaml2ProfileMapping().getUsername()); + final SAML2Client saml2Client = new SAML2Client(saml2ClientConfiguration); saml2Client.setName("Saml2Client"); + saml2Client.addAuthorizationGenerator(saml2ModelAuthorizationGenerator); + + SAML2Authenticator saml2Authenticator = new SAML2Authenticator(saml2ClientConfiguration.getAttributeAsId(), saml2ClientConfiguration.getMappedAttributes()); + saml2Authenticator.setProfileDefinition(new CommonProfileDefinition<>(p -> new BetterSAML2Profile(pac4jConfigurationProperties.getSaml2ProfileMapping().getUsername()))); + saml2Client.setAuthenticator(saml2Authenticator); final Clients clients = new Clients(pac4jConfigurationProperties.getCallbackUrl(), saml2Client); diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java index afb1369a1..cf36d8e65 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java @@ -1,10 +1,12 @@ package net.unicon.shibui.pac4j; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.stereotype.Component; @Component @ConfigurationProperties(prefix = "shibui.pac4j") +@EnableConfigurationProperties public class Pac4jConfigurationProperties { private String keystorePath = "/tmp/samlKeystore.jks"; private String keystorePassword = "changeit"; @@ -16,6 +18,46 @@ public class Pac4jConfigurationProperties { private boolean forceServiceProviderMetadataGeneration = false; private String callbackUrl; private boolean wantAssertionsSigned = true; + private SAML2ProfileMapping saml2ProfileMapping; + + public static class SAML2ProfileMapping { + private String username; + private String email; + private String firstName; + private String lastName; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + } public String getKeystorePath() { return keystorePath; @@ -96,4 +138,12 @@ public boolean isWantAssertionsSigned() { public void setWantAssertionsSigned(boolean wantAssertionsSigned) { this.wantAssertionsSigned = wantAssertionsSigned; } + + public SAML2ProfileMapping getSaml2ProfileMapping() { + return saml2ProfileMapping; + } + + public void setSaml2ProfileMapping(SAML2ProfileMapping saml2ProfileMapping) { + this.saml2ProfileMapping = saml2ProfileMapping; + } } diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/SAML2ModelAuthorizationGenerator.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/SAML2ModelAuthorizationGenerator.java new file mode 100644 index 000000000..165477627 --- /dev/null +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/SAML2ModelAuthorizationGenerator.java @@ -0,0 +1,24 @@ +package net.unicon.shibui.pac4j; + +import edu.internet2.tier.shibboleth.admin.ui.security.model.User; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; +import org.pac4j.core.authorization.generator.AuthorizationGenerator; +import org.pac4j.core.context.WebContext; +import org.pac4j.saml.profile.SAML2Profile; + +import java.util.Optional; + +public class SAML2ModelAuthorizationGenerator implements AuthorizationGenerator { + private final UserRepository userRepository; + + public SAML2ModelAuthorizationGenerator(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public SAML2Profile generate(WebContext context, SAML2Profile profile) { + Optional user = userRepository.findByUsername(profile.getUsername()); + user.ifPresent( u -> profile.addRole(u.getRole())); + return profile; + } +} diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java index e0e156eec..120a45f36 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java @@ -20,8 +20,8 @@ @AutoConfigureOrder(-1) public class WebSecurity { @Bean("webSecurityConfig") - public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter(final Config config, UserRepository userRepository, RoleRepository roleRepository, EmailService emailService) { - return new Pac4jWebSecurityConfigurerAdapter(config, userRepository, roleRepository, emailService); + public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter(final Config config, UserRepository userRepository, RoleRepository roleRepository, EmailService emailService, Pac4jConfigurationProperties pac4jConfigurationProperties) { + return new Pac4jWebSecurityConfigurerAdapter(config, userRepository, roleRepository, emailService, pac4jConfigurationProperties); } @Configuration @@ -35,10 +35,10 @@ protected void configure(HttpSecurity http) throws Exception { @Configuration @Order(1) - public static class StaticSecurityConfiguration extends WebSecurityConfigurerAdapter { + public static class UnsecuredSecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { - http.antMatcher("/static.html").authorizeRequests().antMatchers("/static.html").permitAll(); + http.antMatcher("/unsecured/**/*").authorizeRequests().antMatchers("/unsecured/**/*").permitAll(); } } @@ -57,12 +57,14 @@ public static class Pac4jWebSecurityConfigurerAdapter extends WebSecurityConfigu private UserRepository userRepository; private RoleRepository roleRepository; private EmailService emailService; + private Pac4jConfigurationProperties pac4jConfigurationProperties; - public Pac4jWebSecurityConfigurerAdapter(final Config config, UserRepository userRepository, RoleRepository roleRepository, EmailService emailService) { + public Pac4jWebSecurityConfigurerAdapter(final Config config, UserRepository userRepository, RoleRepository roleRepository, EmailService emailService, Pac4jConfigurationProperties pac4jConfigurationProperties) { this.config = config; this.userRepository = userRepository; this.roleRepository = roleRepository; this.emailService = emailService; + this.pac4jConfigurationProperties = pac4jConfigurationProperties; } @Override @@ -72,7 +74,7 @@ protected void configure(HttpSecurity http) throws Exception { final CallbackFilter callbackFilter = new CallbackFilter(this.config); http.antMatcher("/**").addFilterBefore(callbackFilter, BasicAuthenticationFilter.class) .addFilterBefore(securityFilter, BasicAuthenticationFilter.class) - .addFilterAfter(new AddNewUserFilter(userRepository, roleRepository, emailService), SecurityFilter.class); + .addFilterAfter(new AddNewUserFilter(pac4jConfigurationProperties, userRepository, roleRepository, emailService), SecurityFilter.class); http.authorizeRequests().anyRequest().fullyAuthenticated(); diff --git a/pac4j-module/src/main/resources/META-INF/spring.factories b/pac4j-module/src/main/resources/META-INF/spring.factories index 90f792d84..beca677ec 100644 --- a/pac4j-module/src/main/resources/META-INF/spring.factories +++ b/pac4j-module/src/main/resources/META-INF/spring.factories @@ -1,4 +1,4 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ net.unicon.shibui.pac4j.Pac4jConfiguration,\ net.unicon.shibui.pac4j.WebSecurity,\ - net.unicon.shibui.pac4j.Pac4jConfigurationProperties \ No newline at end of file + net.unicon.shibui.pac4j.Pac4jConfigurationProperties diff --git a/pac4j-module/src/main/resources/application.yml b/pac4j-module/src/main/resources/application.yml new file mode 100644 index 000000000..4042fd939 --- /dev/null +++ b/pac4j-module/src/main/resources/application.yml @@ -0,0 +1,7 @@ +shibui: + pac4j: + saml2ProfileMapping: + username: urn:oid:0.9.2342.19200300.100.1.3 + firstName: givenName + lastName: sn + email: mail \ No newline at end of file diff --git a/pac4j-module/src/test/docker/conf/application.yml b/pac4j-module/src/test/docker/conf/application.yml index e4083f2d3..e24389d17 100644 --- a/pac4j-module/src/test/docker/conf/application.yml +++ b/pac4j-module/src/test/docker/conf/application.yml @@ -1,3 +1,6 @@ +spring: + profiles: + include: dev server: port: 8443 ssl: diff --git a/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy index 98c5e99f3..9a824e97e 100644 --- a/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy +++ b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy @@ -5,6 +5,10 @@ import edu.internet2.tier.shibboleth.admin.ui.security.model.User import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository import edu.internet2.tier.shibboleth.admin.ui.service.EmailService +import org.pac4j.saml.profile.SAML2Profile +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.test.context.SpringBootTest import org.springframework.security.core.Authentication import org.springframework.security.core.context.SecurityContext import org.springframework.security.core.context.SecurityContextHolder @@ -18,6 +22,8 @@ import javax.servlet.http.HttpServletResponse /** * @author Bill Smith (wsmith@unicon.net) */ +@SpringBootTest(classes = [Pac4jConfigurationProperties]) +@EnableConfigurationProperties([Pac4jConfigurationProperties]) class AddNewUserFilterTests extends Specification { UserRepository userRepository = Mock() @@ -30,18 +36,33 @@ class AddNewUserFilterTests extends Specification { SecurityContext securityContext = Mock() Authentication authentication = Mock() + SAML2Profile saml2Profile = Mock() + + @Autowired + Pac4jConfigurationProperties pac4jConfigurationProperties + + Pac4jConfigurationProperties.SAML2ProfileMapping saml2ProfileMapping @Subject - AddNewUserFilter addNewUserFilter = new AddNewUserFilter(userRepository, roleRepository, emailService) + AddNewUserFilter addNewUserFilter def setup() { SecurityContextHolder.setContext(securityContext) securityContext.getAuthentication() >> authentication + authentication.getPrincipal() >> saml2Profile + + addNewUserFilter = new AddNewUserFilter(pac4jConfigurationProperties, userRepository, roleRepository, emailService) + saml2ProfileMapping = pac4jConfigurationProperties.saml2ProfileMapping } def "new users are redirected"() { given: - authentication.getName() >> 'newUser' + ['Username': 'newUser', + 'FirstName': 'New', + 'LastName': 'User', + 'Email': 'newuser@institution.edu'].each { key, value -> + saml2Profile.getAttribute(saml2ProfileMapping."get${key}"()) >> [value] + } userRepository.findByUsername('newUser') >> Optional.empty() roleRepository.findByName('ROLE_NONE') >> Optional.of(new Role('ROLE_NONE')) @@ -50,14 +71,14 @@ class AddNewUserFilterTests extends Specification { then: 1 * roleRepository.save(_) - 1 * userRepository.save(_) + 1 * userRepository.save(_ as User) >> { User user -> user } 1 * emailService.sendNewUserMail('newUser') - 1 * response.sendRedirect("/static.html") + 1 * response.sendRedirect("/unsecured/error.html") } def "existing users are not redirected"() { given: - authentication.getName() >> 'existingUser' + saml2Profile.getAttribute(saml2ProfileMapping.getUsername()) >> ['existingUser'] userRepository.findByUsername('existingUser') >> Optional.of(new User().with { it.username = 'existingUser' it.roles = [new Role('ROLE_USER')] diff --git a/ui/build.js b/ui/build.js new file mode 100644 index 000000000..e68d3d848 --- /dev/null +++ b/ui/build.js @@ -0,0 +1,20 @@ +const fs = require('fs-extra'); + +fs.ensureDir('./dist/unsecured').then(function () { + try { + fs.copySync('./src/error.html', './dist/unsecured/error.html') + console.log('copy error page success!') + } catch (err) { + console.error(err) + } + + try { + fs.copySync('./node_modules/font-awesome/fonts', './dist/unsecured'); + console.log('copy fonts success!') + } catch (err) { + console.log(err); + } +}); + + + diff --git a/ui/package-lock.json b/ui/package-lock.json index 19c42e7e3..841137feb 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -183,7 +183,6 @@ "anymatch": "2.0.0", "async-each": "1.0.1", "braces": "2.3.2", - "fsevents": "1.2.4", "glob-parent": "3.1.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -192,537 +191,6 @@ "path-is-absolute": "1.0.1", "readdirp": "2.1.0", "upath": "1.0.5" - }, - "dependencies": { - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.11.1", - "node-pre-gyp": "0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "2.1.2" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.9", - "iconv-lite": "0.4.21", - "sax": "1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "1.0.3", - "mkdirp": "0.5.1", - "needle": "2.2.0", - "nopt": "4.0.1", - "npm-packlist": "1.1.10", - "npmlog": "4.1.2", - "rc": "1.2.7", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "4.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "3.0.1", - "npm-bundled": "1.0.3" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.5.1", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "1.0.1", - "fs-minipass": "1.2.5", - "minipass": "2.2.4", - "minizlib": "1.1.0", - "mkdirp": "0.5.1", - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "dev": true - } - } - } } }, "expand-brackets": { @@ -1057,7 +525,6 @@ "anymatch": "2.0.0", "async-each": "1.0.1", "braces": "2.3.2", - "fsevents": "1.2.4", "glob-parent": "3.1.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -1066,537 +533,6 @@ "path-is-absolute": "1.0.1", "readdirp": "2.1.0", "upath": "1.0.5" - }, - "dependencies": { - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.11.1", - "node-pre-gyp": "0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "2.1.2" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.9", - "iconv-lite": "0.4.21", - "sax": "1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "1.0.3", - "mkdirp": "0.5.1", - "needle": "2.2.0", - "nopt": "4.0.1", - "npm-packlist": "1.1.10", - "npmlog": "4.1.2", - "rc": "1.2.7", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "4.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "3.0.1", - "npm-bundled": "1.0.3" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.5.1", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "1.0.1", - "fs-minipass": "1.2.5", - "minipass": "2.2.4", - "minizlib": "1.1.0", - "mkdirp": "0.5.1", - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "dev": true - } - } - } } }, "expand-brackets": { @@ -1986,7 +922,6 @@ "anymatch": "2.0.0", "async-each": "1.0.1", "braces": "2.3.2", - "fsevents": "1.2.4", "glob-parent": "3.1.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -1995,537 +930,6 @@ "path-is-absolute": "1.0.1", "readdirp": "2.1.0", "upath": "1.0.5" - }, - "dependencies": { - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.11.1", - "node-pre-gyp": "0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "2.1.2" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.9", - "iconv-lite": "0.4.21", - "sax": "1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "1.0.3", - "mkdirp": "0.5.1", - "needle": "2.2.0", - "nopt": "4.0.1", - "npm-packlist": "1.1.10", - "npmlog": "4.1.2", - "rc": "1.2.7", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "4.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "3.0.1", - "npm-bundled": "1.0.3" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.5.1", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "1.0.1", - "fs-minipass": "1.2.5", - "minipass": "2.2.4", - "minizlib": "1.1.0", - "mkdirp": "0.5.1", - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "dev": true - } - } - } } }, "expand-brackets": { @@ -3068,7 +1472,6 @@ "anymatch": "2.0.0", "async-each": "1.0.1", "braces": "2.3.2", - "fsevents": "1.2.4", "glob-parent": "3.1.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -3077,537 +1480,6 @@ "path-is-absolute": "1.0.1", "readdirp": "2.1.0", "upath": "1.0.5" - }, - "dependencies": { - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.11.1", - "node-pre-gyp": "0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "2.1.2" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.9", - "iconv-lite": "0.4.21", - "sax": "1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "1.0.3", - "mkdirp": "0.5.1", - "needle": "2.2.0", - "nopt": "4.0.1", - "npm-packlist": "1.1.10", - "npmlog": "4.1.2", - "rc": "1.2.7", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "4.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "3.0.1", - "npm-bundled": "1.0.3" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.5.1", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "1.0.1", - "fs-minipass": "1.2.5", - "minipass": "2.2.4", - "minizlib": "1.1.0", - "mkdirp": "0.5.1", - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "dev": true - } - } - } } }, "expand-brackets": { @@ -4935,7 +2807,6 @@ "requires": { "anymatch": "1.3.2", "async-each": "1.0.1", - "fsevents": "1.2.4", "glob-parent": "2.0.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -6747,6 +4618,17 @@ "null-check": "1.0.0" } }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.2" + } + }, "fs-write-stream-atomic": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", @@ -6765,535 +4647,6 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.11.1", - "node-pre-gyp": "0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "2.1.2" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.9", - "iconv-lite": "0.4.21", - "sax": "1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "1.0.3", - "mkdirp": "0.5.1", - "needle": "2.2.0", - "nopt": "4.0.1", - "npm-packlist": "1.1.10", - "npmlog": "4.1.2", - "rc": "1.2.7", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "4.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "3.0.1", - "npm-bundled": "1.0.3" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.5.1", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "1.0.1", - "fs-minipass": "1.2.5", - "minipass": "2.2.4", - "minizlib": "1.1.0", - "mkdirp": "0.5.1", - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "dev": true - } - } - }, "fstream": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", @@ -9072,6 +6425,15 @@ "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "dev": true }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, "jsonify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", @@ -10019,13 +7381,6 @@ "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", "dev": true }, - "nan": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", - "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==", - "dev": true, - "optional": true - }, "nanomatch": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", @@ -13370,6 +10725,12 @@ "imurmurhash": "0.1.4" } }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -13742,7 +11103,6 @@ "anymatch": "2.0.0", "async-each": "1.0.1", "braces": "2.3.2", - "fsevents": "1.2.4", "glob-parent": "3.1.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -13751,537 +11111,6 @@ "path-is-absolute": "1.0.1", "readdirp": "2.1.0", "upath": "1.0.5" - }, - "dependencies": { - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.11.1", - "node-pre-gyp": "0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "2.1.2" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.9", - "iconv-lite": "0.4.21", - "sax": "1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "1.0.3", - "mkdirp": "0.5.1", - "needle": "2.2.0", - "nopt": "4.0.1", - "npm-packlist": "1.1.10", - "npmlog": "4.1.2", - "rc": "1.2.7", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "4.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "3.0.1", - "npm-bundled": "1.0.3" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.5.1", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "1.0.1", - "fs-minipass": "1.2.5", - "minipass": "2.2.4", - "minizlib": "1.1.0", - "mkdirp": "0.5.1", - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "dev": true - } - } - } } }, "expand-brackets": { @@ -15121,7 +11950,6 @@ "anymatch": "2.0.0", "async-each": "1.0.1", "braces": "2.3.2", - "fsevents": "1.2.4", "glob-parent": "3.1.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -15130,537 +11958,6 @@ "path-is-absolute": "1.0.1", "readdirp": "2.1.0", "upath": "1.0.5" - }, - "dependencies": { - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.11.1", - "node-pre-gyp": "0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "2.1.2" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "2.2.4" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.9", - "iconv-lite": "0.4.21", - "sax": "1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "1.0.3", - "mkdirp": "0.5.1", - "needle": "2.2.0", - "nopt": "4.0.1", - "npm-packlist": "1.1.10", - "npmlog": "4.1.2", - "rc": "1.2.7", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "4.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "3.0.1", - "npm-bundled": "1.0.3" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.5.1", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "1.0.1", - "fs-minipass": "1.2.5", - "minipass": "2.2.4", - "minizlib": "1.1.0", - "mkdirp": "0.5.1", - "safe-buffer": "5.1.1", - "yallist": "3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "dev": true - } - } - } } }, "cliui": { diff --git a/ui/package.json b/ui/package.json index afe00523b..6f18e59dd 100644 --- a/ui/package.json +++ b/ui/package.json @@ -9,9 +9,9 @@ "test": "ng test --code-coverage", "lint": "ng lint", "e2e": "ng e2e", - "build:static": "node-sass src/static.scss ./dist/static.css", - "copy:static": "ncp ./src/static.html ./dist/static.html", - "copy": "npm run build:static && npm run copy:static", + "build:static": "node-sass src/static.scss ./dist/unsecured/static.css", + "copy:static": "node ./build", + "copy": "npm run copy:static && npm run build:static", "buildProd": "ng build --prod && npm run copy", "bundle-report": "webpack-bundle-analyzer dist/stats.json" }, @@ -55,6 +55,7 @@ "@types/jasminewd2": "~2.0.2", "@types/node": "~6.0.60", "codelyzer": "~4.2.1", + "fs-extra": "^7.0.1", "jasmine-core": "~2.99.0", "jasmine-marbles": "^0.3.1", "jasmine-spec-reporter": "~4.1.0", diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index f29cfd620..fb19ae16a 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -24,6 +24,7 @@ diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts index ebc8921d8..673f176c1 100644 --- a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts @@ -60,6 +60,28 @@ export class MetadataSourceBase implements Wizard { } getValidators(entityIdList: string[]): { [key: string]: any } { + const checkRequiredChild = (value, property, form) => { + if (!value) { + return { + code: 'REQUIRED', + path: `#${property.path}`, + message: `message.required`, + params: [value] + }; + } + return null; + }; + const checkRequiredChildren = (value, property, form) => { + let errors; + Object.keys(value).forEach((item, index, all) => { + const error = checkRequiredChild(item, { path: `${index}` }, form); + if (error) { + errors = errors || []; + errors.push(error); + } + }); + return errors; + }; const checkOrg = (value, property, form) => { const org = property.parent; const orgValue = org.value || {}; diff --git a/ui/src/app/metadata/metadata.module.ts b/ui/src/app/metadata/metadata.module.ts index 946c89423..fd00a2274 100644 --- a/ui/src/app/metadata/metadata.module.ts +++ b/ui/src/app/metadata/metadata.module.ts @@ -9,7 +9,8 @@ import { MetadataRoutingModule } from './metadata.routing'; import { ProviderModule } from './provider/provider.module'; import { I18nModule } from '../i18n/i18n.module'; import { CustomWidgetRegistry } from '../schema-form/registry'; -import { WidgetRegistry } from 'ngx-schema-form'; +import { WidgetRegistry, SchemaValidatorFactory } from 'ngx-schema-form'; +import { CustomSchemaValidatorFactory } from '../schema-form/service/schema-validator'; @NgModule({ @@ -23,7 +24,11 @@ import { WidgetRegistry } from 'ngx-schema-form'; I18nModule ], providers: [ - { provide: WidgetRegistry, useClass: CustomWidgetRegistry } + { provide: WidgetRegistry, useClass: CustomWidgetRegistry }, + { + provide: SchemaValidatorFactory, + useClass: CustomSchemaValidatorFactory + } ], declarations: [ MetadataPageComponent diff --git a/ui/src/app/schema-form/model/messages.ts b/ui/src/app/schema-form/model/messages.ts new file mode 100644 index 000000000..6d6fa61fd --- /dev/null +++ b/ui/src/app/schema-form/model/messages.ts @@ -0,0 +1,3 @@ +export const HARD_CODED_REQUIRED_MSG = RegExp('Missing required property'); + +export const REQUIRED_MSG_OVERRIDE = 'message.required'; \ No newline at end of file diff --git a/ui/src/app/schema-form/schema-form.module.ts b/ui/src/app/schema-form/schema-form.module.ts index 842e903dd..13aba1369 100644 --- a/ui/src/app/schema-form/schema-form.module.ts +++ b/ui/src/app/schema-form/schema-form.module.ts @@ -1,5 +1,5 @@ import { NgModule } from '@angular/core'; -import { SchemaFormModule } from 'ngx-schema-form'; +import { SchemaFormModule, SchemaValidatorFactory } from 'ngx-schema-form'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; @@ -23,6 +23,7 @@ import { CustomObjectWidget } from './widget/object/object.component'; import { CustomRadioComponent } from './widget/radio/radio.component'; import { InlineObjectListComponent } from './widget/array/inline-obj-list.component'; import { InlineObjectComponent } from './widget/object/inline-obj.component'; +import { CustomSchemaValidatorFactory } from './service/schema-validator'; export const COMPONENTS = [ BooleanRadioComponent, diff --git a/ui/src/app/schema-form/service/schema-validator.ts b/ui/src/app/schema-form/service/schema-validator.ts new file mode 100644 index 000000000..075a78ec9 --- /dev/null +++ b/ui/src/app/schema-form/service/schema-validator.ts @@ -0,0 +1,19 @@ +import * as ZSchema from 'z-schema'; +import { ZSchemaValidatorFactory } from 'ngx-schema-form'; + +export class CustomSchemaValidatorFactory extends ZSchemaValidatorFactory { + + protected zschema; + + constructor() { + super(); + this.createSchemaValidator(); + } + + private createSchemaValidator() { + this.zschema = new ZSchema({ + breakOnFirstError: false + }); + } +} + diff --git a/ui/src/app/schema-form/service/schema.service.ts b/ui/src/app/schema-form/service/schema.service.ts index 9cd7346de..617769de6 100644 --- a/ui/src/app/schema-form/service/schema.service.ts +++ b/ui/src/app/schema-form/service/schema.service.ts @@ -31,7 +31,8 @@ export class SchemaService { Object .keys(condition.properties) .some( - key => values.hasOwnProperty(key) ? condition.properties[key].enum[0] === values[key] : false + key => values.hasOwnProperty(key) && condition.properties[key].enum ? + condition.properties[key].enum[0] === values[key] : false ) ); currentConditions.forEach(el => { diff --git a/ui/src/app/schema-form/widget/datalist/datalist.component.html b/ui/src/app/schema-form/widget/datalist/datalist.component.html index 6ec585342..ece3d7878 100644 --- a/ui/src/app/schema-form/widget/datalist/datalist.component.html +++ b/ui/src/app/schema-form/widget/datalist/datalist.component.html @@ -21,4 +21,10 @@ role="textbox" [attr.aria-label]="schema.title | translate"> + + + , + error + + diff --git a/ui/src/app/schema-form/widget/datalist/datalist.component.ts b/ui/src/app/schema-form/widget/datalist/datalist.component.ts index 3393bd2c5..aecbe2fc3 100644 --- a/ui/src/app/schema-form/widget/datalist/datalist.component.ts +++ b/ui/src/app/schema-form/widget/datalist/datalist.component.ts @@ -2,6 +2,7 @@ import { Component, AfterViewInit } from '@angular/core'; import { ControlWidget } from 'ngx-schema-form'; import { SchemaService } from '../../service/schema.service'; +import { HARD_CODED_REQUIRED_MSG } from '../../model/messages'; @Component({ selector: 'datalist-component', @@ -26,4 +27,8 @@ export class DatalistComponent extends ControlWidget implements AfterViewInit { get required(): boolean { return this.widgetService.isRequired(this.formProperty); } + + getError(error: string): string { + return HARD_CODED_REQUIRED_MSG.test(error) ? 'message.required' : error; + } } diff --git a/ui/src/app/schema-form/widget/select/select.component.html b/ui/src/app/schema-form/widget/select/select.component.html index e66a83350..a7d1dd2ff 100644 --- a/ui/src/app/schema-form/widget/select/select.component.html +++ b/ui/src/app/schema-form/widget/select/select.component.html @@ -42,7 +42,7 @@ , - error + error diff --git a/ui/src/app/schema-form/widget/select/select.component.ts b/ui/src/app/schema-form/widget/select/select.component.ts index 5c6a2b65d..ecd50a75b 100644 --- a/ui/src/app/schema-form/widget/select/select.component.ts +++ b/ui/src/app/schema-form/widget/select/select.component.ts @@ -1,17 +1,21 @@ -import { Component, AfterViewInit } from '@angular/core'; +import { Component, AfterViewInit, OnDestroy } from '@angular/core'; import { SelectWidget } from 'ngx-schema-form'; import { SchemaService } from '../../service/schema.service'; -import { map, shareReplay } from 'rxjs/operators'; +import { map, shareReplay, startWith } from 'rxjs/operators'; +import { Subscription } from 'rxjs'; +import { HARD_CODED_REQUIRED_MSG } from '../../model/messages'; @Component({ selector: 'select-component', templateUrl: `./select.component.html` }) -export class CustomSelectComponent extends SelectWidget implements AfterViewInit { +export class CustomSelectComponent extends SelectWidget implements AfterViewInit, OnDestroy { options$: any; + errorSub: Subscription; + constructor( private widgetService: SchemaService ) { @@ -38,9 +42,23 @@ export class CustomSelectComponent extends SelectWidget implements AfterViewInit ) ); } + + this.errorSub = this.control.valueChanges.pipe(startWith(this.control.value)).subscribe(v => { + if (!v && this.required && !this.errorMessages.some(msg => HARD_CODED_REQUIRED_MSG.test(msg))) { + this.errorMessages.push('message.required'); + } + }); + } + + ngOnDestroy(): void { + this.errorSub.unsubscribe(); } get required(): boolean { return this.widgetService.isRequired(this.formProperty); } + + getError(error: string): string { + return HARD_CODED_REQUIRED_MSG.test(error) ? 'message.required' : error; + } } diff --git a/ui/src/app/schema-form/widget/string/string.component.html b/ui/src/app/schema-form/widget/string/string.component.html index 26964a59f..4986846ff 100644 --- a/ui/src/app/schema-form/widget/string/string.component.html +++ b/ui/src/app/schema-form/widget/string/string.component.html @@ -13,6 +13,7 @@ validate="true" [attr.readonly]="schema.readOnly?true:null" class="text-widget.id textline-widget form-control" + [class.is-invalid]="control.touched && !control.value && errorMessages.length" [attr.type]="this.getInputType()" [attr.id]="id" [formControl]="control" @@ -25,7 +26,7 @@ , - error + error { + if (!this.control.value + && this.required + && !this.errorMessages.some(msg => HARD_CODED_REQUIRED_MSG.test(msg)) + && this.errorMessages.indexOf(REQUIRED_MSG_OVERRIDE) < 0) { + this.errorMessages.push(REQUIRED_MSG_OVERRIDE); + } + if (!this.required) { + this.errorMessages = this.errorMessages.filter(e => e !== REQUIRED_MSG_OVERRIDE); + } + }); + } + + ngOnDestroy(): void { + this.errorSub.unsubscribe(); + } + get required(): boolean { - return this.widgetService.isRequired(this.formProperty); + const req = this.widgetService.isRequired(this.formProperty); + + return req; + } + + getError(error: string): string { + return HARD_CODED_REQUIRED_MSG.test(error) ? REQUIRED_MSG_OVERRIDE : error; } } diff --git a/ui/src/app/schema-form/widget/textarea/textarea.component.html b/ui/src/app/schema-form/widget/textarea/textarea.component.html index fb8164600..d47d6007d 100644 --- a/ui/src/app/schema-form/widget/textarea/textarea.component.html +++ b/ui/src/app/schema-form/widget/textarea/textarea.component.html @@ -18,4 +18,10 @@ [rows]="schema.widget.rows || 5" [formControl]="control" [attr.aria-label]="schema.title"> + + + , + error + + diff --git a/ui/src/app/schema-form/widget/textarea/textarea.component.ts b/ui/src/app/schema-form/widget/textarea/textarea.component.ts index 0f68e524f..fdbce4b35 100644 --- a/ui/src/app/schema-form/widget/textarea/textarea.component.ts +++ b/ui/src/app/schema-form/widget/textarea/textarea.component.ts @@ -1,20 +1,43 @@ -import { Component } from '@angular/core'; +import { Component, AfterViewInit, OnDestroy } from '@angular/core'; import { TextAreaWidget } from 'ngx-schema-form'; import { SchemaService } from '../../service/schema.service'; +import { Subscription } from 'rxjs'; +import { startWith } from 'rxjs/operators'; +import { HARD_CODED_REQUIRED_MSG } from '../../model/messages'; @Component({ selector: 'textarea-component', templateUrl: `./textarea.component.html` }) -export class CustomTextAreaComponent extends TextAreaWidget { +export class CustomTextAreaComponent extends TextAreaWidget implements AfterViewInit, OnDestroy { + + errorSub: Subscription; + constructor( private widgetService: SchemaService ) { super(); } + ngAfterViewInit(): void { + super.ngAfterViewInit(); + this.errorSub = this.control.valueChanges.pipe(startWith(this.control.value)).subscribe(v => { + if (!v && this.required && !this.errorMessages.some(msg => HARD_CODED_REQUIRED_MSG.test(msg))) { + this.errorMessages.push('message.required'); + } + }); + } + + ngOnDestroy(): void { + this.errorSub.unsubscribe(); + } + get required(): boolean { return this.widgetService.isRequired(this.formProperty); } + + getError(error: string): string { + return error.match('required').length ? 'message.required' : error; + } } diff --git a/ui/src/app/shared/autocomplete/autocomplete.component.scss b/ui/src/app/shared/autocomplete/autocomplete.component.scss index b721d9ea2..f794dce0c 100644 --- a/ui/src/app/shared/autocomplete/autocomplete.component.scss +++ b/ui/src/app/shared/autocomplete/autocomplete.component.scss @@ -1,5 +1,6 @@ @import "~bootstrap/scss/functions"; @import "~bootstrap/scss/variables"; +@import "../../../theme/palette"; .dropdown.form-group { margin-bottom: 0px; @@ -14,4 +15,16 @@ .btn-outline-secondary { border-color: $input-border-color; } + + &.is-invalid { + input.form-control { + border-color: $brand-danger; + } + } + + &.is-valid { + input.form-control { + border-color: $brand-success; + } + } } \ No newline at end of file diff --git a/ui/src/assets/schema/provider/filebacked-http-filters.schema.json b/ui/src/assets/schema/provider/filebacked-http-filters.schema.json index 4bb2f399c..820063bfa 100644 --- a/ui/src/assets/schema/provider/filebacked-http-filters.schema.json +++ b/ui/src/assets/schema/provider/filebacked-http-filters.schema.json @@ -50,8 +50,7 @@ "title": "label.certificate-file", "description": "tooltip.certificate-file", "type": "string", - "widget": "textline", - "default": "" + "widget": "textline" } }, "anyOf": [ @@ -59,6 +58,10 @@ "properties": { "requireSignedRoot": { "enum": [ true ] + }, + "certificateFile": { + "minLength": 1, + "type": "string" } }, "required": [ diff --git a/ui/src/static.html b/ui/src/error.html similarity index 100% rename from ui/src/static.html rename to ui/src/error.html