diff --git a/.gitignore b/.gitignore index 699a7b9c8..0848cc3ed 100644 --- a/.gitignore +++ b/.gitignore @@ -408,6 +408,7 @@ beacon/spring/out *.project *bin /a.json +/testbed/authentication/.idea/workspace.xml # macOS jenv -.java-version \ No newline at end of file +.java-version diff --git a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerVersionEndpointsIntegrationTests.groovy b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerVersionEndpointsIntegrationTests.groovy index a8d5a68ab..eeee3fe5c 100644 --- a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerVersionEndpointsIntegrationTests.groovy +++ b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerVersionEndpointsIntegrationTests.groovy @@ -1,22 +1,11 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor -import edu.internet2.tier.shibboleth.admin.ui.domain.Organization -import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationDisplayName -import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationName -import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationURL import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository -import groovy.json.JsonOutput -import groovy.json.JsonSlurper import org.springframework.beans.factory.annotation.Autowired 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.test.annotation.DirtiesContext import org.springframework.test.context.ActiveProfiles import spock.lang.Specification @@ -111,44 +100,9 @@ class EntityDescriptorControllerVersionEndpointsIntegrationTests extends Specifi edv2.body.serviceProviderName == 'SP2' } - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) - def 'SHIBUI-1414'() { - given: - def ed = new EntityDescriptor(entityID: 'testme', serviceProviderName: 'testme').with { - entityDescriptorRepository.save(it) - }.with { - it.setOrganization(new Organization().with { - it.organizationNames = [new OrganizationName(value: 'testme', XMLLang: 'en')] - it.organizationDisplayNames = [new OrganizationDisplayName(value: 'testme', XMLLang: 'en')] - it.organizationURLs = [new OrganizationURL(value: 'http://testme.org', XMLLang: 'en')] - it - }) - entityDescriptorRepository.save(it) - } - when: - def headers = new HttpHeaders().with { - it.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - it - } - - def allVersions = getAllEntityDescriptorVersions(ed.resourceId, List) - String edv1 = getEntityDescriptorForVersion(ed.resourceId, allVersions.body[0].id, String).body - def tedv2 = getEntityDescriptorForVersion(ed.resourceId, allVersions.body[1].id, EntityDescriptorRepresentation).body - - def aedv1 = new JsonSlurper().parseText(edv1).with { - it.put('version', tedv2.version) - it - }.with { - JsonOutput.toJson(it) - } - - def request = new HttpEntity(aedv1, headers) - def response = this.restTemplate.exchange("/api/EntityDescriptor/${ed.resourceId}", HttpMethod.PUT, request, String) - - then: - response.statusCodeValue != 400 - noExceptionThrown() - } +// Moved to its own test in the main testing package +// def 'SHIBUI-1414'() { +// } private getAllEntityDescriptorVersions(String resourceId, responseType) { this.restTemplate.getForEntity(resourceUriFor(ALL_VERSIONS_URI, resourceId), responseType) @@ -165,4 +119,4 @@ class EntityDescriptorControllerVersionEndpointsIntegrationTests extends Specifi private static resourceUriFor(String uriTemplate, String resourceId) { String.format(uriTemplate, resourceId) } -} +} \ No newline at end of file diff --git a/backend/src/integration/groovy/edu/internet2/tier/shibboleth/admin/ui/SeleniumSIDETest.groovy b/backend/src/integration/groovy/edu/internet2/tier/shibboleth/admin/ui/SeleniumSIDETest.groovy index be0e47ad5..e52aedcca 100644 --- a/backend/src/integration/groovy/edu/internet2/tier/shibboleth/admin/ui/SeleniumSIDETest.groovy +++ b/backend/src/integration/groovy/edu/internet2/tier/shibboleth/admin/ui/SeleniumSIDETest.groovy @@ -132,6 +132,8 @@ class SeleniumSIDETest extends Specification { 'SHIBUI-1740: Verify nonadmin-owned resource visibility' | '/SHIBUI-1740-4.side' 'SHIBUI-1742: Verify enabler role allows enabling' | '/SHIBUI-1742-1.side' 'SHIBUI-1742: Verify role CRUD operations' | '/SHIBUI-1742-2.side' + 'SHIBUI-1743: Verify group regex CRUD operations' | '/SHIBUI-1743-1.side' + 'SHIBUI-1743: Verify nonadmin group regex validation' | '/SHIBUI-1743-2.side' 'SHIBUI-1744: Verify attribute bundle CRUD operations' | '/SHIBUI-1744-1.side' 'SHIBUI-1744: Verify attribute bundles in metadata sources' | '/SHIBUI-1744-2.side' 'SHIBUI-1744: Verify attribute bundles in entity attribute filters' | '/SHIBUI-1744-3.side' diff --git a/backend/src/integration/resources/SHIBUI-1743-1.side b/backend/src/integration/resources/SHIBUI-1743-1.side new file mode 100644 index 000000000..76a1a3a75 --- /dev/null +++ b/backend/src/integration/resources/SHIBUI-1743-1.side @@ -0,0 +1,270 @@ +{ + "id": "3ac57a4d-2fe9-4904-b496-48105fcbf11e", + "version": "2.0", + "name": "SHIBUI-1743-1", + "url": "http://localhost:10101", + "tests": [{ + "id": "aea7965a-db5c-47ec-b902-87e1bd318290", + "name": "SHIBUI-1743-1", + "commands": [{ + "id": "035adf50-a869-4192-ad3d-435b6ca04ba1", + "comment": "", + "command": "open", + "target": "/login", + "targets": [], + "value": "" + }, { + "id": "ceb04e82-d01c-429e-b1ea-072acf584f09", + "comment": "", + "command": "type", + "target": "id=username", + "targets": [ + ["id=username", "id"], + ["name=username", "name"], + ["css=#username", "css:finder"], + ["xpath=//input[@id='username']", "xpath:attributes"], + ["xpath=//input", "xpath:position"] + ], + "value": "admin" + }, { + "id": "c0ddf5d2-56a4-4d09-b822-f13aa9032266", + "comment": "", + "command": "type", + "target": "id=password", + "targets": [ + ["id=password", "id"], + ["name=password", "name"], + ["css=#password", "css:finder"], + ["xpath=//input[@id='password']", "xpath:attributes"], + ["xpath=//p[2]/input", "xpath:position"] + ], + "value": "adminpass" + }, { + "id": "b2d4dcb0-95f2-480e-a8ba-6bcafc0e9414", + "comment": "", + "command": "sendKeys", + "target": "id=password", + "targets": [ + ["id=password", "id"], + ["name=password", "name"], + ["css=#password", "css:finder"], + ["xpath=//input[@id='password']", "xpath:attributes"], + ["xpath=//p[2]/input", "xpath:position"] + ], + "value": "${KEY_ENTER}" + }, { + "id": "938e82d0-6e75-428a-9415-d2aa38003793", + "comment": "", + "command": "waitForElementEditable", + "target": "xpath=//button[contains(.,'Advanced')]", + "targets": [], + "value": "30000" + }, { + "id": "b2cef5a2-f0e1-4ca6-9ce5-e4a26314b4cd", + "comment": "", + "command": "open", + "target": "/api/heheheheheheheWipeout", + "targets": [], + "value": "" + }, { + "id": "34c76d5c-89e1-4e62-a424-2b1c845a01db", + "comment": "", + "command": "assertText", + "target": "css=body", + "targets": [], + "value": "yes, you did it" + }, { + "id": "07677e0d-e61d-4b65-ae09-9f3231532c72", + "comment": "", + "command": "open", + "target": "/dashboard", + "targets": [], + "value": "" + }, { + "id": "c3698856-333c-4ab9-b455-d72cb6d3dfe5", + "comment": "", + "command": "waitForElementEditable", + "target": "xpath=//button[contains(.,'Advanced')]", + "targets": [], + "value": "30000" + }, { + "id": "461caf72-51b6-44ff-a9f6-845c4f134f0d", + "comment": "", + "command": "click", + "target": "xpath=//button[contains(.,'Advanced')]", + "targets": [ + ["id=dropdown-basic", "id"], + ["xpath=//button[@id='dropdown-basic']", "xpath:attributes"], + ["xpath=//div[@id='basic-nav-dropdown']/button", "xpath:idRelative"], + ["xpath=//div/button", "xpath:position"], + ["xpath=//button[contains(.,'Advanced')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "dc417d90-3574-460e-a8fc-1207e7824835", + "comment": "", + "command": "click", + "target": "linkText=Groups", + "targets": [ + ["linkText=Groups", "linkText"], + ["css=.text-primary:nth-child(2)", "css:finder"], + ["xpath=//a[contains(text(),'Groups')]", "xpath:link"], + ["xpath=//div[@id='basic-nav-dropdown']/div/a[2]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/groups')]", "xpath:href"], + ["xpath=//a[2]", "xpath:position"], + ["xpath=//a[contains(.,'Groups')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "6e2a02bf-dcdb-4e00-ab4b-7a9d92ce5c7d", + "comment": "", + "command": "click", + "target": "css=tr:nth-child(2) .text-primary > .svg-inline--fa", + "targets": [ + ["css=tr:nth-child(1) .text-primary > .svg-inline--fa", "css:finder"] + ], + "value": "" + }, { + "id": "a9de6741-0c08-4eae-b2a4-e1cef4c1326b", + "comment": "", + "command": "type", + "target": "id=root_validationRegex", + "targets": [], + "value": "" + }, { + "id": "8ab0b37b-3584-4e37-9154-ed766fe6546b", + "comment": "", + "command": "type", + "target": "id=root_validationRegex", + "targets": [], + "value": "foo.*" + }, { + "id": "e207cc31-d81c-4128-b029-88c8539d914f", + "comment": "", + "command": "click", + "target": "css=.fa-save > path", + "targets": [ + ["css=.fa-save > path", "css:finder"] + ], + "value": "" + }, { + "id": "1d6fa603-2386-4c9d-86dd-66ed0b1b2727", + "comment": "", + "command": "click", + "target": "css=tr:nth-child(2) .text-primary path", + "targets": [ + ["css=tr:nth-child(1) .text-primary path", "css:finder"] + ], + "value": "" + }, { + "id": "6de5933b-41ca-4e77-bdc3-24ffb1d2c87a", + "comment": "", + "command": "assertValue", + "target": "id=root_validationRegex", + "targets": [], + "value": "foo.*" + }, { + "id": "d00f2c01-9fdf-4112-a476-2b09fcf44fe1", + "comment": "", + "command": "type", + "target": "id=root_validationRegex", + "targets": [], + "value": "" + }, { + "id": "18a4f992-4776-47b0-a1be-8b9eab4201d4", + "comment": "", + "command": "type", + "target": "id=root_validationRegex", + "targets": [], + "value": "foo.*bar" + }, { + "id": "c135922a-ba8e-49e3-9e3c-e5d8fe181f39", + "comment": "", + "command": "click", + "target": "css=.btn-info", + "targets": [ + ["css=.btn-info", "css:finder"], + ["xpath=(//button[@type='button'])[4]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/button", "xpath:idRelative"], + ["xpath=//div[2]/div/div/button", "xpath:position"], + ["xpath=//button[contains(.,' Save')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "5ceb11cd-a144-471f-b628-3c623876aac9", + "comment": "", + "command": "click", + "target": "css=tr:nth-child(2) .text-primary > .svg-inline--fa", + "targets": [ + ["css=tr:nth-child(1) .text-primary > .svg-inline--fa", "css:finder"] + ], + "value": "" + }, { + "id": "b35bf5fc-6e88-4594-a2be-410a8cffa5b7", + "comment": "", + "command": "assertValue", + "target": "id=root_validationRegex", + "targets": [], + "value": "foo.*bar" + }, { + "id": "8196a13f-c609-48e7-9bf7-cf82baa92de3", + "comment": "", + "command": "type", + "target": "id=root_validationRegex", + "targets": [], + "value": "" + }, { + "id": "104a4b2d-4d7e-49e7-880e-5d5a3a3d0c6c", + "comment": "", + "command": "type", + "target": "id=root_validationRegex", + "targets": [], + "value": " " + }, { + "id": "bf4306b6-8daa-44c8-918b-0308facb1168", + "comment": "", + "command": "click", + "target": "css=.btn-info", + "targets": [ + ["css=.btn-info", "css:finder"], + ["xpath=(//button[@type='button'])[4]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/button", "xpath:idRelative"], + ["xpath=//div[2]/div/div/button", "xpath:position"], + ["xpath=//button[contains(.,' Save')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "3cb986b0-40b0-4f40-a684-c1167c7f6165", + "comment": "", + "command": "click", + "target": "css=tr:nth-child(2) .text-primary > .svg-inline--fa", + "targets": [ + ["css=tr:nth-child(1) .text-primary > .svg-inline--fa", "css:finder"] + ], + "value": "" + }, { + "id": "967fef94-832c-493c-83a2-92b174b00567", + "comment": "", + "command": "assertValue", + "target": "id=root_validationRegex", + "targets": [ + ["id=root_validationRegex", "id"], + ["css=#root_validationRegex", "css:finder"], + ["xpath=//input[@id='root_validationRegex']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div/form/div/div/div/div[3]/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//div[3]/div/div/div/div/input", "xpath:position"] + ], + "value": "" + }] + }], + "suites": [{ + "id": "c5aa01a8-f302-4538-85f8-2f57971ea640", + "name": "Default Suite", + "persistSession": false, + "parallel": false, + "timeout": 300, + "tests": ["aea7965a-db5c-47ec-b902-87e1bd318290"] + }], + "urls": ["http://localhost:10101/"], + "plugins": [] +} \ No newline at end of file diff --git a/backend/src/integration/resources/SHIBUI-1743-2.side b/backend/src/integration/resources/SHIBUI-1743-2.side new file mode 100644 index 000000000..3908e652d --- /dev/null +++ b/backend/src/integration/resources/SHIBUI-1743-2.side @@ -0,0 +1,612 @@ +{ + "id": "393f49cf-1aed-4071-aed8-65eaf66a8f78", + "version": "2.0", + "name": "SHIBUI-1743-2", + "url": "http://localhost:10101", + "tests": [{ + "id": "5d823c03-1448-4bd0-ad8f-41f0bfc365a7", + "name": "SHIBUI-1743-2", + "commands": [{ + "id": "c27e336d-8789-4da1-bf03-9a8e9b900cf2", + "comment": "", + "command": "open", + "target": "/login", + "targets": [], + "value": "" + }, { + "id": "22cded73-0131-439b-bad6-f87dd48d9233", + "comment": "", + "command": "type", + "target": "id=username", + "targets": [ + ["id=username", "id"], + ["name=username", "name"], + ["css=#username", "css:finder"], + ["xpath=//input[@id='username']", "xpath:attributes"], + ["xpath=//input", "xpath:position"] + ], + "value": "admin" + }, { + "id": "5808aa96-a5b4-4c13-83b4-6cb71565fbce", + "comment": "", + "command": "type", + "target": "id=password", + "targets": [ + ["id=password", "id"], + ["name=password", "name"], + ["css=#password", "css:finder"], + ["xpath=//input[@id='password']", "xpath:attributes"], + ["xpath=//p[2]/input", "xpath:position"] + ], + "value": "adminpass" + }, { + "id": "6f4a6682-64a3-4501-a24a-d5477e976e24", + "comment": "", + "command": "sendKeys", + "target": "id=password", + "targets": [ + ["id=password", "id"], + ["name=password", "name"], + ["css=#password", "css:finder"], + ["xpath=//input[@id='password']", "xpath:attributes"], + ["xpath=//p[2]/input", "xpath:position"] + ], + "value": "${KEY_ENTER}" + }, { + "id": "168a6d36-5eda-41a5-8b6e-59567648dd2c", + "comment": "", + "command": "waitForElementVisible", + "target": "id=advanced-nav-dropdown-toggle", + "targets": [ + ["id=advanced-nav-dropdown-toggle", "id"], + ["css=#advanced-nav-dropdown-toggle", "css:finder"], + ["xpath=//button[@id='advanced-nav-dropdown-toggle']", "xpath:attributes"], + ["xpath=//div[@id='advanced-nav-dropdown']/button", "xpath:idRelative"], + ["xpath=//div[3]/button", "xpath:position"], + ["xpath=//button[contains(.,'Advanced')]", "xpath:innerText"] + ], + "value": "30000" + }, { + "id": "803e22d7-949f-4fd0-97e6-d2a6b0be988b", + "comment": "", + "command": "open", + "target": "/api/heheheheheheheWipeout", + "targets": [], + "value": "" + }, { + "id": "05fa781a-7867-4b41-b9ee-8f69c67c8e51", + "comment": "", + "command": "assertText", + "target": "css=body", + "targets": [], + "value": "yes, you did it" + }, { + "id": "c3f11028-c8de-4742-ab4d-121b72b51abe", + "comment": "", + "command": "open", + "target": "/", + "targets": [], + "value": "" + }, { + "id": "fefc39c6-9484-4e28-b0ba-2cddbdab5c3d", + "comment": "", + "command": "waitForElementVisible", + "target": "id=advanced-nav-dropdown-toggle", + "targets": [], + "value": "30000" + }, { + "id": "acc47c80-37a0-493d-9ee7-4461999c2462", + "comment": "", + "command": "click", + "target": "id=advanced-nav-dropdown-toggle", + "targets": [ + ["id=dropdown-basic", "id"], + ["xpath=//button[@id='dropdown-basic']", "xpath:attributes"], + ["xpath=//div[@id='basic-nav-dropdown']/button", "xpath:idRelative"], + ["xpath=//div/button", "xpath:position"], + ["xpath=//button[contains(.,'Advanced')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "0517a6a2-3a03-41cb-a762-9513436aeabd", + "comment": "", + "command": "click", + "target": "linkText=Groups", + "targets": [ + ["linkText=Groups", "linkText"], + ["css=.text-primary:nth-child(2)", "css:finder"], + ["xpath=//a[contains(text(),'Groups')]", "xpath:link"], + ["xpath=//div[@id='basic-nav-dropdown']/div/a[2]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/groups')]", "xpath:href"], + ["xpath=//a[2]", "xpath:position"], + ["xpath=//a[contains(.,'Groups')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "a9865fff-afe0-4786-8a32-18eb1f920424", + "comment": "", + "command": "click", + "target": "css=tr:nth-child(4) .text-primary path", + "targets": [ + ["css=tr:nth-child(4) .text-primary path", "css:finder"] + ], + "value": "" + }, { + "id": "b8d76db3-6b4d-42ab-b672-12b645546725", + "comment": "", + "command": "type", + "target": "id=root_validationRegex", + "targets": [ + ["id=root_validationRegex", "id"], + ["css=#root_validationRegex", "css:finder"], + ["xpath=//input[@id='root_validationRegex']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div/form/div/div/div/div[3]/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//div[3]/div/div/div/div/input", "xpath:position"] + ], + "value": "foo.*" + }, { + "id": "2d470f1a-bf0c-4e31-bd7b-24da8e214fc8", + "comment": "", + "command": "click", + "target": "css=.btn-info", + "targets": [ + ["css=.btn-info", "css:finder"], + ["xpath=(//button[@type='button'])[4]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/button", "xpath:idRelative"], + ["xpath=//div[2]/div/div/button", "xpath:position"], + ["xpath=//button[contains(.,' Save')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "3e012b4f-0c0f-4f58-89ff-f32b8d78f163", + "comment": "", + "command": "click", + "target": "id=user-nav-dropdown-toggle", + "targets": [ + ["id=user-nav-dropdown-toggle", "id"], + ["css=#user-nav-dropdown-toggle", "css:finder"], + ["xpath=//button[@id='user-nav-dropdown-toggle']", "xpath:attributes"], + ["xpath=//div[@id='user-nav-dropdown']/button", "xpath:idRelative"], + ["xpath=//div[4]/button", "xpath:position"], + ["xpath=//button[contains(.,'Logged in as admin')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "e2cc961b-051e-4ae5-b075-8215dfbefc1b", + "comment": "", + "command": "click", + "target": "id=user-nav-dropdown-logout", + "targets": [ + ["id=user-nav-dropdown-logout", "id"], + ["linkText=Logout", "linkText"], + ["css=#user-nav-dropdown-logout", "css:finder"], + ["xpath=//a[contains(text(),'Logout')]", "xpath:link"], + ["xpath=//a[@id='user-nav-dropdown-logout']", "xpath:attributes"], + ["xpath=//div[@id='user-nav-dropdown']/div/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/logout')]", "xpath:href"], + ["xpath=//div[4]/div/a", "xpath:position"], + ["xpath=//a[contains(.,'Logout')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "2fc8aa68-5a28-41e3-8355-5948cf5564f0", + "comment": "", + "command": "type", + "target": "id=username", + "targets": [ + ["id=username", "id"], + ["name=username", "name"], + ["css=#username", "css:finder"], + ["xpath=//input[@id='username']", "xpath:attributes"], + ["xpath=//input", "xpath:position"] + ], + "value": "nonadmin" + }, { + "id": "b295f36e-7e11-42f6-970d-d93b3ccb5411", + "comment": "", + "command": "type", + "target": "id=password", + "targets": [ + ["id=password", "id"], + ["name=password", "name"], + ["css=#password", "css:finder"], + ["xpath=//input[@id='password']", "xpath:attributes"], + ["xpath=//p[2]/input", "xpath:position"] + ], + "value": "nonadminpass" + }, { + "id": "5508285d-34e4-40a2-8670-e520ac59ada6", + "comment": "", + "command": "click", + "target": "css=.btn", + "targets": [ + ["css=.btn", "css:finder"], + ["xpath=//button[@type='submit']", "xpath:attributes"], + ["xpath=//button", "xpath:position"], + ["xpath=//button[contains(.,'Sign in')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "cdfa1d56-26e2-4cfe-8985-ccd1b8af1ffe", + "comment": "", + "command": "click", + "target": "id=metadata-nav-dropdown-toggle", + "targets": [ + ["id=metadata-nav-dropdown-toggle", "id"], + ["css=#metadata-nav-dropdown-toggle", "css:finder"], + ["xpath=//button[@id='metadata-nav-dropdown-toggle']", "xpath:attributes"], + ["xpath=//div[@id='metadata-nav-dropdown']/button", "xpath:idRelative"], + ["xpath=//div[2]/button", "xpath:position"], + ["xpath=//button[contains(.,'Add New')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "61a52996-637f-4a17-bd83-1391da2a1582", + "comment": "", + "command": "click", + "target": "id=metadata-nav-dropdown-source", + "targets": [ + ["id=metadata-nav-dropdown-source", "id"], + ["linkText=Add a new metadata source", "linkText"], + ["css=#metadata-nav-dropdown-source", "css:finder"], + ["xpath=//a[contains(text(),'Add a new metadata source')]", "xpath:link"], + ["xpath=//a[@id='metadata-nav-dropdown-source']", "xpath:attributes"], + ["xpath=//div[@id='metadata-nav-dropdown']/div/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/metadata/source/new')]", "xpath:href"], + ["xpath=//div[2]/div/a", "xpath:position"], + ["xpath=//a[contains(.,'Add a new metadata source')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "6f43b33d-2def-4561-817f-1c929c084188", + "comment": "", + "command": "waitForElementEditable", + "target": "id=root_serviceProviderName", + "targets": [], + "value": "30000" + }, { + "id": "487281e7-a78e-4dbf-b1d6-291e044ff503", + "comment": "", + "command": "type", + "target": "id=root_serviceProviderName", + "targets": [ + ["id=root_serviceProviderName", "id"], + ["css=#root_serviceProviderName", "css:finder"], + ["xpath=//input[@id='root_serviceProviderName']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[3]/div/div/form/div/div/div/div/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "test" + }, { + "id": "e16eec26-1941-4386-8d56-6a95992c7bb2", + "comment": "", + "command": "type", + "target": "id=root_entityId", + "targets": [ + ["id=root_entityId", "id"], + ["css=#root_entityId", "css:finder"], + ["xpath=//input[@id='root_entityId']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[3]/div/div/form/div/div/div/div/div/div[2]/div/div/input", "xpath:idRelative"], + ["xpath=//div[2]/div/div/input", "xpath:position"] + ], + "value": "bar" + }, { + "id": "9bb1915a-a238-463e-985a-b34e82eb65d7", + "comment": "", + "command": "click", + "target": "css=body", + "targets": [], + "value": "" + }, { + "id": "1a61840b-2ac7-4335-be8c-b1152de04316", + "comment": "", + "command": "assertText", + "target": "css=.border-0 > .m-0", + "targets": [ + ["css=.border-0 > .m-0", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[3]/div/div/form/div/div/div/div/div/div[2]/div/div/ul/li/small", "xpath:idRelative"], + ["xpath=//small", "xpath:position"], + ["xpath=//small[contains(.,'Pattern must match group url validation pattern: foo.*')]", "xpath:innerText"] + ], + "value": "Pattern must match group url validation pattern: foo.*" + }, { + "id": "a59f7ca5-78bc-4c14-8631-2625030291b4", + "comment": "", + "command": "type", + "target": "id=root_entityId", + "targets": [], + "value": "" + }, { + "id": "858b0e07-544c-4a0f-bb98-2aca2df27cee", + "comment": "", + "command": "type", + "target": "id=root_entityId", + "targets": [ + ["id=root_entityId", "id"], + ["css=#root_entityId", "css:finder"], + ["xpath=//input[@id='root_entityId']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[3]/div/div/form/div/div/div/div/div/div[2]/div/div/input", "xpath:idRelative"], + ["xpath=//div[2]/div/div/input", "xpath:position"] + ], + "value": "foobar" + }, { + "id": "cc3ac3a6-09da-4118-8cb3-76bea91ee72f", + "comment": "", + "command": "click", + "target": "css=.label", + "targets": [ + ["css=.label", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[2]/div/nav/ul/li[2]/button/span", "xpath:idRelative"], + ["xpath=//li[2]/button/span", "xpath:position"], + ["xpath=//span[contains(.,'2. Organization Information')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "70830668-455e-4300-9b1c-9d8533d154a6", + "comment": "", + "command": "click", + "target": "css=.label:nth-child(1)", + "targets": [ + ["css=.label:nth-child(1)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button/span", "xpath:idRelative"], + ["xpath=//li[3]/button/span", "xpath:position"], + ["xpath=//span[contains(.,'3. User Interface / MDUI Information')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "df50b6ab-a87f-4127-ad7a-680c5d978d41", + "comment": "", + "command": "click", + "target": "css=.label:nth-child(1)", + "targets": [ + ["css=.label:nth-child(1)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button/span", "xpath:idRelative"], + ["xpath=//li[3]/button/span", "xpath:position"], + ["xpath=//span[contains(.,'4. SP SSO Descriptor Information')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "d4fb5c40-7f38-46ff-b264-732dd10d6f80", + "comment": "", + "command": "click", + "target": "css=.label:nth-child(1)", + "targets": [ + ["css=.label:nth-child(1)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button/span", "xpath:idRelative"], + ["xpath=//li[3]/button/span", "xpath:position"], + ["xpath=//span[contains(.,'5. Logout Endpoints')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "3b3497f1-4f3e-447d-a6d2-0bada12696e7", + "comment": "", + "command": "click", + "target": "css=.label:nth-child(1)", + "targets": [ + ["css=.label:nth-child(1)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button/span", "xpath:idRelative"], + ["xpath=//li[3]/button/span", "xpath:position"], + ["xpath=//span[contains(.,'6. Security Information')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "4330cd77-3176-4486-b163-11a74ed5730e", + "comment": "", + "command": "click", + "target": "css=.label:nth-child(1)", + "targets": [ + ["css=.label:nth-child(1)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button/span", "xpath:idRelative"], + ["xpath=//li[3]/button/span", "xpath:position"], + ["xpath=//span[contains(.,'7. Assertion Consumer Service')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "11cbd07a-2cc4-496a-8b78-5d3c335314bc", + "comment": "", + "command": "click", + "target": "css=.array-add-button", + "targets": [ + ["css=.array-add-button", "css:finder"], + ["xpath=(//button[@type='button'])[5]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[2]/div/div/form/div/div/div/div/div[8]/div/div/div/div/div/button", "xpath:idRelative"], + ["xpath=//div/div/div/div/button", "xpath:position"], + ["xpath=//button[contains(.,'Add ')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "c1983487-c602-41b1-98a2-321beb58598b", + "comment": "", + "command": "click", + "target": "id=root_assertionConsumerServices_0_locationUrl", + "targets": [ + ["id=root_assertionConsumerServices_0_locationUrl", "id"], + ["css=#root_assertionConsumerServices_0_locationUrl", "css:finder"], + ["xpath=//input[@id='root_assertionConsumerServices_0_locationUrl']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[2]/div/div/form/div/div/div/div/div[8]/div/div/div/div/div[2]/div/div/div/div[2]/div/div/div/div/div/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "" + }, { + "id": "866ed333-fc97-4eef-934f-34d1300778ea", + "comment": "", + "command": "type", + "target": "id=root_assertionConsumerServices_0_locationUrl", + "targets": [ + ["id=root_assertionConsumerServices_0_locationUrl", "id"], + ["css=#root_assertionConsumerServices_0_locationUrl", "css:finder"], + ["xpath=//input[@id='root_assertionConsumerServices_0_locationUrl']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[2]/div/div/form/div/div/div/div/div[8]/div/div/div/div/div[2]/div/div/div/div[2]/div/div/div/div/div/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "bar" + }, { + "id": "0192730a-0697-464d-a457-0201a1422178", + "comment": "", + "command": "click", + "target": "css=body", + "targets": [], + "value": "" + }, { + "id": "2627de6e-206f-4825-9918-dc8df830b17a", + "comment": "", + "command": "assertText", + "target": "css=.mb-0 .border-0 > .m-0", + "targets": [ + ["css=.mb-0 .border-0 > .m-0", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[2]/div/div/form/div/div/div/div/div[8]/div/div/div/div/div[2]/div/div/div/div[2]/div/div/div/div/div/div/div/div/div/ul/li/small", "xpath:idRelative"], + ["xpath=//small", "xpath:position"], + ["xpath=//small[contains(.,'Pattern must match group url validation pattern: foo.*')]", "xpath:innerText"] + ], + "value": "Pattern must match group url validation pattern: foo.*" + }, { + "id": "d84cb246-00d8-486b-8d5f-db96a5771c33", + "comment": "", + "command": "type", + "target": "id=root_assertionConsumerServices_0_locationUrl", + "targets": [ + ["id=root_assertionConsumerServices_0_locationUrl", "id"], + ["css=#root_assertionConsumerServices_0_locationUrl", "css:finder"], + ["xpath=//input[@id='root_assertionConsumerServices_0_locationUrl']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[2]/div/div/form/div/div/div/div/div[8]/div/div/div/div/div[2]/div/div/div/div[2]/div/div/div/div/div/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "" + }, { + "id": "9ed5bbd2-416a-42cf-a12c-c99073f464eb", + "comment": "", + "command": "type", + "target": "id=root_assertionConsumerServices_0_locationUrl", + "targets": [ + ["id=root_assertionConsumerServices_0_locationUrl", "id"], + ["css=#root_assertionConsumerServices_0_locationUrl", "css:finder"], + ["xpath=//input[@id='root_assertionConsumerServices_0_locationUrl']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[2]/div/div/form/div/div/div/div/div[8]/div/div/div/div/div[2]/div/div/div/div[2]/div/div/div/div/div/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "foobar" + }, { + "id": "fdad62d8-ee52-422f-b0f0-e60883135d28", + "comment": "", + "command": "select", + "target": "id=root_assertionConsumerServices_0_binding", + "targets": [], + "value": "label=urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + }, { + "id": "62d7dc6d-1799-45ad-a43d-d1c92062b16c", + "comment": "", + "command": "click", + "target": "css=.label:nth-child(1)", + "targets": [ + ["css=.label:nth-child(1)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button/span", "xpath:idRelative"], + ["xpath=//li[3]/button/span", "xpath:position"], + ["xpath=//span[contains(.,'8. Relying Party Overrides')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "16b9cdc0-d46c-4997-a233-bc316707bd01", + "comment": "", + "command": "click", + "target": "css=.label:nth-child(1)", + "targets": [ + ["css=.label:nth-child(1)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button/span", "xpath:idRelative"], + ["xpath=//li[3]/button/span", "xpath:position"], + ["xpath=//span[contains(.,'9. Attribute Release')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "cf683aa0-c8ea-42c1-8ea4-85b6cb4b4f11", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[4]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button", "xpath:idRelative"], + ["xpath=//li[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "d302d27d-3729-441c-9292-0deae42296e9", + "comment": "", + "command": "mouseOver", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[4]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button", "xpath:idRelative"], + ["xpath=//li[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "56bd41ba-c79e-4c68-b1fd-c6f39ddcb54a", + "comment": "", + "command": "click", + "target": "css=.save", + "targets": [ + ["css=.save", "css:finder"], + ["xpath=(//button[@type='button'])[4]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button", "xpath:idRelative"], + ["xpath=//li[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "51032b88-b73e-4a16-8fe1-7286b25daaaf", + "comment": "", + "command": "click", + "target": "linkText=test", + "targets": [ + ["linkText=test", "linkText"], + ["css=.align-middle > a", "css:finder"], + ["xpath=//a[contains(text(),'test')]", "xpath:link"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/div/table/tbody/tr/td/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/metadata/source/5710d5cd-9241-45dc-8ed6-abe9fd01c8c2/configuration/options')]", "xpath:href"], + ["xpath=//td/a", "xpath:position"], + ["xpath=//a[contains(.,'test')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "8553e5d5-449c-42b1-be8b-92d6d4895800", + "comment": "", + "command": "waitForElementVisible", + "target": "css=div:nth-child(2) > div:nth-child(2) > .d-flex > .text-truncate", + "targets": [], + "value": "30000" + }, { + "id": "93b99f62-f09e-4ac3-a69f-d3fc5d8576d7", + "comment": "", + "command": "assertText", + "target": "css=div:nth-child(2) > div:nth-child(2) > .d-flex > .text-truncate", + "targets": [ + ["css=div:nth-child(2) > div:nth-child(2) > .d-flex > .text-truncate", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div/section/div/div[2]/div[2]/div[2]/div/span[2]", "xpath:idRelative"], + ["xpath=//div[2]/div/span[2]", "xpath:position"], + ["xpath=//span[contains(.,'foobar')]", "xpath:innerText"] + ], + "value": "foobar" + }, { + "id": "7036842f-1cd0-4ed5-a036-136943326c11", + "comment": "", + "command": "assertText", + "target": "css=.py-2:nth-child(1) > .d-block", + "targets": [ + ["css=.py-2:nth-child(1) > .d-block", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div/section[6]/div/div[2]/div[2]/div/div[2]/div/span", "xpath:idRelative"], + ["xpath=//section[6]/div/div[2]/div[2]/div/div[2]/div/span", "xpath:position"] + ], + "value": "foobar" + }] + }], + "suites": [{ + "id": "c1770d48-99b6-4866-af1c-d4e485b851ab", + "name": "Default Suite", + "persistSession": false, + "parallel": false, + "timeout": 300, + "tests": ["5d823c03-1448-4bd0-ad8f-41f0bfc365a7"] + }], + "urls": ["http://localhost:10101/"], + "plugins": [] +} \ No newline at end of file diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DurationMetadataResolverValidator.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DurationMetadataResolverValidator.groovy similarity index 78% rename from backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DurationMetadataResolverValidator.groovy rename to backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DurationMetadataResolverValidator.groovy index 56a2ecd77..85dfc3845 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DurationMetadataResolverValidator.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DurationMetadataResolverValidator.groovy @@ -1,8 +1,11 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicMetadataResolverAttributes +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ReloadableMetadataResolverAttributes import edu.internet2.tier.shibboleth.admin.util.DurationUtility -class DurationMetadataResolverValidator implements MetadataResolverValidator { +class DurationMetadataResolverValidator implements IMetadataResolverValidator { boolean supports(MetadataResolver resolver) { return resolver.hasProperty('dynamicMetadataResolverAttributes') || resolver.hasProperty('reloadableMetadataResolverAttributes') } @@ -27,4 +30,4 @@ class DurationMetadataResolverValidator implements MetadataResolverValidator { } return new ValidationResult() } -} +} \ No newline at end of file diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy index bec5e1e38..e9faeb69c 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy @@ -76,6 +76,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { @Autowired private UserService userService + // TODO: enhance void constructXmlNodeForEntityAttributeNamespaceProtection(def markupBuilderDelegate) { markupBuilderDelegate.MetadataFilter('xsi:type': 'EntityAttributes') { AttributeFilterScript() { @@ -124,6 +125,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } + // TODO: enhance void constructXmlNodeForFilter(EntityRoleWhiteListFilter filter, def markupBuilderDelegate) { if (!filter.retainedRoles?.isEmpty()) { markupBuilderDelegate.MetadataFilter( @@ -138,7 +140,6 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } - void constructXmlNodeForFilter(NameIdFormatFilter filter, def markupBuilderDelegate) { def type = filter.nameIdFormatFilterTarget.nameIdFormatFilterTargetType markupBuilderDelegate.MetadataFilter( @@ -201,6 +202,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } + void constructXmlNodeForResolver(DynamicHttpMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { markupBuilderDelegate.MetadataProvider(id: resolver.xmlId, 'xsi:type': 'DynamicHTTPMetadataProvider', @@ -236,7 +238,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { disregardTLSCertificate: resolver.httpMetadataResolverAttributes?.disregardTLSCertificate ?: null, httpClientSecurityParametersRef: resolver.httpMetadataResolverAttributes?.httpClientSecurityParametersRef, proxyHost: resolver.httpMetadataResolverAttributes?.proxyHost, - proxyPort: resolver.httpMetadataResolverAttributes?.proxyHost, + proxyPort: resolver.httpMetadataResolverAttributes?.proxyPort, proxyUser: resolver.httpMetadataResolverAttributes?.proxyUser, proxyPassword: resolver.httpMetadataResolverAttributes?.proxyPassword, httpCaching: resolver.httpMetadataResolverAttributes?.httpCaching, @@ -307,7 +309,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { disregardTLSCertificate: resolver.httpMetadataResolverAttributes?.disregardTLSCertificate ?: null, httpClientSecurityParametersRef: resolver.httpMetadataResolverAttributes?.httpClientSecurityParametersRef, proxyHost: resolver.httpMetadataResolverAttributes?.proxyHost, - proxyPort: resolver.httpMetadataResolverAttributes?.proxyHost, + proxyPort: resolver.httpMetadataResolverAttributes?.proxyPort, proxyUser: resolver.httpMetadataResolverAttributes?.proxyUser, proxyPassword: resolver.httpMetadataResolverAttributes?.proxyPassword, httpCaching: resolver.httpMetadataResolverAttributes?.httpCaching, @@ -403,7 +405,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { 'username': resolver.svnMetadataResource.username, 'password': resolver.svnMetadataResource.password, 'proxyHost': resolver.svnMetadataResource.proxyHost, - 'proxyPort': resolver.svnMetadataResource.proxyHost, + 'proxyPort': resolver.svnMetadataResource.proxyPort, 'proxyUserName': resolver.svnMetadataResource.proxyUserName, 'proxyPassword': resolver.svnMetadataResource.proxyPassword) @@ -540,7 +542,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { if (metadataFilter instanceof NameIdFormatFilter) { NameIdFormatFilter nameIdFormatFilter = NameIdFormatFilter.cast(metadataFilter) NameIDFormatFilter openSamlTargetFilter = new OpenSamlNameIdFormatFilter() - openSamlTargetFilter.removeExistingFormats = nameIdFormatFilter.removeExistingFormats + openSamlTargetFilter.removeExistingFormats = nameIdFormatFilter.removeExistingFormats == null ? false : nameIdFormatFilter.removeExistingFormats Map, Collection> predicateRules = [:] def type = nameIdFormatFilter.nameIdFormatFilterTarget.nameIdFormatFilterTargetType def values = nameIdFormatFilter.nameIdFormatFilterTarget.value @@ -588,7 +590,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { throw new MetadataFileNotFoundException("message.file-doesnt-exist") } try { - OpenSamlChainingMetadataResolverUtil.updateChainingMetadataResolver((OpenSamlChainingMetadataResolver) chainingMetadataResolver, openSamlRepresentation); + OpenSamlChainingMetadataResolverUtil.updateChainingMetadataResolver((OpenSamlChainingMetadataResolver) chainingMetadataResolver, openSamlRepresentation) } catch (Throwable e) { throw new InitializationException(e); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConverterConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConverterConfiguration.java new file mode 100644 index 000000000..6380e0018 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConverterConfiguration.java @@ -0,0 +1,17 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration; + +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverConverterService; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverConverterServiceImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@Configuration +public class MetadataResolverConverterConfiguration { + @Bean + public MetadataResolverConverterService metadataResolverConverterService() { + return new MetadataResolverConverterServiceImpl(); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java index 86d3f9f58..7e09c32e3 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java @@ -1,9 +1,13 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DurationMetadataResolverValidator; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidationService; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolverValidator; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.DurationMetadataResolverValidator; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.DynamicHttpMetadataResolverValidator; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.FileBackedHttpMetadataResolverValidator; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.MetadataResolverValidationService; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.ResourceBackedIMetadataResolverValidator; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -12,19 +16,26 @@ @Configuration public class MetadataResolverValidationConfiguration { + @Bean DurationMetadataResolverValidator durationMetadataResolverValidator() { + return new DurationMetadataResolverValidator(); + } + + @Bean DynamicHttpMetadataResolverValidator dynamicHttpMetadataResolverValidator(IGroupService groupService, UserService userService) { + return new DynamicHttpMetadataResolverValidator(groupService, userService); + } + @Bean - ResourceBackedMetadataResolverValidator resourceBackedMetadataResolverValidator() { - return new ResourceBackedMetadataResolverValidator(); + FileBackedHttpMetadataResolverValidator fileBackedHttpMetadataResolverValidator(IGroupService groupService, UserService userService) { + return new FileBackedHttpMetadataResolverValidator(groupService, userService); } @Bean @SuppressWarnings("Unchecked") - MetadataResolverValidationService metadataResolverValidationService(List metadataResolverValidators) { - return new MetadataResolverValidationService(metadataResolverValidators); + MetadataResolverValidationService metadataResolverValidationService(List IMetadataResolverValidators) { + return new MetadataResolverValidationService(IMetadataResolverValidators); } - @Bean - DurationMetadataResolverValidator durationMetadataResolverValidator() { - return new DurationMetadataResolverValidator(); + @Bean ResourceBackedIMetadataResolverValidator resourceBackedMetadataResolverValidator() { + return new ResourceBackedIMetadataResolverValidator(); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/MigrationTasksContextLoadedListener.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/MigrationTasksContextLoadedListener.java index 11ea80113..4dbe3656d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/MigrationTasksContextLoadedListener.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/MigrationTasksContextLoadedListener.java @@ -1,5 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration.auto; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; @@ -14,6 +16,8 @@ import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import java.util.List; + /** * After the context loads, do any needed migration tasks */ @@ -62,6 +66,5 @@ void doshibui_1740_migration() { userService.save(user); // this will ensure group is set as the default user group } }); - } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/DangerController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/DangerController.java index 383667fe6..b2d291dd2 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/DangerController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/DangerController.java @@ -26,10 +26,10 @@ @Slf4j public class DangerController { @Autowired - DevConfig devConfig; + private AttributeBundleRepository attributeBundleRepository; @Autowired - private AttributeBundleRepository attributeBundleRepository; + private DevConfig devConfig; @Autowired private EntityDescriptorService entityDescriptorService; @@ -45,7 +45,7 @@ public class DangerController { @Autowired private GroupsRepository groupRepository; - + @Autowired private MetadataResolverRepository metadataResolverRepository; @@ -57,7 +57,7 @@ public class DangerController { @Autowired UserRepository userRepository; - + @Transactional @GetMapping public ResponseEntity wipeOut() { 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 99eb079f6..3122eb90e 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 @@ -2,14 +2,14 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidPatternMatchException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionService; import lombok.extern.slf4j.Slf4j; - import org.opensaml.core.xml.io.MarshallingException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.client.RestTemplateBuilder; @@ -29,7 +29,6 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import javax.annotation.PostConstruct; - import java.net.URI; import java.util.ConcurrentModificationException; @@ -64,8 +63,8 @@ public EntityDescriptorController(EntityDescriptorVersionService versionService) @PostMapping("/EntityDescriptor") @Transactional - public ResponseEntity create(@RequestBody EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, - ObjectIdExistsException { + public ResponseEntity create(@RequestBody EntityDescriptorRepresentation edRepresentation) + throws ForbiddenException, ObjectIdExistsException, InvalidPatternMatchException { EntityDescriptorRepresentation persistedEd = entityDescriptorService.createNew(edRepresentation); return ResponseEntity.created(getResourceUriFor(persistedEd.getId())).body(persistedEd); } @@ -115,11 +114,11 @@ public ResponseEntity getOneXml(@PathVariable String resourceId) throws Marsh } @GetMapping("/EntityDescriptor/{resourceId}/Versions/{versionId}") - @Transactional public ResponseEntity getSpecificVersion(@PathVariable String resourceId, @PathVariable String versionId) throws EntityNotFoundException, ForbiddenException { // this "get by resource id" verifies that both the ED exists and the user has proper access, so needs to remain EntityDescriptor ed = entityDescriptorService.getEntityDescriptorByResourceId(resourceId); - return ResponseEntity.ok(versionService.findSpecificVersionOfEntityDescriptor(ed.getResourceId(), versionId)); + EntityDescriptorRepresentation result = versionService.findSpecificVersionOfEntityDescriptor(ed.getResourceId(), versionId); + return ResponseEntity.ok(result); } private ResponseEntity handleUploadingEntityDescriptorXml(byte[] rawXmlBytes, String spName) throws Exception { @@ -137,7 +136,9 @@ public void initRestTemplate() { @PutMapping("/EntityDescriptor/{resourceId}") @Transactional - public ResponseEntity update(@RequestBody EntityDescriptorRepresentation edRepresentation, @PathVariable String resourceId) throws ForbiddenException, ConcurrentModificationException, EntityNotFoundException { + public ResponseEntity update(@RequestBody EntityDescriptorRepresentation edRepresentation, @PathVariable String resourceId) + throws ForbiddenException, ConcurrentModificationException, EntityNotFoundException, + InvalidPatternMatchException { edRepresentation.setId(resourceId); // This should be the same already, but just to be safe... EntityDescriptorRepresentation result = entityDescriptorService.update(edRepresentation); return ResponseEntity.ok().body(result); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java index fd48f68e6..32d3cd4be 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java @@ -1,7 +1,9 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; -import java.util.ConcurrentModificationException; - +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidPatternMatchException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -10,9 +12,7 @@ import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; -import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; -import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import java.util.ConcurrentModificationException; @ControllerAdvice(assignableTypes = {EntityDescriptorController.class}) public class EntityDescriptorControllerExceptionHandler extends ResponseEntityExceptionHandler { @@ -21,17 +21,22 @@ public class EntityDescriptorControllerExceptionHandler extends ResponseEntityEx public ResponseEntity handleConcurrentModificationException(ConcurrentModificationException e, WebRequest request) { return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponse(HttpStatus.CONFLICT, e.getMessage())); } - + @ExceptionHandler({ EntityNotFoundException.class }) public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e, WebRequest request) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(HttpStatus.NOT_FOUND, e.getMessage())); } - + @ExceptionHandler({ ForbiddenException.class }) public ResponseEntity handleForbiddenAccess(ForbiddenException e, WebRequest request) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, e.getMessage())); } + @ExceptionHandler({ InvalidPatternMatchException.class }) + public ResponseEntity handleInvalidUrlMatchException(InvalidPatternMatchException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse(HttpStatus.BAD_REQUEST, e.getMessage())); + } + @ExceptionHandler({ ObjectIdExistsException.class }) public ResponseEntity handleObjectIdExistsException(ObjectIdExistsException e, WebRequest request) { HttpHeaders headers = new HttpHeaders(); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java index 33d4458b5..a9191dffc 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java @@ -1,18 +1,18 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter; -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityRoleWhiteListFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.ITargetable; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilter; -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilFilter; -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.SignatureValidationFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; - +import static java.util.stream.Collectors.toList; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.NOT_FOUND; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.interceptor.TransactionAspectSupport; @@ -36,53 +36,36 @@ import java.util.function.Supplier; import java.util.stream.Stream; -import static java.util.stream.Collectors.toList; -import static org.springframework.http.HttpStatus.NOT_FOUND; - @RestController @RequestMapping("/api/MetadataResolvers/{metadataResolverId}") public class MetadataFiltersController { + private static final Supplier HTTP_400_BAD_REQUEST_EXCEPTION = () -> new HttpClientErrorException(BAD_REQUEST); + private static final Supplier HTTP_404_CLIENT_ERROR_EXCEPTION = () -> new HttpClientErrorException(NOT_FOUND); @Autowired - private MetadataResolverRepository repository; - - @Autowired - private MetadataResolverService metadataResolverService; + org.opensaml.saml.metadata.resolver.MetadataResolver chainingMetadataResolver; @Autowired private FilterRepository filterRepository; @Autowired - org.opensaml.saml.metadata.resolver.MetadataResolver chainingMetadataResolver; - - private static final Supplier HTTP_404_CLIENT_ERROR_EXCEPTION = () -> new HttpClientErrorException(NOT_FOUND); + private IGroupService groupService; - @ExceptionHandler - public ResponseEntity notFoundHandler(HttpClientErrorException ex) { - if (ex.getStatusCode() == NOT_FOUND) { - return ResponseEntity.notFound().build(); - } - throw ex; - } + @Autowired + private MetadataResolverService metadataResolverService; - @GetMapping("/Filters") - @Transactional(readOnly = true) - public ResponseEntity getAll(@PathVariable String metadataResolverId) { - MetadataResolver resolver = findResolverOrThrowHttp404(metadataResolverId); - return ResponseEntity.ok(resolver.getMetadataFilters()); - } + @Autowired + private MetadataResolverRepository repository; - @GetMapping("/Filters/{resourceId}") - @Transactional(readOnly = true) - public ResponseEntity getOne(@PathVariable String metadataResolverId, @PathVariable String resourceId) { - MetadataResolver resolver = findResolverOrThrowHttp404(metadataResolverId); - return ResponseEntity.ok(findFilterOrThrowHttp404(resourceId)); - } + @Autowired + private UserService userService; @PostMapping("/Filters") @Transactional public ResponseEntity create(@PathVariable String metadataResolverId, @RequestBody MetadataFilter createdFilter) { MetadataResolver metadataResolver = findResolverOrThrowHttp404(metadataResolverId); + validateFilterOrThrowHttp400(createdFilter); + metadataResolver.addFilter(createdFilter); MetadataResolver persistedMr = repository.save(metadataResolver); @@ -92,57 +75,14 @@ public ResponseEntity create(@PathVariable String metadataResolverId, @Reques MetadataFilter persistedFilter = newlyPersistedFilter(persistedMr.getMetadataFilters().stream(), createdFilter.getResourceId()); return ResponseEntity - .created(getResourceUriFor(persistedMr, createdFilter.getResourceId())) - .body(persistedFilter); - } - - @PutMapping("/Filters/{resourceId}") - @Transactional - public ResponseEntity update(@PathVariable String metadataResolverId, - @PathVariable String resourceId, - @RequestBody MetadataFilter updatedFilter) { - - MetadataResolver metadataResolver = findResolverOrThrowHttp404(metadataResolverId); - - //Now we operate directly on the filter attached to MetadataResolver, - //Instead of fetching filter separately, to accommodate correct envers versioning with uni-directional one-to-many - Optional filterTobeUpdatedOptional = metadataResolver.getMetadataFilters() - .stream() - .filter(it -> it.getResourceId().equals(resourceId)) - .findFirst(); - if (!filterTobeUpdatedOptional.isPresent()) { - return ResponseEntity.notFound().build(); - } - MetadataFilter filterTobeUpdated = filterTobeUpdatedOptional.get(); - if (!resourceId.equals(updatedFilter.getResourceId())) { - return new ResponseEntity(HttpStatus.CONFLICT); - } - - // Verify we're the only one attempting to update the filter - if (updatedFilter.getVersion() != filterTobeUpdated.getVersion()) { - return new ResponseEntity(HttpStatus.CONFLICT); - } - - filterTobeUpdated.setName(updatedFilter.getName()); - filterTobeUpdated.setFilterEnabled(updatedFilter.isFilterEnabled()); - updatedFilter.updateConcreteFilterTypeData(filterTobeUpdated); - - MetadataFilter persistedFilter = filterRepository.save(filterTobeUpdated); - - //To support envers versioning from MetadataResolver side - metadataResolver.markAsModified(); - repository.save(metadataResolver); - - // TODO: do we need to reload filters here? - reloadFiltersAndHandleScriptException(metadataResolver.getResourceId()); - - return ResponseEntity.ok().body(persistedFilter); + .created(getResourceUriFor(persistedMr, createdFilter.getResourceId())) + .body(persistedFilter); } @DeleteMapping("/Filters/{resourceId}") @Transactional public ResponseEntity delete(@PathVariable String metadataResolverId, - @PathVariable String resourceId) { + @PathVariable String resourceId) { MetadataResolver resolver = findResolverOrThrowHttp404(metadataResolverId); MetadataFilter filterToDelete = findFilterOrThrowHttp404(resourceId); @@ -168,6 +108,66 @@ public ResponseEntity delete(@PathVariable String metadataResolverId, return ResponseEntity.noContent().build(); } + private MetadataFilter findFilterOrThrowHttp404(String filterResourceId) { + MetadataFilter filter = filterRepository.findByResourceId(filterResourceId); + if (filter == null) { + throw HTTP_404_CLIENT_ERROR_EXCEPTION.get(); + } + return filter; + } + + private MetadataResolver findResolverOrThrowHttp404(String resolverResourceId) { + MetadataResolver resolver = repository.findByResourceId(resolverResourceId); + if (resolver == null) { + throw HTTP_404_CLIENT_ERROR_EXCEPTION.get(); + } + return resolver; + } + + @GetMapping("/Filters") + @Transactional(readOnly = true) + public ResponseEntity getAll(@PathVariable String metadataResolverId) { + MetadataResolver resolver = findResolverOrThrowHttp404(metadataResolverId); + return ResponseEntity.ok(resolver.getMetadataFilters()); + } + + @GetMapping("/Filters/{resourceId}") + @Transactional(readOnly = true) + public ResponseEntity getOne(@PathVariable String metadataResolverId, @PathVariable String resourceId) { + findResolverOrThrowHttp404(metadataResolverId); + return ResponseEntity.ok(findFilterOrThrowHttp404(resourceId)); + } + + private static URI getResourceUriFor(MetadataResolver mr, String filterResourceId) { + return ServletUriComponentsBuilder + .fromCurrentServletMapping().path("/api/MetadataResolvers/") + .pathSegment(mr.getResourceId()) + .pathSegment("Filters") + .pathSegment(filterResourceId) + .build() + .toUri(); + } + + private MetadataFilter newlyPersistedFilter(Stream filters, final String filterResourceId) { + MetadataFilter persistedFilter = filters + .filter(f -> f.getResourceId().equals(filterResourceId)) + .collect(toList()).get(0); + + return persistedFilter; + } + + @ExceptionHandler + public ResponseEntity notFoundHandler(HttpClientErrorException ex) { + switch (ex.getStatusCode()) { + case NOT_FOUND: + return ResponseEntity.notFound().build(); + case BAD_REQUEST: + return ResponseEntity.badRequest().build(); + default: + throw ex; + } + } + private void reloadFiltersAndHandleScriptException(String resolverResourceId) { try { metadataResolverService.reloadFilters(resolverResourceId); @@ -182,37 +182,65 @@ private void reloadFiltersAndHandleScriptException(String resolverResourceId) { } } - private MetadataResolver findResolverOrThrowHttp404(String resolverResourceId) { - MetadataResolver resolver = repository.findByResourceId(resolverResourceId); - if (resolver == null) { - throw HTTP_404_CLIENT_ERROR_EXCEPTION.get(); + @PutMapping("/Filters/{resourceId}") + @Transactional + public ResponseEntity update(@PathVariable String metadataResolverId, + @PathVariable String resourceId, + @RequestBody MetadataFilter updatedFilter) { + + MetadataResolver metadataResolver = findResolverOrThrowHttp404(metadataResolverId); + + //Now we operate directly on the filter attached to MetadataResolver, + //Instead of fetching filter separately, to accommodate correct envers versioning with uni-directional one-to-many + Optional filterTobeUpdatedOptional = metadataResolver.getMetadataFilters() + .stream() + .filter(it -> it.getResourceId().equals(resourceId)) + .findFirst(); + if (filterTobeUpdatedOptional.isEmpty()) { + return ResponseEntity.notFound().build(); + } + MetadataFilter filterTobeUpdated = filterTobeUpdatedOptional.get(); + if (!resourceId.equals(updatedFilter.getResourceId())) { + return new ResponseEntity(HttpStatus.CONFLICT); } - return resolver; - } - private MetadataFilter findFilterOrThrowHttp404(String filterResourceId) { - MetadataFilter filter = filterRepository.findByResourceId(filterResourceId); - if (filter == null) { - throw HTTP_404_CLIENT_ERROR_EXCEPTION.get(); + // Verify we're the only one attempting to update the filter + if (updatedFilter.getVersion() != filterTobeUpdated.getVersion()) { + return new ResponseEntity(HttpStatus.CONFLICT); } - return filter; - } - private MetadataFilter newlyPersistedFilter(Stream filters, final String filterResourceId) { - MetadataFilter persistedFilter = filters - .filter(f -> f.getResourceId().equals(filterResourceId)) - .collect(toList()).get(0); + // perform validation if necessary on the entity ids (if the filter is the right configuration to need such a check) + validateFilterOrThrowHttp400(updatedFilter); - return persistedFilter; + filterTobeUpdated.setName(updatedFilter.getName()); + filterTobeUpdated.setFilterEnabled(updatedFilter.isFilterEnabled()); + updatedFilter.updateConcreteFilterTypeData(filterTobeUpdated); + + MetadataFilter persistedFilter = filterRepository.save(filterTobeUpdated); + + //To support envers versioning from MetadataResolver side + metadataResolver.markAsModified(); + repository.save(metadataResolver); + + // TODO: do we need to reload filters here? + reloadFiltersAndHandleScriptException(metadataResolver.getResourceId()); + + return ResponseEntity.ok().body(persistedFilter); } - private static URI getResourceUriFor(MetadataResolver mr, String filterResourceId) { - return ServletUriComponentsBuilder - .fromCurrentServletMapping().path("/api/MetadataResolvers/") - .pathSegment(mr.getResourceId()) - .pathSegment("Filters") - .pathSegment(filterResourceId) - .build() - .toUri(); + /** + * If the filter is "Targetable" AND the target is "ENTITY" THEN check each of the values (which are entityIds) + */ + private void validateFilterOrThrowHttp400(MetadataFilter createdFilter) { + if (createdFilter instanceof ITargetable){ + ITargetable filter = (ITargetable) createdFilter; + if (filter.getTarget() != null && "ENTITY".equals(filter.getTarget().getTargetTypeValue())) { + for (String entityId : filter.getTarget().getValue()) { + if (!groupService.doesStringMatchGroupPattern(userService.getCurrentUser().getGroupId(), entityId)) { + throw HTTP_400_BAD_REQUEST_EXCEPTION.get(); + } + } + } + } } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java index 6bce7af7b..d8e09ff56 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidationService; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.MetadataResolverValidationService; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.domain.versioning.Version; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; @@ -43,7 +43,7 @@ import java.net.URI; import java.util.List; -import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator.ValidationResult; +import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator.ValidationResult; @RestController @RequestMapping("/api") @@ -151,7 +151,7 @@ public ResponseEntity update(@PathVariable String resourceId, @RequestBody Me MetadataResolver persistedResolver = resolverRepository.save(updatedResolver); doResolverInitialization(persistedResolver); - return ResponseEntity.ok(persistedResolver); + return ResponseEntity.ok(resolverRepository.findByResourceId(resourceId)); } //Versioning endpoints @@ -212,4 +212,4 @@ private void doResolverInitialization(MetadataResolver persistedResolver) throws OpenSamlChainingMetadataResolverUtil.updateChainingMetadataResolver((OpenSamlChainingMetadataResolver) chainingMetadataResolver, openSamlRepresentation); } } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java index 7643f483d..d80b68242 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java @@ -2,6 +2,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; +import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getAttributeListFromAttributeReleaseList; +import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getAttributeListFromRelyingPartyOverridesRepresentation; +import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getAttributeReleaseListFromAttributeList; +import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getRelyingPartyOverridesRepresentationFromAttributeList; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -9,43 +13,27 @@ import org.hibernate.envers.Audited; import javax.persistence.CascadeType; -import javax.persistence.CollectionTable; -import javax.persistence.Column; import javax.persistence.Entity; -import javax.persistence.JoinColumn; +import javax.persistence.FetchType; import javax.persistence.OneToMany; import javax.persistence.OneToOne; import javax.persistence.OrderColumn; import javax.persistence.PostLoad; import javax.persistence.Transient; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; - -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getAttributeListFromAttributeReleaseList; -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getAttributeListFromRelyingPartyOverridesRepresentation; -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getAttributeReleaseListFromAttributeList; -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getRelyingPartyOverridesRepresentationFromAttributeList; @Entity -@EqualsAndHashCode(callSuper = true, exclude={"attributeRelease", "relyingPartyOverrides"}) +@EqualsAndHashCode(callSuper = true, exclude = { "attributeRelease", "relyingPartyOverrides" }) @Getter @Setter @ToString @Audited -public class EntityAttributesFilter extends MetadataFilter { +public class EntityAttributesFilter extends MetadataFilter implements ITargetable { private static final long serialVersionUID = 1L; - public EntityAttributesFilter() { - type = "EntityAttributes"; - } - - @OneToOne(cascade = CascadeType.ALL) - private EntityAttributesFilterTarget entityAttributesFilterTarget; - @OneToMany(cascade = CascadeType.ALL) @OrderColumn @JsonIgnore @@ -53,25 +41,21 @@ public EntityAttributesFilter() { @Transient private List attributeRelease = new ArrayList<>(); - - public void setAttributeRelease(List attributeRelease) { - this.attributeRelease = attributeRelease; - this.rebuildAttributes(); - } + + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER) + private EntityAttributesFilterTarget entityAttributesFilterTarget; @Transient private Map relyingPartyOverrides; - public void setRelyingPartyOverrides(Map relyingPartyOverridesRepresentation) { - this.relyingPartyOverrides = relyingPartyOverridesRepresentation; - this.rebuildAttributes(); + public EntityAttributesFilter() { + type = "EntityAttributes"; } - //TODO: yeah, I'm not too happy, either - private void rebuildAttributes() { - this.attributes.clear(); - this.attributes.addAll((List) (List)getAttributeListFromAttributeReleaseList(this.attributeRelease)); - this.attributes.addAll((List) (List)getAttributeListFromRelyingPartyOverridesRepresentation(this.relyingPartyOverrides)); + @Override + @JsonIgnore + public IFilterTarget getTarget() { + return entityAttributesFilterTarget; } @PostLoad @@ -82,6 +66,23 @@ public void intoTransientRepresentation() { this.relyingPartyOverrides = getRelyingPartyOverridesRepresentationFromAttributeList(this.attributes); } + //TODO: yeah, I'm not too happy, either + private void rebuildAttributes() { + this.attributes.clear(); + this.attributes.addAll((List) (List) getAttributeListFromAttributeReleaseList(this.attributeRelease)); + this.attributes.addAll((List) (List) getAttributeListFromRelyingPartyOverridesRepresentation(this.relyingPartyOverrides)); + } + + public void setAttributeRelease(List attributeRelease) { + this.attributeRelease = attributeRelease; + this.rebuildAttributes(); + } + + public void setRelyingPartyOverrides(Map relyingPartyOverridesRepresentation) { + this.relyingPartyOverrides = relyingPartyOverridesRepresentation; + this.rebuildAttributes(); + } + private EntityAttributesFilter updateConcreteFilterTypeData(EntityAttributesFilter filterToBeUpdated) { filterToBeUpdated.setEntityAttributesFilterTarget(getEntityAttributesFilterTarget()); filterToBeUpdated.setRelyingPartyOverrides(getRelyingPartyOverrides()); @@ -89,8 +90,7 @@ private EntityAttributesFilter updateConcreteFilterTypeData(EntityAttributesFilt return filterToBeUpdated; } - @Override - public MetadataFilter updateConcreteFilterTypeData(MetadataFilter filterToBeUpdated) { + @Override public MetadataFilter updateConcreteFilterTypeData(MetadataFilter filterToBeUpdated) { return updateConcreteFilterTypeData((EntityAttributesFilter) filterToBeUpdated); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTarget.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTarget.java index 0a0da096f..a7959dbbf 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTarget.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTarget.java @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.filters; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; import lombok.EqualsAndHashCode; @@ -9,6 +10,7 @@ import javax.persistence.Column; import javax.persistence.ElementCollection; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.OrderColumn; import java.util.ArrayList; import java.util.List; @@ -18,14 +20,11 @@ @Audited @AuditOverride(forClass = AbstractAuditable.class) @JsonIgnoreProperties({"handler", "hibernateLazyInitializer"}) -public class EntityAttributesFilterTarget extends AbstractAuditable { - public enum EntityAttributesFilterTargetType { - ENTITY, CONDITION_SCRIPT, CONDITION_REF, REGEX - } +public class EntityAttributesFilterTarget extends AbstractAuditable implements IFilterTarget { private EntityAttributesFilterTargetType entityAttributesFilterTargetType; - @ElementCollection + @ElementCollection (fetch = FetchType.EAGER) @OrderColumn @Column(length = 760) private List value; @@ -34,12 +33,19 @@ public EntityAttributesFilterTargetType getEntityAttributesFilterTargetType() { return entityAttributesFilterTargetType; } - public void setEntityAttributesFilterTargetType(EntityAttributesFilterTargetType entityAttributesFilterTarget) { - this.entityAttributesFilterTargetType = entityAttributesFilterTarget; + @Override + @JsonIgnore + public String getTargetTypeValue() { + return entityAttributesFilterTargetType == null ? "NONE" : entityAttributesFilterTargetType.name(); } + @Override public List getValue() { - return value; + return value == null ? new ArrayList<>() : value; + } + + public void setEntityAttributesFilterTargetType(EntityAttributesFilterTargetType entityAttributesFilterTarget) { + this.entityAttributesFilterTargetType = entityAttributesFilterTarget; } public void setSingleValue(String value) { @@ -59,4 +65,8 @@ public String toString() { ", value=" + value + '}'; } -} + + public enum EntityAttributesFilterTargetType { + ENTITY, CONDITION_SCRIPT, CONDITION_REF, REGEX + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/IFilterTarget.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/IFilterTarget.java new file mode 100644 index 000000000..d0a2683b1 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/IFilterTarget.java @@ -0,0 +1,13 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.filters; + +import java.util.List; + +public interface IFilterTarget { + String getTargetTypeValue(); + + List getValue(); + + void setSingleValue(String value); + + void setValue(List value); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/ITargetable.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/ITargetable.java new file mode 100644 index 000000000..c46ff1c7f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/ITargetable.java @@ -0,0 +1,5 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.filters; + +public interface ITargetable { + public IFilterTarget getTarget(); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilter.java index 47bb0810b..76c3cebae 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilter.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilter.java @@ -1,19 +1,18 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.filters; -import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.envers.Audited; import javax.persistence.CascadeType; import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.OneToOne; import javax.persistence.OrderColumn; - -import org.hibernate.envers.Audited; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; +import java.util.List; @Entity @EqualsAndHashCode(callSuper = true) @@ -21,7 +20,7 @@ @Setter @ToString @Audited -public class NameIdFormatFilter extends MetadataFilter { +public class NameIdFormatFilter extends MetadataFilter implements ITargetable { public NameIdFormatFilter() { type = "NameIDFormat"; @@ -36,6 +35,12 @@ public NameIdFormatFilter() { @OneToOne(cascade = CascadeType.ALL) private NameIdFormatFilterTarget nameIdFormatFilterTarget; + @Override + @JsonIgnore + public IFilterTarget getTarget() { + return nameIdFormatFilterTarget; + } + private NameIdFormatFilter updateConcreteFilterTypeData(NameIdFormatFilter filterToBeUpdated) { filterToBeUpdated.setRemoveExistingFormats(getRemoveExistingFormats()); filterToBeUpdated.setFormats(getFormats()); @@ -47,5 +52,4 @@ private NameIdFormatFilter updateConcreteFilterTypeData(NameIdFormatFilter filte public MetadataFilter updateConcreteFilterTypeData(MetadataFilter filterToBeUpdated) { return updateConcreteFilterTypeData((NameIdFormatFilter) filterToBeUpdated); } - -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilterTarget.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilterTarget.java index eef906500..90445ff75 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilterTarget.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilterTarget.java @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.filters; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; import lombok.EqualsAndHashCode; @@ -10,6 +11,7 @@ import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.OrderColumn; +import javax.persistence.Transient; import java.util.ArrayList; import java.util.List; @@ -19,30 +21,32 @@ @Audited @AuditOverride(forClass = AbstractAuditable.class) @JsonIgnoreProperties({"handler", "hibernateLazyInitializer"}) -public class NameIdFormatFilterTarget extends AbstractAuditable { - - public enum NameIdFormatFilterTargetType { - ENTITY, CONDITION_SCRIPT, REGEX - } +public class NameIdFormatFilterTarget extends AbstractAuditable implements IFilterTarget { private NameIdFormatFilterTargetType nameIdFormatFilterTargetType; + @ElementCollection + @OrderColumn + private List value; + public NameIdFormatFilterTargetType getNameIdFormatFilterTargetType() { return nameIdFormatFilterTargetType; } - public void setNameIdFormatFilterTargetType(NameIdFormatFilterTargetType nameIdFormatFilterTargetType) { - this.nameIdFormatFilterTargetType = nameIdFormatFilterTargetType; + @Override + @JsonIgnore + public String getTargetTypeValue() { + return nameIdFormatFilterTargetType.name(); } - @ElementCollection - @OrderColumn - private List value; - public List getValue() { return value; } + public void setNameIdFormatFilterTargetType(NameIdFormatFilterTargetType nameIdFormatFilterTargetType) { + this.nameIdFormatFilterTargetType = nameIdFormatFilterTargetType; + } + public void setSingleValue(String value) { List values = new ArrayList<>(); values.add(value); @@ -53,5 +57,7 @@ public void setValue(List value) { this.value = value; } - -} + public enum NameIdFormatFilterTargetType { + ENTITY, CONDITION_SCRIPT, REGEX + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/SvnMetadataResource.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/SvnMetadataResource.java index 06f039bc6..7e34fe8c8 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/SvnMetadataResource.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/SvnMetadataResource.java @@ -16,19 +16,21 @@ @EqualsAndHashCode public class SvnMetadataResource { - private String repositoryURL; + private String password; - private String workingCopyDirectory; + private String proxyHost; - private String resourceFile; + private String proxyPassword; - private String username; + private String proxyPort; - private String password; + private String proxyUserName; - private String proxyHost; + private String repositoryURL; - private String proxyUserName; + private String resourceFile; - private String proxyPassword; -} + private String username; + + private String workingCopyDirectory; +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidator.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidator.java new file mode 100644 index 000000000..2b14ded1e --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidator.java @@ -0,0 +1,33 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; + +public class DynamicHttpMetadataResolverValidator implements IMetadataResolverValidator { + @Autowired + private IGroupService groupService; + + @Autowired + private UserService userService; + + public DynamicHttpMetadataResolverValidator(IGroupService groupService, UserService userService) { + this.groupService = groupService; + this.userService = userService; + } + + @Override public boolean supports(MetadataResolver resolver) { return resolver instanceof DynamicHttpMetadataResolver; } + + @Override public ValidationResult validate(MetadataResolver resolver) { + DynamicHttpMetadataResolver dynamicResolver = (DynamicHttpMetadataResolver) resolver; + if ("MetadataQueryProtocol".equals(dynamicResolver.getMetadataRequestURLConstructionScheme().getType())) { + String url = dynamicResolver.getMetadataRequestURLConstructionScheme().getContent(); + if (!groupService.doesStringMatchGroupPattern(userService.getCurrentUser().getGroupId(), url)) { + return new ValidationResult("Metadata Query Protocol URL not acceptable for user's group"); + } + } + return new ValidationResult(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidator.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidator.java new file mode 100644 index 000000000..e387e9bea --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidator.java @@ -0,0 +1,31 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; + +public class FileBackedHttpMetadataResolverValidator implements IMetadataResolverValidator { + @Autowired + private IGroupService groupService; + + @Autowired + private UserService userService; + + public FileBackedHttpMetadataResolverValidator(IGroupService groupService, UserService userService) { + this.groupService = groupService; + this.userService = userService; + } + + @Override public boolean supports(MetadataResolver resolver) { return resolver instanceof FileBackedHttpMetadataResolver; } + + @Override public ValidationResult validate(MetadataResolver resolver) { + FileBackedHttpMetadataResolver fbhmResolver = (FileBackedHttpMetadataResolver) resolver; + String url = fbhmResolver.getMetadataURL(); + if (!groupService.doesStringMatchGroupPattern(userService.getCurrentUser().getGroupId(), url)) { + return new ValidationResult("Metadata URL not acceptable for user's group"); + } + return new ValidationResult(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidator.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/IMetadataResolverValidator.java similarity index 82% rename from backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidator.java rename to backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/IMetadataResolverValidator.java index a57bc6f18..33ceefa00 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidator.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/IMetadataResolverValidator.java @@ -1,4 +1,6 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; import java.util.ArrayList; import java.util.List; @@ -12,7 +14,7 @@ * * @author Dmitriy Kopylenko */ -public interface MetadataResolverValidator { +public interface IMetadataResolverValidator { boolean supports(MetadataResolver resolver); @@ -38,4 +40,4 @@ public boolean isValid() { return this.errorMessages == null || this.errorMessages.isEmpty(); } } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationService.java similarity index 59% rename from backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationService.java rename to backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationService.java index 676755b26..3e9e3df51 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationService.java @@ -1,13 +1,15 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator.ValidationResult; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator.ValidationResult; import java.util.ArrayList; import java.util.List; /** - * A facade that aggregates {@link MetadataResolverValidator}s available to call just one of them supporting the type of a given resolver. - * If no {@link MetadataResolverValidator}s are configured, considers provided MetadataResolver as valid. + * A facade that aggregates {@link IMetadataResolverValidator}s available to call just one of them supporting the type of a given resolver. + * If no {@link IMetadataResolverValidator}s are configured, considers provided MetadataResolver as valid. *

* Uses chain-of-responsibility design pattern * @@ -15,9 +17,9 @@ */ public class MetadataResolverValidationService { - private List> validators; + List> validators; - public MetadataResolverValidationService(List> validators) { + public MetadataResolverValidationService(List> validators) { this.validators = validators != null ? validators : new ArrayList<>(); } @@ -36,4 +38,4 @@ public ValidationResult validateIfNecessary(T metadataResolver) { boolean noValidatorsConfigured() { return this.validators.size() == 0; } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataResolverValidator.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/ResourceBackedIMetadataResolverValidator.java similarity index 57% rename from backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataResolverValidator.java rename to backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/ResourceBackedIMetadataResolverValidator.java index 480491465..6b4c7ba6d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataResolverValidator.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/ResourceBackedIMetadataResolverValidator.java @@ -1,6 +1,9 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator; -public class ResourceBackedMetadataResolverValidator implements MetadataResolverValidator { +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver; + +public class ResourceBackedIMetadataResolverValidator implements IMetadataResolverValidator { @Override public boolean supports(MetadataResolver resolver) { @@ -17,4 +20,4 @@ public ValidationResult validate(ResourceBackedMetadataResolver resolver) { } return new ValidationResult(); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidPatternMatchException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidPatternMatchException.java new file mode 100644 index 000000000..57cdfcd42 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidPatternMatchException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class InvalidPatternMatchException extends Exception { + public InvalidPatternMatchException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupController.java index c0fc0a8ea..bcb1b047c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupController.java @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.security.controller; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -32,7 +33,7 @@ public class GroupController { @Secured("ROLE_ADMIN") @PostMapping @Transactional - public ResponseEntity create(@RequestBody Group group) throws GroupExistsConflictException { + public ResponseEntity create(@RequestBody Group group) throws GroupExistsConflictException, InvalidGroupRegexException { Group result = groupService.createGroup(group); return ResponseEntity.status(HttpStatus.CREATED).body(result); } @@ -64,8 +65,8 @@ public ResponseEntity getOne(@PathVariable String resourceId) throws EntityNo @Secured("ROLE_ADMIN") @PutMapping @Transactional - public ResponseEntity update(@RequestBody Group group) throws EntityNotFoundException { + public ResponseEntity update(@RequestBody Group group) throws EntityNotFoundException, InvalidGroupRegexException { Group result = groupService.updateGroup(group); return ResponseEntity.ok(result); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupControllerExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupControllerExceptionHandler.java index e8bb3b4af..39778e21a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupControllerExceptionHandler.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupControllerExceptionHandler.java @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.security.controller; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -43,4 +44,14 @@ public ResponseEntity handleGroupExistsConflict(GroupExistsConflictException return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).headers(headers) .body(new ErrorResponse(String.valueOf(HttpStatus.METHOD_NOT_ALLOWED.value()), e.getMessage())); } -} + + @ExceptionHandler({ InvalidGroupRegexException.class }) + public ResponseEntity handleInvalidGroupRegexException(InvalidGroupRegexException e, WebRequest request) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/admin/groups").build().toUri()); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).headers(headers) + .body(new ErrorResponse(String.valueOf(HttpStatus.BAD_REQUEST.value()), e.getMessage())); + } + +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/InvalidGroupRegexException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/InvalidGroupRegexException.java new file mode 100644 index 000000000..ae9a034d1 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/InvalidGroupRegexException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.exception; + +public class InvalidGroupRegexException extends Exception { + public InvalidGroupRegexException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java index aeded7229..c0d579c30 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java @@ -23,6 +23,8 @@ @EntityListeners(GroupUpdatedEntityListener.class) @Entity(name = "user_groups") public class Group implements Owner { + public static final String DEFAULT_REGEX = "/(?!^()$)^(.*)$/"; //everything except an empty string + @Transient @JsonIgnore public static Group ADMIN_GROUP; @@ -46,6 +48,9 @@ public class Group implements Owner { @Column(name = "resource_id") private String resourceId = UUID.randomUUID().toString(); + @Column(name = "validation_regex") + private String validationRegex; + /** * Define a Group object based on the user */ diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/User.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/User.java index cb769c662..d96a18be8 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/User.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/User.java @@ -71,7 +71,6 @@ public class User extends AbstractAuditable implements Owner, Ownable { private Set roles = new HashSet<>(); @EqualsAndHashCode.Exclude - @JsonIgnore @Transient private Set userGroups = new HashSet<>(); @@ -82,12 +81,14 @@ public class User extends AbstractAuditable implements Owner, Ownable { * @return the initial implementation, while supporting a user having multiple groups in the db side, acts as if the * user can only belong to a single group */ - @JsonIgnore public Group getGroup() { return getUserGroups().isEmpty() ? null : (Group) userGroups.toArray()[0]; } public String getGroupId() { + if (getRole().equals("ROLE_ADMIN")) { + groupId = Group.ADMIN_GROUP.getResourceId(); + } if (groupId == null) { groupId = getUserGroups().isEmpty() ? null : getGroup().getResourceId(); } @@ -131,7 +132,8 @@ public Set getUserGroups() { } return userGroups; } - + + @JsonIgnore public void setGroup(Group g) { groupId = g.getResourceId(); userGroups.clear(); @@ -145,4 +147,4 @@ public void setGroups(Set groups) { public void registerLoader(ILazyLoaderHelper lazyLoaderHelper) { this.lazyLoaderHelper = lazyLoaderHelper; } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/GroupUpdatedEntityListener.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/GroupUpdatedEntityListener.java index 515adface..d477ae78c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/GroupUpdatedEntityListener.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/GroupUpdatedEntityListener.java @@ -1,31 +1,31 @@ package edu.internet2.tier.shibboleth.admin.ui.security.model.listener; -import java.util.Set; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository; +import org.springframework.beans.factory.annotation.Autowired; import javax.persistence.PostLoad; import javax.persistence.PostPersist; +import javax.persistence.PostRemove; import javax.persistence.PostUpdate; - -import org.springframework.beans.factory.annotation.Autowired; - -import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; -import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership; -import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository; +import java.util.Set; public class GroupUpdatedEntityListener implements ILazyLoaderHelper { private static OwnershipRepository ownershipRepository; /** - * see https://stackoverflow.com/questions/12155632/injecting-a-spring-dependency-into-a-jpa-entitylistener + * @see https://stackoverflow.com/questions/12155632/injecting-a-spring-dependency-into-a-jpa-entitylistener */ @Autowired - public void init(OwnershipRepository repo) { + public static void init(OwnershipRepository repo) { GroupUpdatedEntityListener.ownershipRepository = repo; } @PostPersist @PostUpdate @PostLoad + @PostRemove public synchronized void groupSavedOrFetched(Group group) { // Because of the JPA spec, the listener can't do queries in the callback, so we force lazy loading through // another callback to this at the time that the owned items are needed @@ -34,9 +34,8 @@ public synchronized void groupSavedOrFetched(Group group) { @Override public void loadOwnedItems(Group group) { - group.registerLoader(null); // once loaded, remove the helper from the group Set ownedItems = ownershipRepository.findAllByOwner(group); - group.setOwnedItems(ownedItems); + group.setOwnedItems(ownedItems); } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/UserUpdatedEntityListener.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/UserUpdatedEntityListener.java index 2adbde700..9e01c41d9 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/UserUpdatedEntityListener.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/UserUpdatedEntityListener.java @@ -1,31 +1,28 @@ package edu.internet2.tier.shibboleth.admin.ui.security.model.listener; -import java.util.HashSet; -import java.util.Set; - -import javax.persistence.PostLoad; -import javax.persistence.PostPersist; -import javax.persistence.PostUpdate; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; - import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership; import edu.internet2.tier.shibboleth.admin.ui.security.model.User; import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository; import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.persistence.PostLoad; +import javax.persistence.PostPersist; +import javax.persistence.PostRemove; +import javax.persistence.PostUpdate; +import java.util.HashSet; +import java.util.Set; public class UserUpdatedEntityListener implements ILazyLoaderHelper { private static GroupsRepository groupRepository; private static OwnershipRepository ownershipRepository; /** - * see https://stackoverflow.com/questions/12155632/injecting-a-spring-dependency-into-a-jpa-entitylistener + * @see https://stackoverflow.com/questions/12155632/injecting-a-spring-dependency-into-a-jpa-entitylistener */ @Autowired - public void init(OwnershipRepository repo, GroupsRepository groupRepo) { + public static void init(OwnershipRepository repo, GroupsRepository groupRepo) { UserUpdatedEntityListener.ownershipRepository = repo; UserUpdatedEntityListener.groupRepository = groupRepo; } @@ -33,18 +30,26 @@ public void init(OwnershipRepository repo, GroupsRepository groupRepo) { @PostPersist @PostUpdate @PostLoad - @Transactional(propagation = Propagation.REQUIRES_NEW) + @PostRemove public synchronized void userSavedOrFetched(User user) { // Because of the JPA spec, the listener can't do queries in the callback, so we force lazy loading through // another callback to this at the time that the groups are needed user.registerLoader(this); } - + + @Override public void loadGroups(User user) { - user.setLazyLoaderHelper(null); Set ownerships = ownershipRepository.findAllGroupsForUser(user.getUsername()); HashSet groups = new HashSet<>(); - ownerships.forEach(ownership -> groups.add(groupRepository.findByResourceId(ownership.getOwnerId()))); + final boolean isAdmin = user.getRole().equals("ROLE_ADMIN"); + + // if the user is an admin, clear out the regex value so it isn't sent out to the UI + ownerships.stream().map(ownership -> groupRepository.findByResourceId(ownership.getOwnerId())).forEach(userGroup -> { + if (isAdmin) { + userGroup.setValidationRegex(null); + } + groups.add(userGroup); + }); user.setGroups(groups); } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java index 2501f007e..65ee10764 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java @@ -1,28 +1,36 @@ package edu.internet2.tier.shibboleth.admin.ui.security.service; -import java.util.List; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupDeleteException; import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException; import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository; import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository; import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; +import java.util.List; @Service @NoArgsConstructor public class GroupServiceImpl implements IGroupService { + private static final String CHECK_REGEX = "function isValid(exp){try{new RegExp(exp);return true;}catch(e){return false;}};isValid(rgx);"; + private static final String REGEX_MATCHER = "function validate(r, s){ return RegExp(r).test(s);};validate(rgx, str);"; + private final ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript"); + @Autowired protected GroupsRepository groupRepository; - + @Autowired protected OwnershipRepository ownershipRepository; - + public GroupServiceImpl(GroupsRepository repo, OwnershipRepository ownershipRepository) { this.groupRepository = repo; this.ownershipRepository = ownershipRepository; @@ -30,7 +38,7 @@ public GroupServiceImpl(GroupsRepository repo, OwnershipRepository ownershipRepo @Override @Transactional - public Group createGroup(Group group) throws GroupExistsConflictException { + public Group createGroup(Group group) throws GroupExistsConflictException, InvalidGroupRegexException { Group foundGroup = find(group.getResourceId()); // If already defined, we don't want to create a new one, nor do we want this call update the definition if (foundGroup != null) { @@ -38,6 +46,7 @@ public Group createGroup(Group group) throws GroupExistsConflictException { String.format("Call update (PUT) to modify the group with resource id: [%s] and name: [%s]", foundGroup.getResourceId(), foundGroup.getName())); } + validateGroupRegex(group); return groupRepository.save(group); } @@ -53,6 +62,34 @@ public void deleteDefinition(String resourceId) throws EntityNotFoundException, groupRepository.delete(group); } + /** + * Though the name URI is used here, any string value that we want to validate against the group's regex is accepted and checked. + * Designed usage is that this would be a URL or an entity Id (which is a URI that does not have to follow the URL conventions) + */ + @Override + public boolean doesStringMatchGroupPattern(String groupId, String uri) { + if (Group.ADMIN_GROUP.getResourceId().equals(groupId)) { + return true; + } + Group group = find(groupId); + + String regExp = group.getValidationRegex(); + if (StringUtils.isEmpty(regExp)) { + return true; + } + + engine.put("str", uri); + try { + engine.put("rgx", regExp ); + Object value = engine.eval(REGEX_MATCHER); + return Boolean.valueOf(value.toString()); + } + catch (ScriptException e) { + return false; + } + + } + @Override @Transactional public void ensureAdminGroupExists() { @@ -61,6 +98,7 @@ public void ensureAdminGroupExists() { g = new Group(); g.setName("ADMIN-GROUP"); g.setResourceId("admingroup"); + g.setValidationRegex(Group.DEFAULT_REGEX); g = groupRepository.save(g); } Group.ADMIN_GROUP = g; @@ -78,12 +116,32 @@ public List findAll() { } @Override - public Group updateGroup(Group group) throws EntityNotFoundException { + public Group updateGroup(Group group) throws EntityNotFoundException, InvalidGroupRegexException { Group g = find(group.getResourceId()); if (g == null) { throw new EntityNotFoundException(String.format("Unable to find group with resource id: [%s] and name: [%s]", group.getResourceId(), group.getName())); } + validateGroupRegex(group); return groupRepository.save(group); } + + /** + * If the regex is blank simply return + */ + private void validateGroupRegex(Group group) throws InvalidGroupRegexException { + if (StringUtils.isEmpty(group.getValidationRegex())) { + return; + } + try { + engine.put("rgx", group.getValidationRegex()); + Object value = engine.eval(CHECK_REGEX); + if (!Boolean.valueOf(value.toString())) { + throw new InvalidGroupRegexException("Invalid Regular Expression [ " + group.getValidationRegex() + " ]"); + } + } + catch (ScriptException e) { + throw new InvalidGroupRegexException("Invalid Regular Expression [ " + group.getValidationRegex() + " ]"); + } + } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IGroupService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IGroupService.java index 1c4229d84..d6e44e5ec 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IGroupService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IGroupService.java @@ -6,11 +6,12 @@ import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupDeleteException; import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException; import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; public interface IGroupService { - Group createGroup(Group group) throws GroupExistsConflictException; + Group createGroup(Group group) throws GroupExistsConflictException, InvalidGroupRegexException; void deleteDefinition(String resourceId) throws EntityNotFoundException, GroupDeleteException; @@ -20,6 +21,7 @@ public interface IGroupService { List findAll(); - Group updateGroup(Group g) throws EntityNotFoundException; + Group updateGroup(Group g) throws EntityNotFoundException, InvalidGroupRegexException; -} + boolean doesStringMatchGroupPattern(String groupId, String uri); +} \ No newline at end of file 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 304b93028..df200f482 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 @@ -4,6 +4,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.IActivatable; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException; import edu.internet2.tier.shibboleth.admin.ui.security.exception.OwnershipConflictException; import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownable; @@ -179,7 +180,8 @@ public User save(User user) { try { g = groupService.createGroup(g); } - catch (GroupExistsConflictException e) { + catch (GroupExistsConflictException | InvalidGroupRegexException e) { + // Invalid shouldn't happen for a group created this way. g = groupService.find(user.getUsername()); } } else { @@ -197,8 +199,8 @@ public User save(User user) { ownershipRepository.saveAndFlush(new Ownership(newGroup, user)); g = groupService.createGroup(newGroup); } - catch (GroupExistsConflictException e) { - // we just checked, this shouldn't happen + catch (GroupExistsConflictException | InvalidGroupRegexException e) { + // this shouldn't happen g = ug; } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java index 50f29951c..b2bf96ac3 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java @@ -3,9 +3,10 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidPatternMatchException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; import java.util.ConcurrentModificationException; import java.util.List; @@ -31,7 +32,8 @@ public interface EntityDescriptorService { * @throws ForbiddenException If user is unauthorized to perform this operation * @throws ObjectIdExistsException If any EntityDescriptor already exists with the same EntityId */ - EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, ObjectIdExistsException; + EntityDescriptorRepresentation createNew(EntityDescriptor ed) + throws ForbiddenException, ObjectIdExistsException, InvalidPatternMatchException; /** * @param edRepresentation Incoming representation to save @@ -39,8 +41,8 @@ public interface EntityDescriptorService { * @throws ForbiddenException If user is unauthorized to perform this operation * @throws ObjectIdExistsException If the entity already exists */ - EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, - ObjectIdExistsException; + EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepresentation) + throws ForbiddenException, ObjectIdExistsException, InvalidPatternMatchException; /** * Map from opensaml implementation of entity descriptor model to front-end data representation of entity descriptor @@ -94,12 +96,14 @@ EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepres Map getRelyingPartyOverridesRepresentationFromAttributeList(List attributeList); /** - * @param edRepresentation Incoming representation to save - * @return EntityDescriptorRepresentation - * @throws ForbiddenException If user is unauthorized to perform this operation - * @throws ConcurrentModificationException If the entity was already modified by another user + * @throws ForbiddenException If the user is not permitted to perform the action + * @throws EntityNotFoundException If the entity doesn't already exist in the database + * @throws ConcurrentModificationException IF the entity is being modified in another session + * @throws InvalidPatternMatchException If the entity id or the ACS location urls don't match the supplied regex */ - EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityNotFoundException, ConcurrentModificationException; + EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRepresentation) + throws ForbiddenException, EntityNotFoundException, ConcurrentModificationException, + InvalidPatternMatchException; /** * Update an instance of entity descriptor with information from the front-end representation diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java index 1b660ab9f..d1f1f0ccc 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java @@ -1,33 +1,69 @@ package edu.internet2.tier.shibboleth.admin.ui.service; -import edu.internet2.tier.shibboleth.admin.ui.domain.*; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.*; -import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; +import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributes; +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; +import edu.internet2.tier.shibboleth.admin.ui.domain.IRelyingPartyOverrideProperty; +import edu.internet2.tier.shibboleth.admin.ui.domain.KeyDescriptor; +import edu.internet2.tier.shibboleth.admin.ui.domain.UIInfo; +import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean; +import edu.internet2.tier.shibboleth.admin.ui.domain.XSInteger; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ContactRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.LogoutEndpointRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.MduiRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSsoDescriptorRepresentation; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidPatternMatchException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; 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.*; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Owner; +import edu.internet2.tier.shibboleth.admin.ui.security.model.OwnerType; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership; +import edu.internet2.tier.shibboleth.admin.ui.security.model.User; import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.setupACSs; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.setupContacts; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.setupLogout; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.setupOrganization; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.setupRelyingPartyOverrides; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.setupSPSSODescriptor; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.setupSecurity; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.setupUIInfo; import edu.internet2.tier.shibboleth.admin.util.MDDCConstants; import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; +import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getStringListOfAttributeValues; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.util.*; +import java.util.ArrayList; +import java.util.ConcurrentModificationException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; -import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.*; -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getStringListOfAttributeValues; - @Slf4j @Service public class JPAEntityDescriptorServiceImpl implements EntityDescriptorService { @Autowired private EntityDescriptorRepository entityDescriptorRepository; + @Autowired + IGroupService groupService; + @Autowired private OpenSamlObjects openSamlObjects; @@ -66,13 +102,14 @@ public EntityDescriptor createDescriptorFromRepresentation(final EntityDescripto } @Override - public EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, ObjectIdExistsException { + public EntityDescriptorRepresentation createNew(EntityDescriptor ed) + throws ForbiddenException, ObjectIdExistsException, InvalidPatternMatchException { return createNew(createRepresentationFromDescriptor(ed)); } @Override - public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) throws ForbiddenException, - ObjectIdExistsException { + public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) + throws ForbiddenException, ObjectIdExistsException, InvalidPatternMatchException { if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { throw new ForbiddenException("You do not have the permissions necessary to enable this service."); } @@ -81,14 +118,18 @@ public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation e throw new ObjectIdExistsException(edRep.getEntityId()); } + // "Create new" will use the current user's group as the owner + String ownerId = userService.getCurrentUserGroup().getOwnerId(); + edRep.setIdOfOwner(ownerId); + validateEntityIdAndACSUrls(edRep); + EntityDescriptor ed = (EntityDescriptor) createDescriptorFromRepresentation(edRep); ed.setIdOfOwner(userService.getCurrentUserGroup().getOwnerId()); - ed = entityDescriptorRepository.save(ed); ownershipRepository.deleteEntriesForOwnedObject(ed); ownershipRepository.save(new Ownership(userService.getCurrentUserGroup(), ed)); - return createRepresentationFromDescriptor(ed); + return createRepresentationFromDescriptor(entityDescriptorRepository.save(ed)); } @Override @@ -338,6 +379,10 @@ public List getAllRepresentationsBasedOnUserAcce @Override public List getAttributeReleaseListFromAttributeList(List attributeList) { + if (attributeList == null) { + return new ArrayList<>(); + } + attributeList.removeIf(Objects::isNull); return ModelRepresentationConversions.getAttributeReleaseListFromAttributeList(attributeList); } @@ -359,7 +404,8 @@ public Map getRelyingPartyOverridesRepresentationFromAttributeLi } @Override - public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRep) throws ForbiddenException, EntityNotFoundException { + public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRep) + throws ForbiddenException, EntityNotFoundException, InvalidPatternMatchException { EntityDescriptor existingEd = entityDescriptorRepository.findByResourceId(edRep.getId()); if (existingEd == null) { throw new EntityNotFoundException(String.format("The entity descriptor with entity id [%s] was not found for update.", edRep.getId())); @@ -367,6 +413,9 @@ public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRe if (edRep.isServiceEnabled() && !userService.currentUserCanEnable(existingEd)) { throw new ForbiddenException("You do not have the permissions necessary to enable this service."); } + if (StringUtils.isEmpty(edRep.getIdOfOwner())) { + edRep.setIdOfOwner(StringUtils.isNotEmpty(existingEd.getIdOfOwner()) ? existingEd.getIdOfOwner() : userService.getCurrentUserGroup().getOwnerId()); + } if (!userService.isAuthorizedFor(existingEd)) { throw new ForbiddenException(); } @@ -374,6 +423,8 @@ public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRe if (edRep.getVersion() != existingEd.hashCode()) { throw new ConcurrentModificationException(String.format("A concurrent modification has occured on entity descriptor with entity id [%s]. Please refresh and try again", edRep.getId())); } + + validateEntityIdAndACSUrls(edRep); updateDescriptorFromRepresentation(existingEd, edRep); existingEd = entityDescriptorRepository.save(existingEd); ownershipRepository.deleteEntriesForOwnedObject(existingEd); @@ -406,4 +457,21 @@ public EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String ed = entityDescriptorRepository.save(ed); return createRepresentationFromDescriptor(ed); } + + private void validateEntityIdAndACSUrls(EntityDescriptorRepresentation edRep) throws InvalidPatternMatchException { + // Check the entity id first + if (!groupService.doesStringMatchGroupPattern(edRep.getIdOfOwner(), edRep.getEntityId())) { + throw new InvalidPatternMatchException("EntityId is not a pattern match to the group"); + } + + // Check the ACS locations + if (edRep.getAssertionConsumerServices() != null && edRep.getAssertionConsumerServices().size() > 0) { + for (AssertionConsumerServiceRepresentation acs : edRep.getAssertionConsumerServices()) { + if (!groupService.doesStringMatchGroupPattern(edRep.getIdOfOwner(), acs.getLocationUrl())) { + throw new InvalidPatternMatchException( + "ACS location [ " + acs.getLocationUrl() + " ] is not a pattern match to the group"); + } + } + } + } } \ No newline at end of file diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 22433dda3..46bcc5304 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -178,6 +178,7 @@ label.default-value=Default Value label.groups-management=Groups Management label.new-group=New Group label.new-attribute=New Custom Entity Attribute +label.edit-group=Edit Group label.metadata-source=Metadata Source label.metadata-sources=Metadata Sources @@ -493,6 +494,9 @@ label.by=By label.source=Metadata Source label.provider=Metadata Provider +label.url-validation-regex=URL validation regular expression +tooltip.url-validation-regex=URL validation regular expression + label.bundle-name=Bundle name label.bundle-disp=Bundle - {name} action.add-new-bundle=Add bundle @@ -525,6 +529,8 @@ message.delete-group-body=You are requesting to delete a group. If you complete message.delete-attribute-title=Delete Attribute? message.delete-attribute-body=You are requesting to delete a custom attribute. If you complete this process the attribute will be removed. This cannot be undone. Do you wish to continue? +message.group-pattern-fail=Pattern must match group url validation pattern: {regex} + message.must-be-unique=Must be unique. message.must-be-number=Must be a number. message.name-must-be-unique=Name must be unique. diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/AbstractBaseDataJpaTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/AbstractBaseDataJpaTest.groovy new file mode 100644 index 000000000..da12d8bc2 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/AbstractBaseDataJpaTest.groovy @@ -0,0 +1,102 @@ +package edu.internet2.tier.shibboleth.admin.ui + +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.GroupUpdatedEntityListener +import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.UserUpdatedEntityListener +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository +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.GroupServiceForTesting +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.annotation.Transactional +import spock.lang.Specification + +import javax.persistence.EntityManager + +// The commented out lines show how to run the JPA tests using a file back h2 db - typically you'd switch if you want +// to access the db during testing to see what is happening in the db. Additionally, you have to use the file version of h2 +// if you want to use the reset, as the in mem version won't allow multiple different access connections to be created. +//@DataJpaTest (properties = ["spring.datasource.url=jdbc:h2:file:/tmp/myApplicationDb;AUTO_SERVER=TRUE", +// "spring.datasource.username=sa", +// "spring.datasource.password=", +// "spring.jpa.hibernate.ddl-auto=create-drop"]) +//@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@DataJpaTest +@ContextConfiguration(classes = [BaseDataJpaTestConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +abstract class AbstractBaseDataJpaTest extends Specification implements ResetsDatabaseTrait { + @Autowired + EntityManager entityManager + + @Autowired + GroupsRepository groupRepository + + @Autowired + GroupServiceForTesting groupService + + @Autowired + OwnershipRepository ownershipRepository + + @Autowired + RoleRepository roleRepository + + @Autowired + UserRepository userRepository + + @Autowired + UserService userService + + // ensure roles are in a known good state and that we have an admin user and group + @Transactional + def setup() { +// dbsetup() + groupService.clearAllForTesting() + userRepository.deleteAll() + ownershipRepository.deleteAll() + roleRepository.deleteAll() + + def roles = [new Role().with { + name = 'ROLE_ADMIN' + it + }, new Role().with { + name = 'ROLE_USER' + it + }, new Role().with { + name = 'ROLE_ENABLE' + it + }, new Role().with { + name = 'ROLE_NONE' + it + }] + roles.each { + if (roleRepository.findByName(it.name).isEmpty()) { + roleRepository.save(it) + } + } + + createAdminUser() + GroupUpdatedEntityListener.init(ownershipRepository) + UserUpdatedEntityListener.init(ownershipRepository, groupRepository) + } + + def cleanup() { + entityManager.clear() +// dbcleanup() + } + + protected createAdminUser() { + if (userRepository.findByUsername("admin").isEmpty()) { + Optional adminRole = roleRepository.findByName("ROLE_ADMIN") + User adminUser = new User(username: "admin", roles: [adminRole.get()], password: "foo") + userService.save(adminUser) + } + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/BaseDataJpaTestConfiguration.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/BaseDataJpaTestConfiguration.groovy new file mode 100644 index 000000000..3ab919759 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/BaseDataJpaTestConfiguration.groovy @@ -0,0 +1,100 @@ +package edu.internet2.tier.shibboleth.admin.ui + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.StringTrimModule +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.GroupUpdatedEntityListener +import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.UserUpdatedEntityListener +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceForTesting +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import edu.internet2.tier.shibboleth.admin.util.AttributeUtility +import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import +import org.springframework.context.annotation.Primary + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL + +@Configuration +@Import([ShibUIConfiguration.class, CustomPropertiesConfiguration.class, SearchConfiguration.class]) +@ComponentScan(basePackages=[ "edu.internet2.tier.shibboleth.admin.ui.service", "edu.internet2.tier.shibboleth.admin.ui.security.service", + "edu.internet2.tier.shibboleth.admin.ui.security.model.listener"]) +class BaseDataJpaTestConfiguration { + @Bean + AttributeUtility attributeUtility(OpenSamlObjects openSamlObjects) { + return new AttributeUtility(openSamlObjects) + } + + @Bean + @Primary + GroupServiceForTesting groupServiceForTesting(GroupsRepository groupRepo, OwnershipRepository ownershipRepository) { + GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { + it.groupRepository = groupRepo + it.ownershipRepository = ownershipRepository + return it + }) + result.ensureAdminGroupExists() + return result + } + + @Bean + GroupUpdatedEntityListener groupUpdatedEntityListener(OwnershipRepository ownershipRepository) { + GroupUpdatedEntityListener listener = new GroupUpdatedEntityListener() + listener.init(ownershipRepository) + return listener + } + + @Bean + JPAEntityServiceImpl jpaEntityService(OpenSamlObjects openSamlObjects, AttributeUtility attributeUtility, + CustomPropertiesConfiguration customPropertiesConfiguration) { + return new JPAEntityServiceImpl(openSamlObjects, attributeUtility, customPropertiesConfiguration) + } + + @Bean + ModelRepresentationConversions modelRepresentationConversions(CustomPropertiesConfiguration customPropertiesConfiguration) { + return new ModelRepresentationConversions(customPropertiesConfiguration) + } + + @Bean + ObjectMapper objectMapper() { + ObjectMapper mapper = new ObjectMapper() + mapper.enable(SerializationFeature.INDENT_OUTPUT) + mapper.setSerializationInclusion(NON_NULL) + mapper.registerModule(new JavaTimeModule()) + mapper.registerModule(new StringTrimModule()) + return mapper + } + + @Bean + @Primary + OpenSamlObjects openSamlObjects() { + OpenSamlObjects result = new OpenSamlObjects() + result.init() + return result + } + + @Bean + @Primary + TestObjectGenerator testObjectGenerator (AttributeUtility attributeUtility, CustomPropertiesConfiguration customPropertiesConfiguration) { + customPropertiesConfiguration.postConstruct() + return new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) + } + + @Bean + UserUpdatedEntityListener userUpdatedEntityListener(OwnershipRepository ownershipRepository, GroupsRepository groupRepo) { + UserUpdatedEntityListener listener = new UserUpdatedEntityListener() + listener.init(ownershipRepository, groupRepo) + return listener + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/ResetsDatabaseTrait.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/ResetsDatabaseTrait.groovy new file mode 100644 index 000000000..d99fb85e7 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/ResetsDatabaseTrait.groovy @@ -0,0 +1,34 @@ +package edu.internet2.tier.shibboleth.admin.ui + +import groovy.sql.Sql + +trait ResetsDatabaseTrait { + static final String H2_BACKUP_LOCATION = '/tmp/h2backup.sql' + + void dbsetup() { + if (new File(H2_BACKUP_LOCATION).exists()) { + print "Previous test was interrupted and did not clean up after itself. " + loadH2FromBackup() + } + else { + Sql sql = connectToSql() + sql.execute("SCRIPT TO ?", [H2_BACKUP_LOCATION]) + } + } + + void dbcleanup() { + loadH2FromBackup() + new File(H2_BACKUP_LOCATION).delete() + } + + private void loadH2FromBackup() { + println "Restoring H2 from backup location." + Sql sql = connectToSql() + sql.execute("DROP ALL OBJECTS") + sql.execute("RUNSCRIPT FROM ?", [H2_BACKUP_LOCATION]) + } + + private Sql connectToSql() { + Sql.newInstance('jdbc:h2:file:/tmp/myApplicationDb;AUTO_SERVER=TRUE', 'sa', '', 'org.h2.Driver') + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomPropertiesConfigurationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomPropertiesConfigurationTests.groovy index 74ecb9796..4113b372c 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomPropertiesConfigurationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomPropertiesConfigurationTests.groovy @@ -1,54 +1,24 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration -import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.domain.CustomEntityAttributeDefinition -import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.CustomEntityAttributeDefinitionRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository import edu.internet2.tier.shibboleth.admin.ui.service.CustomEntityAttributesDefinitionService -import edu.internet2.tier.shibboleth.admin.ui.service.CustomEntityAttributesDefinitionServiceImpl - -import javax.persistence.EntityManager - -import org.opensaml.saml.metadata.resolver.MetadataResolver import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.security.core.userdetails.UsernameNotFoundException -import org.springframework.test.context.ActiveProfiles -import org.springframework.test.context.ContextConfiguration - -import spock.lang.Specification /** * Tests for edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration */ -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, InternationalizationConfiguration, SearchConfiguration, edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -class CustomPropertiesConfigurationTests extends Specification { +class CustomPropertiesConfigurationTests extends AbstractBaseDataJpaTest { @Autowired - @Qualifier(value="customPropertiesConfiguration") CustomPropertiesConfiguration configUnderTest @Autowired CustomEntityAttributesDefinitionService ceadService @Autowired - CustomEntityAttributeDefinitionRepository repository; - - @Autowired - EntityManager entityManager + CustomEntityAttributeDefinitionRepository repository def "Updating Custom Entity Attribute Definitions will update the CustomPropertiesConfiguration automatically"() { given: 'Test defaults loaded (ie no CEADs in DB)' @@ -89,4 +59,4 @@ class CustomPropertiesConfigurationTests extends Specification { ceadService.getAllDefinitions().size() == 1 configUnderTest.getOverrides().size() == 11 } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestMetadataResolverValidationConfiguration.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestMetadataResolverValidationConfiguration.groovy index d5c2f4f29..5ccf3efea 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestMetadataResolverValidationConfiguration.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestMetadataResolverValidationConfiguration.groovy @@ -1,19 +1,16 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidationService -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolverValidator - +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.MetadataResolverValidationService +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.ResourceBackedIMetadataResolverValidator import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration - @Configuration class TestMetadataResolverValidationConfiguration { - @Bean - ResourceBackedMetadataResolverValidator resourceBackedMetadataResolverValidator() { - new ResourceBackedMetadataResolverValidator() + ResourceBackedIMetadataResolverValidator resourceBackedMetadataResolverValidator() { + new ResourceBackedIMetadataResolverValidator() } @Bean @@ -22,8 +19,8 @@ class TestMetadataResolverValidationConfiguration { } @Bean - MetadataResolverValidationService metadataResolverValidationServiceOneValidator(List metadataResolverValidators) { + MetadataResolverValidationService metadataResolverValidationService(List metadataResolverValidators) { new MetadataResolverValidationService(metadataResolverValidators) } -} +} \ No newline at end of file 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 5affa3e26..0261fceb5 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,50 +1,35 @@ 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.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository -import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl -import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import net.shibboleth.ext.spring.resource.ResourceHelper import net.shibboleth.utilities.java.support.resolver.CriteriaSet - import org.opensaml.core.criterion.EntityIdCriterion import org.opensaml.saml.metadata.resolver.impl.ResourceBackedMetadataResolver import org.spockframework.spring.SpringBean 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.HttpHeaders 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 import static org.hamcrest.Matchers.is import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +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 { +class EntitiesControllerTests extends AbstractBaseDataJpaTest { @Autowired JPAEntityDescriptorServiceImpl serviceImpl - - @Autowired - UserService userService - - def openSamlObjects = new OpenSamlObjects().with { - init() + + // Yeah, the setup here is dumb, but the order here is important and we don't need to repeat it, so leave all this alone + OpenSamlObjects openSamlObjects = new OpenSamlObjects().with { + it.init() it } @@ -56,7 +41,7 @@ class EntitiesControllerTests extends Specification { initialize() it } - + // This stub will spit out the results from the resolver instead of actually finding them in the DB @SpringBean EntityDescriptorRepository edr = Stub(EntityDescriptorRepository) { @@ -66,7 +51,7 @@ class EntitiesControllerTests extends Specification { @Subject def controller - def mockMvc + def mockMvc def setup() { controller = new EntitiesController() 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 74ca259a0..5f14adf1c 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 @@ -1,51 +1,33 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper -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.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor -import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidPatternMatchException +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException 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.Group import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership import edu.internet2.tier.shibboleth.admin.ui.security.model.Role import edu.internet2.tier.shibboleth.admin.ui.security.model.User -import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository -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.GroupServiceForTesting -import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl -import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionService import edu.internet2.tier.shibboleth.admin.ui.service.EntityService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Profile -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.test.context.support.WithMockUser -import org.springframework.test.annotation.DirtiesContext -import org.springframework.test.annotation.Rollback -import org.springframework.test.context.ActiveProfiles -import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.transaction.annotation.Transactional import org.springframework.web.client.RestTemplate -import spock.lang.Specification +import org.springframework.web.util.NestedServletException import spock.lang.Subject import javax.persistence.EntityManager @@ -53,140 +35,98 @@ import javax.persistence.EntityManager import static org.hamcrest.CoreMatchers.containsString import static org.springframework.http.MediaType.APPLICATION_JSON import static org.springframework.http.MediaType.APPLICATION_XML -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* - -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration, LocalConfig]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@DirtiesContext -@ActiveProfiles(["local"]) -class EntityDescriptorControllerTests extends 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.post +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { @Autowired EntityDescriptorRepository entityDescriptorRepository @Autowired EntityManager entityManager - + @Autowired EntityService entityService - - @Autowired - GroupServiceForTesting groupService - - @Autowired - OwnershipRepository ownershipRepository - + @Autowired - RoleRepository roleRepository + TestObjectGenerator generator @Autowired - JPAEntityDescriptorServiceImpl service - + ObjectMapper mapper + @Autowired - UserRepository userRepository - + OpenSamlObjects openSamlObjects + @Autowired - UserService userService - - RandomGenerator randomGenerator - TestObjectGenerator generator + JPAEntityDescriptorServiceImpl jpaEntityDescriptorService - def mapper - + RandomGenerator randomGenerator def mockRestTemplate = Mock(RestTemplate) - - def openSamlObjects = new OpenSamlObjects().with { - init() - it - } - def mockMvc @Subject def controller - Authentication authentication = Mock() - SecurityContext securityContext = Mock() EntityDescriptorVersionService versionService = Mock() @Transactional def setup() { - groupService.ensureAdminGroupExists() - generator = new TestObjectGenerator() - randomGenerator = new RandomGenerator() - mapper = new ObjectMapper() + openSamlObjects.init() - service.userService = userService + Group gb = new Group() + gb.setResourceId("testingGroupBBB") + gb.setName("Group BBB") + gb.setValidationRegex("^(?:https?:\\/\\/)?(?:[^.]+\\.)?shib\\.org(\\/.*)?\$") + gb = groupService.createGroup(gb) + + randomGenerator = new RandomGenerator() controller = new EntityDescriptorController(versionService) controller.openSamlObjects = openSamlObjects - controller.entityDescriptorService = service + controller.entityDescriptorService = jpaEntityDescriptorService controller.restTemplate = mockRestTemplate mockMvc = MockMvcBuilders.standaloneSetup(controller).build() - - securityContext.getAuthentication() >> authentication - SecurityContextHolder.setContext(securityContext) - - if (roleRepository.count() == 0) { - def roles = [new Role().with { - name = 'ROLE_ADMIN' - it - }, new Role().with { - name = 'ROLE_USER' - it - }, new Role().with { - name = 'ROLE_NONE' - it - }] - roles.each { - roleRepository.save(it) - } - } - - Optional adminRole = roleRepository.findByName("ROLE_ADMIN") - User adminUser = new User(username: "admin", roles: [adminRole.get()], password: "foo") - userService.save(adminUser) - + Optional userRole = roleRepository.findByName("ROLE_USER") User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") + user.setGroup(gb) userService.save(user) EntityDescriptorConversionUtils.setOpenSamlObjects(openSamlObjects) EntityDescriptorConversionUtils.setEntityService(entityService) } - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def 'DELETE as admin'() { given: - authentication.getName() >> 'admin' def entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false) entityDescriptorRepository.save(entityDescriptor) - + when: 'pre-check' entityManager.flush() - + then: entityDescriptorRepository.findAll().size() == 1 - + when: def result = mockMvc.perform(delete("/api/EntityDescriptor/uuid-1")) - + then: result.andExpect(status().isNoContent()) entityDescriptorRepository.findByResourceId("uuid-1") == null entityDescriptorRepository.findAll().size() == 0 } - - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + + @WithMockAdmin def 'GET /EntityDescriptors with empty repository as admin'() { given: - authentication.getName() >> 'admin' - def expectedEmptyListResponseBody = '[]' def expectedResponseContentType = APPLICATION_JSON def expectedHttpResponseStatus = status().isOk() @@ -200,42 +140,35 @@ class EntityDescriptorControllerTests extends Specification { .andExpect(content().json(expectedEmptyListResponseBody)) } - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def 'GET /EntityDescriptors with 1 record in repository as admin'() { given: - authentication.getName() >> 'admin' - - def entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "admingroup") + def entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "admingroup") entityDescriptorRepository.saveAndFlush(entityDescriptor) - + def expectedResponseContentType = APPLICATION_JSON def expectedHttpResponseStatus = status().isOk() - when: + when: def result = mockMvc.perform(get('/api/EntityDescriptors')) - then: + then: result.andExpect(expectedHttpResponseStatus).andExpect(content().contentType(expectedResponseContentType)) - .andExpect(jsonPath("\$.[0].id").value("uuid-1")) - .andExpect(jsonPath("\$.[0].entityId").value("eid1")) - .andExpect(jsonPath("\$.[0].serviceEnabled").value(true)) - .andExpect(jsonPath("\$.[0].idOfOwner").value("admingroup")) + .andExpect(jsonPath("\$.[0].id").value("uuid-1")) + .andExpect(jsonPath("\$.[0].entityId").value("eid1")) + .andExpect(jsonPath("\$.[0].serviceEnabled").value(true)) + .andExpect(jsonPath("\$.[0].idOfOwner").value("admingroup")) } - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def 'GET /EntityDescriptors with 2 records in repository as admin'() { given: - authentication.getName() >> 'admin' - - def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "admingroup") def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: "admingroup") - + entityDescriptorRepository.saveAndFlush(entityDescriptorOne) entityDescriptorRepository.saveAndFlush(entityDescriptorTwo) - + def expectedResponseContentType = APPLICATION_JSON def expectedHttpResponseStatus = status().isOk() @@ -253,20 +186,86 @@ class EntityDescriptorControllerTests extends Specification { .andExpect(jsonPath("\$.[1].entityId").value("eid2")) .andExpect(jsonPath("\$.[1].serviceEnabled").value(false)) .andExpect(jsonPath("\$.[1].idOfOwner").value("admingroup")) - } - - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + } + + @WithMockUser(value = "someUser", roles = ["USER"]) + def 'POST create new - entity id does not match pattern'() { + when: + def expectedEntityId = 'https://google.com/blah/blah' + EntityDescriptorRepresentation edRep = new EntityDescriptorRepresentation() + edRep.setEntityId(expectedEntityId) + edRep.setServiceProviderName("spName") + + def edRepJson = mapper.writeValueAsString(edRep) + + then: + try { + mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(edRepJson)) + false + } catch (NestedServletException expected) { + expected.getCause() instanceof InvalidPatternMatchException + } + } + + @WithMockUser(value = "someUser", roles = ["USER"]) + def 'POST create new - verifying validation on entityID and ACS locations'() { + given: + def expectedEntityId = 'https://shib.org/blah/blah' + EntityDescriptorRepresentation edRep = new EntityDescriptorRepresentation() + edRep.setEntityId(expectedEntityId) + edRep.setServiceProviderName("spName") + + def acsList = new ArrayList() + AssertionConsumerServiceRepresentation acsRep = new AssertionConsumerServiceRepresentation() + acsRep.setIndex(0) + acsRep.setLocationUrl("http://logout.shib.org/dologout") + acsList.add(acsRep) + edRep.setAssertionConsumerServices(acsList) + + def edRepJson = mapper.writeValueAsString(edRep) + + when: + def result = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(edRepJson)) + + then: + result.andExpect(status().isCreated()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.entityId").value("https://shib.org/blah/blah")) + .andExpect(jsonPath("\$.serviceEnabled").value(false)) + .andExpect(jsonPath("\$.idOfOwner").value("testingGroupBBB")) + + when: "ACS url is bad" + expectedEntityId = 'https://shib.org/blah/blah/again' + edRep = new EntityDescriptorRepresentation() + edRep.setEntityId(expectedEntityId) + edRep.setServiceProviderName("spName") + + acsList = new ArrayList() + acsRep = new AssertionConsumerServiceRepresentation() + acsRep.setIndex(0) + acsRep.setLocationUrl("http://shib.com/dologout") + acsList.add(acsRep) + edRep.setAssertionConsumerServices(acsList) + edRepJson = mapper.writeValueAsString(edRep) + + then: + try { + mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(edRepJson)) + false + } catch (NestedServletException expected) { + expected.getCause() instanceof InvalidPatternMatchException + } + } + + @WithMockAdmin def 'POST /EntityDescriptor and successfully create new record'() { given: - authentication.getName() >> 'admin' - def expectedEntityId = 'https://shib' def expectedSpName = 'sp1' def expectedResponseHeader = 'Location' def expectedResponseHeaderValue = "/api/EntityDescriptor/" - def postedJsonBody = """ + def postedJsonBody = """ { "serviceProviderName": "$expectedSpName", "entityId": "$expectedEntityId", @@ -280,7 +279,7 @@ class EntityDescriptorControllerTests extends Specification { "securityInfo": null, "assertionConsumerServices": null, "current": false - } + } """ when: @@ -294,18 +293,15 @@ class EntityDescriptorControllerTests extends Specification { .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) } - @Rollback @WithMockUser(value = "someUser", roles = ["USER"]) def 'POST /EntityDescriptor as user disallows enabling'() { given: - authentication.getName() >> 'someUser' - def expectedEntityId = 'https://shib' def expectedSpName = 'sp1' when: def postedJsonBody = """ - { + { "serviceProviderName": "$expectedSpName", "entityId": "$expectedEntityId", "organization": null, @@ -321,9 +317,9 @@ class EntityDescriptorControllerTests extends Specification { "assertionConsumerServices": null, "relyingPartyOverrides": null, "attributeRelease": null - } + } """ - + then: try { mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) @@ -333,13 +329,10 @@ class EntityDescriptorControllerTests extends Specification { } } - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def 'POST /EntityDescriptor record already exists'() { given: - authentication.getName() >> 'admin' - - def postedJsonBody = """ + def postedJsonBody = """ { "serviceProviderName": "sp1", "entityId": "eid1", @@ -356,17 +349,17 @@ class EntityDescriptorControllerTests extends Specification { "assertionConsumerServices": null, "relyingPartyOverrides": null, "attributeRelease": null - } + } """ when: def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true) def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false) - + entityDescriptorRepository.save(entityDescriptorOne) entityDescriptorRepository.save(entityDescriptorTwo) entityManager.flush() - + then: try { mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) @@ -375,14 +368,10 @@ class EntityDescriptorControllerTests extends Specification { e instanceof ObjectIdExistsException } } - - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) - def 'GET /EntityDescriptor/{resourceId} non-existent'() { - when: - authentication.getName() >> 'admin' - then: + @WithMockAdmin + def 'GET /EntityDescriptor/{resourceId} non-existent'() { + expect: try { mockMvc.perform(get("/api/EntityDescriptor/uuid-1")) } @@ -390,19 +379,17 @@ class EntityDescriptorControllerTests extends Specification { e instanceof EntityNotFoundException } } - - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + + @WithMockAdmin def 'GET /EntityDescriptor/{resourceId} existing'() { given: - authentication.getName() >> 'admin' def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "admingroup") entityDescriptorRepository.save(entityDescriptorOne) entityManager.flush() - + when: - def result = mockMvc.perform(get("/api/EntityDescriptor/uuid-1")) - + def result = mockMvc.perform(get("/api/EntityDescriptor/uuid-1")) + then: result.andExpect(status().isOk()) .andExpect(jsonPath("\$.entityId").value("eid1")) @@ -411,23 +398,20 @@ class EntityDescriptorControllerTests extends Specification { .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) } - @Rollback @WithMockUser(value = "someUser", roles = ["USER"]) def 'GET /EntityDescriptor/{resourceId} existing, validate group access'() { given: - authentication.getName() >> 'someUser' Group g = userService.getCurrentUserGroup() - + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "someUser") def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) - + entityDescriptorRepository.saveAndFlush(entityDescriptorOne) entityDescriptorRepository.saveAndFlush(entityDescriptorTwo) - + ownershipRepository.saveAndFlush(new Ownership(g, entityDescriptorOne)) ownershipRepository.saveAndFlush(new Ownership(Group.ADMIN_GROUP, entityDescriptorTwo)) - - + when: def result = mockMvc.perform(get("/api/EntityDescriptor/uuid-1")) @@ -439,19 +423,17 @@ class EntityDescriptorControllerTests extends Specification { .andExpect(jsonPath("\$.idOfOwner").value("someUser")) } - @Rollback @WithMockUser(value = "someUser", roles = ["USER"]) def 'GET /EntityDescriptor/{resourceId} existing, owned by some other user'() { when: - authentication.getName() >> 'someUser' Group g = userService.getCurrentUserGroup() - + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) - + entityDescriptorRepository.saveAndFlush(entityDescriptorOne) entityDescriptorRepository.saveAndFlush(entityDescriptorTwo) - + ownershipRepository.saveAndFlush(new Ownership(g, entityDescriptorOne)) ownershipRepository.saveAndFlush(new Ownership(Group.ADMIN_GROUP, entityDescriptorTwo)) @@ -461,15 +443,13 @@ class EntityDescriptorControllerTests extends Specification { } catch (Exception e) { e instanceof ForbiddenException - } + } } - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def 'GET /EntityDescriptor/{resourceId} existing (xml)'() { given: - authentication.getName() >> 'admin' - def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true) + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true) entityDescriptorOne.setElementLocalName("EntityDescriptor") entityDescriptorOne.setNamespacePrefix("md") entityDescriptorOne.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") @@ -487,13 +467,11 @@ class EntityDescriptorControllerTests extends Specification { result.andExpect(status().isOk()).andExpect(content().xml(expectedXML)) } - @Rollback @WithMockUser(value = "someUser", roles = ["USER"]) def 'GET /EntityDescriptor/{resourceId} existing (xml), user-owned'() { given: - authentication.getName() >> 'someUser' Group g = userService.getCurrentUserGroup() - + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) entityDescriptorOne.setElementLocalName("EntityDescriptor") entityDescriptorOne.setNamespacePrefix("md") @@ -512,20 +490,18 @@ class EntityDescriptorControllerTests extends Specification { result.andExpect(status().isOk()).andExpect(content().xml(expectedXML)) } - @Rollback @WithMockUser(value = "someUser", roles = ["USER"]) def 'GET /EntityDescriptor/{resourceId} existing (xml), other user-owned'() { when: - authentication.getName() >> 'someUser' Group g = Group.ADMIN_GROUP - + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) entityDescriptorOne.setElementLocalName("EntityDescriptor") entityDescriptorOne.setNamespacePrefix("md") entityDescriptorOne.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") entityDescriptorRepository.save(entityDescriptorOne) entityManager.flush() - + then: try { mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId").accept(APPLICATION_XML)) @@ -535,12 +511,9 @@ class EntityDescriptorControllerTests extends Specification { } } - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def "POST /EntityDescriptor handles XML happily"() { given: - authentication.getName() >> 'admin' - def postedBody = ''' @@ -574,18 +547,16 @@ class EntityDescriptorControllerTests extends Specification { .andExpect(jsonPath("\$.serviceEnabled").value(false)) .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) .andExpect(jsonPath("\$.serviceProviderSsoDescriptor.protocolSupportEnum").value("SAML 2")) - .andExpect(jsonPath("\$.serviceProviderSsoDescriptor.nameIdFormats[0]").value("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified")) + .andExpect(jsonPath("\$.serviceProviderSsoDescriptor.nameIdFormats[0]").value("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified")) .andExpect(jsonPath("\$.assertionConsumerServices[0].binding").value("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST")) .andExpect(jsonPath("\$.assertionConsumerServices[0].makeDefault").value(false)) .andExpect(jsonPath("\$.assertionConsumerServices[0].locationUrl").value("https://test.scaldingspoon.org/test1/acs")) } - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def "POST /EntityDescriptor returns error for duplicate entity id"() { when: - authentication.getName() >> 'admin' def postedBody = ''' @@ -608,11 +579,11 @@ class EntityDescriptorControllerTests extends Specification { def spName = randomGenerator.randomString() def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true) def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'http://test.scaldingspoon.org/test1', serviceProviderName: 'sp2', serviceEnabled: false) - + entityDescriptorRepository.save(entityDescriptorOne) entityDescriptorRepository.save(entityDescriptorTwo) entityManager.flush() - + then: try { mockMvc.perform(post("/api/EntityDescriptor").contentType(APPLICATION_XML).content(postedBody).param("spName", spName)) @@ -622,22 +593,19 @@ class EntityDescriptorControllerTests extends Specification { } } - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def "PUT /EntityDescriptor updates entity descriptors properly as admin"() { given: - authentication.getName() >> 'admin' - def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) - + entityDescriptorTwo = entityDescriptorRepository.save(entityDescriptorTwo) entityManager.flush() entityManager.clear() - def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(entityDescriptorTwo) + def updatedEntityDescriptorRepresentation = jpaEntityDescriptorService.createRepresentationFromDescriptor(entityDescriptorTwo) updatedEntityDescriptorRepresentation.setServiceProviderName("newName") def postedJsonBody = mapper.writeValueAsString(updatedEntityDescriptorRepresentation) - + when: def result = mockMvc.perform(put("/api/EntityDescriptor/uuid-2").contentType(APPLICATION_JSON).content(postedJsonBody)) @@ -649,21 +617,19 @@ class EntityDescriptorControllerTests extends Specification { .andExpect(jsonPath("\$.serviceProviderName").value("newName")) } - @Rollback @WithMockUser(value = "someUser", roles = ["USER"]) - def "PUT /EntityDescriptor disallows non-admin user from enabling"() { + def "PUT /EntityDescriptor disallows non-admin user from enabling"() { given: - authentication.getName() >> 'someUser' Group g = userService.getCurrentUserGroup() - - def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false, idOfOwner: g.getOwnerId()) + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false, idOfOwner: g.getOwnerId()) entityDescriptorOne = entityDescriptorRepository.save(entityDescriptorOne) entityManager.flush() when: entityDescriptorOne.serviceEnabled = true entityDescriptorOne.resourceId = 'uuid-1' - def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(entityDescriptorOne) + def updatedEntityDescriptorRepresentation = jpaEntityDescriptorService.createRepresentationFromDescriptor(entityDescriptorOne) updatedEntityDescriptorRepresentation.version = entityDescriptorOne.hashCode() def postedJsonBody = mapper.writeValueAsString(updatedEntityDescriptorRepresentation) @@ -676,21 +642,19 @@ class EntityDescriptorControllerTests extends Specification { } } - @Rollback @WithMockUser(value = "someUser", roles = ["USER"]) - def "PUT /EntityDescriptor denies the request if the PUTing user is not an ADMIN and not the createdBy user"() { + def "PUT /EntityDescriptor denies the request if the PUTing user is not an ADMIN and not the createdBy user"() { given: - authentication.getName() >> 'someUser' Group g = userService.getCurrentUserGroup() - - def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) entityDescriptorOne = entityDescriptorRepository.save(entityDescriptorOne) entityManager.flush() when: entityDescriptorOne.serviceProviderName = 'foo' entityDescriptorOne.resourceId = 'uuid-1' - def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(entityDescriptorOne) + def updatedEntityDescriptorRepresentation = jpaEntityDescriptorService.createRepresentationFromDescriptor(entityDescriptorOne) updatedEntityDescriptorRepresentation.version = entityDescriptorOne.hashCode() def postedJsonBody = mapper.writeValueAsString(updatedEntityDescriptorRepresentation) @@ -703,21 +667,18 @@ class EntityDescriptorControllerTests extends Specification { } } - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def "PUT /EntityDescriptor throws a concurrent mod exception if the version numbers don't match"() { - given: - authentication.getName() >> 'admin' - - def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) + given: + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) entityDescriptorOne = entityDescriptorRepository.save(entityDescriptorOne) entityManager.flush() when: entityDescriptorOne.serviceProviderName = 'foo' entityDescriptorOne.resourceId = 'uuid-1' - def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(entityDescriptorOne) - + def updatedEntityDescriptorRepresentation = jpaEntityDescriptorService.createRepresentationFromDescriptor(entityDescriptorOne) + def postedJsonBody = mapper.writeValueAsString(updatedEntityDescriptorRepresentation) then: @@ -725,22 +686,7 @@ class EntityDescriptorControllerTests extends Specification { mockMvc.perform(put("/api/EntityDescriptor/$resourceId").contentType(APPLICATION_JSON).content(postedJsonBody)) } catch (Exception e) { - e instanceof ConcurrentModificationException - } - } - - @org.springframework.boot.test.context.TestConfiguration - @Profile(value = "local") - static class LocalConfig { - @Bean - GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { - GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { - it.groupRepository = repo - it.ownershipRepository = ownershipRepository - return it - }) - result.ensureAdminGroupExists() - return result + e instanceof ConcurrentModificationException } } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorOwnershipIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorOwnershipIntegrationTests.groovy index 59510ac27..6462482d0 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorOwnershipIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorOwnershipIntegrationTests.groovy @@ -3,10 +3,12 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest 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.domain.EntityDescriptor import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository @@ -23,6 +25,7 @@ import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionService import edu.internet2.tier.shibboleth.admin.ui.service.EntityService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.domain.EntityScan @@ -51,47 +54,22 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. /** * Test to recreate an issue discovered while trying to validate fixes for other bugs - SHIBUI-2033 */ -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration, LocalConfig]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@ActiveProfiles(["edoi-test"]) @Stepwise -class EntityDescriptorOwnershipIntegrationTests extends Specification { +class EntityDescriptorOwnershipIntegrationTests extends AbstractBaseDataJpaTest { @Autowired EntityDescriptorRepository entityDescriptorRepository - @Autowired - EntityManager entityManager - @Autowired EntityService entityService - - @Autowired - GroupServiceForTesting groupService - - @Autowired - OwnershipRepository ownershipRepository - + @Autowired - RoleRepository roleRepository + OpenSamlObjects openSamlObjects @Autowired JPAEntityDescriptorServiceImpl service - - @Autowired - UserRepository userRepository - - @Autowired - UserService userService def mockRestTemplate = Mock(RestTemplate) - def openSamlObjects = new OpenSamlObjects().with { - init() - it - } - Group cuGroup = new Group().with { it.name = "College Users" it.resourceId = "cu-group" @@ -105,8 +83,6 @@ class EntityDescriptorOwnershipIntegrationTests extends Specification { @Transactional def setup() { - groupService.clearAllForTesting() - EntityDescriptorVersionService versionService = Mock() controller = new EntityDescriptorController(versionService) controller.openSamlObjects = openSamlObjects @@ -115,26 +91,6 @@ class EntityDescriptorOwnershipIntegrationTests extends Specification { mockMvc = MockMvcBuilders.standaloneSetup(controller).build() - if (roleRepository.count() == 0) { - def roles = [new Role().with { - name = 'ROLE_ADMIN' - it - }, new Role().with { - name = 'ROLE_USER' - it - }, new Role().with { - name = 'ROLE_ENABLE' - it - }] - roles.each { - roleRepository.save(it) - } - } - - Optional adminRole = roleRepository.findByName("ROLE_ADMIN") - User adminUser = new User(username: "admin", roles: [adminRole.get()], password: "foo") - userService.save(adminUser) - Optional userRole = roleRepository.findByName("ROLE_USER") User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") userService.save(user) @@ -143,7 +99,7 @@ class EntityDescriptorOwnershipIntegrationTests extends Specification { EntityDescriptorConversionUtils.setEntityService(entityService) } - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def "The test scenario"() { when:"step 1 - create new group" cuGroup = groupService.createGroup(cuGroup) @@ -185,11 +141,8 @@ class EntityDescriptorOwnershipIntegrationTests extends Specification { ownershipRepository.findAllByOwner(Group.ADMIN_GROUP).size() == 2 // admin user + entity descriptor when: "step 4 - change ownership of the ED" - String contentAsString = result.andReturn().getResponse().getContentAsString() - def mapper = new ObjectMapper() - mapper.enable(SerializationFeature.INDENT_OUTPUT) - mapper.registerModule(new JavaTimeModule()) - EntityDescriptorRepresentation edRep = mapper.readValue(contentAsString, EntityDescriptorRepresentation.class) + EntityDescriptor ed = entityDescriptorRepository.findByEntityID(expectedEntityId) + EntityDescriptorRepresentation edRep = service.createRepresentationFromDescriptor(ed) edRep.setIdOfOwner(cuGroup.getOwnerId()) service.update(edRep) @@ -197,20 +150,4 @@ class EntityDescriptorOwnershipIntegrationTests extends Specification { ownershipRepository.findAllByOwner(cuGroup).size() == 2 // someUser + entity descriptor ownershipRepository.findAllByOwner(Group.ADMIN_GROUP).size() == 1 // admin user } - - @org.springframework.boot.test.context.TestConfiguration - @Profile(value = "edoi-test") - static class LocalConfig { - @Bean - @Primary - GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { - GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { - it.groupRepository = repo - it.ownershipRepository = ownershipRepository - return it - }) - result.ensureAdminGroupExists() - return result - } - } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorVersionControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorVersionControllerTests.groovy new file mode 100644 index 000000000..f5714441d --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorVersionControllerTests.groovy @@ -0,0 +1,168 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.Organization +import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationDisplayName +import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationName +import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationURL +import edu.internet2.tier.shibboleth.admin.ui.envers.EnversVersionServiceSupport +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.Group +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionService +import edu.internet2.tier.shibboleth.admin.ui.service.EntityService +import edu.internet2.tier.shibboleth.admin.ui.service.EnversEntityDescriptorVersionService +import edu.internet2.tier.shibboleth.admin.ui.service.EnversMetadataResolverVersionService +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverVersionService +import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.client.RestTemplate +import spock.lang.Subject + +import javax.persistence.EntityManager + +import static org.springframework.http.MediaType.APPLICATION_JSON +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@ContextConfiguration(classes=[EDCLocalConfig]) +class EntityDescriptorVersionControllerTests extends AbstractBaseDataJpaTest { + @Autowired + EntityDescriptorRepository entityDescriptorRepository + + @Autowired + private TestEntityManager testEntityManager + + @Autowired + EntityService entityService + + @Autowired + JPAEntityDescriptorServiceImpl jpaEntityDescriptorService + + @Autowired + ObjectMapper mapper + + @Autowired + OpenSamlObjects openSamlObjects + + @Autowired + EntityDescriptorVersionService versionService + + def mockMvc + def mockRestTemplate = Mock(RestTemplate) + def resId + + @Subject + def controller + + @Transactional + def setup() { + openSamlObjects.init() + + Group gb = new Group() + gb.setResourceId("testingGroupBBB") + gb.setName("Group BBB") + gb.setValidationRegex("/^(?:https?:\\/\\/)?(?:[^.]+\\.)?shib\\.org(\\/.*)?\$/") + gb = groupService.createGroup(gb) + + controller = new EntityDescriptorController(versionService) + controller.openSamlObjects = openSamlObjects + controller.entityDescriptorService = jpaEntityDescriptorService + controller.restTemplate = mockRestTemplate + + mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + + Optional userRole = roleRepository.findByName("ROLE_USER") + User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") + user.setGroup(gb) + userService.save(user) + + EntityDescriptorConversionUtils.setOpenSamlObjects(openSamlObjects) + EntityDescriptorConversionUtils.setEntityService(entityService) + + // Because the audit is done with hibernate envers (which is done by a listener after a transaction commit), we have to jump + // through some hoops to get this to work like it should in this DataJPATest + // Use the TestEntityManager to get the versions saved to the db + EntityDescriptor ed = new EntityDescriptor(entityID: 'testme', serviceProviderName: 'testme').with { + entityDescriptorRepository.saveAndFlush(it) + } + testEntityManager.getEntityManager().getTransaction().commit() // get envers to write version + resId = ed.resourceId + ed = entityDescriptorRepository.findByResourceId(resId) + + testEntityManager.getEntityManager().getTransaction().begin() + ed.setOrganization(new Organization().with { + it.organizationNames = [new OrganizationName(value: 'testme', XMLLang: 'en')] + it.organizationDisplayNames = [new OrganizationDisplayName(value: 'testme', XMLLang: 'en')] + it.organizationURLs = [new OrganizationURL(value: 'http://testme.org', XMLLang: 'en')] + it + }) + entityDescriptorRepository.saveAndFlush(ed) + testEntityManager.getEntityManager().getTransaction().commit() // get envers to write version + } + + /** + * + * - No @Transactional on the method + * - + */ + @WithMockAdmin + @Transactional + def 'SHIBUI-1414'() { + when: + def result = mockMvc.perform(get("/api/EntityDescriptor/" + resId + "/Versions")) + def allVersions = mapper.readValue(result.andReturn().getResponse().getContentAsString(), List.class) + + String edv1 = mockMvc.perform(get("/api/EntityDescriptor/" + resId + "/Versions/" + allVersions.get(0).id)).andReturn().getResponse().getContentAsString() + String edv2 = mockMvc.perform(get("/api/EntityDescriptor/" + resId + "/Versions/" + allVersions.get(1).id)).andReturn().getResponse().getContentAsString() + + def v2Version = new JsonSlurper().parseText(edv2).get("version") + def aedv1 = new JsonSlurper().parseText(edv1).with { + it.put('version', v2Version) + it + }.with { + JsonOutput.toJson(it) + } + testEntityManager.getEntityManager().getTransaction().begin() + def response = mockMvc.perform(put("/api/EntityDescriptor/" + resId).contentType(APPLICATION_JSON).content(aedv1)) + testEntityManager.getEntityManager().getTransaction().commit() + + then: + response.andExpect(status().isOk()) + noExceptionThrown() + } + + @TestConfiguration + private static class EDCLocalConfig { + @Bean + EntityDescriptorVersionService entityDescriptorVersionService(EnversVersionServiceSupport support, EntityDescriptorService entityDescriptorService) { + return new EnversEntityDescriptorVersionService(support, entityDescriptorService) + } + + @Bean + MetadataResolverVersionService metadataResolverVersionService(EnversVersionServiceSupport support) { + return new EnversMetadataResolverVersionService(support) + } + + @Bean + EnversVersionServiceSupport enversVersionServiceSupport(EntityManager entityManager) { + return new EnversVersionServiceSupport(entityManager) + } + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy index e223e0012..41ed176ad 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy @@ -20,6 +20,7 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Profile import org.springframework.http.HttpEntity import org.springframework.http.HttpHeaders import org.springframework.test.context.ActiveProfiles @@ -32,7 +33,7 @@ import static org.springframework.http.HttpMethod.PUT * @author Dmitriy Kopylenko */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@ActiveProfiles("no-auth") +@ActiveProfiles(["no-auth", "mfci-test"]) class MetadataFiltersControllerIntegrationTests extends Specification { @Autowired @@ -249,7 +250,8 @@ class MetadataFiltersControllerIntegrationTests extends Specification { } @TestConfiguration - static class Config { + @Profile("mfci-test") + static class LocalConfig { @Bean MetadataResolver metadataResolver() { new OpenSamlChainingMetadataResolver().with { @@ -259,4 +261,4 @@ class MetadataFiltersControllerIntegrationTests extends Specification { } } } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy index b915d6fac..c0a7d0e68 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy @@ -2,51 +2,47 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository import edu.internet2.tier.shibboleth.admin.ui.service.FilterService +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin import edu.internet2.tier.shibboleth.admin.util.AttributeUtility import groovy.json.JsonOutput import groovy.json.JsonSlurper import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.transaction.annotation.Transactional import org.w3c.dom.Document -import spock.lang.Specification import spock.lang.Unroll import static org.hamcrest.CoreMatchers.containsString import static org.springframework.http.MediaType.APPLICATION_JSON -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* - -/** - * @author Bill Smith (wsmith@unicon.net) - */ -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -class MetadataFiltersControllerTests extends Specification { +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@ContextConfiguration(classes=[ MFCLocalConfig ]) +class MetadataFiltersControllerTests extends AbstractBaseDataJpaTest { @Autowired AttributeUtility attributeUtility @@ -71,6 +67,7 @@ class MetadataFiltersControllerTests extends Specification { static BASE_URI = '/api/MetadataResolvers' + @Transactional def setup() { randomGenerator = new RandomGenerator() testObjectGenerator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) @@ -80,6 +77,8 @@ class MetadataFiltersControllerTests extends Specification { controller = new MetadataFiltersController ( repository: metadataResolverRepository, filterRepository: metadataFilterRepository, + groupService: groupService, + userService: userService, metadataResolverService: new MetadataResolverService() { @Override void reloadFilters(String metadataResolverName) { @@ -90,15 +89,15 @@ class MetadataFiltersControllerTests extends Specification { Document generateConfiguration() { return null } - + @Override - public MetadataResolver updateMetadataResolverEnabledStatus(MetadataResolver existingResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException { + MetadataResolver updateMetadataResolverEnabledStatus(MetadataResolver existingResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException { // This won't get called return null } - + @Override - public MetadataResolver findByResourceId(String resourceId) throws EntityNotFoundException { + MetadataResolver findByResourceId(String resourceId) throws EntityNotFoundException { // This won't get called return null } @@ -110,10 +109,10 @@ class MetadataFiltersControllerTests extends Specification { it } ) - mockMvc = MockMvcBuilders.standaloneSetup(controller).build() } + @WithMockAdmin def "FilterController.getAll gets all available types of filters"() { given: def metadataResolver = new MetadataResolver() @@ -133,6 +132,7 @@ class MetadataFiltersControllerTests extends Specification { .andExpect(content().json(mapper.writeValueAsString(expectedContent))) } + @WithMockAdmin def "FilterController.getOne gets the desired filter"() { given: def metadataResolver = new MetadataResolver() @@ -155,6 +155,7 @@ class MetadataFiltersControllerTests extends Specification { } @Unroll + @WithMockAdmin def "FilterController.create creates the desired filter (filterType: #filterType)"(String filterType) { given: def randomFilter = testObjectGenerator.buildRandomFilterOfType(filterType) @@ -177,10 +178,9 @@ class MetadataFiltersControllerTests extends Specification { def postedJsonBody = expectedJsonBody - ~/"id":.*?,/ // remove the "id:," when: - def result = mockMvc.perform( - post("$BASE_URI/foo/Filters") - .contentType(APPLICATION_JSON) - .content(postedJsonBody)) + def result = mockMvc.perform(post("$BASE_URI/foo/Filters") + .contentType(APPLICATION_JSON) + .content(postedJsonBody)) then: println postedJsonBody @@ -198,13 +198,14 @@ class MetadataFiltersControllerTests extends Specification { } @Unroll + @WithMockAdmin def "FilterController.update updates the target #filterType filter as desired"(String filterType) { given: def originalFilter = testObjectGenerator.buildRandomFilterOfType(filterType) def updatedFilter = testObjectGenerator.copyOf(originalFilter) updatedFilter.name = 'Updated Filter' updatedFilter.version = originalFilter.hashCode() - def postedJsonBody = mapper.writeValueAsString(updatedFilter) + def updatedFilterJson = mapper.writeValueAsString(updatedFilter) def originalMetadataResolver = new MetadataResolver() originalMetadataResolver.setResourceId('foo') @@ -222,13 +223,11 @@ class MetadataFiltersControllerTests extends Specification { def filterUUID = updatedFilter.getResourceId() when: - def result = mockMvc.perform( - put("$BASE_URI/foo/Filters/$filterUUID") - .contentType(APPLICATION_JSON) - .content(postedJsonBody)) + def result = mockMvc.perform(put("$BASE_URI/foo/Filters/$filterUUID") + .contentType(APPLICATION_JSON).content(updatedFilterJson)) then: - def expectedJson = new JsonSlurper().parseText(postedJsonBody) + def expectedJson = new JsonSlurper().parseText(updatedFilterJson) expectedJson << [version: updatedFilter.getVersion()] result.andExpect(status().isOk()) .andExpect(content().json(JsonOutput.toJson(expectedJson), true)) @@ -242,6 +241,7 @@ class MetadataFiltersControllerTests extends Specification { 'nameIdFormat' | _ } + @WithMockAdmin def "FilterController.update filter 409's if the version numbers don't match"() { given: def randomFilter = testObjectGenerator.entityAttributesFilter() @@ -267,4 +267,13 @@ class MetadataFiltersControllerTests extends Specification { then: result.andExpect(status().is(409)) } -} + + @TestConfiguration + private static class MFCLocalConfig { + @Bean + JPAEntityServiceImpl jpaEntityService(OpenSamlObjects openSamlObjects, AttributeUtility attributeUtility, + CustomPropertiesConfiguration customPropertiesConfiguration) { + return new JPAEntityServiceImpl(openSamlObjects, attributeUtility, customPropertiesConfiguration) + } + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy index 05f6b62c8..90c70f38e 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy @@ -3,86 +3,106 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.EntitiesVersioningConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConverterConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverValidationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.PlaceholderResolverComponentsConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.StringTrimModule import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataQueryProtocolScheme -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.MetadataResolverValidationService +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolversPositionOrderContainerRepository +import edu.internet2.tier.shibboleth.admin.ui.service.DefaultMetadataResolversPositionOrderContainerService +import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryService +import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService +import edu.internet2.tier.shibboleth.admin.ui.service.JPAMetadataResolverServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverConverterService +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverVersionService +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolversPositionOrderContainerService import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin import edu.internet2.tier.shibboleth.admin.util.AttributeUtility -import groovy.json.JsonOutput import groovy.json.JsonSlurper import org.opensaml.saml.metadata.resolver.MetadataResolver import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration -import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Profile -import org.springframework.http.HttpEntity -import org.springframework.http.HttpHeaders -import org.springframework.test.annotation.DirtiesContext -import org.springframework.test.context.ActiveProfiles -import spock.lang.Specification +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.MvcResult +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.transaction.annotation.Transactional import spock.lang.Unroll -import static com.fasterxml.jackson.annotation.JsonInclude.Include.* -import static org.springframework.http.HttpMethod.PUT - -/** - * @author Dmitriy Kopylenko - */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@ActiveProfiles("no-auth") -class MetadataResolversControllerIntegrationTests extends Specification { +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL +import static org.springframework.http.MediaType.APPLICATION_JSON +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@ContextConfiguration(classes=[MetadataResolverValidationConfiguration, MetadataResolverConverterConfiguration, + MetadataResolverConfiguration, EntitiesVersioningConfiguration, + edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration, + PlaceholderResolverComponentsConfiguration, MRCILocalConfig]) +class MetadataResolversControllerIntegrationTests extends AbstractBaseDataJpaTest { + @Autowired + AttributeUtility attributeUtility @Autowired - private TestRestTemplate restTemplate + MetadataResolversController controller @Autowired - MetadataResolverRepository metadataResolverRepository + CustomPropertiesConfiguration customPropertiesConfiguration @Autowired - AttributeUtility attributeUtility + ObjectMapper mapper @Autowired - CustomPropertiesConfiguration customPropertiesConfiguration + MetadataResolverRepository metadataResolverRepository - ObjectMapper mapper TestObjectGenerator generator + MockMvc mockMvc - JsonSlurper jsonSlurper = new JsonSlurper() - - static BASE_URI = '/api/MetadataResolvers' + static String BASE_URI = '/api/MetadataResolvers' + @Transactional def setup() { generator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) - mapper = new ObjectMapper() - mapper.enable(SerializationFeature.INDENT_OUTPUT) - mapper.setSerializationInclusion(NON_NULL) - mapper.registerModule(new JavaTimeModule()) - mapper.registerModule(new StringTrimModule()) metadataResolverRepository.deleteAll() + + mockMvc = MockMvcBuilders.standaloneSetup(controller).setMessageConverters(new MappingJackson2HttpMessageConverter(mapper)).build() } def cleanup() { metadataResolverRepository.deleteAll() } + @WithMockAdmin def "GET empty -> /api/MetadataResolvers"() { when: 'No resolvers are available in data store' - def result = this.restTemplate.getForEntity(BASE_URI, String) - def returnedResolvers = jsonSlurper.parseText(result.body) + def result = mockMvc.perform(get(BASE_URI)) then: - result.statusCodeValue == 200 - returnedResolvers.size() == 0 + result.andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$").isEmpty()) } + @WithMockAdmin def "GET one available MetadataResolver -> /api/MetadataResolvers"() { given: 'One resolver is available in data store' def resolver = new DynamicHttpMetadataResolver().with { @@ -92,17 +112,16 @@ class MetadataResolversControllerIntegrationTests extends Specification { metadataResolverRepository.save(resolver) when: 'GET request is made' - def result = this.restTemplate.getForEntity(BASE_URI, String) - def returnedResolvers = jsonSlurper.parseText(result.body) + def result = mockMvc.perform(get(BASE_URI)) then: - result.statusCodeValue == 200 - returnedResolvers.size() == 1 - returnedResolvers[0]['@type'] == 'DynamicHttpMetadataResolver' - returnedResolvers[0].name == 'Test DynamicHttpMetadataResolver' + result.andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.[0].name").value("Test DynamicHttpMetadataResolver")) + .andExpect(jsonPath("\$.[0].['@type']").value("DynamicHttpMetadataResolver")) } + @WithMockAdmin def "GET multiple available MetadataResolvers -> /api/MetadataResolvers"() { given: 'Two resolvers are available in data store' def resolvers = [ @@ -120,19 +139,17 @@ class MetadataResolversControllerIntegrationTests extends Specification { } when: 'GET request is made' - def result = this.restTemplate.getForEntity(BASE_URI, String) - def returnedResolvers = jsonSlurper.parseText(result.body) + def result = mockMvc.perform(get(BASE_URI)) then: - result.statusCodeValue == 200 - returnedResolvers.size() == 2 - returnedResolvers[0]['@type'] == 'DynamicHttpMetadataResolver' - returnedResolvers[0].name == 'Test DynamicHttpMetadataResolver' - returnedResolvers[1]['@type'] == 'FileBackedHttpMetadataResolver' - returnedResolvers[1].name == 'Test FileBackedHttpMetadataResolver' - + result.andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.[0].name").value("Test DynamicHttpMetadataResolver")) + .andExpect(jsonPath("\$.[0].['@type']").value("DynamicHttpMetadataResolver")) + .andExpect(jsonPath("\$.[1].name").value("Test FileBackedHttpMetadataResolver")) + .andExpect(jsonPath("\$.[1].['@type']").value("FileBackedHttpMetadataResolver")) } + @WithMockAdmin def "GET concrete MetadataResolver -> /api/MetadataResolvers/{resourceId}"() { given: 'One resolver is available in data store' def resolver = new DynamicHttpMetadataResolver().with { @@ -143,25 +160,25 @@ class MetadataResolversControllerIntegrationTests extends Specification { metadataResolverRepository.save(resolver) when: 'GET request is made with resource Id matching the existing resolver' - def result = this.restTemplate.getForEntity("$BASE_URI/$resolverResourceId", String) - def returnedResolver = jsonSlurper.parseText(result.body) + def result = mockMvc.perform(get("$BASE_URI/$resolverResourceId")) then: - result.statusCodeValue == 200 - returnedResolver['@type'] == 'DynamicHttpMetadataResolver' - returnedResolver.name == 'Test DynamicHttpMetadataResolver' + result.andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.name").value("Test DynamicHttpMetadataResolver")) + .andExpect(jsonPath("\$.['@type']").value("DynamicHttpMetadataResolver")) } + @WithMockAdmin def "GET non-existent MetadataResolver -> /api/MetadataResolvers/{resourceId}"() { when: 'GET request is made with resource Id not matching any resolvers' - def result = this.restTemplate.getForEntity("$BASE_URI/bogus-resource-id", String) + def result = mockMvc.perform(get("$BASE_URI/bogus-resource-id")) then: - result.statusCodeValue == 404 + result.andExpect(status().isNotFound()) } - @DirtiesContext + @WithMockAdmin def "SHIBUI-839 - POST resolver with spaces in the provider name results in trimmed name"() { given: def resolver = generator.buildRandomMetadataResolverOfType('DynamicHttp') @@ -169,29 +186,29 @@ class MetadataResolversControllerIntegrationTests extends Specification { def expectedName = 'This name has spaces' when: - def result = this.restTemplate.postForEntity(BASE_URI, createRequestHttpEntityFor { mapper.writeValueAsString(resolver) }, String) + def result = mockMvc.perform(post(BASE_URI).contentType(APPLICATION_JSON).content(mapper.writeValueAsString(resolver))) then: - def metadataResolverMap = new JsonSlurper().parseText(result.body) - metadataResolverMap.name == expectedName + result.andExpect(status().isCreated()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.name").value(expectedName)) } + @WithMockAdmin @Unroll - @DirtiesContext def "POST new concrete MetadataResolver of type #resolverType -> /api/MetadataResolvers"(String resolverType) { given: 'New MetadataResolver JSON representation' def resolver = generator.buildRandomMetadataResolverOfType(resolverType) String sourceDirectory - if (resolverType.equals('LocalDynamic')) { + if (resolverType == 'LocalDynamic') { sourceDirectory = ((LocalDynamicMetadataResolver) resolver).sourceDirectory } - when: 'POST request is made with new DynamicHttpMetadataResolver JSON representation' - def result = this.restTemplate.postForEntity(BASE_URI, createRequestHttpEntityFor { mapper.writeValueAsString(resolver) }, String) + when: 'POST request is made with new Resolver JSON representation' + def result = mockMvc.perform(post(BASE_URI).contentType(APPLICATION_JSON).content(mapper.writeValueAsString(resolver))) then: - result.statusCodeValue == 201 - result.headers.Location[0].contains(BASE_URI) + result.andExpect(status().isCreated()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.['@type']").value(resolver.getType())) cleanup: if (sourceDirectory != null) { @@ -210,7 +227,7 @@ class MetadataResolversControllerIntegrationTests extends Specification { 'Filesystem' | _ } - @DirtiesContext + @WithMockAdmin def "SHIBUI-1992 - error creating FileBackedHTTPMetadata"() { def resolver = new FileBackedHttpMetadataResolver().with { it.name = 'FBHMR' @@ -223,42 +240,36 @@ class MetadataResolversControllerIntegrationTests extends Specification { } when: - def result = this.restTemplate.postForEntity(BASE_URI, createRequestHttpEntityFor { mapper.writeValueAsString(resolver) }, String) + def result = mockMvc.perform(post(BASE_URI).contentType(APPLICATION_JSON).content(mapper.writeValueAsString(resolver))) then: - result.statusCodeValue == 201 + result.andExpect(status().isCreated()) } + @WithMockAdmin @Unroll def "PUT concrete MetadataResolver of type #resolverType with updated changes -> /api/MetadataResolvers/{resourceId}"(String resolverType) { given: 'One resolver is available in data store' def resolver = generator.buildRandomMetadataResolverOfType(resolverType) String sourceDirectory - if (resolverType.equals('Localdynamic')) { + if (resolverType == 'Localdynamic') { sourceDirectory = ((LocalDynamicMetadataResolver) resolver).sourceDirectory } def resolverResourceId = resolver.resourceId metadataResolverRepository.save(resolver) when: 'GET request is made with resource Id matching the existing resolver' - def result = this.restTemplate.getForEntity("$BASE_URI/$resolverResourceId", String) + def result = mockMvc.perform(get("$BASE_URI/$resolverResourceId")).andReturn() and: 'Resolver data is updated and sent back to the server' - def metadataResolverMap = new JsonSlurper().parseText(result.body) - metadataResolverMap.name = 'Updated DynamicHttpMetadataResolver' - def updatedResult = this.restTemplate.exchange( - "$BASE_URI/${metadataResolverMap.resourceId}", - PUT, - createRequestHttpEntityFor { JsonOutput.toJson(metadataResolverMap) }, - String) - then: - updatedResult.statusCodeValue == 200 + def metadataResolverMap = new JsonSlurper().parseText(result.getResponse().getContentAsString()) - and: - def updatedResolverMap = new JsonSlurper().parseText(updatedResult.body) + metadataResolverMap.name = 'Updated Resolver Name' + def updatedResult = mockMvc.perform(put("$BASE_URI/${metadataResolverMap.resourceId}").contentType(APPLICATION_JSON).content(mapper.writeValueAsString(metadataResolverMap))) then: - updatedResolverMap.name == 'Updated DynamicHttpMetadataResolver' + updatedResult.andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.name").value('Updated Resolver Name')) cleanup: if (sourceDirectory != null) { @@ -277,6 +288,7 @@ class MetadataResolversControllerIntegrationTests extends Specification { 'Filesystem' | _ } + @WithMockAdmin def "PUT concrete MetadataResolver with version conflict -> /api/MetadataResolvers/{resourceId}"() { given: 'One resolver is available in data store' def resolver = new DynamicHttpMetadataResolver().with { @@ -293,44 +305,45 @@ class MetadataResolversControllerIntegrationTests extends Specification { def persistedResolver = metadataResolverRepository.save(resolver) when: 'GET request is made with resource Id matching the existing resolver' - def result = this.restTemplate.getForEntity("$BASE_URI/$resolverResourceId", String) + MvcResult result = mockMvc.perform(get("$BASE_URI/$resolverResourceId")).andReturn() and: 'Resolver data is updated and sent back to the server, but then original resolver is changed in data store' persistedResolver.name = 'Some other name' metadataResolverRepository.save(persistedResolver) - def metadataResolverMap = new JsonSlurper().parseText(result.body) + + def metadataResolverMap = mapper.readValue(result.getResponse().getContentAsString(), DynamicHttpMetadataResolver.class) metadataResolverMap.name = 'Updated DynamicHttpMetadataResolver' - def updatedResult = this.restTemplate.exchange( - "$BASE_URI/${metadataResolverMap.resourceId}", - PUT, - createRequestHttpEntityFor { JsonOutput.toJson(metadataResolverMap) }, - String) + def updatedResult = mockMvc.perform(put("$BASE_URI/${metadataResolverMap.resourceId}").contentType(APPLICATION_JSON).content(mapper.writeValueAsString(metadataResolverMap))) then: - updatedResult.statusCodeValue == 409 + updatedResult.andExpect(status().isConflict()) } + @WithMockAdmin def "POST new MetadataResolver with one EntityAttributesFilters attached -> /api/MetadataResolvers"() { given: 'New MetadataResolver with attached entity attributes filter JSON representation' def resolver = generator.buildRandomMetadataResolverOfType('FileBacked') resolver.metadataFilters << generator.entityAttributesFilter() when: 'POST request is made with new FileBackedMetadataResolver with EntityAttributesFilter JSON representation' - def result = this.restTemplate.postForEntity(BASE_URI, createRequestHttpEntityFor { mapper.writeValueAsString(resolver) }, String) + def result = mockMvc.perform(post(BASE_URI).contentType(APPLICATION_JSON).content(mapper.writeValueAsString(resolver))) then: - result.statusCodeValue == 201 - result.headers.Location[0].contains(BASE_URI) + def location = result.andExpect(status().isCreated()).andReturn().getResponse().getHeaderValue("Location") + + location.contains(BASE_URI) when: 'Query REST API for newly created resolver' - def createdResolverResult = this.restTemplate.getForEntity(result.headers.Location[0], String) - def createdResolver = mapper.readValue(createdResolverResult.body, edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver) + def createdResolverResult = mockMvc.perform(get(location)).andReturn().getResponse().getContentAsString() + def createdResolver = mapper.readValue(createdResolverResult, edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver) then: createdResolver.metadataFilters.size() == 1 createdResolver.metadataFilters[0] instanceof EntityAttributesFilter } + @WithMockAdmin + @Transactional def "PUT MetadataResolver with one EntityAttributesFilters attached and check version -> /api/MetadataResolvers"() { given: 'MetadataResolver with attached entity attributes is available in data store' def resolver = generator.buildRandomMetadataResolverOfType('FileBacked') @@ -339,34 +352,50 @@ class MetadataResolversControllerIntegrationTests extends Specification { metadataResolverRepository.save(resolver) when: 'GET request is made with resource Id matching the existing resolver' - def result = this.restTemplate.getForEntity("$BASE_URI/$resolverResourceId", String) - def existingMetadataResolverMap = new JsonSlurper().parseText(result.body) - def existingMetadataVersion = existingMetadataResolverMap.version + def result = mockMvc.perform(get("$BASE_URI/$resolverResourceId")).andReturn().getResponse().getContentAsString() + def existingMetadataResolverMap = new JsonSlurper().parseText(result) and: 'PUT call is made with' existingMetadataResolverMap.name = 'Updated' - def updatedResultFromPUT = this.restTemplate.exchange( - "$BASE_URI/${existingMetadataResolverMap.resourceId}", - PUT, - createRequestHttpEntityFor { JsonOutput.toJson(existingMetadataResolverMap) }, - String) - def updatedResultFromGET = this.restTemplate.getForEntity("$BASE_URI/$resolverResourceId", String) - def updatedVersionReturnedFromPUT = new JsonSlurper().parseText(updatedResultFromPUT.body).version - def updatedVersionReturnedFromGET = new JsonSlurper().parseText(updatedResultFromGET.body).version + def updatedResultFromPUT = mockMvc.perform(put("$BASE_URI/${existingMetadataResolverMap.resourceId}") + .contentType(APPLICATION_JSON).content(mapper.writeValueAsString(existingMetadataResolverMap))) + .andReturn().getResponse().getContentAsString() + def updatedResultFromGET = mockMvc.perform(get("$BASE_URI/$existingMetadataResolverMap.resourceId")).andReturn().getResponse().getContentAsString() then: - updatedVersionReturnedFromPUT == updatedVersionReturnedFromGET - } - - private HttpEntity createRequestHttpEntityFor(Closure jsonBodySupplier) { - new HttpEntity(jsonBodySupplier(), ['Content-Type': 'application/json'] as HttpHeaders) + updatedResultFromPUT == updatedResultFromGET } @TestConfiguration - static class LocalConfig { + private static class MRCILocalConfig { + @Bean + DirectoryService directoryService() { + return new DirectoryServiceImpl() + } + + @Bean + MetadataResolversController metadataResolversController(MetadataResolverRepository metadataResolverRepository, MetadataResolverValidationService metadataResolverValidationService, + MetadataResolverService metadataResolverService, MetadataResolversPositionOrderContainerService positionOrderContainerService, + IndexWriterService indexWriterService, MetadataResolver chainingMetadataResolver, + MetadataResolverConverterService metadataResolverConverterService, MetadataResolverVersionService versionService) { + MetadataResolversController mrc = new MetadataResolversController().with { + it.resolverRepository = metadataResolverRepository + it.metadataResolverValidationService = metadataResolverValidationService + it.metadataResolverService = metadataResolverService + it.positionOrderContainerService = positionOrderContainerService + it.indexWriterService = indexWriterService + it.chainingMetadataResolver = chainingMetadataResolver + it.metadataResolverConverterService = metadataResolverConverterService + it.versionService = versionService + it + } + return mrc + } + @Bean - MetadataResolver metadataResolver() { - new OpenSamlChainingMetadataResolver() + MetadataResolversPositionOrderContainerService metadataResolversPositionOrderContainerService(MetadataResolversPositionOrderContainerRepository positionOrderContainerRepository, + MetadataResolverRepository resolverRepository) { + return new DefaultMetadataResolversPositionOrderContainerService(positionOrderContainerRepository, resolverRepository) } } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptorTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptorTest.groovy index 0747b9d4c..8fc49acde 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptorTest.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptorTest.groovy @@ -1,18 +1,13 @@ package edu.internet2.tier.shibboleth.admin.ui.domain -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.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.configuration.PlaceholderResolverComponentsConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.HttpMetadataResolverAttributes import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ReloadableMetadataResolverAttributes import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlFileBackedHTTPMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects -import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository -import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator @@ -20,44 +15,26 @@ import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver import org.opensaml.saml.metadata.resolver.MetadataResolver import org.opensaml.saml.metadata.resolver.RefreshableMetadataResolver import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Profile -import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.test.annotation.DirtiesContext -import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration -import spock.lang.Specification - -import java.nio.file.Files /** * @author Bill Smith (wsmith@unicon.net) */ -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, InternationalizationConfiguration, MyConfig, PlaceholderResolverComponentsConfiguration, edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) -@ActiveProfiles(value="local") -class EntityDescriptorTest extends Specification { - - RandomGenerator randomGenerator - TestObjectGenerator generator +@ContextConfiguration(classes = [ EDLocalConfig, PlaceholderResolverComponentsConfiguration ]) +class EntityDescriptorTest extends AbstractBaseDataJpaTest { + @Autowired + IndexWriterService indexWriterService @Autowired MetadataResolver metadataResolver @Autowired - IndexWriterService indexWriterService + OpenSamlObjects openSamlObjects - def openSamlObjects = new OpenSamlObjects().with { - init() - it - } + RandomGenerator randomGenerator + TestObjectGenerator generator def setup() { generator = new TestObjectGenerator() @@ -90,8 +67,7 @@ class EntityDescriptorTest extends Specification { } @TestConfiguration - @Profile("local") - static class MyConfig { + private static class EDLocalConfig { @Bean MetadataResolver metadataResolver() { ChainingMetadataResolver metadataResolver = new OpenSamlChainingMetadataResolver() @@ -100,14 +76,5 @@ class EntityDescriptorTest extends Specification { metadataResolver.initialize() return metadataResolver } - - @Bean - GroupServiceImpl groupService(GroupsRepository repo, OwnershipRepository ownershipRepository) { - new GroupServiceImpl().with { - it.groupRepository = repo - it.ownershipRepository = ownershipRepository - return it - } - } } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/PolymorphicFiltersJacksonHandlingTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/PolymorphicFiltersJacksonHandlingTests.groovy index 9c1ba5342..fa9239ec5 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/PolymorphicFiltersJacksonHandlingTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/PolymorphicFiltersJacksonHandlingTests.groovy @@ -2,37 +2,28 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.filters import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration -import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator -import edu.internet2.tier.shibboleth.admin.util.AttributeUtility +import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import spock.lang.Specification -@SpringBootTest -class PolymorphicFiltersJacksonHandlingTests extends Specification { - - ObjectMapper mapper - - AttributeUtility attributeUtility +class PolymorphicFiltersJacksonHandlingTests extends AbstractBaseDataJpaTest { @Autowired CustomPropertiesConfiguration customPropertiesConfiguration + @Autowired + ObjectMapper mapper + + @Autowired TestObjectGenerator testObjectGenerator def setup() { - mapper = new ObjectMapper() - mapper.enable(SerializationFeature.INDENT_OUTPUT) - - attributeUtility = new AttributeUtility(new OpenSamlObjects().with { - it.init() - it - }) - testObjectGenerator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) + customPropertiesConfiguration.postConstruct() } + @WithMockAdmin def "Correct polymorphic serialization of EntityRoleWhiteListFilter"() { given: def givenFilterJson = """ @@ -68,6 +59,7 @@ class PolymorphicFiltersJacksonHandlingTests extends Specification { roundTripFilter instanceof EntityRoleWhiteListFilter } + @WithMockAdmin def "Correct polymorphic serialization of EntityAttributesFilter"() { given: def simulatedPersistentFilter = testObjectGenerator.entityAttributesFilter() @@ -81,6 +73,7 @@ class PolymorphicFiltersJacksonHandlingTests extends Specification { simulatedPersistentFilter.attributes.size() == simulatedPrePersistentFilter.attributes.size() } + @WithMockAdmin def "Correct polymorphic serialization of RequiredValidUntilFilter"() { given: def givenFilterJson = """ @@ -113,6 +106,7 @@ class PolymorphicFiltersJacksonHandlingTests extends Specification { roundTripFilter instanceof RequiredValidUntilFilter } + @WithMockAdmin def "List of filters with correct types"() { given: def filters = testObjectGenerator.buildAllTypesOfFilterList() @@ -129,6 +123,7 @@ class PolymorphicFiltersJacksonHandlingTests extends Specification { } + @WithMockAdmin def "Deserialization of EntityAttributes filter"() { given: def filterJson = """ @@ -176,6 +171,7 @@ class PolymorphicFiltersJacksonHandlingTests extends Specification { EntityAttributesFilter.class.cast(filter).entityAttributesFilterTarget.value == ['GATCCLk32V'] } + @WithMockAdmin def "Correct polymorphic serialization of NameIdFormatFilter"() { given: def givenFilterJson = """ @@ -216,4 +212,4 @@ class PolymorphicFiltersJacksonHandlingTests extends Specification { deSerializedFilter instanceof NameIdFormatFilter roundTripFilter instanceof NameIdFormatFilter } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/PolymorphicResolversJacksonHandlingTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/PolymorphicResolversJacksonHandlingTests.groovy index f22c875aa..d9a1ead3a 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/PolymorphicResolversJacksonHandlingTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/PolymorphicResolversJacksonHandlingTests.groovy @@ -2,37 +2,21 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature -import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityRoleWhiteListFilter -import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator -import edu.internet2.tier.shibboleth.admin.util.AttributeUtility import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import spock.lang.Specification - -@SpringBootTest -class PolymorphicResolversJacksonHandlingTests extends Specification { - - ObjectMapper mapper - - AttributeUtility attributeUtility +class PolymorphicResolversJacksonHandlingTests extends AbstractBaseDataJpaTest { @Autowired - CustomPropertiesConfiguration customPropertiesConfiguration - TestObjectGenerator testObjectGenerator + ObjectMapper mapper + def setup() { mapper = new ObjectMapper() mapper.enable(SerializationFeature.INDENT_OUTPUT) - - attributeUtility = new AttributeUtility(new OpenSamlObjects().with { - it.init() - it - }) - testObjectGenerator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) } def "Correct polymorphic serialization of LocalDynamicMetadataResolver"() { @@ -241,113 +225,12 @@ class PolymorphicResolversJacksonHandlingTests extends Specification { def "Correct polymorphic serialization of FileBackedHttpMetadataResolver"() { given: - MetadataResolver resolver = new FileBackedHttpMetadataResolver().with { - it.httpMetadataResolverAttributes = new HttpMetadataResolverAttributes() - it.reloadableMetadataResolverAttributes = new ReloadableMetadataResolverAttributes() - it.metadataFilters = [testObjectGenerator.entityAttributesFilter(), testObjectGenerator.entityRoleWhitelistFilter()] - it - } - def givenResolverJson = """ - { - "createdDate" : null, - "modifiedDate" : null, - "createdBy" : null, - "modifiedBy" : null, - "name" : null, - "resourceId" : "f3e615d5-960b-4fed-bff6-86fc4620be95", - "requireValidMetadata" : true, - "failFastInitialization" : true, - "sortKey" : null, - "criterionPredicateRegistryRef" : null, - "useDefaultPredicateRegistry" : true, - "satisfyAnyPredicates" : false, - "metadataFilters" : [ { - "createdDate" : null, - "modifiedDate" : null, - "createdBy" : null, - "modifiedBy" : null, - "name" : "EntityAttributes", - "resourceId" : "4149cc5f-137e-4045-9369-8fedafcdd8c8", - "filterEnabled" : false, - "version" : -1249726767, - "entityAttributesFilterTarget" : { - "createdDate" : null, - "modifiedDate" : null, - "createdBy" : null, - "modifiedBy" : null, - "entityAttributesFilterTargetType" : "CONDITION_SCRIPT", - "value" : [ "6EksoLF7Q0" ], - "audId" : null - }, - "attributeRelease" : [ ], - "relyingPartyOverrides" : { - "signAssertion" : false, - "dontSignResponse" : true, - "turnOffEncryption" : false, - "useSha" : false, - "ignoreAuthenticationMethod" : false, - "omitNotBefore" : false, - "responderId" : "3267361e-7d8c-45d2-92ce-7642dc3bb432", - "nameIdFormats" : [ "baHO7CzFHH" ], - "authenticationMethods" : [ ] - }, - "audId" : null, - "@type" : "EntityAttributes" - }, { - "createdDate" : null, - "modifiedDate" : null, - "createdBy" : null, - "modifiedBy" : null, - "name" : "EntityRoleWhiteList", - "resourceId" : "75117ec7-c74a-45cb-b216-cbbc9118fe70", - "filterEnabled" : false, - "version" : 0, - "removeRolelessEntityDescriptors" : true, - "removeEmptyEntitiesDescriptors" : true, - "retainedRoles" : [ "role1", "role2" ], - "audId" : null, - "@type" : "EntityRoleWhiteList" - } ], - "version" : 0, - "metadataURL" : null, - "backingFile" : null, - "initializeFromBackupFile" : true, - "backupFileInitNextRefreshDelay" : null, - "reloadableMetadataResolverAttributes" : { - "parserPoolRef" : null, - "taskTimerRef" : null, - "minRefreshDelay" : null, - "maxRefreshDelay" : null, - "refreshDelayFactor" : null, - "indexesRef" : null, - "resolveViaPredicatesOnly" : null, - "expirationWarningThreshold" : null - }, - "httpMetadataResolverAttributes" : { - "httpClientRef" : null, - "connectionRequestTimeout" : null, - "connectionTimeout" : null, - "socketTimeout" : null, - "disregardTLSCertificate" : false, - "tlsTrustEngineRef" : null, - "httpClientSecurityParametersRef" : null, - "proxyHost" : null, - "proxyPort" : null, - "proxyUser" : null, - "proxyPassword" : null, - "httpCaching" : null, - "httpCacheDirectory" : null, - "httpMaxCacheEntries" : null, - "httpMaxCacheEntrySize" : null - }, - "audId" : null, - "@type" : "FileBackedHttpMetadataResolver" - } - """ + MetadataResolver resolver = testObjectGenerator.buildFileBackedHttpMetadataResolver() + def resolverJson = mapper.writeValueAsString(resolver) when: //println mapper.writeValueAsString(resolver) - def deSerializedResolver = mapper.readValue(givenResolverJson, MetadataResolver) + def deSerializedResolver = mapper.readValue(resolverJson, MetadataResolver) def json = mapper.writeValueAsString(deSerializedResolver) println(json) def roundTripResolver = mapper.readValue(json, MetadataResolver) @@ -361,4 +244,4 @@ class PolymorphicResolversJacksonHandlingTests extends Specification { deSerializedResolver.metadataFilters[0] instanceof EntityAttributesFilter deSerializedResolver.metadataFilters[1] instanceof EntityRoleWhiteListFilter } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidatorTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidatorTests.groovy new file mode 100644 index 000000000..21af72924 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidatorTests.groovy @@ -0,0 +1,59 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator + +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverValidationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataQueryProtocolScheme +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceForTesting +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.annotation.Transactional + +@ContextConfiguration(classes = [MetadataResolverValidationConfiguration]) +class DynamicHttpMetadataResolverValidatorTests extends AbstractBaseDataJpaTest { + @Autowired + GroupServiceForTesting groupServiceForTesting + + @Autowired + MetadataResolverValidationService metadataResolverValidationService + + @Transactional + def setup() { + Group g = new Group() + g.setResourceId("shib") + g.setName("shib") + // This is valid for a url with "shib.org" in it + g.setValidationRegex("^(?:https?:\\/\\/)?(?:[^.]+\\.)?shib\\.org(\\/.*)?\$") + g = groupServiceForTesting.createGroup(g) + + Optional userRole = roleRepository.findByName("ROLE_USER") + User user = new User(username: "someUser", roles:[userRole.get()], password: "foo", group: g) + userService.save(user) + } + + @WithMockUser(value = "someUser", roles = ["USER"]) + def "test validation by service works properly"() { + given: + DynamicHttpMetadataResolver metadataResolver = new DynamicHttpMetadataResolver() + MetadataQueryProtocolScheme scheme = new MetadataQueryProtocolScheme() + scheme.setContent("http://foo.shib.org/bar") + metadataResolver.setMetadataRequestURLConstructionScheme(scheme) + + when: + IMetadataResolverValidator.ValidationResult result = metadataResolverValidationService.validateIfNecessary(metadataResolver) + + then: + result.isValid() + + when: "using a bad url (no match)" + metadataResolver.getMetadataRequestURLConstructionScheme().setContent("http://foo.shib.com/bar") + result = metadataResolverValidationService.validateIfNecessary(metadataResolver) + + then: + !result.isValid() + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidatorTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidatorTests.groovy new file mode 100644 index 000000000..c82f0aa18 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidatorTests.groovy @@ -0,0 +1,52 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator + +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverValidationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.annotation.Transactional + +@ContextConfiguration(classes = [MetadataResolverValidationConfiguration]) +class FileBackedHttpMetadataResolverValidatorTests extends AbstractBaseDataJpaTest { + @Autowired + MetadataResolverValidationService metadataResolverValidationService + + @Transactional + def setup() { + Group g = new Group() + g.setResourceId("shib") + g.setName("shib") + // This is valid for a url with "shib.org" in it + g.setValidationRegex("^(?:https?:\\/\\/)?(?:[^.]+\\.)?shib\\.org(\\/.*)?\$") + g = groupService.createGroup(g) + + Optional userRole = roleRepository.findByName("ROLE_USER") + User user = new User(username: "someUser", roles:[userRole.get()], password: "foo", group: g) + userService.save(user) + } + + @WithMockUser(value = "someUser", roles = ["USER"]) + def "test validation by service works properly"() { + given: + FileBackedHttpMetadataResolver metadataResolver = new FileBackedHttpMetadataResolver() + metadataResolver.setMetadataURL("http://foo.shib.org/bar") + + when: + IMetadataResolverValidator.ValidationResult result = metadataResolverValidationService.validateIfNecessary(metadataResolver) + + then: + result.isValid() + + when: "using a bad url (no match)" + metadataResolver.setMetadataURL("http://foo.shib.com/bar") + result = metadataResolverValidationService.validateIfNecessary(metadataResolver) + + then: + !result.isValid() + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationServiceConfigurationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationServiceConfigurationTests.groovy similarity index 80% rename from backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationServiceConfigurationTests.groovy rename to backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationServiceConfigurationTests.groovy index 9007cb15e..f2834aa9c 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationServiceConfigurationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationServiceConfigurationTests.groovy @@ -1,4 +1,4 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator import edu.internet2.tier.shibboleth.admin.ui.configuration.TestMetadataResolverValidationConfiguration import org.springframework.beans.factory.annotation.Autowired @@ -11,14 +11,13 @@ import spock.lang.Specification */ @ContextConfiguration(classes=[TestMetadataResolverValidationConfiguration]) class MetadataResolverValidationServiceConfigurationTests extends Specification { - @Autowired @Qualifier("metadataResolverValidationServiceEmpty") MetadataResolverValidationService metadataResolverValidationServiceNoValidators @Autowired - @Qualifier("metadataResolverValidationServiceOneValidator") - MetadataResolverValidationService metadataResolverValidationServiceOneValidator + @Qualifier("metadataResolverValidationService") + MetadataResolverValidationService metadataResolverValidationService def "Validation service with no validators"() { expect: @@ -27,6 +26,6 @@ class MetadataResolverValidationServiceConfigurationTests extends Specification def "Validation service with one validator"() { expect: - !metadataResolverValidationServiceOneValidator.noValidatorsConfigured() + !metadataResolverValidationService.noValidatorsConfigured() } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationServiceTests.groovy similarity index 86% rename from backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationServiceTests.groovy rename to backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationServiceTests.groovy index d62f07170..f0aaf8aa4 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationServiceTests.groovy @@ -1,9 +1,10 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver import spock.lang.Specification import spock.lang.Subject -import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator.* +import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator.* /** * @author Dmitriy Kopylenko @@ -26,7 +27,7 @@ class MetadataResolverValidationServiceTests extends Specification { def "Validation service with one validator not supporting the type of resolver returns default valid result"() { given: 'Sample metadata resolver and validation service with one validator not supporting that type' def resolver = Mock(MetadataResolver) - def validator = Mock(MetadataResolverValidator) + def validator = Mock(IMetadataResolverValidator) validator.supports(_) >> false @Subject def validationService = new MetadataResolverValidationService([validator]) @@ -41,7 +42,7 @@ class MetadataResolverValidationServiceTests extends Specification { def "Validation service with one validator supporting the type of resolver but fails its validation"() { given: 'Sample metadata resolver and validation service with one validator supporting that type' def resolver = Mock(MetadataResolver) - def validator = Mock(MetadataResolverValidator) + def validator = Mock(IMetadataResolverValidator) validator.supports(_) >> true validator.validate(_) >> new ValidationResult('Invalid') @Subject @@ -57,10 +58,10 @@ class MetadataResolverValidationServiceTests extends Specification { def "Validation service with with two validators supporting the type of resolver, first fails, second passes validation"() { given: 'Sample metadata resolver and validation service with two validators supporting that type' def resolver = Mock(MetadataResolver) - def validator1 = Mock(MetadataResolverValidator) + def validator1 = Mock(IMetadataResolverValidator) validator1.supports(_) >> true validator1.validate(_) >> new ValidationResult('Invalid') - def validator2 = Mock(MetadataResolverValidator) + def validator2 = Mock(IMetadataResolverValidator) validator2.supports(_) >> true validator2.validate(_) >> new ValidationResult(null) @Subject @@ -76,9 +77,9 @@ class MetadataResolverValidationServiceTests extends Specification { def "Validation service with with two validators, only one supporting the type of resolver, passes validation"() { given: 'Sample metadata resolver and validation service with two validators, with one supporting that type' def resolver = Mock(MetadataResolver) - def validator1 = Mock(MetadataResolverValidator) + def validator1 = Mock(IMetadataResolverValidator) validator1.supports(_) >> false - def validator2 = Mock(MetadataResolverValidator) + def validator2 = Mock(IMetadataResolverValidator) validator2.supports(_) >> true validator2.validate(_) >> new ValidationResult(null) @Subject @@ -90,4 +91,4 @@ class MetadataResolverValidationServiceTests extends Specification { then: validationResult.valid } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataValidatorTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/ResourceBackedMetadataValidatorTests.groovy similarity index 54% rename from backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataValidatorTests.groovy rename to backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/ResourceBackedMetadataValidatorTests.groovy index 6447a7f1a..66102f4fe 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataValidatorTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/ResourceBackedMetadataValidatorTests.groovy @@ -1,12 +1,18 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers - +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ClasspathMetadataResource +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.SvnMetadataResource +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.ResourceBackedIMetadataResolverValidator import spock.lang.Specification class ResourceBackedMetadataValidatorTests extends Specification { def "Does not support foreign resolver type"() { given: - MetadataResolverValidator validator = new ResourceBackedMetadataResolverValidator() + IMetadataResolverValidator validator = new ResourceBackedIMetadataResolverValidator() FileBackedHttpMetadataResolver resolver = new FileBackedHttpMetadataResolver() expect: @@ -15,7 +21,7 @@ class ResourceBackedMetadataValidatorTests extends Specification { def "Passes validation"() { given: - MetadataResolverValidator validator = new ResourceBackedMetadataResolverValidator() + IMetadataResolverValidator validator = new ResourceBackedIMetadataResolverValidator() ResourceBackedMetadataResolver resolver = new ResourceBackedMetadataResolver().with { it.classpathMetadataResource = new ClasspathMetadataResource() it @@ -28,7 +34,7 @@ class ResourceBackedMetadataValidatorTests extends Specification { def "Does not pass validation with both resource types missing"() { given: - MetadataResolverValidator validator = new ResourceBackedMetadataResolverValidator() + IMetadataResolverValidator validator = new ResourceBackedIMetadataResolverValidator() ResourceBackedMetadataResolver resolver = new ResourceBackedMetadataResolver() expect: @@ -38,7 +44,7 @@ class ResourceBackedMetadataValidatorTests extends Specification { def "Does not pass validation with both resource types present"() { given: - MetadataResolverValidator validator = new ResourceBackedMetadataResolverValidator() + IMetadataResolverValidator validator = new ResourceBackedIMetadataResolverValidator() ResourceBackedMetadataResolver resolver = new ResourceBackedMetadataResolver().with { it.classpathMetadataResource = new ClasspathMetadataResource() it.svnMetadataResource = new SvnMetadataResource() @@ -49,4 +55,4 @@ class ResourceBackedMetadataValidatorTests extends Specification { validator.supports(resolver) !validator.validate(resolver).valid } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepositoryTests.groovy index bfdc8f57a..828d82cbe 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepositoryTests.groovy @@ -1,29 +1,17 @@ package edu.internet2.tier.shibboleth.admin.ui.repository +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.domain.CustomEntityAttributeDefinition +import org.springframework.beans.factory.annotation.Autowired import org.springframework.dao.DataIntegrityViolationException import javax.persistence.EntityManager -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.test.context.ContextConfiguration - -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.domain.CustomEntityAttributeDefinition -import spock.lang.Specification - /** * Tests to validate the repo and model for custom entity attributes * @author chasegawa */ -@DataJpaTest -@ContextConfiguration(classes=[InternationalizationConfiguration]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -class CustomEntityAttributeDefinitionRepositoryTests extends Specification { - +class CustomEntityAttributeDefinitionRepositoryTests extends AbstractBaseDataJpaTest { @Autowired CustomEntityAttributeDefinitionRepository repo @@ -87,7 +75,7 @@ class CustomEntityAttributeDefinitionRepositoryTests extends Specification { then: // Missing non-nullable field should thrown error - final def exception = thrown(DataIntegrityViolationException) + thrown(DataIntegrityViolationException) } def "basic CRUD operations validated"() { @@ -118,7 +106,7 @@ class CustomEntityAttributeDefinitionRepositoryTests extends Specification { def cas = repo.findAll() cas.size() == 1 def caFromDb1 = cas.get(0).asType(CustomEntityAttributeDefinition) - caFromDb1.equals(ca) + caFromDb1 == ca // fetch checks repo.findByName("not a name") == null @@ -128,7 +116,7 @@ class CustomEntityAttributeDefinitionRepositoryTests extends Specification { caFromDb1.with { it.helpText = "some new text that wasn't there before" } - caFromDb1.equals(ca) == false + caFromDb1 != ca when: repo.save(caFromDb1) @@ -139,8 +127,8 @@ class CustomEntityAttributeDefinitionRepositoryTests extends Specification { def cas2 = repo.findAll() cas2.size() == 1 def caFromDb2 = cas2.get(0).asType(CustomEntityAttributeDefinition) - caFromDb2.equals(ca) == false - caFromDb2.equals(caFromDb1) + caFromDb2 != ca + caFromDb2 == caFromDb1 // delete tests when: @@ -173,7 +161,7 @@ class CustomEntityAttributeDefinitionRepositoryTests extends Specification { def cas = repo.findAll() cas.size() == 1 def ca3FromDb = cas.get(0).asType(CustomEntityAttributeDefinition) - ca3FromDb.equals(ca3) + ca3FromDb == ca3 // now update the attribute list items ca3FromDb.with { @@ -192,7 +180,7 @@ class CustomEntityAttributeDefinitionRepositoryTests extends Specification { it.resourceId = ca3FromDb.resourceId it } - caFromDb4.equals(ca4) + caFromDb4 == ca4 // now remove items ca3FromDb.with { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy index de4c0ac9e..f3e36f7dc 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy @@ -1,76 +1,36 @@ package edu.internet2.tier.shibboleth.admin.ui.repository -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.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.security.model.Group -import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository -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.GroupServiceImpl -import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import edu.internet2.tier.shibboleth.admin.ui.service.CustomEntityAttributesDefinitionServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService -import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl -import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import org.apache.lucene.analysis.Analyzer import org.apache.lucene.analysis.en.EnglishAnalyzer import org.opensaml.saml.metadata.resolver.MetadataResolver import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.ComponentScan -import org.springframework.context.annotation.Profile -import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.test.annotation.DirtiesContext -import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration -import spock.lang.Specification - -import java.util.stream.Stream import javax.persistence.EntityManager +import java.util.stream.Stream -/** - * A highly unnecessary test so that I can check to make sure that persistence is correct for the model - */ -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, InternationalizationConfiguration, LocalConfig]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) -@ActiveProfiles(value = "local") -class EntityDescriptorRepositoryTest extends Specification { +@ContextConfiguration(classes = [EDRLocalConfig]) +class EntityDescriptorRepositoryTest extends AbstractBaseDataJpaTest { @Autowired EntityDescriptorRepository entityDescriptorRepository @Autowired - private CustomEntityAttributeDefinitionRepository repository - - @Autowired - EntityManager entityManager - - @Autowired - RoleRepository roleRepository + EntityDescriptorService entityDescriptorService @Autowired - UserRepository userRepository - - @Autowired - GroupsRepository groupRepository - - OpenSamlObjects openSamlObjects = new OpenSamlObjects().with { - it.init() - it - } + EntityManager entityManager @Autowired - EntityDescriptorService service + OpenSamlObjects openSamlObjects def "SHIBUI-553.2"() { when: @@ -102,7 +62,7 @@ class EntityDescriptorRepositoryTest extends Specification { then: noExceptionThrown() } - + def "SHIBUI-1849 - extend data model for ownership"() { given: def group = new Group().with { @@ -110,39 +70,38 @@ class EntityDescriptorRepositoryTest extends Specification { it.description = "some description" it } - groupRepository.saveAndFlush(group) + group = groupService.createGroup(group) - def gList = groupRepository.findAll() + def gList = groupService.findAll() def groupFromDb = gList.get(0).asType(Group) - + def ed = openSamlObjects.unmarshalFromXml(this.class.getResource('/metadata/SHIBUI-553.2.xml').bytes) as EntityDescriptor ed.with { it.idOfOwner = groupFromDb.resourceId } entityDescriptorRepository.saveAndFlush(ed) - + when: def edStreamFromDb = entityDescriptorRepository.findAllStreamByIdOfOwner(null) - + then: - ((Stream)edStreamFromDb).count() == 0 - + ((Stream) edStreamFromDb).count() == 0 + when: def edStreamFromDb2 = entityDescriptorRepository.findAllStreamByIdOfOwner("random value") - + then: - ((Stream)edStreamFromDb2).count() == 0 - + ((Stream) edStreamFromDb2).count() == 0 + when: def edStreamFromDb3 = entityDescriptorRepository.findAllStreamByIdOfOwner(groupFromDb.resourceId) - + then: - ((Stream)edStreamFromDb3).count() == 1 + ((Stream) edStreamFromDb3).count() == 1 } @TestConfiguration - @Profile("local") - static class LocalConfig { + private static class EDRLocalConfig { @Bean MetadataResolver metadataResolver() { new OpenSamlChainingMetadataResolver().with { @@ -156,22 +115,13 @@ class EntityDescriptorRepositoryTest extends Specification { Analyzer analyzer() { return new EnglishAnalyzer() } - - @Bean - GroupServiceImpl groupService(GroupsRepository repo, OwnershipRepository ownershipRepository) { - new GroupServiceImpl().with { - it.groupRepository = repo - it.ownershipRepository = ownershipRepository - return it - } - } - + @Bean - CustomEntityAttributesDefinitionServiceImpl customEntityAttributesDefinitionServiceImpl() { + CustomEntityAttributesDefinitionServiceImpl customEntityAttributesDefinitionServiceImpl(EntityManager entityManager, CustomEntityAttributeDefinitionRepository customEntityAttributeDefinitionRepository) { new CustomEntityAttributesDefinitionServiceImpl().with { - it.entityManager = entityManager - it.repository = repository - return it + it.entityManager = entityManager + it.repository = customEntityAttributeDefinitionRepository + return it } } } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepositoryTests.groovy index f233684fd..4041b9be1 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepositoryTests.groovy @@ -1,35 +1,25 @@ package edu.internet2.tier.shibboleth.admin.ui.repository -import javax.persistence.EntityManager - -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.test.context.ContextConfiguration - import com.fasterxml.jackson.databind.ObjectMapper - -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.domain.CustomEntityAttributeDefinition +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator -import spock.lang.Specification - -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -class FilterRepositoryTests extends Specification { +import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.transaction.annotation.Transactional +class FilterRepositoryTests extends AbstractBaseDataJpaTest { @Autowired - FilterRepository repositoryUnderTest - - @Autowired - EntityManager entityManager + FilterRepository filterRepository + + @Transactional + def setup() { + filterRepository.deleteAll() + } + + def cleanup() { + filterRepository.deleteAll() + } def "EntityAttributesFilter hashcode works as desired"() { given: @@ -64,26 +54,23 @@ class FilterRepositoryTests extends Specification { when: def filter = new ObjectMapper().readValue(entityAttributesFilterJson.bytes, EntityAttributesFilter) - def persistedFilter = repositoryUnderTest.save(filter) - entityManager.flush() + def persistedFilter = filterRepository.save(filter) + def item1 = filterRepository.findByResourceId("29a5d409-562a-41cd-acee-e9b3d7098d05") then: - def item1 = repositoryUnderTest.findByResourceId(persistedFilter.resourceId) - entityManager.clear() - def item2 = repositoryUnderTest.findByResourceId(persistedFilter.resourceId) - - item1.hashCode() == item2.hashCode() + item1.hashCode() == persistedFilter.hashCode() } + @WithMockAdmin def "NameIdFormatFilter is able to be persisted to RDBMS"() { given: def nameIdFormatFilter = TestObjectGenerator.nameIdFormatFilter() when: - def persistedFilter = repositoryUnderTest.save(nameIdFormatFilter) + def persistedFilter = filterRepository.save(nameIdFormatFilter) then: persistedFilter.audId > 0L persistedFilter.formats.size() == 1 } -} +} \ No newline at end of file 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 99ff22481..2030a25d6 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 @@ -1,26 +1,15 @@ package edu.internet2.tier.shibboleth.admin.ui.repository -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver 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.security.service.UserService import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService -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 -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.test.annotation.DirtiesContext -import org.springframework.test.context.ContextConfiguration -import spock.lang.Specification +import org.springframework.test.annotation.Rollback import javax.persistence.EntityManager @@ -29,12 +18,8 @@ import static edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttrib /** * Testing persistence of the MetadataResolver models */ -@DataJpaTest -@ContextConfiguration(classes = [CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) -class MetadataResolverRepositoryTests extends Specification { +@Rollback +class MetadataResolverRepositoryTests extends AbstractBaseDataJpaTest { @Autowired MetadataResolverRepository metadataResolverRepository @@ -245,4 +230,4 @@ class MetadataResolverRepositoryTests extends Specification { } resolver } -} +} \ No newline at end of file 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 41aa6a9cb..b9e9856dd 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 @@ -1,42 +1,29 @@ package edu.internet2.tier.shibboleth.admin.ui.scheduled -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.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation 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.service.IGroupService import edu.internet2.tier.shibboleth.admin.ui.service.FileCheckingFileWritingService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.test.context.ContextConfiguration import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input -import spock.lang.Specification /** * @author Bill Smith (wsmith@unicon.net) */ -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -class EntityDescriptorFilesScheduledTasksTests extends Specification { +class EntityDescriptorFilesScheduledTasksTests extends AbstractBaseDataJpaTest { @Autowired OpenSamlObjects openSamlObjects @Autowired - IGroupService groupService - + JPAEntityDescriptorServiceImpl service + def tempPath = "/tmp/shibui" def directory @@ -46,10 +33,7 @@ class EntityDescriptorFilesScheduledTasksTests extends Specification { def entityDescriptorFilesScheduledTasks def randomGenerator - - - def service - + def setup() { randomGenerator = new RandomGenerator() tempPath = tempPath + randomGenerator.randomRangeInt(10000, 20000) @@ -57,9 +41,10 @@ class EntityDescriptorFilesScheduledTasksTests extends Specification { entityDescriptorFilesScheduledTasks = new EntityDescriptorFilesScheduledTasks(tempPath, entityDescriptorRepository, openSamlObjects, new FileCheckingFileWritingService()) directory = new File(tempPath) directory.mkdir() - - service = new JPAEntityDescriptorServiceImpl() - service.openSamlObjects = openSamlObjects + } + + def cleanup() { + directory.deleteDir() } def "generateEntityDescriptorFiles properly generates a file from an Entity Descriptor"() { @@ -131,8 +116,4 @@ class EntityDescriptorFilesScheduledTasksTests extends Specification { def files = new File(tempPath, file) files.size() == 0 } - - def cleanup() { - directory.deleteDir() - } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy index a19e4f807..c4a76e832 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy @@ -1,27 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.security.controller -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* - -import javax.persistence.EntityManager - -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.http.HttpStatus -import org.springframework.http.MediaType -import org.springframework.security.test.context.support.WithMockUser -import org.springframework.test.annotation.DirtiesContext -import org.springframework.test.annotation.Rollback -import org.springframework.test.context.ContextConfiguration -import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.setup.MockMvcBuilders - -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.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupDeleteException import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException @@ -29,79 +8,44 @@ import edu.internet2.tier.shibboleth.admin.ui.security.model.Group import edu.internet2.tier.shibboleth.admin.ui.security.model.Role import edu.internet2.tier.shibboleth.admin.ui.security.model.User import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository -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.GroupServiceImpl -import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService +import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin import groovy.json.JsonOutput -import spock.lang.Specification +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.MediaType +import org.springframework.test.annotation.Rollback +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.transaction.annotation.Transactional -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, TestConfiguration, InternationalizationConfiguration, SearchConfiguration]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@DirtiesContext -class GroupsControllerIntegrationTests extends Specification { - @Autowired - EntityManager entityManager +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* +@Rollback +class GroupsControllerIntegrationTests extends AbstractBaseDataJpaTest { @Autowired GroupsRepository groupsRepository - - @Autowired - GroupServiceImpl groupService - - @Autowired - RoleRepository roleRepository - - @Autowired - UserRepository userRepository - - @Autowired - UserService userService - + static RESOURCE_URI = '/api/admin/groups' MockMvc mockMvc - + + @Transactional def setup() { - groupService.ensureAdminGroupExists() - GroupController groupController = new GroupController().with ({ it.groupService = this.groupService it }) mockMvc = MockMvcBuilders.standaloneSetup(groupController).build() - - if (roleRepository.count() == 0) { - def roles = [new Role().with { - name = 'ROLE_ADMIN' - it - }, new Role().with { - name = 'ROLE_USER' - it - }, new Role().with { - name = 'ROLE_NONE' - it - }] - roles.each { - roleRepository.save(it) - } + + if (userRepository.findByUsername("someUser").isEmpty()) { + Optional userRole = roleRepository.findByName("ROLE_USER") + User user = new User(username: "someUser", roles: [userRole.get()], password: "foo") + userService.save(user) } - - Optional adminRole = roleRepository.findByName("ROLE_ADMIN") - User adminUser = new User(username: "admin", roles: [adminRole.get()], password: "foo") - userService.save(adminUser) - - Optional userRole = roleRepository.findByName("ROLE_USER") - User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") - userService.save(user) - entityManager.flush() } - - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def 'POST new group persists properly'() { given: def newGroup = [name: 'Foo', @@ -130,14 +74,13 @@ class GroupsControllerIntegrationTests extends Specification { mockMvc.perform(post(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON) .content(JsonOutput.toJson(newGroup)) .accept(MediaType.APPLICATION_JSON)) - 1 == 2 + false } catch (Throwable expected) { expected instanceof GroupExistsConflictException } } - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def 'PUT (update) existing group persists properly'() { given: groupsRepository.deleteByResourceId("AAA") @@ -172,14 +115,13 @@ class GroupsControllerIntegrationTests extends Specification { mockMvc.perform(put(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON) .content(JsonOutput.toJson(newGroup)) .accept(MediaType.APPLICATION_JSON)) - 1 == 2 + false } catch (Throwable expected) { expected instanceof EntityNotFoundException } } - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def 'GET checks for groups (when there are existing groups)'() { given: groupsRepository.deleteByResourceId("AAA") @@ -214,26 +156,22 @@ class GroupsControllerIntegrationTests extends Specification { // 'GET request for a single non-existent group in a system that has groups' try { mockMvc.perform(get("$RESOURCE_URI/CCC")) - 1 == 2 + false } catch (Throwable expected) { expected instanceof EntityNotFoundException } } - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def 'DELETE performs correctly when group attached to a user'() { // When the user is created in the setup method above, a new group "someUser" is created to be associated with that user // User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") // userService.save(user) - - when: 'try to delete group that is attached to a user' - def nothingtodo - - then: + + expect: try { mockMvc.perform(delete("$RESOURCE_URI/someUser")) - 1 == 2 + false } catch(Throwable expected) { expected instanceof GroupDeleteException } @@ -254,4 +192,32 @@ class GroupsControllerIntegrationTests extends Specification { then: mockMvc.perform(delete("$RESOURCE_URI/someUser")) } + + def 'group regex checks'() { + given: + groupsRepository.deleteByResourceId("AAA") + Group groupAAA = new Group().with({ + it.name = "AAA" + it.description = "AAA" + it.resourceId = "AAA" + it.validationRegex = "/foo.*/" + it + }) + + when: + def result = mockMvc.perform(post(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON) + .content(JsonOutput.toJson(groupAAA)).accept(MediaType.APPLICATION_JSON)) + + then: + result.andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("\$.name").value("AAA")) + .andExpect(jsonPath("\$.resourceId").value("AAA")) + .andExpect(jsonPath("\$.description").value("AAA")) + .andExpect(jsonPath("\$.validationRegex").value("/foo.*/")) + + !groupService.doesStringMatchGroupPattern("AAA", "foobar") + !groupService.doesStringMatchGroupPattern("AAA", "something") + groupService.doesStringMatchGroupPattern("AAA", "/foobar/") + } } \ No newline at end of file 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 a5e604b3d..84f6cb33c 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,86 +1,50 @@ package edu.internet2.tier.shibboleth.admin.ui.security.controller -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.controller.support.RestControllersSupport +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin +import groovy.json.JsonOutput import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.ComponentScan -import org.springframework.context.annotation.Profile -import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.http.MediaType import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder -import org.springframework.security.test.context.support.WithMockUser -import org.springframework.test.annotation.DirtiesContext import org.springframework.test.annotation.Rollback -import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.transaction.annotation.Transactional +import org.springframework.web.client.HttpClientErrorException +import org.springframework.web.util.NestedServletException -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.SerializationFeature -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule -import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter -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.controller.support.RestControllersSupport -import edu.internet2.tier.shibboleth.admin.ui.security.model.Group -import edu.internet2.tier.shibboleth.admin.ui.security.model.Role -import edu.internet2.tier.shibboleth.admin.ui.security.model.User -import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository -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.GroupServiceForTesting -import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl -import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService -import groovy.json.JsonOutput -import spock.lang.Specification - -import static org.springframework.http.MediaType.* -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* - -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, TestConfiguration, InternationalizationConfiguration, SearchConfiguration, LocalConfig]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@DirtiesContext -@ActiveProfiles(["no-auth", "local"]) -@ComponentScan(basePackages="{ edu.internet2.tier.shibboleth.admin.ui.configuration }") -class UsersControllerIntegrationTests extends Specification { - @Autowired - GroupsRepository groupsRepository - - @Autowired - GroupServiceForTesting groupService - - @Autowired - def ObjectMapper mapper - - @Autowired - OwnershipRepository ownershipRepository - - @Autowired - RoleRepository roleRepository - - @Autowired - UserRepository userRepository - +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.jsonPath +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@ContextConfiguration(classes=[UCILocalConfig]) +@Rollback +class UsersControllerIntegrationTests extends AbstractBaseDataJpaTest { @Autowired - UserService userService + ObjectMapper mapper - def MockMvc mockMvc + MockMvc mockMvc def users static RESOURCE_URI = '/api/admin/users' + static VALIDATION_REGEX = "/^(?:https?:\\/\\/)?(?:[^.]+\\.)?shib\\.org(\\/.*)?\$/" def setup() { def controller = new UsersController(userRepository, userService) @@ -95,48 +59,30 @@ class UsersControllerIntegrationTests extends Specification { } userRepository.flush() - roleRepository.deleteAll() - roleRepository.flush() - groupService.clearAllForTesting() //leaves us just the admingroup - def groups = [ new Group().with { it.name = "A1" it.description = "AAA Group" it.resourceId = "AAA" + it.validationRegex = VALIDATION_REGEX it }, new Group().with { it.name = "B1" it.description = "BBB Group" it.resourceId = "BBB" + it.validationRegex = VALIDATION_REGEX it }] groups.each { try { - groupsRepository.save(it) + groupRepository.save(it) } catch (Throwable e) { - // Must already exist (from a unit test) + // ??? } } - groupsRepository.flush() + groupRepository.flush() - if (roleRepository.count() == 0) { - def roles = [new Role().with { - name = 'ROLE_ADMIN' - it - }, new Role().with { - name = 'ROLE_USER' - it - }, new Role().with { - name = 'ROLE_NONE' - it - }] - roles.each { - roleRepository.save(it) - } - } - roleRepository.flush() if (userRepository.count() == 0) { users = [new User().with { username = 'admin' @@ -172,12 +118,12 @@ class UsersControllerIntegrationTests extends Specification { it }] users.each { - it = userService.save(it) + userService.save(it) } } } - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def 'GET ALL users (when there are existing users)'() { // given: users created in setup @@ -190,21 +136,25 @@ class UsersControllerIntegrationTests extends Specification { .andExpect(jsonPath("\$.[0].emailAddress").value("joe@institution.edu")) .andExpect(jsonPath("\$.[0].role").value("ROLE_ADMIN")) .andExpect(jsonPath("\$.[0].groupId").value("admingroup")) + .andExpect(jsonPath("\$.[0].userGroups.[0].validationRegex").isEmpty()) .andExpect(jsonPath("\$.[1].username").value("nonadmin")) .andExpect(jsonPath("\$.[1].emailAddress").value("peter@institution.edu")) .andExpect(jsonPath("\$.[1].role").value("ROLE_USER")) - .andExpect(jsonPath("\$.[1].groupId").value("nonadmin")) + .andExpect(jsonPath("\$.[1].groupId").value("nonadmin")) + .andExpect(jsonPath("\$.[1].userGroups.[0].validationRegex").value(null)) .andExpect(jsonPath("\$.[2].username").value("none")) .andExpect(jsonPath("\$.[2].emailAddress").value("badboy@institution.edu")) .andExpect(jsonPath("\$.[2].role").value("ROLE_NONE")) - .andExpect(jsonPath("\$.[2].groupId").value("none")) + .andExpect(jsonPath("\$.[2].groupId").value("none")) + .andExpect(jsonPath("\$.[2].userGroups.[0].validationRegex").value(null)) .andExpect(jsonPath("\$.[3].username").value("anonymousUser")) .andExpect(jsonPath("\$.[3].emailAddress").value("anon@institution.edu")) .andExpect(jsonPath("\$.[3].role").value("ROLE_ADMIN")) .andExpect(jsonPath("\$.[3].groupId").value("admingroup")) + .andExpect(jsonPath("\$.[3].userGroups.[0].validationRegex").isEmpty()) } - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def 'GET ONE existing user'() { when: 'GET request is made for one existing user' def result = mockMvc.perform(get("$RESOURCE_URI/admin")) @@ -216,9 +166,10 @@ class UsersControllerIntegrationTests extends Specification { .andExpect(jsonPath("\$.emailAddress").value("joe@institution.edu")) .andExpect(jsonPath("\$.role").value("ROLE_ADMIN")) .andExpect(jsonPath("\$.groupId").value("admingroup")) + .andExpect(jsonPath("\$.userGroups.[0].validationRegex").isEmpty()) } - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def 'GET ONE NON-existing user'() { when: 'GET request is made for one NON-existing user' def result = mockMvc.perform(get("$RESOURCE_URI/bogus")) @@ -227,8 +178,7 @@ class UsersControllerIntegrationTests extends Specification { result.andExpect(status().isNotFound()) } - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def 'DELETE ONE existing user'() { when: 'GET request is made for one existing user' def result = mockMvc.perform(get("$RESOURCE_URI/nonadmin")) @@ -244,16 +194,15 @@ class UsersControllerIntegrationTests extends Specification { // 'GET request is made for the deleted user' try { - result = mockMvc.perform(get("$RESOURCE_URI/nonadmin")) + mockMvc.perform(get("$RESOURCE_URI/nonadmin")) false } - catch (org.springframework.web.util.NestedServletException expectedResult) { - expectedResult.getCause() instanceof org.springframework.web.client.HttpClientErrorException + catch (NestedServletException expectedResult) { + expectedResult.getCause() instanceof HttpClientErrorException } } - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def 'POST new user persists properly'() { given: def newUser = [firstName: 'Foo', @@ -273,8 +222,7 @@ class UsersControllerIntegrationTests extends Specification { result.andExpect(status().isOk()) } - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def 'POST new duplicate username returns 409'() { given: def newUser = [firstName: 'Foo', @@ -298,14 +246,13 @@ class UsersControllerIntegrationTests extends Specification { result.andExpect(status().isConflict()) } - @Rollback - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def 'PATCH updates user properly'() { given: - def String userString = mockMvc.perform(get("$RESOURCE_URI/none")).andReturn().getResponse().getContentAsString() - def User user = mapper.readValue(userString, User.class); + String userString = mockMvc.perform(get("$RESOURCE_URI/none")).andReturn().getResponse().getContentAsString() + User user = mapper.readValue(userString, User.class) user.setFirstName("somethingnew") - + when: def result = mockMvc.perform(patch("$RESOURCE_URI/$user.username") .contentType(MediaType.APPLICATION_JSON) @@ -314,37 +261,39 @@ class UsersControllerIntegrationTests extends Specification { then: result.andExpect(status().isOk()) - + when: user.setGroupId("AAA") def resultNewGroup = mockMvc.perform(patch("$RESOURCE_URI/$user.username").contentType(MediaType.APPLICATION_JSON) .content(mapper.writeValueAsString(user)).accept(MediaType.APPLICATION_JSON)) - + then: resultNewGroup.andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("\$.groupId").value("AAA")) - - def groups = ownershipRepository.findAllGroupsForUser(user.username) + .andExpect(jsonPath("\$.userGroups.[0].validationRegex").value(VALIDATION_REGEX)) + + def groups = ownershipRepository.findAllGroupsForUser(user.username) groups.size() == 1 - + when: 'Updating user role to admin puts the user in the admin group' user.setRole("ROLE_ADMIN") user.setGroupId("AAA") // Dont care that this is different, ROLE_ADMIN should take precedence def resultUserNewRole = mockMvc.perform(patch("$RESOURCE_URI/$user.username").contentType(MediaType.APPLICATION_JSON) .content(mapper.writeValueAsString(user)).accept(MediaType.APPLICATION_JSON)) - + then: resultUserNewRole.andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("\$.groupId").value("admingroup")) - + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("\$.groupId").value("admingroup")) + .andExpect(jsonPath("\$.userGroups.[0].validationRegex").isEmpty()) + def groupsCheck = ownershipRepository.findAllGroupsForUser(user.username) groupsCheck.size() == 1 - + } - @WithMockUser(value = "admin", roles = ["ADMIN"]) + @WithMockAdmin def 'PATCH detects unknown username'() { given: def newUser = [firstName: 'Foo', @@ -362,27 +311,15 @@ class UsersControllerIntegrationTests extends Specification { then: result.andExpect(status().isNotFound()) } - - @org.springframework.boot.test.context.TestConfiguration - @Profile(value = "local") - static class LocalConfig { - @Bean - GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { - GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { - it.groupRepository = repo - it.ownershipRepository = ownershipRepository - return it - }) - result.ensureAdminGroupExists() - return result - } - + + @TestConfiguration + private static class UCILocalConfig { @Bean ObjectMapper objectMapper() { JavaTimeModule module = new JavaTimeModule() LocalDateTimeDeserializer localDateTimeDeserializer = new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS")) module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer) - + return Jackson2ObjectMapperBuilder.json().modules(module).featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build() } } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepositoryTests.groovy index b2d079dfc..a81443166 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepositoryTests.groovy @@ -1,43 +1,24 @@ package edu.internet2.tier.shibboleth.admin.ui.security.repository -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Profile -import org.springframework.dao.DataIntegrityViolationException - -import javax.persistence.EntityManager -import javax.transaction.Transactional - +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.context.annotation.Bean -import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.dao.DataIntegrityViolationException import org.springframework.test.annotation.Rollback -import org.springframework.test.context.ActiveProfiles -import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.annotation.Transactional -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.security.model.Group -import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership -import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.GroupUpdatedEntityListener -import spock.lang.Specification +import javax.persistence.EntityManager /** * Tests to validate the repo and model for groups * @author chasegawa */ -@DataJpaTest -@ContextConfiguration(classes=[InternationalizationConfiguration, LocalConfig]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@ActiveProfiles(["test","local"]) -class GroupsRepositoryTests extends Specification { +@Rollback +class GroupsRepositoryTests extends AbstractBaseDataJpaTest { @Autowired GroupsRepository groupsRepo - - @Autowired - OwnershipRepository ownershipRepository - + @Transactional def setup() { groupsRepo.deleteAll() @@ -90,7 +71,7 @@ class GroupsRepositoryTests extends Specification { ownershipRepository.save(it) } } - + def "group ownership tests"() { when: "Simple create test" def group = new Group().with { @@ -99,18 +80,20 @@ class GroupsRepositoryTests extends Specification { it.resourceId = "g1" it } - Group savedGroup = groupsRepo.saveAndFlush(group) - Collection all = ownershipRepository.findAllByOwner(savedGroup) + groupsRepo.saveAndFlush(group) + entityManager.clear() + Group groupFromDb = groupsRepo.findByResourceId("g1") + groupFromDb.getOwnedItems() + Collection groupOwnedItems = ownershipRepository.findAllByOwner(groupFromDb) then: - all.size() == 3 - savedGroup.ownedItems.size() == 3 - all.each { - savedGroup.ownedItems.contains(it) + groupOwnedItems.size() == 3 + groupFromDb.getOwnedItems().size() == 3 + groupOwnedItems.each { + groupFromDb.ownedItems.contains(it) } } - @Rollback def "simple create test"() { given: def group = new Group().with { @@ -134,12 +117,12 @@ class GroupsRepositoryTests extends Specification { // save check def gList = groupsRepo.findAll() gList.size() == 1 - def groupFromDb = gList.get(0).asType(Group) - groupFromDb.equals(group) + def groupFromDb = gList.get(0) as Group + groupFromDb == group // fetch checks groupsRepo.findByResourceId("not an id") == null - groupsRepo.findByResourceId(groupFromDb.resourceId).equals(group) + groupsRepo.findByResourceId(groupFromDb.resourceId) == group } def "expected error"() { @@ -154,7 +137,7 @@ class GroupsRepositoryTests extends Specification { def gList = groupsRepo.findAll() then: - gList.size() == 0 + gList.isEmpty() // save check when: @@ -163,9 +146,10 @@ class GroupsRepositoryTests extends Specification { then: // Missing non-nullable field (name) should thrown error thrown(DataIntegrityViolationException) + entityManager.clear() + groupsRepo.findAll().isEmpty() } - @Rollback def "basic CRUD operations validated"() { given: def group = new Group().with { @@ -189,8 +173,8 @@ class GroupsRepositoryTests extends Specification { // save check def gList = groupsRepo.findAll() gList.size() == 1 - def groupFromDb = gList.get(0).asType(Group) - groupFromDb.equals(group) + def groupFromDb = gList.get(0) as Group + groupFromDb == group // update check groupFromDb.with { @@ -204,16 +188,16 @@ class GroupsRepositoryTests extends Specification { then: def gList2 = groupsRepo.findAll() gList2.size() == 1 - def groupFromDb2 = gList2.get(0).asType(Group) + def groupFromDb2 = gList2.get(0) as Group groupFromDb2.equals(group) == false - groupFromDb2.equals(groupFromDb) + groupFromDb2 == groupFromDb // delete tests when: groupsRepo.delete(groupFromDb2) then: - groupsRepo.findAll().size() == 0 + groupsRepo.findAll().isEmpty() when: def nothingThere = groupsRepo.findByResourceId(null) @@ -221,15 +205,4 @@ class GroupsRepositoryTests extends Specification { then: nothingThere == null } - - @TestConfiguration - @Profile("local") - static class LocalConfig { - @Bean - GroupUpdatedEntityListener groupUpdatedEntityListener(OwnershipRepository repo) { - GroupUpdatedEntityListener result = new GroupUpdatedEntityListener() - result.init(repo) - return result - } - } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepositoryTests.groovy index 5e36f19ce..7da58882b 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepositoryTests.groovy @@ -1,36 +1,20 @@ package edu.internet2.tier.shibboleth.admin.ui.security.repository -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.test.annotation.Rollback -import org.springframework.test.context.ContextConfiguration -import org.springframework.transaction.annotation.Transactional - -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownable import edu.internet2.tier.shibboleth.admin.ui.security.model.OwnableType import edu.internet2.tier.shibboleth.admin.ui.security.model.Owner import edu.internet2.tier.shibboleth.admin.ui.security.model.OwnerType import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership -import spock.lang.Specification +import org.springframework.transaction.annotation.Transactional /** * Tests to validate the repo and model for groups * @author chasegawa */ -@DataJpaTest -@ContextConfiguration(classes=[InternationalizationConfiguration]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -class OwnershipRepositoryTests extends Specification { - @Autowired - OwnershipRepository repo - +class OwnershipRepositoryTests extends AbstractBaseDataJpaTest { @Transactional def setup() { - repo.deleteAll() def ownerships = [ new Ownership().with { it.ownedId = "aaa" @@ -76,21 +60,20 @@ class OwnershipRepositoryTests extends Specification { } ] ownerships.each { - repo.save(it) + ownershipRepository.save(it) } } - - @Rollback + def "test clearUsersGroups"() { when: "remove entries where the user is the owned object of a group" - repo.clearUsersGroups("aaa") - def result = repo.findAllGroupsForUser("aaa") + ownershipRepository.clearUsersGroups("aaa") + def result = ownershipRepository.findAllGroupsForUser("aaa") then: result.size() == 0 when: "find objects owned by user aaa has not changed" - result = repo.findOwnedByUser("aaa") + result = ownershipRepository.findOwnedByUser("aaa") then: result.size() == 1 @@ -102,28 +85,26 @@ class OwnershipRepositoryTests extends Specification { } when: "remove entries where the user is the owned object of groups" - repo.clearUsersGroups("ccc") - result = repo.findAllGroupsForUser("ccc") + ownershipRepository.clearUsersGroups("ccc") + result = ownershipRepository.findAllGroupsForUser("ccc") then: result.size() == 0 } - @Rollback def "test deleteEntriesForOwnedObject"() { when: "remove entries where the user is the owned object of a group" - repo.deleteEntriesForOwnedObject(new Ownable() { + ownershipRepository.deleteEntriesForOwnedObject(new Ownable() { String getObjectId() { return "aaa" } - OwnableType getOwnableType() { OwnableType.USER } }) - def result = repo.findAllGroupsForUser("aaa") + def result = ownershipRepository.findAllGroupsForUser("aaa") then: result.size() == 0 when: "find objects owned by user aaa has not changed" - result = repo.findOwnedByUser("aaa") + result = ownershipRepository.findOwnedByUser("aaa") then: result.size() == 1 @@ -135,12 +116,11 @@ class OwnershipRepositoryTests extends Specification { } when: "remove entries where the user is the owned object of groups" - repo.deleteEntriesForOwnedObject(new Ownable() { + ownershipRepository.deleteEntriesForOwnedObject(new Ownable() { String getObjectId() { return "ccc" } - OwnableType getOwnableType() { OwnableType.USER } }) - result = repo.findAllGroupsForUser("ccc") + result = ownershipRepository.findAllGroupsForUser("ccc") then: result.size() == 0 @@ -152,9 +132,8 @@ class OwnershipRepositoryTests extends Specification { userIds.add("aaa") userIds.add("bbb") userIds.add("ccc") - def result = repo.findUsersByOwner(new Owner() { + def result = ownershipRepository.findUsersByOwner(new Owner() { String getOwnerId() { return "g1" } - OwnerType getOwnerType() { OwnerType.GROUP } }) @@ -166,9 +145,8 @@ class OwnershipRepositoryTests extends Specification { } when: - result = repo.findUsersByOwner(new Owner() { + result = ownershipRepository.findUsersByOwner(new Owner() { String getOwnerId() { return "aaa" } - OwnerType getOwnerType() { return OwnerType.USER } }) @@ -178,7 +156,7 @@ class OwnershipRepositoryTests extends Specification { def "test findOwnedByUser"() { when: "find objects owned by user" - def result = repo.findOwnedByUser("aaa") + def result = ownershipRepository.findOwnedByUser("aaa") then: result.size() == 1 @@ -195,7 +173,7 @@ class OwnershipRepositoryTests extends Specification { ArrayList groupIds = new ArrayList<>() groupIds.add("g1") groupIds.add("g2") - def result = repo.findOwnableObjectOwners(new Ownable() { + def result = ownershipRepository.findOwnableObjectOwners(new Ownable() { String getObjectId() { return "ccc" } OwnableType getOwnableType() { return OwnableType.USER } }) @@ -211,7 +189,7 @@ class OwnershipRepositoryTests extends Specification { def "test findAllGroupsForUser"() { when: "find all groups for user aaa" - def result = repo.findAllGroupsForUser("aaa") + def result = ownershipRepository.findAllGroupsForUser("aaa") then: result.size() == 1 @@ -224,7 +202,7 @@ class OwnershipRepositoryTests extends Specification { ArrayList groupIds = new ArrayList<>() groupIds.add("g1") groupIds.add("g2") - result = repo.findAllGroupsForUser("ccc") + result = ownershipRepository.findAllGroupsForUser("ccc") then: result.size() == 2 @@ -241,9 +219,8 @@ class OwnershipRepositoryTests extends Specification { userIds.add("aaa") userIds.add("bbb") userIds.add("ccc") - def result = repo.findAllByOwner(new Owner() { + def result = ownershipRepository.findAllByOwner(new Owner() { String getOwnerId() { return "g1" } - OwnerType getOwnerType() { return OwnerType.GROUP } }) @@ -255,9 +232,8 @@ class OwnershipRepositoryTests extends Specification { } when: "Find all items owned by user aaa" - result = repo.findAllByOwner(new Owner() { + result = ownershipRepository.findAllByOwner(new Owner() { String getOwnerId() { return "aaa" } - OwnerType getOwnerType() { return OwnerType.USER } }) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy new file mode 100644 index 000000000..c88838875 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy @@ -0,0 +1,83 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service + +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import org.springframework.test.annotation.Rollback + +@Rollback +class GroupServiceTests extends AbstractBaseDataJpaTest { + def "Test setting group regex works"() { + given: + Group g = new Group() + g.setResourceId("twitter") + g.setName("twitter") + g.setValidationRegex(null) + + when: + try { + g = groupService.createGroup(g) + } catch (Exception shouldNotOccur) { + false + } + + then: + g.getValidationRegex() == null + + when: + g.setValidationRegex("/\\w\\b\\w/") + try { + g = groupService.updateGroup(g) + } catch (Exception shouldNotOccur) { + false + } + + then: + g.getValidationRegex() == "/\\w\\b\\w/" + + when: + g.setValidationRegex("/^(?:https?:\\/\\/)?(?:[^.]+\\.)?shib\\.org(\\/.*)?\$/") + try { + g = groupService.updateGroup(g) + } catch (Exception shouldNotOccur) { + false + } + + then: + g.getValidationRegex() == "/^(?:https?:\\/\\/)?(?:[^.]+\\.)?shib\\.org(\\/.*)?\$/" + + when: + g.setValidationRegex("*") + + then: + try { + groupService.updateGroup(g) + false + } catch (InvalidGroupRegexException shouldOccur) { + true + } + } + + def "Group regex evaluates properly" () { + when: + Group g = new Group() + g.setResourceId("AAA") + g.setName("AAA") + g.setValidationRegex("/foo.*/") + groupRepository.saveAndFlush(g) + + then: + !groupService.doesStringMatchGroupPattern("AAA", "foobar") + !groupService.doesStringMatchGroupPattern("AAA", "something") + groupService.doesStringMatchGroupPattern("AAA", "/foobar/") + + when: + g.setValidationRegex("foo.*") + groupRepository.saveAndFlush(g) + + then: + groupService.doesStringMatchGroupPattern("AAA", "foobar") + !groupService.doesStringMatchGroupPattern("AAA", "something") + groupService.doesStringMatchGroupPattern("AAA", "/foobar/") + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy index 80ce68ab9..73c56aa91 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy @@ -1,326 +1,50 @@ package edu.internet2.tier.shibboleth.admin.ui.security.service -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.SerializationFeature -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule -import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer -import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration -import edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType -import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor -import edu.internet2.tier.shibboleth.admin.ui.domain.IActivatable -import edu.internet2.tier.shibboleth.admin.ui.security.model.* -import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository -import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.ComponentScan -import org.springframework.context.annotation.Profile -import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder -import org.springframework.security.test.context.support.WithMockUser -import org.springframework.test.annotation.DirtiesContext -import org.springframework.test.annotation.Rollback -import org.springframework.test.context.ActiveProfiles -import org.springframework.test.context.ContextConfiguration -import org.springframework.transaction.annotation.Transactional -import spock.lang.Specification - -import javax.persistence.EntityManager -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, CustomPropertiesConfiguration, LocalConfig]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@DirtiesContext -@ActiveProfiles(["test", "local"]) -@ComponentScan(basePackages="{ edu.internet2.tier.shibboleth.admin.ui.configuration }") -class UserServiceTests extends Specification { - - @Autowired - EntityManager entityManager - - @Autowired - GroupServiceForTesting groupService - - @Autowired - OwnershipRepository ownershipRepository - - @Autowired - RoleRepository roleRepository - - @Autowired - ShibUIConfiguration shibUIConfiguration - - @Autowired - UserRepository userRepository - - @Autowired - UserService userService - - def users - - @Transactional - def setup() { - userRepository.findAll().forEach { - userService.delete(it.getUsername()) - } - userRepository.flush() - - roleRepository.deleteAll() - roleRepository.flush() - groupService.clearAllForTesting() //leaves us just the admingroup - - shibUIConfiguration.roles.each { it -> - def role = new Role(name: it) - roleRepository.saveAndFlush(role) - } - - if (userRepository.count() == 0) { - users = [new User().with { - username = 'admin' - password = '{noop}adminpass' - firstName = 'Joe' - lastName = 'Doe' - emailAddress = 'joe@institution.edu' - roles.add(roleRepository.findByName('ROLE_ADMIN').get()) - it - }, new User().with { - username = 'nonadmin' - password = '{noop}nonadminpass' - firstName = 'Peter' - lastName = 'Vandelay' - emailAddress = 'peter@institution.edu' - roles.add(roleRepository.findByName('ROLE_USER').get()) - it - }, new User().with { - username = 'robot' - password = '{noop}nonepass' - firstName = 'Bad' - lastName = 'robot' - emailAddress = 'badboy@institution.edu' - roles.add(roleRepository.findByName('ROLE_ENABLE').get()) - it - }, new User().with { - username = 'robot2' - password = '{noop}nonepass' - firstName = 'Bad2' - lastName = 'robot2' - emailAddress = 'badboy2@institution.edu' - roles.add(roleRepository.findByName('ROLE_ENABLE').get()) - it - }] - users.each { - it = userService.save(it) - } - } - } - - @WithMockUser(value = "admin", roles = ["ADMIN"]) - def "Validate isAuthorizedFor with admin user (always auth)"() { - when: "the object doesn't matter if user is admin" - def isAuth = userService.isAuthorizedFor(new Ownable(){ - String getObjectId() { return null } - OwnableType getOwnableType() { return null } - }); - - then: - isAuth - } - - @WithMockUser(value = "admin", roles = ["ADMIN"]) - def "Admin can activate"() { - when: "the object doesn't matter if user is admin" - def canEnable = userService.currentUserCanEnable(new IActivatable() { - ActivatableType getActivatableType() { return ActivatableType.ENTITY_DESCRIPTOR } - void setEnabled(Boolean enabled) { } - }); - - then: - canEnable - - when: "the object doesn't matter if user is admin" - canEnable = userService.currentUserCanEnable(new IActivatable() { - ActivatableType getActivatableType() { return ActivatableType.FILTER } - void setEnabled(Boolean enabled) { } - }); - - then: - canEnable - - when: "the object doesn't matter if user is admin" - canEnable = userService.currentUserCanEnable(new IActivatable() { - ActivatableType getActivatableType() { return ActivatableType.METADATA_RESOLVER } - void setEnabled(Boolean enabled) { } - }); - - then: - canEnable - } - - @WithMockUser(value = "nonadmin", roles = ["USER"]) - def "nonadmin cannot activate without enable role"() { - when: - def canEnable = userService.currentUserCanEnable(new IActivatable() { - ActivatableType getActivatableType() { return ActivatableType.ENTITY_DESCRIPTOR } - void setEnabled(Boolean enabled) { } - }); - - then: - !canEnable - - when: - canEnable = userService.currentUserCanEnable(new IActivatable() { - ActivatableType getActivatableType() { return ActivatableType.FILTER } - void setEnabled(Boolean enabled) { } - }); - - then: - !canEnable - - when: - canEnable = userService.currentUserCanEnable(new IActivatable() { - ActivatableType getActivatableType() { return ActivatableType.METADATA_RESOLVER } - void setEnabled(Boolean enabled) { } - }); +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User - then: - !canEnable - } - - @WithMockUser(value = "robot", roles = ["ENABLE"]) - def "nonadmin can activate with enable role"() { - given: - def ed = new EntityDescriptor().with { - it.idOfOwner = 'robot' - it - } - when: - def canEnable = userService.currentUserCanEnable(ed); - - then: - canEnable +class UserServiceTests extends AbstractBaseDataJpaTest { + Role userRole - when: - canEnable = userService.currentUserCanEnable(new IActivatable() { - ActivatableType getActivatableType() { return ActivatableType.FILTER } - void setEnabled(Boolean enabled) { } - }); - - then: - canEnable - - when: - canEnable = userService.currentUserCanEnable(new IActivatable() { - ActivatableType getActivatableType() { return ActivatableType.METADATA_RESOLVER } - void setEnabled(Boolean enabled) { } - }); - - then: - canEnable - } - - @WithMockUser(value = "robot2", roles = ["ENABLE"]) - def "nonadmin cannot activate entity descriptor with enable role"() { - given: - def ed = new EntityDescriptor().with { - it.idOfOwner = 'robot' - it - } - when: - def canEnable = userService.currentUserCanEnable(ed); - - then: - !canEnable - - when: - canEnable = userService.currentUserCanEnable(new IActivatable() { - ActivatableType getActivatableType() { return ActivatableType.FILTER } - void setEnabled(Boolean enabled) { } - }); - - then: - canEnable - - when: - canEnable = userService.currentUserCanEnable(new IActivatable() { - ActivatableType getActivatableType() { return ActivatableType.METADATA_RESOLVER } - void setEnabled(Boolean enabled) { } - }); - - then: - canEnable - } - - @WithMockUser(value = "nonadmin", roles = ["USER"]) - @Rollback - def "Validate isAuthorizedFor with non-admin user"() { - given: - ownershipRepository.saveAndFlush(new Ownership(new Owner() { - String getOwnerId() { return "nonadmin" } - OwnerType getOwnerType() { return OwnerType.GROUP } - }, new Ownable() { - String getObjectId() { return "foothing" } - OwnableType getOwnableType() { return OwnableType.ENTITY_DESCRIPTOR } - })) - when: - def isAuth = userService.isAuthorizedFor(new Ownable(){ - String getObjectId() { return "foothing" } - OwnableType getOwnableType() { return OwnableType.ENTITY_DESCRIPTOR } - }); - - then: - isAuth + def setup() { + userRole = roleRepository.findByName("ROLE_USER").get() } - def "Double Check that shibUIConfiguration includes the enable role"() { - when: - def Optional role = roleRepository.findByName("ROLE_ENABLE") - - then: - role.isPresent() + protected createAdminUser() { + // don't create the admin user } - @Rollback def "When creating user, user is set to the correct group"() { given: - Group gb = new Group(); + Group gb = new Group() gb.setResourceId("testingGroupBBB") gb.setName("Group BBB") gb = groupService.createGroup(gb) - Optional userRole = roleRepository.findByName("ROLE_USER") - def User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") + User user = new User(username: "someUser", roles:[userRole], password: "foo") user.setGroup(gb) when: - def User result = userService.save(user) + User result = userService.save(user) then: result.groupId == "testingGroupBBB" result.username == "someUser" - result.userGroups.size() == 1 + result.getUserGroups().size() == 1 // Raw check that the DB is correct for ownership - def Set users = ownershipRepository.findUsersByOwner(gb) + Set users = ownershipRepository.findUsersByOwner(gb) users.size() == 1 - users.getAt(0).ownedId == "someUser" + users[0].ownedId == "someUser" // Validate that loading the group has the correct list as well - Group g = groupService.find("testingGroupBBB"); + Group g = groupService.find("testingGroupBBB") g.ownedItems.size() == 1 } - @Rollback def "When updating user, user is set to the correct group"() { given: Group ga = new Group() @@ -328,43 +52,41 @@ class UserServiceTests extends Specification { ga.setName("Group A") ga = groupService.createGroup(ga) - Group gb = new Group(); + Group gb = new Group() gb.setResourceId("testingGroupBBB") gb.setName("Group BBB") gb = groupService.createGroup(gb) - Optional userRole = roleRepository.findByName("ROLE_USER") - def User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") + User user = new User(username: "someUser", roles:[userRole], password: "foo") user.setGroup(gb) - def User userInB = userService.save(user) + User userInB = userService.save(user) when: userInB.setGroupId("testingGroup") // changing groups will happen by updating the user's groupid (from the ui) - def User result = userService.save(userInB) + User result = userService.save(userInB) then: result.groupId == "testingGroup" result.username == "someUser" - result.userGroups.size() == 1 + result.getUserGroups().size() == 1 // Raw check that the DB is correct for ownership - def Set users = ownershipRepository.findUsersByOwner(ga) + Set users = ownershipRepository.findUsersByOwner(ga) users.size() == 1 - users.getAt(0).ownedId == "someUser" + users[0].ownedId == "someUser" // check db is correct for the previous group as well - def Set users2 = ownershipRepository.findUsersByOwner(gb) + Set users2 = ownershipRepository.findUsersByOwner(gb) users2.size() == 0 // Validate that loading the group has the correct list as well - Group g = groupService.find("testingGroup"); + Group g = groupService.find("testingGroup") g.ownedItems.size() == 1 - Group g2 = groupService.find("testingGroupBBB"); + Group g2 = groupService.find("testingGroupBBB") g2.ownedItems.size() == 0 } - @Rollback def "logically try to match user controller test causing headaches"() { given: Group ga = new Group() @@ -372,26 +94,24 @@ class UserServiceTests extends Specification { ga.setName("Group A") ga = groupService.createGroup(ga) - Optional userRole = roleRepository.findByName("ROLE_USER") - def User user = new User(username: "someUser", firstName: "Fred", lastName: "Flintstone", roles:[userRole.get()], password: "foo") + User user = new User(username: "someUser", firstName: "Fred", lastName: "Flintstone", roles:[userRole], password: "foo") user.setGroup(ga) userService.save(user) when: - def User flintstoneUser = userRepository.findByUsername("someUser").get() + User flintstoneUser = userRepository.findByUsername("someUser").get() flintstoneUser.setFirstName("Wilma") flintstoneUser.setGroupId("testingGroup") - def User result = userService.save(flintstoneUser) + User result = userService.save(flintstoneUser) then: result.groupId == "testingGroup" result.username == "someUser" - result.userGroups.size() == 1 + result.getUserGroups().size() == 1 result.firstName == "Wilma" } - @Rollback def "When creating user, user with multiple groups is saved correctly"() { given: Group ga = new Group() @@ -399,15 +119,14 @@ class UserServiceTests extends Specification { ga.setName("Group A") ga = groupService.createGroup(ga) - Group gb = new Group(); + Group gb = new Group() gb.setResourceId("testingGroupBBB") gb.setName("Group BBB") gb = groupService.createGroup(gb) - Optional userRole = roleRepository.findByName("ROLE_USER") User user = new User().with( { it.username = "someUser" - it.roles = [userRole.get()] + it.roles = [userRole] it.password = "foo" it }) @@ -421,19 +140,19 @@ class UserServiceTests extends Specification { def result = userService.save(user) then: - result.userGroups.size() == 2 + result.getUserGroups().size() == 2 // Raw check that the DB is correct for ownership - def Set users = ownershipRepository.findUsersByOwner(ga) + Set users = ownershipRepository.findUsersByOwner(ga) users.size() == 1 - users.getAt(0).ownedId == "someUser" + users[0].ownedId == "someUser" - def Set users2 = ownershipRepository.findUsersByOwner(gb) + Set users2 = ownershipRepository.findUsersByOwner(gb) users2.size() == 1 - users2.getAt(0).ownedId == "someUser" + users2[0].ownedId == "someUser" when: - def userFromDb = userRepository.findById(result.id).get(); + def userFromDb = userRepository.findById(result.id).get() then: userFromDb.getUserGroups().size() == 2 @@ -444,27 +163,4 @@ class UserServiceTests extends Specification { then: gbUpdated.ownedItems.size() == 1 } - - @TestConfiguration - @Profile("local") - static class LocalConfig { - @Bean - GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { - GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { - it.groupRepository = repo - it.ownershipRepository = ownershipRepository - return it - }) - result.ensureAdminGroupExists() - return result - } - - @Bean - ObjectMapper objectMapper() { - JavaTimeModule module = new JavaTimeModule() - LocalDateTimeDeserializer localDateTimeDeserializer = new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS")) - module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer) - return Jackson2ObjectMapperBuilder.json().modules(module).featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build() - } - } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy index 6ceae1eef..3f72adf98 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy @@ -1,25 +1,41 @@ package edu.internet2.tier.shibboleth.admin.ui.security.springsecurity +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.configuration.DevConfig +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository 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.IGroupService +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.test.context.ActiveProfiles -import spock.lang.Specification +import org.springframework.test.context.ContextConfiguration /** * Tests for AdminUserService (well, really it tests that the DevConfig worked as much as anything) * * @author Dmitriy Kopylenko */ -@SpringBootTest -@ActiveProfiles('dev') -class AdminUserServiceTests extends Specification { - +@ContextConfiguration(classes=[AUSLocalConfig]) +class AdminUserServiceTests extends AbstractBaseDataJpaTest { @Autowired AdminUserService adminUserService + @Autowired + DevConfig devConfig + + def setup() { + // db is cleaned by test setup, so we have to re-run before each test + devConfig.createDevUsersAndGroups() + } + def "Loading existing admin user with admin role"() { given: 'Valid user with admin role is available (loaded by Spring Boot Listener in dev profile)' def user = adminUserService.loadUserByUsername('admin') @@ -41,4 +57,30 @@ class AdminUserServiceTests extends Specification { then: thrown UsernameNotFoundException } -} + + @Override + protected createAdminUser() { + // Do nothing so that the dev config will create the admin user rather than the test setup + } + + @TestConfiguration + private static class AUSLocalConfig { + @Bean + AdminUserService adminUserService(UserRepository userRepository) { + return new AdminUserService(userRepository) + } + + // Rather than having a specific dev context needed, we just stand up the needed bean. + @Bean + DevConfig devConfig(UserRepository adminUserRepository, GroupsRepository groupsRepository, IGroupService groupService, + MetadataResolverRepository metadataResolverRepository, OpenSamlObjects openSamlObjects, UserService userService, + RoleRepository roleRepository, EntityDescriptorRepository entityDescriptorRepository) { + DevConfig dc = new DevConfig( adminUserRepository, groupsRepository, metadataResolverRepository, roleRepository, + entityDescriptorRepository, openSamlObjects, groupService).with { + it.userService = userService + it + } + return dc + } + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy index 3733a5fe1..a44b4beed 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy @@ -1,54 +1,36 @@ package edu.internet2.tier.shibboleth.admin.ui.service -import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.domain.XSString import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityRoleWhiteListFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.SignatureValidationFilter -import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository -import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolversPositionOrderContainerRepository import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility -import groovy.xml.XmlUtil import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver import org.opensaml.saml.metadata.resolver.MetadataResolver import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Profile -import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration -import spock.lang.Specification +import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.generatedXmlIsTheSameAsExpectedXml -import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.* +@ContextConfiguration(classes = [IJPAMRSILocalConfig]) +class IncommonJPAMetadataResolverServiceImplTests extends AbstractBaseDataJpaTest { + @Autowired + AttributeUtility attributeUtility -@DataJpaTest -@ContextConfiguration(classes = [CoreShibUiConfiguration, SearchConfiguration, InternationalizationConfiguration, edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration ,LocalConfig]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@ActiveProfiles(value = "local") -class IncommonJPAMetadataResolverServiceImplTests extends Specification { @Autowired MetadataResolverService metadataResolverService @Autowired MetadataResolverRepository metadataResolverRepository - @Autowired - AttributeUtility attributeUtility - def cleanup() { metadataResolverRepository.deleteAll() } @@ -111,19 +93,9 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { } @TestConfiguration - @Profile("local") - static class LocalConfig { - @Autowired - OpenSamlObjects openSamlObjects - - @Autowired - MetadataResolverRepository metadataResolverRepository - - @Autowired - AttributeUtility attributeUtility - + private static class IJPAMRSILocalConfig { @Bean - MetadataResolver metadataResolver() { + MetadataResolver metadataResolver(AttributeUtility attributeUtility, MetadataResolverRepository metadataResolverRepository) { def resolver = new ChainingMetadataResolver().with { it.id = 'chain' @@ -155,12 +127,9 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { } @Bean - GroupServiceImpl groupService(GroupsRepository repo, OwnershipRepository ownershipRepository) { - new GroupServiceImpl().with { - it.groupRepository = repo - it.ownershipRepository = ownershipRepository - return it - } + MetadataResolversPositionOrderContainerService metadataResolversPositionOrderContainerService(MetadataResolversPositionOrderContainerRepository positionOrderContainerRepository, + MetadataResolverRepository resolverRepository) { + return new DefaultMetadataResolversPositionOrderContainerService(positionOrderContainerRepository, resolverRepository) } } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterServiceTests.groovy index 65ec74bc1..bb79bf48a 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IndexWriterServiceTests.groovy @@ -1,25 +1,12 @@ package edu.internet2.tier.shibboleth.admin.ui.service -import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.test.context.ContextConfiguration -import spock.lang.Specification /** * @author Bill Smith (wsmith@unicon.net) */ -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -class IndexWriterServiceTests extends Specification { - +class IndexWriterServiceTests extends AbstractBaseDataJpaTest { @Autowired IndexWriterService service @@ -34,4 +21,4 @@ class IndexWriterServiceTests extends Specification { then: firstIndexWriter == secondIndexWriter } -} +} \ 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 63a5a133b..e9a9aa217 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 @@ -1,16 +1,8 @@ package edu.internet2.tier.shibboleth.admin.ui.service import com.fasterxml.jackson.databind.ObjectMapper -import edu.internet2.tier.shibboleth.admin.ui.ShibbolethUiApplication -import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration -import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor -import edu.internet2.tier.shibboleth.admin.ui.domain.XSAny -import edu.internet2.tier.shibboleth.admin.ui.domain.XSAnyBuilder -import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean -import edu.internet2.tier.shibboleth.admin.ui.domain.XSBooleanBuilder -import edu.internet2.tier.shibboleth.admin.ui.domain.XSStringBuilder import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ContactRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation @@ -20,58 +12,45 @@ 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.IGroupService -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 import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils - -import org.opensaml.saml.ext.saml2mdattr.EntityAttributes import org.skyscreamer.jsonassert.JSONAssert import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.json.JacksonTester import org.springframework.context.annotation.PropertySource -import org.springframework.test.annotation.DirtiesContext -import org.springframework.test.context.ContextConfiguration import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input import org.xmlunit.diff.DefaultNodeMatcher import org.xmlunit.diff.ElementSelectors -import spock.lang.Specification +import spock.lang.Ignore -@ContextConfiguration(classes=[CoreShibUiConfiguration, CustomPropertiesConfiguration]) -@SpringBootTest(classes = ShibbolethUiApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) @PropertySource("classpath:application.yml") -@DirtiesContext -class JPAEntityDescriptorServiceImplTests extends Specification { +class JPAEntityDescriptorServiceImplTests extends AbstractBaseDataJpaTest { + @Autowired + EntityService entityService + @Autowired - EntityService entityService; - + ObjectMapper mapper + + @Autowired + OpenSamlObjects openSamlObjects + @Autowired JPAEntityDescriptorServiceImpl service - def testObjectGenerator - - OpenSamlObjects openSamlObjects = new OpenSamlObjects().with { - init() - it - } - - JacksonTester jacksonTester - ObjectMapper mapper + @Autowired + TestObjectGenerator testObjectGenerator + RandomGenerator generator - + JacksonTester jacksonTester + def setup() { - mapper = new ObjectMapper() JacksonTester.initFields(this, mapper) generator = new RandomGenerator() - testObjectGenerator = new TestObjectGenerator() EntityDescriptorConversionUtils.openSamlObjects = openSamlObjects EntityDescriptorConversionUtils.entityService = entityService + openSamlObjects.init() } def "simple Entity Descriptor"() { @@ -627,7 +606,7 @@ class JPAEntityDescriptorServiceImplTests extends Specification { def output = service.createRepresentationFromDescriptor(service.createDescriptorFromRepresentation(representation)) then: - assert output.securityInfo?.authenticationRequestsSigned == true + assert output.securityInfo?.authenticationRequestsSigned } def "SHIBUI-219-3"() { @@ -651,6 +630,7 @@ class JPAEntityDescriptorServiceImplTests extends Specification { assert output.assertionConsumerServices[0].binding == 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post' } + @Ignore def "payload mapping round trip: JSON->FrontendModel->BackendModel->FrontendModel->JSON"() { when: EntityDescriptorRepresentation inputRepresentation = jacksonTester.readObject('/json/SHIBUI-219-3.json') @@ -783,4 +763,4 @@ class JPAEntityDescriptorServiceImplTests extends Specification { return ed } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy index 11d870149..22e90bf14 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy @@ -1,129 +1,52 @@ package edu.internet2.tier.shibboleth.admin.ui.service -import edu.internet2.tier.shibboleth.admin.ui.ShibbolethUiApplication -import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository import edu.internet2.tier.shibboleth.admin.ui.security.model.Group import edu.internet2.tier.shibboleth.admin.ui.security.model.Role import edu.internet2.tier.shibboleth.admin.ui.security.model.User -import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository -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.GroupServiceForTesting -import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl -import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Primary -import org.springframework.context.annotation.Profile -import org.springframework.context.annotation.PropertySource import org.springframework.security.test.context.support.WithMockUser -import org.springframework.test.annotation.DirtiesContext -import org.springframework.test.annotation.Rollback -import org.springframework.test.context.ActiveProfiles -import org.springframework.test.context.ContextConfiguration import org.springframework.transaction.annotation.Transactional -import spock.lang.Specification -@ContextConfiguration(classes=[CoreShibUiConfiguration, CustomPropertiesConfiguration, LocalConfig]) -@SpringBootTest(classes = ShibbolethUiApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) -@PropertySource("classpath:application.yml") -@DirtiesContext -@ActiveProfiles(value="jpaeds2-test") -class JPAEntityDescriptorServiceImplTests2 extends Specification { - +class JPAEntityDescriptorServiceImplTests2 extends AbstractBaseDataJpaTest { @Autowired - GroupServiceForTesting groupService + EntityDescriptorRepository entityDescriptorRepository - @Autowired - RoleRepository roleRepository - @Autowired JPAEntityDescriptorServiceImpl entityDescriptorService - - @Autowired - UserRepository userRepository - @Autowired - UserService userService - @Transactional def setup() { - // ensure we start fresh with only expected users and roles and groups - userRepository.deleteAll() - roleRepository.deleteAll() - groupService.clearAllForTesting() - - Group ga = new Group() - ga.setResourceId("testingGroup") - ga.setName("Group A") - groupService.createGroup(ga) - Group gb = new Group() gb.setResourceId("testingGroupBBB") gb.setName("Group BBB") - groupService.createGroup(gb) - - def roles = [new Role().with { - name = 'ROLE_ADMIN' - it - }, new Role().with { - name = 'ROLE_USER' - it - }, new Role().with { - name = 'ROLE_NONE' - it - }] - roles.each { - roleRepository.save(it) - } - - Optional adminRole = roleRepository.findByName("ROLE_ADMIN") - User adminUser = new User(username: "admin", roles: [adminRole.get()], password: "foo") - userService.save(adminUser) - + gb.setValidationRegex("^(?:https?:\\/\\/)?(?:[^.]+\\.)?shib\\.org(\\/.*)?\$") + gb = groupService.createGroup(gb) + Optional userRole = roleRepository.findByName("ROLE_USER") - User user = new User(username: "someUser", roles:[userRole.get()], password: "foo", group: gb) + User user = new User(username: "someUser", roles: [userRole.get()], password: "foo", group: gb) userService.save(user) + + entityDescriptorRepository.deleteAll() } @WithMockUser(value = "someUser", roles = ["USER"]) - @Rollback - @Transactional def "When creating Entity Descriptor, ED is assigned to the user's group"() { given: User current = userService.getCurrentUser() - current.setGroupId("testingGroupBBB") - def expectedEntityId = 'https://shib' + def expectedEntityId = 'https://shib.org/groupsomething' def expectedSpName = 'sp1' def expectedUUID = 'uuid-1' def entityDescriptor = new EntityDescriptor(resourceId: expectedUUID, entityID: expectedEntityId, serviceProviderName: expectedSpName, serviceEnabled: false) - + when: def result = entityDescriptorService.createNew(entityDescriptor) - + then: - ((EntityDescriptorRepresentation)result).getIdOfOwner() == "testingGroupBBB" - } - - @TestConfiguration - @Profile("jpaeds2-test") - static class LocalConfig { - @Bean - @Primary - GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { - GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { - it.groupRepository = repo - it.ownershipRepository = ownershipRepository - return it - }) - return result - } + ((EntityDescriptorRepresentation) result).getIdOfOwner() == "testingGroupBBB" } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImplTests.groovy index df888a972..d60792b22 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImplTests.groovy @@ -1,10 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.service +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator @@ -13,41 +10,22 @@ import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility import edu.internet2.tier.shibboleth.admin.util.MDDCConstants import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean import org.springframework.test.context.ContextConfiguration -import spock.lang.Specification /** * @author Bill Smith (wsmith@unicon.net) */ -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -class JPAEntityServiceImplTests extends Specification { - - @Autowired - OpenSamlObjects openSamlObjects - +@ContextConfiguration(classes=[JPAESILocalConfig]) +class JPAEntityServiceImplTests extends AbstractBaseDataJpaTest { @Autowired - AttributeUtility attributeUtility + EntityService service @Autowired - CustomPropertiesConfiguration customPropertiesConfiguration - - def randomGenerator - def testObjectGenerator - - def service + TestObjectGenerator testObjectGenerator - def setup() { - service = new JPAEntityServiceImpl(openSamlObjects, attributeUtility, customPropertiesConfiguration) - - randomGenerator = new RandomGenerator() - testObjectGenerator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) - } + def randomGenerator = new RandomGenerator() def "getAttributeListFromEntityRepresentation builds an appropriate attribute list"() { given: @@ -91,4 +69,12 @@ class JPAEntityServiceImplTests extends Specification { it.schemaTypeNamespacePrefix == expectedSchemaTypeNamespacePrefix } } -} + + @TestConfiguration + private static class JPAESILocalConfig { + @Bean + TestObjectGenerator testObjectGenerator(AttributeUtility attributeUtility, CustomPropertiesConfiguration customPropertiesConfiguration) { + return new TestObjectGenerator(attributeUtility,customPropertiesConfiguration) + } + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImplTests.groovy index 25dd61fa5..68ca161c1 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImplTests.groovy @@ -1,45 +1,30 @@ package edu.internet2.tier.shibboleth.admin.ui.service -import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest 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 edu.internet2.tier.shibboleth.admin.util.AttributeUtility import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean import org.springframework.test.context.ContextConfiguration -import spock.lang.Specification /** * @author Bill Smith (wsmith@unicon.net) */ -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -class JPAFilterServiceImplTests extends Specification { - - RandomGenerator randomGenerator - TestObjectGenerator testObjectGenerator +@ContextConfiguration(classes = [ JPAFSIConfig ]) +class JPAFilterServiceImplTests extends AbstractBaseDataJpaTest { @Autowired - JPAFilterServiceImpl service + JPAFilterServiceImpl filterService @Autowired - AttributeUtility attributeUtility + TestObjectGenerator testObjectGenerator - @Autowired - CustomPropertiesConfiguration customPropertiesConfiguration + RandomGenerator randomGenerator def setup() { randomGenerator = new RandomGenerator() - testObjectGenerator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) } def "createFilterFromRepresentation properly creates a filter from a representation"() { @@ -47,7 +32,7 @@ class JPAFilterServiceImplTests extends Specification { def representation = testObjectGenerator.buildFilterRepresentation() when: - def result = service.createFilterFromRepresentation(representation) + def result = filterService.createFilterFromRepresentation(representation) then: result.name == representation.filterName @@ -71,7 +56,7 @@ class JPAFilterServiceImplTests extends Specification { def filter = testObjectGenerator.buildFilter { testObjectGenerator.entityAttributesFilter() } when: - def result = service.createRepresentationFromFilter(filter) + def result = filterService.createRepresentationFromFilter(filter) then: result.id == filter.resourceId @@ -87,4 +72,12 @@ class JPAFilterServiceImplTests extends Specification { result.filterTarget.type == filter.entityAttributesFilterTarget.entityAttributesFilterTargetType.toString() result.filterTarget.value == filter.entityAttributesFilterTarget.value } -} + + @TestConfiguration + private static class JPAFSIConfig { + @Bean + JPAFilterTargetServiceImpl jpaFilterTargetService() { + return new JPAFilterTargetServiceImpl() + } + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterTargetServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterTargetServiceImplTests.groovy index 0ceaaa82e..376e23732 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterTargetServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterTargetServiceImplTests.groovy @@ -1,22 +1,23 @@ package edu.internet2.tier.shibboleth.admin.ui.service +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import org.springframework.beans.factory.annotation.Autowired import spock.lang.Specification /** * @author Bill Smith (wsmith@unicon.net) */ -class JPAFilterTargetServiceImplTests extends Specification { - - RandomGenerator randomGenerator +class JPAFilterTargetServiceImplTests extends AbstractBaseDataJpaTest { + @Autowired TestObjectGenerator testObjectGenerator + RandomGenerator randomGenerator JPAFilterTargetServiceImpl service def setup() { randomGenerator = new RandomGenerator() - testObjectGenerator = new TestObjectGenerator() service = new JPAFilterTargetServiceImpl() } @@ -44,4 +45,4 @@ class JPAFilterTargetServiceImplTests extends Specification { results.type == filterTarget.entityAttributesFilterTargetType.toString() results.version == filterTarget.hashCode() } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy index 20b4abcde..f61d647fe 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy @@ -1,9 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.service -import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.configuration.PlaceholderResolverComponentsConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget @@ -11,7 +9,6 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilFilter import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ClasspathMetadataResource import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FilesystemMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataQueryProtocolScheme import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.RegexScheme @@ -20,12 +17,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.TemplateScheme import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository -import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator -import edu.internet2.tier.shibboleth.admin.util.AttributeUtility -import edu.internet2.tier.shibboleth.admin.util.TokenPlaceholderResolvers import groovy.xml.DOMBuilder import groovy.xml.MarkupBuilder import net.shibboleth.ext.spring.resource.ResourceHelper @@ -36,67 +28,49 @@ import org.opensaml.saml.metadata.resolver.MetadataResolver import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain 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.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Profile import org.springframework.core.io.ClassPathResource -import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.test.annotation.DirtiesContext -import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input import spock.lang.Ignore -import spock.lang.Specification import spock.lang.Unroll import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.generatedXmlIsTheSameAsExpectedXml -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, InternationalizationConfiguration, PlaceholderResolverComponentsConfiguration, edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration , LocalConfig]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) -@ActiveProfiles(value = "local") -class JPAMetadataResolverServiceImplTests extends Specification { - @Autowired - MetadataResolverRepository metadataResolverRepository +@ContextConfiguration(classes=[ JPAMRSIConfig, PlaceholderResolverComponentsConfiguration ]) +class JPAMetadataResolverServiceImplTests extends AbstractBaseDataJpaTest { @Autowired - MetadataResolverService metadataResolverService + EntityService entityService @Autowired MetadataResolver metadataResolver @Autowired - EntityService entityService + MetadataResolverRepository metadataResolverRepository @Autowired - OpenSamlObjects openSamlObjects + MetadataResolverConverterService mdrConverterService @Autowired - AttributeUtility attributeUtility + MetadataResolverService metadataResolverService @Autowired - MetadataResolverConverterService mdrConverterService + OpenSamlObjects openSamlObjects @Autowired ShibUIConfiguration shibUIConfiguration + @Autowired TestObjectGenerator testObjectGenerator - DOMBuilder domBuilder - - StringWriter writer - + DOMBuilder domBuilder = DOMBuilder.newInstance() + StringWriter writer = new StringWriter() MarkupBuilder markupBuilder def setup() { - testObjectGenerator = new TestObjectGenerator(attributeUtility) - domBuilder = DOMBuilder.newInstance() - writer = new StringWriter() markupBuilder = new MarkupBuilder(writer) markupBuilder.omitNullAttributes = true markupBuilder.omitEmptyAttributes = true @@ -392,7 +366,6 @@ class JPAMetadataResolverServiceImplTests extends Specification { } @Unroll - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) def 'test namespace protection [#namespaces]'() { setup: shibUIConfiguration.protectedAttributeNamespaces = namespaces @@ -416,7 +389,6 @@ class JPAMetadataResolverServiceImplTests extends Specification { ['http://shibboleth.net/ns/profiles', 'http://scaldingspoon.com/iam'] | '/conf/984-2.xml' } - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) def 'test namespace protection in nonURL resolver with resolver setting enabled=true'() { setup: shibUIConfiguration.protectedAttributeNamespaces = ['http://shibboleth.net/ns/profiles'] @@ -434,7 +406,6 @@ class JPAMetadataResolverServiceImplTests extends Specification { generatedXmlIsTheSameAsExpectedXml('/conf/1059-enabled.xml', metadataResolverService.generateConfiguration()) } - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) def 'test namespace protection in nonURL resolver with resolver setting enabled not set'() { setup: shibUIConfiguration.protectedAttributeNamespaces = ['http://shibboleth.net/ns/profiles'] @@ -484,15 +455,10 @@ class JPAMetadataResolverServiceImplTests extends Specification { } @TestConfiguration - @Profile("local") - static class LocalConfig { - @Autowired - OpenSamlObjects openSamlObjects - + private static class JPAMRSIConfig { @Bean - MetadataResolver metadataResolver(TokenPlaceholderResolvers tokenPlaceholderResolvers) { - def resource = ResourceHelper.of(new ClassPathResource("/metadata/aggregate.xml")) - def aggregate = new ResourceBackedMetadataResolver(resource){ + MetadataResolver metadataResolver(OpenSamlObjects openSamlObjects) { + def aggregate = new ResourceBackedMetadataResolver(ResourceHelper.of(new ClassPathResource("/metadata/aggregate.xml"))){ @Override DateTime getLastRefresh() { return null @@ -509,19 +475,17 @@ class JPAMetadataResolverServiceImplTests extends Specification { return new OpenSamlChainingMetadataResolver().with { it.id = 'chain' - //it.resolvers = [aggregate] -// it.resolvers = [] it.initialize() it } } @Bean - GroupServiceImpl groupService(GroupsRepository repo, OwnershipRepository ownershipRepository) { - new GroupServiceImpl().with { - it.groupRepository = repo - it.ownershipRepository = ownershipRepository - return it + MetadataResolverConverterServiceImpl metadataResolverConverterServiceImpl(IndexWriterService indexWriterService, OpenSamlObjects openSamlObjects) { + return new MetadataResolverConverterServiceImpl().with { + it.indexWriterService = indexWriterService + it.openSamlObjects = openSamlObjects + it } } } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImplTests.groovy index f5d0f72e3..363dec3ad 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImplTests.groovy @@ -1,18 +1,17 @@ package edu.internet2.tier.shibboleth.admin.ui.service -import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.configuration.PlaceholderResolverComponentsConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicMetadataResolverAttributes import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean import org.springframework.test.context.ContextConfiguration -import spock.lang.Specification - -@SpringBootTest -class MetadataResolverConverterServiceImplTests extends Specification { - +@ContextConfiguration(classes=[MRCSILocalConfig, PlaceholderResolverComponentsConfiguration]) +class MetadataResolverConverterServiceImplTests extends AbstractBaseDataJpaTest { @Autowired MetadataResolverConverterService mrConverterServiceUnderTest @@ -30,4 +29,21 @@ class MetadataResolverConverterServiceImplTests extends Specification { then: noExceptionThrown() } -} + + @TestConfiguration + private static class MRCSILocalConfig { + @Bean + DirectoryService directoryService() { + return new DirectoryServiceImpl() + } + + @Bean + MetadataResolverConverterServiceImpl metadataResolverConverterServiceImpl(IndexWriterService indexWriterService, OpenSamlObjects openSamlObjects) { + return new MetadataResolverConverterServiceImpl().with { + it.indexWriterService = indexWriterService + it.openSamlObjects = openSamlObjects + it + } + } + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy index 0deb53e69..9c40b1035 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy @@ -1,52 +1,25 @@ package edu.internet2.tier.shibboleth.admin.ui.service -import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration -import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository -import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService -import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.FilterType import org.springframework.core.io.ClassPathResource -import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.test.annotation.DirtiesContext -import org.springframework.test.context.ContextConfiguration -import spock.lang.Specification +import org.springframework.transaction.annotation.Transactional -@DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration, ShibUIConfiguration]) -@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) -@EntityScan(["edu.internet2.tier.shibboleth.admin.ui", "edu.internet2.tier.shibboleth.admin.ui.security.model"]) -@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD, classMode = DirtiesContext.ClassMode.BEFORE_CLASS) @ComponentScan(excludeFilters = [@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = [UserBootstrap])]) -class UserBootstrapTests extends Specification { +class UserBootstrapTests extends AbstractBaseDataJpaTest { @Autowired ShibUIConfiguration shibUIConfiguration - @Autowired - UserRepository userRepository - - @Autowired - RoleRepository roleRepository - - @Autowired - UserService userService - - @Autowired - IGroupService groupService - + @Transactional def setup() { - groupService.ensureAdminGroupExists() roleRepository.deleteAll() + userRepository.deleteAll() + entityManager.flush() } - + def "simple test"() { setup: shibUIConfiguration.roles = [] diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy index 0b9a4e81c..321ef2b38 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy @@ -1,7 +1,5 @@ package edu.internet2.tier.shibboleth.admin.ui.util -import org.opensaml.saml.common.xml.SAMLConstants -import org.opensaml.saml.saml2.metadata.ContactPersonTypeEnumeration import edu.internet2.tier.shibboleth.admin.ui.domain.ContactPerson import edu.internet2.tier.shibboleth.admin.ui.domain.Description @@ -26,12 +24,12 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepres 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.util.EntityDescriptorConversionUtils +import org.opensaml.saml.common.xml.SAMLConstants +import org.opensaml.saml.saml2.metadata.ContactPersonTypeEnumeration import spock.lang.Shared import spock.lang.Specification import spock.lang.Unroll -import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.* - class EntityDescriptorConversionUtilsTests extends Specification { @Shared OpenSamlObjects openSAMLObjects @@ -61,7 +59,7 @@ class EntityDescriptorConversionUtilsTests extends Specification { expected.name = 'testName' when: - def keyDescriptor = createKeyDescriptor('testName', 'signing', 'testValue') + def keyDescriptor = EntityDescriptorConversionUtils.createKeyDescriptor('testName', 'signing', 'testValue') then: assert keyDescriptor == expected @@ -80,19 +78,18 @@ class EntityDescriptorConversionUtilsTests extends Specification { expected.name = 'testName' when: - def keyDescriptor = createKeyDescriptor('testName', 'both', 'testValue') - openSAMLObjects.marshalToXmlString(keyDescriptor) // not sure why we do this?? + def keyDescriptor = EntityDescriptorConversionUtils.createKeyDescriptor('testName', 'both', 'testValue') then: assert keyDescriptor == expected } def 'test createKeyDescriptor equality'() { when: - def key1 = createKeyDescriptor('test', 'signing', 'test') - def key2 = createKeyDescriptor('test', 'signing', 'test') + def key1 = EntityDescriptorConversionUtils.createKeyDescriptor('test', 'signing', 'test') + def key2 = EntityDescriptorConversionUtils.createKeyDescriptor('test', 'signing', 'test') then: - assert key1.equals(key2) + assert key1 == key2 } @Unroll @@ -608,7 +605,7 @@ class EntityDescriptorConversionUtilsTests extends Specification { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { it.addKeyDescriptor( - createKeyDescriptor('test', 'signing', 'test')) + utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) it } ) @@ -632,7 +629,7 @@ class EntityDescriptorConversionUtilsTests extends Specification { starter: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) it } ) @@ -641,8 +638,8 @@ class EntityDescriptorConversionUtilsTests extends Specification { expected: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(createKeyDescriptor('test', 'signing', 'test')) - it.addKeyDescriptor(createKeyDescriptor('test2', 'encryption', 'test2')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test2', 'encryption', 'test2')) it } ) @@ -665,8 +662,8 @@ class EntityDescriptorConversionUtilsTests extends Specification { starter: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(createKeyDescriptor('test', 'signing', 'test')) - it.addKeyDescriptor(createKeyDescriptor('test2', 'encryption', 'test2')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test2', 'encryption', 'test2')) it } ) @@ -675,7 +672,7 @@ class EntityDescriptorConversionUtilsTests extends Specification { expected: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(createKeyDescriptor('test2', 'encryption', 'test2')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test2', 'encryption', 'test2')) it } ) @@ -695,8 +692,8 @@ class EntityDescriptorConversionUtilsTests extends Specification { starter: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(createKeyDescriptor('test', 'signing', 'test')) - it.addKeyDescriptor(createKeyDescriptor('test', 'encryption', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'encryption', 'test')) it } ) @@ -716,8 +713,8 @@ class EntityDescriptorConversionUtilsTests extends Specification { starter: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(createKeyDescriptor('test', 'signing', 'test')) - it.addKeyDescriptor(createKeyDescriptor('test', 'encryption', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'encryption', 'test')) it } ) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy index e28220cc6..7ed0709df 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy @@ -1,12 +1,36 @@ package edu.internet2.tier.shibboleth.admin.ui.util import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration -import edu.internet2.tier.shibboleth.admin.ui.domain.* -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.* +import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute +import edu.internet2.tier.shibboleth.admin.ui.domain.ContactPerson +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.LocalizedName +import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationDisplayName +import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationName +import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationURL +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget.EntityAttributesFilterTargetType +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityRoleWhiteListFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilterTarget +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.SignatureValidationFilter import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterTargetRepresentation -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.* +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ClasspathMetadataResource +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicMetadataResolverAttributes +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FilesystemMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.HttpMetadataResolverAttributes +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataQueryProtocolScheme +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ReloadableMetadataResolverAttributes +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.SvnMetadataResource import edu.internet2.tier.shibboleth.admin.util.AttributeUtility import edu.internet2.tier.shibboleth.admin.util.MDDCConstants import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions @@ -39,25 +63,25 @@ class TestObjectGenerator { this.customPropertiesConfiguration = customPropertiesConfiguration } - DynamicHttpMetadataResolver buildDynamicHttpMetadataResolver() { - def resolver = new DynamicHttpMetadataResolver().with { - it.dynamicMetadataResolverAttributes = buildDynamicMetadataResolverAttributes() - it.httpMetadataResolverAttributes = buildHttpMetadataResolverAttributes() - it.maxConnectionsPerRoute = generator.randomInt(1, 100) - it.maxConnectionsTotal = generator.randomInt(1, 100) - it.supportedContentTypes = generator.randomStringList() - it.name = generator.randomString(10) - it.requireValidMetadata = generator.randomBoolean() - it.failFastInitialization = generator.randomBoolean() - it.sortKey = generator.randomInt(1, 10) - it.criterionPredicateRegistryRef = generator.randomString(10) - it.useDefaultPredicateRegistry = generator.randomBoolean() - it.satisfyAnyPredicates = generator.randomBoolean() - it.metadataFilters = buildAllTypesOfFilterList() - it - } - return resolver - } +// DynamicHttpMetadataResolver buildDynamicHttpMetadataResolver() { +// def resolver = new DynamicHttpMetadataResolver().with { +// it.dynamicMetadataResolverAttributes = buildDynamicMetadataResolverAttributes() +// it.httpMetadataResolverAttributes = buildHttpMetadataResolverAttributes() +// it.maxConnectionsPerRoute = generator.randomInt(1, 100) +// it.maxConnectionsTotal = generator.randomInt(1, 100) +// it.supportedContentTypes = generator.randomStringList() +// it.name = generator.randomString(10) +// it.requireValidMetadata = generator.randomBoolean() +// it.failFastInitialization = generator.randomBoolean() +// it.sortKey = generator.randomInt(1, 10) +// it.criterionPredicateRegistryRef = generator.randomString(10) +// it.useDefaultPredicateRegistry = generator.randomBoolean() +// it.satisfyAnyPredicates = generator.randomBoolean() +// it.metadataFilters = buildAllTypesOfFilterList() +// it +// } +// return resolver +// } HttpMetadataResolverAttributes buildHttpMetadataResolverAttributes() { def attributes = new HttpMetadataResolverAttributes().with { @@ -85,24 +109,24 @@ class TestObjectGenerator { HttpMetadataResolverAttributes.HttpCachingType.values()[generator.randomInt(0, 2)] } - LocalDynamicMetadataResolver buildLocalDynamicMetadataResolver() { - def resolver = new LocalDynamicMetadataResolver().with { - it.dynamicMetadataResolverAttributes = buildDynamicMetadataResolverAttributes() - it.sourceDirectory = generator.randomString(10) - it.sourceKeyGeneratorRef = generator.randomString(10) - it.sourceManagerRef = generator.randomString(10) - it.failFastInitialization = generator.randomBoolean() - it.name = generator.randomString(10) - it.requireValidMetadata = generator.randomBoolean() - it.useDefaultPredicateRegistry = generator.randomBoolean() - it.criterionPredicateRegistryRef = generator.randomString(10) - it.satisfyAnyPredicates = generator.randomBoolean() - it.sortKey = generator.randomInt(1, 10) - it.metadataFilters = buildAllTypesOfFilterList() - it - } - return resolver - } +// LocalDynamicMetadataResolver buildLocalDynamicMetadataResolver() { +// def resolver = new LocalDynamicMetadataResolver().with { +// it.dynamicMetadataResolverAttributes = buildDynamicMetadataResolverAttributes() +// it.sourceDirectory = generator.randomString(10) +// it.sourceKeyGeneratorRef = generator.randomString(10) +// it.sourceManagerRef = generator.randomString(10) +// it.failFastInitialization = generator.randomBoolean() +// it.name = generator.randomString(10) +// it.requireValidMetadata = generator.randomBoolean() +// it.useDefaultPredicateRegistry = generator.randomBoolean() +// it.criterionPredicateRegistryRef = generator.randomString(10) +// it.satisfyAnyPredicates = generator.randomBoolean() +// it.sortKey = generator.randomInt(1, 10) +// it.metadataFilters = buildAllTypesOfFilterList() +// it +// } +// return resolver +// } DynamicMetadataResolverAttributes buildDynamicMetadataResolverAttributes() { def attributes = new DynamicMetadataResolverAttributes().with { @@ -127,13 +151,11 @@ class TestObjectGenerator { List buildAllTypesOfFilterList() { List filterList = new ArrayList<>() - (1..generator.randomInt(4, 10)).each { - filterList.add(buildFilter { entityAttributesFilter() }) - filterList.add(buildFilter { entityRoleWhitelistFilter() }) - filterList.add(buildFilter { signatureValidationFilter() }) - filterList.add(buildFilter { requiredValidUntilFilter() }) - filterList.add(buildFilter { nameIdFormatFilter() }) - } + filterList.add(buildFilter { entityAttributesFilter() }) + filterList.add(buildFilter { entityRoleWhitelistFilter() }) + filterList.add(buildFilter { signatureValidationFilter() }) + filterList.add(buildFilter { requiredValidUntilFilter() }) + filterList.add(buildFilter { nameIdFormatFilter() }) return filterList } @@ -156,7 +178,7 @@ class TestObjectGenerator { randomFilter = nameIdFormatFilter() break default: - throw new RuntimeException("Did you forget to create a TestObjectGenerator.copyOf method for filtertype: ${filterType} ?"); + throw new RuntimeException("Did you forget to create a TestObjectGenerator.copyOf method for filtertype: ${filterType} ?") } randomFilter } @@ -223,12 +245,7 @@ class TestObjectGenerator { return new NameIdFormatFilter().with { it.name = "NameIDFormat" it.formats = ['urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'] - it.nameIdFormatFilterTarget = new NameIdFormatFilterTarget(nameIdFormatFilterTargetType: ENTITY, singleValue: 'https://sp1.example.org') - - /*it.name = "NameIDFormat" - it.formats = ['urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', 'urn:oasis:names:tc:SAML:2.0:nameid-format:email'] - it.nameIdFormatFilterTarget = new NameIdFormatFilterTarget(nameIdFormatFilterTargetType: CONDITION_SCRIPT, singleValue: 'eval(true);')*/ - + it.setNameIdFormatFilterTarget(new NameIdFormatFilterTarget(nameIdFormatFilterTargetType: ENTITY, singleValue: 'https://sp1.example.org')) it } } @@ -285,6 +302,7 @@ class TestObjectGenerator { it.resourceId = nameIdFormatFilter.resourceId it.removeExistingFormats = nameIdFormatFilter.removeExistingFormats it.formats = nameIdFormatFilter.formats + it.nameIdFormatFilterTarget = nameIdFormatFilter.nameIdFormatFilterTarget it } } @@ -300,33 +318,27 @@ class TestObjectGenerator { List attributes = new ArrayList<>() customPropertiesConfiguration.getOverrides().each { override -> - if (generator.randomBoolean()) { - switch (ModelRepresentationConversions.AttributeTypes.valueOf(override.getDisplayType().toUpperCase())) { - case ModelRepresentationConversions.AttributeTypes.BOOLEAN: - if (override.getPersistType() != null && - override.getPersistType() != override.getDisplayType()) { - attributes.add(attributeUtility.createAttributeWithStringValues(override.getAttributeName(), override.getAttributeFriendlyName(), override.persistValue)) - } else { - attributes.add(attributeUtility.createAttributeWithBooleanValue(override.getAttributeName(), override.getAttributeFriendlyName(), Boolean.valueOf(override.invert) ^ true)) - } - break - case ModelRepresentationConversions.AttributeTypes.INTEGER: - attributes.add(attributeUtility.createAttributeWithIntegerValue(override.getAttributeName(), override.getAttributeFriendlyName(), generator.randomInt(0, 999999))) - break - case ModelRepresentationConversions.AttributeTypes.STRING: - attributes.add(attributeUtility.createAttributeWithStringValues(override.getAttributeName(), override.getAttributeFriendlyName(), generator.randomString(30))) - break - case ModelRepresentationConversions.AttributeTypes.SET: - case ModelRepresentationConversions.AttributeTypes.LIST: - attributes.add(attributeUtility.createAttributeWithStringValues(override.getAttributeName(), override.getAttributeFriendlyName(), generator.randomStringList())) - break - } + switch (ModelRepresentationConversions.AttributeTypes.valueOf(override.getDisplayType().toUpperCase())) { + case ModelRepresentationConversions.AttributeTypes.BOOLEAN: + if (override.getPersistType() != null && override.getPersistType() != override.getDisplayType()) { + attributes.add(attributeUtility.createAttributeWithStringValues(override.getAttributeName(), override.getAttributeFriendlyName(), override.persistValue)) + } else { + attributes.add(attributeUtility.createAttributeWithBooleanValue(override.getAttributeName(), override.getAttributeFriendlyName(), Boolean.valueOf(override.invert) ^ true)) + } + break + case ModelRepresentationConversions.AttributeTypes.INTEGER: + attributes.add(attributeUtility.createAttributeWithIntegerValue(override.getAttributeName(), override.getAttributeFriendlyName(), generator.randomInt(0, 999999))) + break + case ModelRepresentationConversions.AttributeTypes.STRING: + attributes.add(attributeUtility.createAttributeWithStringValues(override.getAttributeName(), override.getAttributeFriendlyName(), generator.randomString(30))) + break + case ModelRepresentationConversions.AttributeTypes.SET: + case ModelRepresentationConversions.AttributeTypes.LIST: + attributes.add(attributeUtility.createAttributeWithStringValues(override.getAttributeName(), override.getAttributeFriendlyName(), generator.randomStringList())) + break } } - if (generator.randomBoolean()) { - attributes.add(attributeUtility.createAttributeWithStringValues(MDDCConstants.RELEASE_ATTRIBUTES, generator.randomStringList())) - } - + attributes.add(attributeUtility.createAttributeWithStringValues(MDDCConstants.RELEASE_ATTRIBUTES, generator.randomStringList())) return attributes } @@ -383,11 +395,8 @@ class TestObjectGenerator { EntityAttributesFilterTarget buildEntityAttributesFilterTarget() { EntityAttributesFilterTarget entityAttributesFilterTarget = new EntityAttributesFilterTarget() - - entityAttributesFilterTarget.setEntityAttributesFilterTargetType(randomFilterTargetType()) - entityAttributesFilterTarget.setSingleValue( - buildEntityAttributesFilterTargetValueByType(entityAttributesFilterTarget.getEntityAttributesFilterTargetType())) - + entityAttributesFilterTarget.setEntityAttributesFilterTargetType(EntityAttributesFilterTargetType.ENTITY) + entityAttributesFilterTarget.setSingleValue(buildEntityAttributesFilterTargetValueByType(EntityAttributesFilterTargetType.ENTITY)) return entityAttributesFilterTarget } @@ -414,18 +423,12 @@ class TestObjectGenerator { FilterTargetRepresentation buildFilterTargetRepresentation() { FilterTargetRepresentation representation = new FilterTargetRepresentation() - representation.setType(randomFilterTargetType().toString()) - representation.setValue([ - buildEntityAttributesFilterTargetValueByType(EntityAttributesFilterTargetType.valueOf(representation.getType())) - ]) + representation.setType(EntityAttributesFilterTargetType.ENTITY.name()) + representation.setValue([ buildEntityAttributesFilterTargetValueByType(EntityAttributesFilterTargetType.ENTITY) ]) return representation } - EntityAttributesFilterTarget.EntityAttributesFilterTargetType randomFilterTargetType() { - EntityAttributesFilterTarget.EntityAttributesFilterTargetType.values()[generator.randomInt(0, 2)] - } - EntityDescriptor buildEntityDescriptor() { EntityDescriptor entityDescriptor = new EntityDescriptor() @@ -469,7 +472,7 @@ class TestObjectGenerator { } ContactPerson buildContactPerson() { - ContactPerson contactPerson = new ContactPerson(); + ContactPerson contactPerson = new ContactPerson() contactPerson.setNamespaceURI(generator.randomString(20)) contactPerson.setElementLocalName(generator.randomString(20)) @@ -495,9 +498,9 @@ class TestObjectGenerator { break case 'Filesystem': randomResolver = filesystemMetadataResolver() - break; + break default: - throw new RuntimeException("Did you forget to create a TestObjectGenerator.MetadataResolver method for resolverType: ${metadataResolverType} ?"); + throw new RuntimeException("Did you forget to create a TestObjectGenerator.MetadataResolver method for resolverType: ${metadataResolverType} ?") } randomResolver } @@ -570,25 +573,25 @@ class TestObjectGenerator { } } - ResourceBackedMetadataResolver resourceBackedMetadataResolverForSVN() { - new ResourceBackedMetadataResolver().with { - it.name = 'SVNResourceMetadata' - it.xmlId = 'SVNResourceMetadata' - it.svnMetadataResource = new SvnMetadataResource().with { - it.resourceFile = 'entity.xml' - it.repositoryURL = 'https://svn.example.org/repo/path/to.dir' - it.workingCopyDirectory = '%{idp.home}/metadata/svn' - it - } - it.reloadableMetadataResolverAttributes = new ReloadableMetadataResolverAttributes().with { - it - } - // Changes in MetadataResolver (removing defaults), so adding back those settings here. - it.enabled = Boolean.TRUE - it.doInitialization = Boolean.TRUE - it - } - } +// ResourceBackedMetadataResolver resourceBackedMetadataResolverForSVN() { +// new ResourceBackedMetadataResolver().with { +// it.name = 'SVNResourceMetadata' +// it.xmlId = 'SVNResourceMetadata' +// it.svnMetadataResource = new SvnMetadataResource().with { +// it.resourceFile = 'entity.xml' +// it.repositoryURL = 'https://svn.example.org/repo/path/to.dir' +// it.workingCopyDirectory = '%{idp.home}/metadata/svn' +// it +// } +// it.reloadableMetadataResolverAttributes = new ReloadableMetadataResolverAttributes().with { +// it +// } +// // Changes in MetadataResolver (removing defaults), so adding back those settings here. +// it.enabled = Boolean.TRUE +// it.doInitialization = Boolean.TRUE +// it +// } +// } ResourceBackedMetadataResolver resourceBackedMetadataResolverForClasspath() { new ResourceBackedMetadataResolver().with { @@ -618,7 +621,7 @@ class TestObjectGenerator { resolver.criterionPredicateRegistryRef = generator.randomString(10) resolver.useDefaultPredicateRegistry = generator.randomBoolean() resolver.satisfyAnyPredicates = generator.randomBoolean() - resolver.metadataFilters = [] + resolver.metadataFilters = [entityAttributesFilter(), entityRoleWhitelistFilter()] resolver.reloadableMetadataResolverAttributes = buildReloadableMetadataResolverAttributes() resolver.httpMetadataResolverAttributes = buildHttpMetadataResolverAttributes() return resolver @@ -659,4 +662,4 @@ class TestObjectGenerator { } return list } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/WithMockAdmin.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/WithMockAdmin.groovy new file mode 100644 index 000000000..9bd6f825a --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/WithMockAdmin.groovy @@ -0,0 +1,11 @@ +package edu.internet2.tier.shibboleth.admin.ui.util + + +import org.springframework.security.test.context.support.WithMockUser + +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy + +@Retention(RetentionPolicy.RUNTIME) +@WithMockUser(value = "admin", roles = ["ADMIN"]) +@interface WithMockAdmin {} \ No newline at end of file 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 548380712..5c99d27a7 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 @@ -1,6 +1,7 @@ package net.unicon.shibui.pac4j; import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException; import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; import edu.internet2.tier.shibboleth.admin.ui.security.model.User; @@ -149,7 +150,7 @@ private Set findOrCreateGroups(ArrayList groupNames) { try { groupService.createGroup(g); } - catch (GroupExistsConflictException shouldntHappen) { + catch (GroupExistsConflictException | InvalidGroupRegexException shouldntHappen) { } } result.add(g); diff --git a/ui/package.json b/ui/package.json index af87df3d6..4b40ce671 100644 --- a/ui/package.json +++ b/ui/package.json @@ -2,6 +2,7 @@ "name": "ui", "version": "0.1.0", "private": true, + "homepage": ".", "dependencies": { "@fortawesome/fontawesome-free": "^5.15.3", "@fortawesome/fontawesome-svg-core": "^1.2.35", diff --git a/ui/public/assets/schema/groups/group.json b/ui/public/assets/schema/groups/group.json index 02bcb2dea..518293851 100644 --- a/ui/public/assets/schema/groups/group.json +++ b/ui/public/assets/schema/groups/group.json @@ -17,6 +17,11 @@ "type": "string", "minLength": 1, "maxLength": 255 + }, + "validationRegex": { + "title": "label.url-validation-regex", + "description": "tooltip.url-validation-regex", + "type": "string" } } } \ No newline at end of file diff --git a/ui/src/app/App.constant.js b/ui/src/app/App.constant.js index c08fda543..3b6af7f89 100644 --- a/ui/src/app/App.constant.js +++ b/ui/src/app/App.constant.js @@ -1,7 +1,13 @@ -export const API_BASE_PATH = 'api'; +export const getBasePath = () => { + const url = new URL(document.getElementsByTagName('base')[0].href); + return url.pathname?.replace(/^\/+/g, ''); -export const FILTER_PLUGIN_TYPES = ['RequiredValidUntil', 'SignatureValidation', 'EntityRoleWhiteList']; + //replace(/^\/|\/$/g, '') +}; +export const BASE_PATH = getBasePath(); +export const API_BASE_PATH = `${BASE_PATH}api`; +export const FILTER_PLUGIN_TYPES = ['RequiredValidUntil', 'SignatureValidation', 'EntityRoleWhiteList']; export default API_BASE_PATH; diff --git a/ui/src/app/App.js b/ui/src/app/App.js index e52a45f4a..ba9ced5f5 100644 --- a/ui/src/app/App.js +++ b/ui/src/app/App.js @@ -29,6 +29,7 @@ import { SessionModal } from './core/user/SessionModal'; import { Roles } from './admin/Roles'; import Button from 'react-bootstrap/Button'; import { Groups } from './admin/Groups'; +import { BASE_PATH } from './App.constant'; function App() { @@ -66,7 +67,7 @@ function App() { {(message, confirm, confirmCallback, setConfirm, getConfirmation) => - +

diff --git a/ui/src/app/App.test.js b/ui/src/app/App.test.js index 0b91c687f..3ee0682ba 100644 --- a/ui/src/app/App.test.js +++ b/ui/src/app/App.test.js @@ -2,6 +2,12 @@ import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; +jest.mock('./App.constant', () => ({ + get API_BASE_PATH() { + return '/'; + } +})); + it('renders without crashing', () => { const div = document.createElement('div'); ReactDOM.render(, div); diff --git a/ui/src/app/admin/component/GroupForm.js b/ui/src/app/admin/component/GroupForm.js index da8c5af8b..77ec82135 100644 --- a/ui/src/app/admin/component/GroupForm.js +++ b/ui/src/app/admin/component/GroupForm.js @@ -5,7 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSpinner, faSave } from '@fortawesome/free-solid-svg-icons'; import Translate from '../../i18n/components/translate'; -import { useGroupUiSchema } from '../hooks'; +import { useGroupUiSchema, useGroupUiValidator } from '../hooks'; import { fields, widgets } from '../../form/component'; import { templates } from '../../form/component'; import { FormContext, setFormDataAction, setFormErrorAction } from '../../form/FormManager'; @@ -23,6 +23,7 @@ export function GroupForm ({group = {}, errors = [], loading = false, schema, on }; const uiSchema = useGroupUiSchema(); + const validator = useGroupUiValidator(); return (<>
@@ -49,6 +50,7 @@ export function GroupForm ({group = {}, errors = [], loading = false, schema, on
onChange(form)} + validate={validator} schema={schema} uiSchema={uiSchema} FieldTemplate={templates.FieldTemplate} diff --git a/ui/src/app/admin/container/EditGroup.js b/ui/src/app/admin/container/EditGroup.js index 066350356..3edbac5ec 100644 --- a/ui/src/app/admin/container/EditGroup.js +++ b/ui/src/app/admin/container/EditGroup.js @@ -11,6 +11,7 @@ import { GroupForm } from '../component/GroupForm'; import { GroupProvider } from '../hoc/GroupProvider'; import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; import { useTranslator } from '../../i18n/hooks'; +import { BASE_PATH } from '../../App.constant'; export function EditGroup() { @@ -67,7 +68,7 @@ export function EditGroup() {
{(group) => - + {(schema) => <>{group && diff --git a/ui/src/app/admin/container/EditRole.js b/ui/src/app/admin/container/EditRole.js index 16b503aed..1181b8eac 100644 --- a/ui/src/app/admin/container/EditRole.js +++ b/ui/src/app/admin/container/EditRole.js @@ -11,6 +11,7 @@ import { RoleForm } from '../component/RoleForm'; import { RoleProvider } from '../hoc/RoleProvider'; import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; import { useTranslator } from '../../i18n/hooks'; +import { BASE_PATH } from '../../App.constant'; export function EditRole() { @@ -67,7 +68,7 @@ export function EditRole() {
{(role) => - + {(schema) => <>{role && diff --git a/ui/src/app/admin/container/NewGroup.js b/ui/src/app/admin/container/NewGroup.js index 28994abd0..dfa53bfc1 100644 --- a/ui/src/app/admin/container/NewGroup.js +++ b/ui/src/app/admin/container/NewGroup.js @@ -9,6 +9,7 @@ import { GroupForm } from '../component/GroupForm'; import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; import { useTranslator } from '../../i18n/hooks'; +import { BASE_PATH } from '../../App.constant'; export function NewGroup() { const history = useHistory(); @@ -59,7 +60,7 @@ export function NewGroup() {
- + {(schema) => {(data, errors) => diff --git a/ui/src/app/admin/container/NewRole.js b/ui/src/app/admin/container/NewRole.js index 6d1abfc56..fff718ea7 100644 --- a/ui/src/app/admin/container/NewRole.js +++ b/ui/src/app/admin/container/NewRole.js @@ -9,6 +9,7 @@ import { RoleForm } from '../component/RoleForm'; import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; import { useTranslator } from '../../i18n/hooks'; +import { BASE_PATH } from '../../App.constant'; export function NewRole() { const history = useHistory(); @@ -59,7 +60,7 @@ export function NewRole() {
- + {(schema) => {(data, errors) => diff --git a/ui/src/app/admin/hoc/RoleProvider.test.js b/ui/src/app/admin/hoc/RoleProvider.test.js index a7d3347b4..480118e94 100644 --- a/ui/src/app/admin/hoc/RoleProvider.test.js +++ b/ui/src/app/admin/hoc/RoleProvider.test.js @@ -5,6 +5,12 @@ import {RoleProvider} from './RoleProvider'; import {useRole} from '../hooks'; +jest.mock('../../App.constant', () => ({ + get API_BASE_PATH() { + return '/'; + } +})); + jest.mock('../hooks'); describe('RoleProvider component', () => { diff --git a/ui/src/app/admin/hoc/RolesProvider.test.js b/ui/src/app/admin/hoc/RolesProvider.test.js index 11c01481e..65cc3ee22 100644 --- a/ui/src/app/admin/hoc/RolesProvider.test.js +++ b/ui/src/app/admin/hoc/RolesProvider.test.js @@ -6,6 +6,12 @@ import { RolesProvider } from './RolesProvider'; import { useRoles } from '../hooks'; import { useNotificationDispatcher } from "../../notifications/hoc/Notifications"; +jest.mock('../../App.constant', () => ({ + get API_BASE_PATH() { + return '/'; + } +})); + jest.mock('../hooks'); jest.mock('../../notifications/hoc/Notifications'); diff --git a/ui/src/app/admin/hooks.js b/ui/src/app/admin/hooks.js index 772ea1ec0..66922a97c 100644 --- a/ui/src/app/admin/hooks.js +++ b/ui/src/app/admin/hooks.js @@ -1,4 +1,6 @@ import useFetch from 'use-http'; +import isNil from 'lodash/isNil'; +import {isValidRegex} from '../core/utility/is_valid_regex'; import API_BASE_PATH from '../App.constant'; export function useGroups (opts = { cachePolicy: 'no-cache' }) { @@ -29,6 +31,18 @@ export function useGroupUiSchema () { }; } +export function useGroupUiValidator() { + return (formData, errors) => { + if (!isNil(formData?.validationRegex)) { + const isValid = isValidRegex(formData.validationRegex); + if (!isValid) { + errors.validationRegex.addError('message.invalid-regex-pattern'); + } + } + return errors; + } +} + export function useRoleUiSchema() { return {}; } diff --git a/ui/src/app/admin/hooks.test.js b/ui/src/app/admin/hooks.test.js index d739c9aa1..10a102df3 100644 --- a/ui/src/app/admin/hooks.test.js +++ b/ui/src/app/admin/hooks.test.js @@ -1,4 +1,5 @@ import { + useGroupUiValidator, useRoles, useRole, } from './hooks'; @@ -6,8 +7,22 @@ import { import useFetch from 'use-http'; import API_BASE_PATH from '../App.constant'; +jest.mock('../App.constant', () => ({ + get API_BASE_PATH() { + return '/'; + } +})); + jest.mock('use-http'); +it('should validate against a regex', () => { + const validator = useGroupUiValidator(); + const addErrorSpy = jest.fn(); + const fail = validator({ validationRegex: '))(()' }, { validationRegex: { addError: addErrorSpy } }); + expect(addErrorSpy).toHaveBeenCalled(); + expect(validator({validationRegex: '/*'})).toBeUndefined(); +}); + describe('api hooks', () => { let mockPut; @@ -52,4 +67,4 @@ describe('api hooks', () => { expect(useFetch).toHaveBeenCalledWith(`${API_BASE_PATH}/admin/roles/foo`, opts) }); }); -}); \ No newline at end of file +}); diff --git a/ui/src/app/core/components/Footer.test.js b/ui/src/app/core/components/Footer.test.js index 8f9453338..63c89e9e1 100644 --- a/ui/src/app/core/components/Footer.test.js +++ b/ui/src/app/core/components/Footer.test.js @@ -2,6 +2,12 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { Footer } from './Footer'; +jest.mock('../../App.constant', () => ({ + get API_BASE_PATH() { + return '/'; + } +})); + jest.mock('../../i18n/hooks', () => ({ useTranslation: (value) => value })); diff --git a/ui/src/app/core/components/Header.js b/ui/src/app/core/components/Header.js index 4847c83f5..7bb693b1e 100644 --- a/ui/src/app/core/components/Header.js +++ b/ui/src/app/core/components/Header.js @@ -14,6 +14,7 @@ import { useTranslator } from '../../i18n/hooks'; import { brand } from '../../app.brand'; import { useCurrentUser, useCurrentUserLoading, useIsAdmin } from '../user/UserContext'; +import { BASE_PATH } from '../../App.constant'; export function Header () { @@ -98,7 +99,7 @@ export function Header () { Groups {groupId} - Logout diff --git a/ui/src/app/core/components/Header.test.js b/ui/src/app/core/components/Header.test.js index 6b09a6b5c..70d186d91 100644 --- a/ui/src/app/core/components/Header.test.js +++ b/ui/src/app/core/components/Header.test.js @@ -3,6 +3,12 @@ import { render, screen } from '@testing-library/react'; import { BrowserRouter as Router } from 'react-router-dom'; import { Header } from './Header'; +jest.mock('../../App.constant', () => ({ + get API_BASE_PATH() { + return '/'; + } +})); + jest.mock('../../i18n/hooks', () => ({ useTranslator: () => (value) => value, useTranslation: (value) => value diff --git a/ui/src/app/core/components/VersionInfo.js b/ui/src/app/core/components/VersionInfo.js index ab1fff8d5..1c6badfc1 100644 --- a/ui/src/app/core/components/VersionInfo.js +++ b/ui/src/app/core/components/VersionInfo.js @@ -1,6 +1,7 @@ import React from 'react'; import useFetch from 'use-http'; +import { BASE_PATH } from '../../App.constant'; import Translate from '../../i18n/components/translate'; @@ -11,7 +12,7 @@ const params = { year }; export function VersionInfo () { - const { data = {} } = useFetch('/actuator/info', {}, []); + const { data = {} } = useFetch(`${BASE_PATH}actuator/info`, {}, []); const [ versionData, setVersionData ] = React.useState(''); diff --git a/ui/src/app/core/components/VersionInfo.test.js b/ui/src/app/core/components/VersionInfo.test.js index 28a8a6b47..8a14c3154 100644 --- a/ui/src/app/core/components/VersionInfo.test.js +++ b/ui/src/app/core/components/VersionInfo.test.js @@ -4,6 +4,12 @@ import { render, screen } from '@testing-library/react'; import { VersionInfo } from './VersionInfo'; +jest.mock('../../App.constant', () => ({ + get API_BASE_PATH() { + return '/'; + } +})); + const data = { "git": { "branch": "react", "commit": { "id": "634f1a4", "time": "2021-06-10T19:00:29Z" } }, "build": { "artifact": "shibui", "name": "backend", "time": "2021-06-10T19:26:14.478Z", "version": "2.0.0-SNAPSHOT", "group": "edu.internet2.tier.shibboleth.admin.ui" } }; jest.mock('../../i18n/components/translate', () => { diff --git a/ui/src/app/core/user/UserContext.js b/ui/src/app/core/user/UserContext.js index a3c1e073c..14666abe8 100644 --- a/ui/src/app/core/user/UserContext.js +++ b/ui/src/app/core/user/UserContext.js @@ -12,8 +12,7 @@ const path = '/admin/users/current'; function UserProvider({ children }) { const { get, response, loading } = useFetch(`${API_BASE_PATH}`, { - cacheLife: 10000, - cachePolicy: 'cache-first' + cachePolicy: 'no-cache' }); React.useEffect(() => { loadUser() }, []); @@ -71,4 +70,26 @@ function useCanEnable() { return isAdmin || isEnabler; } -export { UserContext, UserProvider, Consumer as UserConsumer, useCurrentUser, useIsAdmin, useIsAdminOrInGroup, useCanEnable, useCurrentUserLoading}; +function useUserGroup() { + const user = useCurrentUser(); + return user?.userGroups[0]; +} + +function useUserGroupRegexValidator () { + const user = useCurrentUser(); + return (user?.userGroups && user.userGroups.length > 0) ? user?.userGroups[0].validationRegex : null; +} + + +export { + UserContext, + UserProvider, + Consumer as UserConsumer, + useCurrentUser, + useIsAdmin, + useIsAdminOrInGroup, + useCanEnable, + useCurrentUserLoading, + useUserGroupRegexValidator, + useUserGroup +}; diff --git a/ui/src/app/core/user/UserContext.test.js b/ui/src/app/core/user/UserContext.test.js index d5e2ce1cb..9807981bf 100644 --- a/ui/src/app/core/user/UserContext.test.js +++ b/ui/src/app/core/user/UserContext.test.js @@ -2,6 +2,12 @@ import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import { UserProvider, useIsAdmin } from './UserContext'; +jest.mock('../../App.constant', () => ({ + get API_BASE_PATH() { + return '/'; + } +})); + const getFn = jest.fn(); const okFn = jest.fn(); diff --git a/ui/src/app/form/component/fields/FilterTargetField.js b/ui/src/app/form/component/fields/FilterTargetField.js index 3bd6fb26a..b9d44c725 100644 --- a/ui/src/app/form/component/fields/FilterTargetField.js +++ b/ui/src/app/form/component/fields/FilterTargetField.js @@ -45,8 +45,13 @@ const FilterTargetField = ({ onChange, errorSchema, formData, + formContext, ...props }) => { + + const { group } = formContext; + const regex = new RegExp(group?.validationRegex || '/*'); + const typeFieldName = `${name}Type`; const type = schema.properties[typeFieldName]; @@ -59,6 +64,8 @@ const FilterTargetField = ({ const [selectedTarget, setSelectedTarget] = React.useState([...(formData.value && !isNil(formData.value) && !isNil(formData.value[0]) ? formData.value : [])]); const [term, setSearchTerm] = React.useState(''); + const [match, setMatch] = React.useState(true); + const [touched, setTouched] = React.useState(false); const [ids, setSearchIds] = React.useState([]); const { get, response } = useFetch(`${API_BASE_PATH}/EntityIds/search`, { @@ -113,6 +120,7 @@ const FilterTargetField = ({ const onEntityIdsChange = (value) => { setSearchTerm(value); + setMatch(regex ? regex.test(value) : true); }; const selectType = (option) => { @@ -172,19 +180,28 @@ const FilterTargetField = ({ onInputChange={onEntityIdsChange} selected={ [term] } onChange={ () => {} } + onBlur={() => setTouched(true)} onSearch={ (query) => setSearchTerm(query) } - renderMenuItemChildren={(option, { options, text }, index) => { - return {option}; - }}> + renderMenuItemChildren={(option, { options, text }, index) => + {option} + }> {({ isMenuShown, toggleMenu }) => ( toggleMenu()} disabled={disabled || readonly} /> )} - - - You must add at least one entity id target and they must each be unique. - - + {(!touched || match) ? + + + You must add at least one entity id target and they must each be unique. + + + : + + + Invalid URL + + + } } { targetType === 'CONDITION_SCRIPT' && @@ -210,6 +227,7 @@ const FilterTargetField = ({ className="form-control" type="text" name="script" + value={selectedTarget[0]} onChange={ ({target: { value }}) => handleTextChange(value) } /> {errorSchema?.value?.__errors ? @@ -232,7 +250,7 @@ const FilterTargetField = ({