diff --git a/.gitignore b/.gitignore index cb8ad4931..f75d64309 100644 --- a/.gitignore +++ b/.gitignore @@ -406,4 +406,5 @@ beacon/spring/out *.classpath *.settings *.project -*bin \ No newline at end of file +*bin +/a.json diff --git a/backend/build.gradle b/backend/build.gradle index 92ea35284..cc42536b8 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -47,6 +47,23 @@ configurations { def generatedSrcDir = new File(buildDir, 'generated/src/main/java') +//test { +// exclude 'edu/internet2/tier/shibboleth/admin/ui/configuration/*.class' +// exclude 'edu/internet2/tier/shibboleth/admin/ui/controller/*.class' +// exclude 'edu/internet2/tier/shibboleth/admin/ui/domain/*.class' +// exclude 'edu/internet2/tier/shibboleth/admin/ui/domain/filters/*.class' +// exclude 'edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/*.class' +// exclude 'edu/internet2/tier/shibboleth/admin/ui/domain/versioning/*.class' +// exclude 'edu/internet2/tier/shibboleth/admin/ui/repository/*.class' +// exclude 'edu/internet2/tier/shibboleth/admin/ui/scheduled/*.class' +// exclude 'edu/internet2/tier/shibboleth/admin/ui/security/controller/*.class' +// exclude 'edu/internet2/tier/shibboleth/admin/ui/security/repository/*.class' +//// exclude 'edu/internet2/tier/shibboleth/admin/ui/security/service/*.class' +//// exclude 'edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/*.class' +// exclude 'edu/internet2/tier/shibboleth/admin/ui/service/*.class' +// exclude 'edu/internet2/tier/shibboleth/admin/ui/util/*.class' +//} + sourceSets { main { groovy { @@ -353,4 +370,4 @@ dockerRun { daemonize true command '--spring.profiles.include=very-dangerous,dev', '--shibui.default-password={noop}password' clean true -} +} \ No newline at end of file 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 802002fde..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) diff --git a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/service/envers/EnversEntityDescriptorVersionServiceTests.groovy b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/service/envers/EnversEntityDescriptorVersionServiceTests.groovy index c5b1f4138..f996c534d 100644 --- a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/service/envers/EnversEntityDescriptorVersionServiceTests.groovy +++ b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/service/envers/EnversEntityDescriptorVersionServiceTests.groovy @@ -58,7 +58,7 @@ class EnversEntityDescriptorVersionServiceTests extends Specification { when: 'Second version' ed.serviceProviderName = 'SP2' - ed = edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.doInExplicitTransaction(txMgr) { + ed = EnversTestsSupport.doInExplicitTransaction(txMgr) { entityDescriptorRepository.save(ed) } versions = entityDescriptorVersionService.findVersionsForEntityDescriptor(ed.resourceId) @@ -71,7 +71,7 @@ class EnversEntityDescriptorVersionServiceTests extends Specification { when: 'Third version' ed.serviceProviderName = 'SP3' - ed = edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.doInExplicitTransaction(txMgr) { + ed = EnversTestsSupport.doInExplicitTransaction(txMgr) { entityDescriptorRepository.save(ed) } versions = entityDescriptorVersionService.findVersionsForEntityDescriptor(ed.resourceId) @@ -86,7 +86,7 @@ class EnversEntityDescriptorVersionServiceTests extends Specification { def "versioning service returns correct entity descriptor for version number"() { when: 'Initial version' EntityDescriptor ed = new EntityDescriptor(entityID: 'ed', serviceProviderName: 'SP1', createdBy: 'anonymousUser') - ed = edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.doInExplicitTransaction(txMgr) { + ed = EnversTestsSupport.doInExplicitTransaction(txMgr) { entityDescriptorRepository.save(ed) } def versions = entityDescriptorVersionService.findVersionsForEntityDescriptor(ed.resourceId) @@ -98,7 +98,7 @@ class EnversEntityDescriptorVersionServiceTests extends Specification { when: 'Update the original' ed.serviceProviderName = 'SP2' - ed = edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.doInExplicitTransaction(txMgr) { + ed = EnversTestsSupport.doInExplicitTransaction(txMgr) { entityDescriptorRepository.save(ed) } versions = entityDescriptorVersionService.findVersionsForEntityDescriptor(ed.resourceId) @@ -112,17 +112,17 @@ class EnversEntityDescriptorVersionServiceTests extends Specification { def "versioning service throws EntityNotFoundException for non existent version number"() { when: 'Initial version' EntityDescriptor ed = new EntityDescriptor(entityID: 'ed', serviceProviderName: 'SP1', createdBy: 'anonymousUser') - ed = edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.doInExplicitTransaction(txMgr) { + ed = EnversTestsSupport.doInExplicitTransaction(txMgr) { entityDescriptorRepository.save(ed) } then: try { def edRepresentation = entityDescriptorVersionService.findSpecificVersionOfEntityDescriptor(ed.resourceId, '1000') - 1 == 2 + false } catch (EntityNotFoundException expected) { - 1 == 1 + true } } -} +} \ 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 f63de0276..e156451ee 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 @@ -131,6 +131,8 @@ class SeleniumSIDETest extends Specification { 'SHIBUI-1740: Verify dev profile group membership' | '/SHIBUI-1740-2.side' 'SHIBUI-1740: Verify admin-owned resource not visible to nonadmins' | '/SHIBUI-1740-3.side' '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' } } diff --git a/backend/src/integration/resources/SHIBUI-1742-1.side b/backend/src/integration/resources/SHIBUI-1742-1.side new file mode 100644 index 000000000..cc8b94078 --- /dev/null +++ b/backend/src/integration/resources/SHIBUI-1742-1.side @@ -0,0 +1,551 @@ +{ + "id": "13a69ee7-b636-4bfe-8f84-ef4670ded81e", + "version": "2.0", + "name": "SHIBUI-1742-1", + "url": "http://localhost:10101", + "tests": [{ + "id": "a26f2eda-ddf2-4c49-a29d-d86a282bb75a", + "name": "SHIBUI-1742-1", + "commands": [{ + "id": "db1854f2-6ab7-49d9-b2cc-ba4dfcb7cafd", + "comment": "", + "command": "open", + "target": "/login", + "targets": [], + "value": "" + }, { + "id": "46f32c9e-84a3-4aad-b61c-9fb6a9de3b1a", + "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": "9a5a9a52-3c62-47fd-b6f6-4525bcde990d", + "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": "06d4ce18-c03f-424d-9eac-0c44b52dd9ab", + "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": "9572c7f3-5365-4864-a335-a0ed4c87ec7f", + "comment": "", + "command": "open", + "target": "/api/heheheheheheheWipeout", + "targets": [], + "value": "" + }, { + "id": "f7a3bd08-36e1-4b1d-97ad-88b3fcca1569", + "comment": "", + "command": "assertText", + "target": "css=body", + "targets": [], + "value": "yes, you did it" + }, { + "id": "661c8c9a-0a29-46e2-b29a-0649f16d7ed3", + "comment": "", + "command": "open", + "target": "/", + "targets": [], + "value": "" + }, { + "id": "5665bd34-1f12-40ed-b33a-7c800d24988b", + "comment": "", + "command": "click", + "target": "linkText=Admin", + "targets": [ + ["linkText=Admin", "linkText"], + ["css=.nav-item:nth-child(3) > .nav-link", "css:finder"], + ["xpath=//a[contains(text(),'Admin')]", "xpath:link"], + ["xpath=//div[@id='root']/div/main/div/div/div[3]/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/dashboard/admin/management')]", "xpath:href"], + ["xpath=//div[3]/a", "xpath:position"], + ["xpath=//a[contains(.,'Admin')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "5d37e95f-2c20-4ff4-9d78-95471f0999e2", + "comment": "", + "command": "click", + "target": "id=role-anonymousUser", + "targets": [ + ["id=role-anonymousUser", "id"], + ["name=role-anonymousUser", "name"], + ["css=#role-anonymousUser", "css:finder"], + ["xpath=//select[@id='role-anonymousUser']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[4]/td[4]/select", "xpath:idRelative"], + ["xpath=//tr[4]/td[4]/select", "xpath:position"] + ], + "value": "" + }, { + "id": "2f0250cc-19c8-4ef2-aeb9-1199292ab2f9", + "comment": "", + "command": "select", + "target": "id=role-anonymousUser", + "targets": [], + "value": "label=ROLE_ENABLE" + }, { + "id": "58c0e31f-f73c-4b5c-82b2-7825dc5efa67", + "comment": "", + "command": "click", + "target": "id=group-anonymousUser", + "targets": [ + ["id=group-anonymousUser", "id"], + ["name=group-anonymousUser", "name"], + ["css=#group-anonymousUser", "css:finder"], + ["xpath=//select[@id='group-anonymousUser']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[4]/td[5]/span/select", "xpath:idRelative"], + ["xpath=//tr[4]/td[5]/span/select", "xpath:position"] + ], + "value": "" + }, { + "id": "e6960825-45d2-4399-871d-5db5295ee797", + "comment": "", + "command": "select", + "target": "id=group-anonymousUser", + "targets": [], + "value": "label=A1" + }, { + "id": "b2e8d04b-d72f-4095-be44-da0608dcbb90", + "comment": "", + "command": "click", + "target": "id=group-nonadmin", + "targets": [ + ["id=group-nonadmin", "id"], + ["name=group-nonadmin", "name"], + ["css=#group-nonadmin", "css:finder"], + ["xpath=//select[@id='group-nonadmin']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[2]/td[5]/span/select", "xpath:idRelative"], + ["xpath=//tr[2]/td[5]/span/select", "xpath:position"] + ], + "value": "" + }, { + "id": "6ac3fde8-10fb-4ebd-b29a-13c194714fd3", + "comment": "", + "command": "select", + "target": "id=group-nonadmin", + "targets": [], + "value": "label=A1" + }, { + "id": "7109b2ff-b84f-403f-afd0-9e260bc1fc81", + "comment": "", + "command": "click", + "target": "linkText=Logout", + "targets": [ + ["linkText=Logout", "linkText"], + ["css=.nav-link:nth-child(4)", "css:finder"], + ["xpath=//a[contains(text(),'Logout')]", "xpath:link"], + ["xpath=//div[@id='basic-navbar-nav']/div/a[2]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/logout')]", "xpath:href"], + ["xpath=//a[2]", "xpath:position"], + ["xpath=//a[contains(.,'Logout')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "cdb975a8-eb89-4d03-a544-fde32d069448", + "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": "fb80084d-56db-4dec-9ca8-4eaefa6e4178", + "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": "1c46ef5e-e997-40f5-87a6-d8ef4c7f4f2a", + "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": "bbbdc0b9-5e2d-4283-8cbf-7ccfa089d23d", + "comment": "", + "command": "click", + "target": "id=dropdown-basic", + "targets": [ + ["id=dropdown-basic", "id"], + ["css=#dropdown-basic", "css:finder"], + ["xpath=//button[@id='dropdown-basic']", "xpath:attributes"], + ["xpath=//div[@id='basic-nav-dropdown']/button", "xpath:idRelative"], + ["xpath=//div/button", "xpath:position"], + ["xpath=//button[contains(.,'Add New')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "76fc5d62-a49c-4788-91cb-c285cb9456d5", + "comment": "", + "command": "click", + "target": "linkText=Add a new metadata source", + "targets": [ + ["linkText=Add a new metadata source", "linkText"], + ["css=.text-primary", "css:finder"], + ["xpath=//a[contains(text(),'Add a new metadata source')]", "xpath:link"], + ["xpath=//div[@id='basic-nav-dropdown']/div/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/metadata/source/new')]", "xpath:href"], + ["xpath=//div/a", "xpath:position"], + ["xpath=//a[contains(.,'Add a new metadata source')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "40684f5b-1c6b-4d6d-839d-f87e74f793a6", + "comment": "", + "command": "click", + "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": "" + }, { + "id": "e06e32d9-4979-4ba0-95bb-57db5ab2a591", + "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": "e3c65f02-18d9-45a0-82bf-e3c3d79a5f66", + "comment": "", + "command": "click", + "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": "" + }, { + "id": "a0664314-8c95-43c9-9b56-8686e686c68a", + "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": "test" + }, { + "id": "c8db7219-1b35-4e73-a8ae-1f166aad6562", + "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": "088c3f4f-7a89-494f-95e0-0ad9b3385109", + "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": "10ab17b6-b92c-480e-867f-3a579cea41a3", + "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": "396d28c6-0c02-4cba-929c-1cae2c373b56", + "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": "0ca516e1-78bf-4a19-b18a-edefe5b13144", + "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": "c504aa92-cf30-4384-ad03-c1f0b2fe4c88", + "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": "13d6a627-02ea-4dab-b600-9f3dfab8ccb7", + "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": "53f22ab7-97c2-46ae-96f8-1f783941d800", + "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": "f9bcd184-7f40-481a-ab13-0e59f020528c", + "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": "77caf09c-ad60-4f46-81df-c90b23b4c7bc", + "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": "7adae5ea-9424-4709-8472-54514cd189a5", + "comment": "", + "command": "click", + "target": "linkText=Logout", + "targets": [ + ["linkText=Logout", "linkText"], + ["css=.nav-link:nth-child(3)", "css:finder"], + ["xpath=//a[contains(text(),'Logout')]", "xpath:link"], + ["xpath=//div[@id='basic-navbar-nav']/div/a[2]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/logout')]", "xpath:href"], + ["xpath=//a[2]", "xpath:position"], + ["xpath=//a[contains(.,'Logout')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "06af2891-5091-40ba-9b0a-5a98f3e96b00", + "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": "anonymousUser" + }, { + "id": "857dc0c2-943b-4a2c-8903-415ff473db3b", + "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": "anonymous" + }, { + "id": "53c20686-7317-4dd8-913a-e6db052853c1", + "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": "9585df73-48e3-4ca3-b13c-74ecdee11461", + "comment": "", + "command": "click", + "target": "xpath=//table/tbody/tr/td[5]/span/div/input", + "targets": [ + ["css=.justify-content-center", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/div/table/tbody/tr/td[5]/span", "xpath:idRelative"], + ["xpath=//td[5]/span", "xpath:position"] + ], + "value": "" + }, { + "id": "0d322ab9-f8d7-4cc3-8b91-48c684955b1a", + "comment": "", + "command": "click", + "target": "linkText=Logout", + "targets": [ + ["linkText=Logout", "linkText"], + ["css=.nav-link:nth-child(3)", "css:finder"], + ["xpath=//a[contains(text(),'Logout')]", "xpath:link"], + ["xpath=//div[@id='basic-navbar-nav']/div/a[2]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/logout')]", "xpath:href"], + ["xpath=//a[2]", "xpath:position"], + ["xpath=//a[contains(.,'Logout')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "fbf1ba38-145e-42fb-899b-a7d8bd86d8a2", + "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": "d1af6437-5307-4102-aadb-7d87bbaa08cb", + "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": "7dbd5a0c-31ea-4cf6-83fd-de40f4e239fc", + "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": "79423a30-b82b-443f-b0ea-80370a6d397b", + "comment": "", + "command": "assertChecked", + "target": "xpath=//table/tbody/tr/td[5]/span/div/input", + "targets": [ + ["css=.custom-control-label", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/div/table/tbody/tr/td[5]/span/div/label", "xpath:idRelative"], + ["xpath=//span/div/label", "xpath:position"] + ], + "value": "" + }] + }], + "suites": [{ + "id": "8a97286b-5660-452c-9f23-4c5f5bf8de3b", + "name": "Default Suite", + "persistSession": false, + "parallel": false, + "timeout": 300, + "tests": ["a26f2eda-ddf2-4c49-a29d-d86a282bb75a"] + }], + "urls": ["http://localhost:10101/"], + "plugins": [] +} \ No newline at end of file diff --git a/backend/src/integration/resources/SHIBUI-1742-2.side b/backend/src/integration/resources/SHIBUI-1742-2.side new file mode 100644 index 000000000..a0684ca69 --- /dev/null +++ b/backend/src/integration/resources/SHIBUI-1742-2.side @@ -0,0 +1,462 @@ +{ + "id": "913c2183-c59c-4c54-8434-5866b49a982b", + "version": "2.0", + "name": "SHIBUI-1742-2", + "url": "http://localhost:10101", + "tests": [{ + "id": "629ba6a4-486d-4dbe-9ea1-4727a1e35567", + "name": "SHIBUI-1742-2", + "commands": [{ + "id": "252698e5-62bc-4087-b465-6cce517e4f42", + "comment": "", + "command": "open", + "target": "/login", + "targets": [], + "value": "" + }, { + "id": "68e6f41c-273d-47d5-a3bc-3886f3bb0361", + "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": "890b33ad-e28f-430d-a8f1-99d028c16c1e", + "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": "52216d6c-70de-47ee-a385-8b015e244b42", + "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": "53ffb74f-8635-453b-8b3e-22c0a11e0902", + "comment": "", + "command": "pause", + "target": "5000", + "targets": [], + "value": "" + }, { + "id": "c820e378-569c-44ba-834a-c7d6203d3e12", + "comment": "", + "command": "click", + "target": "xpath=//button[text()='Advanced']", + "targets": [], + "value": "30000" + }, { + "id": "8f1af018-7f7f-441c-bb59-9591469cc0da", + "comment": "", + "command": "click", + "target": "linkText=Roles", + "targets": [ + ["linkText=Roles", "linkText"], + ["css=.text-primary:nth-child(3)", "css:finder"], + ["xpath=//a[contains(text(),'Roles')]", "xpath:link"], + ["xpath=//div[@id='basic-nav-dropdown']/div/a[3]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/roles')]", "xpath:href"], + ["xpath=//a[3]", "xpath:position"], + ["xpath=//a[contains(.,'Roles')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "be331a79-e161-460b-87e5-0458bd6d87bc", + "comment": "", + "command": "click", + "target": "linkText=Add new role", + "targets": [ + ["linkText=Add new role", "linkText"], + ["css=.btn-success", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/roles/new')]", "xpath:href"], + ["xpath=//div[2]/div/a", "xpath:position"], + ["xpath=//a[contains(.,'  Add new role')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "b9f67743-c715-4db6-9380-77c39f23bc89", + "comment": "", + "command": "type", + "target": "id=root_name", + "targets": [ + ["id=root_name", "id"], + ["css=#root_name", "css:finder"], + ["xpath=//input[@id='root_name']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div/form/div/div/div/div/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "test" + }, { + "id": "548d864c-87ba-4889-823e-3e9729d596db", + "comment": "", + "command": "click", + "target": "css=.fa-save", + "targets": [ + ["css=.fa-save", "css:finder"] + ], + "value": "" + }, { + "id": "5a6af58f-5509-4d8c-ab15-11bb8ea124fc", + "comment": "", + "command": "waitForElementVisible", + "target": "xpath=//div[@role=\"alert\" and contains(., \"Added role successfully.\")]", + "targets": [], + "value": "30000" + }, { + "id": "7721b7e5-53d3-4000-b9b3-7553f517a3e2", + "comment": "", + "command": "click", + "target": "css=tr:nth-child(5) .text-primary > .svg-inline--fa", + "targets": [ + ["css=tr:nth-child(5) .text-primary > .svg-inline--fa", "css:finder"] + ], + "value": "" + }, { + "id": "49fd8a6e-9483-483a-9a51-cc41a58b8f8e", + "comment": "", + "command": "type", + "target": "id=root_name", + "targets": [ + ["id=root_name", "id"], + ["css=#root_name", "css:finder"], + ["xpath=//input[@id='root_name']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div/form/div/div/div/div/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "" + }, { + "id": "905bf81b-a0c1-4ec4-a874-621b2deea715", + "comment": "", + "command": "type", + "target": "id=root_name", + "targets": [ + ["id=root_name", "id"], + ["css=#root_name", "css:finder"], + ["xpath=//input[@id='root_name']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div/form/div/div/div/div/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "test role" + }, { + "id": "a2365448-e076-49ed-8fe6-daf35dc6e800", + "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": "159a7bf1-39e9-460f-890f-47c42d3e22ce", + "comment": "", + "command": "assertElementPresent", + "target": "xpath=//div[@role=\"alert\" and contains(., \"Updated role successfully.\")]", + "targets": [], + "value": "" + }, { + "id": "2317a47a-e308-4c1f-bd08-50360950c082", + "comment": "", + "command": "click", + "target": "xpath=(//button[@id='dropdown-basic'])[2]", + "targets": [ + ["xpath=(//button[@id='dropdown-basic'])[2]", "xpath:attributes"], + ["xpath=(//div[@id='basic-nav-dropdown']/button)[2]", "xpath:idRelative"], + ["xpath=//div[2]/button", "xpath:position"], + ["xpath=//button[contains(.,'Add New')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "cfe9cc6f-d96a-48ad-a273-d03eba7cefce", + "comment": "", + "command": "click", + "target": "id=dropdown-basic", + "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": "231fa40a-bb22-4d83-99e2-9a5a9d3ff823", + "comment": "", + "command": "click", + "target": "css=.p-3 > .d-flex", + "targets": [ + ["css=.p-3 > .d-flex", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div", "xpath:idRelative"], + ["xpath=//section/div/div[2]/div", "xpath:position"] + ], + "value": "" + }, { + "id": "e20c0727-c04f-465d-a7dc-7c7a9210c47b", + "comment": "", + "command": "click", + "target": "linkText=Dashboard", + "targets": [ + ["linkText=Dashboard", "linkText"], + ["css=.nav-link:nth-child(3)", "css:finder"], + ["xpath=//a[contains(text(),'Dashboard')]", "xpath:link"], + ["xpath=//div[@id='basic-navbar-nav']/div/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/dashboard')]", "xpath:href"], + ["xpath=//nav/div/div/a", "xpath:position"], + ["xpath=//a[contains(.,'Dashboard')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "733d176c-c0a3-4427-a7dd-1bc5ed42d96e", + "comment": "", + "command": "click", + "target": "linkText=Admin", + "targets": [ + ["linkText=Admin", "linkText"], + ["css=.nav-item:nth-child(3) > .nav-link", "css:finder"], + ["xpath=//a[contains(text(),'Admin')]", "xpath:link"], + ["xpath=//div[@id='root']/div/main/div/div/div[3]/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/dashboard/admin/management')]", "xpath:href"], + ["xpath=//div[3]/a", "xpath:position"], + ["xpath=//a[contains(.,'Admin')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "a5c105ee-9b6c-4226-bda6-fc17a77f2b35", + "comment": "", + "command": "click", + "target": "id=role-nonadmin", + "targets": [ + ["id=role-nonadmin", "id"], + ["name=role-nonadmin", "name"], + ["css=#role-nonadmin", "css:finder"], + ["xpath=//select[@id='role-nonadmin']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[2]/td[4]/select", "xpath:idRelative"], + ["xpath=//tr[2]/td[4]/select", "xpath:position"] + ], + "value": "" + }, { + "id": "eb737ab9-42bf-45c4-82c0-04c14fa26e63", + "comment": "", + "command": "select", + "target": "id=role-nonadmin", + "targets": [], + "value": "label=test role" + }, { + "id": "aada4cb0-0f3f-436c-b342-f83ea1e35ba4", + "comment": "", + "command": "waitForElementVisible", + "target": "//div[@role=\"alert\" and contains(., \"User update successful for nonadmin\")]", + "targets": [], + "value": "30000" + }, { + "id": "e8bedd18-aafa-4bff-85a4-ba36d7685225", + "comment": "", + "command": "click", + "target": "id=dropdown-basic", + "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": "e091ca7a-286d-4982-80e3-b180cf03a233", + "comment": "", + "command": "click", + "target": "linkText=Roles", + "targets": [ + ["linkText=Roles", "linkText"], + ["css=.text-primary:nth-child(3)", "css:finder"], + ["xpath=//a[contains(text(),'Roles')]", "xpath:link"], + ["xpath=//div[@id='basic-nav-dropdown']/div/a[3]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/roles')]", "xpath:href"], + ["xpath=//a[3]", "xpath:position"], + ["xpath=//a[contains(.,'Roles')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "a378e628-4227-451b-9fb6-ef9edb7fc4fa", + "comment": "", + "command": "click", + "target": "css=tr:nth-child(5) .text-danger path", + "targets": [ + ["css=tr:nth-child(5) .text-danger path", "css:finder"] + ], + "value": "" + }, { + "id": "0c411e9d-b9de-40e4-a39d-99a3b734f6a1", + "comment": "", + "command": "click", + "target": "css=.btn-danger", + "targets": [ + ["css=.btn-danger", "css:finder"], + ["xpath=(//button[@type='button'])[9]", "xpath:attributes"], + ["xpath=//div[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "22bbdc62-995b-43e4-abe1-24d7035b1d49", + "comment": "", + "command": "assertElementPresent", + "target": "xpath=//div[@role=\"alert\" and contains(., \"remove role from all users first\")]", + "targets": [], + "value": "" + }, { + "id": "e704f343-5942-4053-aed7-1fa94e13320c", + "comment": "", + "command": "click", + "target": "id=dropdown-basic", + "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": "78b2edf1-a85d-4a0e-9880-33a2bdef7b68", + "comment": "", + "command": "click", + "target": "linkText=Dashboard", + "targets": [ + ["linkText=Dashboard", "linkText"], + ["css=.nav-link:nth-child(3)", "css:finder"], + ["xpath=//a[contains(text(),'Dashboard')]", "xpath:link"], + ["xpath=//div[@id='basic-navbar-nav']/div/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/dashboard')]", "xpath:href"], + ["xpath=//nav/div/div/a", "xpath:position"], + ["xpath=//a[contains(.,'Dashboard')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "28b88c8a-a4f1-49ad-9f7c-0d81a75c25e9", + "comment": "", + "command": "click", + "target": "linkText=Admin", + "targets": [ + ["linkText=Admin", "linkText"], + ["css=.nav-item:nth-child(3) > .nav-link", "css:finder"], + ["xpath=//a[contains(text(),'Admin')]", "xpath:link"], + ["xpath=//div[@id='root']/div/main/div/div/div[3]/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/dashboard/admin/management')]", "xpath:href"], + ["xpath=//div[3]/a", "xpath:position"], + ["xpath=//a[contains(.,'Admin')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "246af097-6ab4-4173-a7cb-c4054e572fc5", + "comment": "", + "command": "click", + "target": "id=role-nonadmin", + "targets": [ + ["id=role-nonadmin", "id"], + ["name=role-nonadmin", "name"], + ["css=#role-nonadmin", "css:finder"], + ["xpath=//select[@id='role-nonadmin']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[2]/td[4]/select", "xpath:idRelative"], + ["xpath=//tr[2]/td[4]/select", "xpath:position"] + ], + "value": "" + }, { + "id": "dd177cf2-605d-4e49-934d-6d88b3efa7b6", + "comment": "", + "command": "select", + "target": "id=role-nonadmin", + "targets": [], + "value": "label=ROLE_USER" + }, { + "id": "2f499bda-2230-45d2-85c5-efee606c5121", + "comment": "", + "command": "click", + "target": "id=dropdown-basic", + "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": "e59ac7eb-4100-4c7f-8666-5aeb7980362b", + "comment": "", + "command": "click", + "target": "linkText=Roles", + "targets": [ + ["linkText=Roles", "linkText"], + ["css=.text-primary:nth-child(3)", "css:finder"], + ["xpath=//a[contains(text(),'Roles')]", "xpath:link"], + ["xpath=//div[@id='basic-nav-dropdown']/div/a[3]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/roles')]", "xpath:href"], + ["xpath=//a[3]", "xpath:position"], + ["xpath=//a[contains(.,'Roles')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "f4201d01-e582-4071-921b-ede543ce181a", + "comment": "", + "command": "click", + "target": "css=tr:nth-child(5) .text-danger path", + "targets": [ + ["css=tr:nth-child(5) .text-danger path", "css:finder"] + ], + "value": "" + }, { + "id": "7ff656ef-a5c1-4d32-abc6-bac132c45349", + "comment": "", + "command": "click", + "target": "css=.btn-danger", + "targets": [ + ["css=.btn-danger", "css:finder"], + ["xpath=(//button[@type='button'])[9]", "xpath:attributes"], + ["xpath=//div[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "7092ea4b-e3ea-4ef4-9172-dbf84910ec3c", + "comment": "", + "command": "assertElementPresent", + "target": "xpath=//div[@role=\"alert\" and contains(., \"Deleted role successfully.\")]", + "targets": [], + "value": "" + }] + }], + "suites": [{ + "id": "2feb47ff-b645-47f5-9128-6b3a51eca540", + "name": "Default Suite", + "persistSession": false, + "parallel": false, + "timeout": 300, + "tests": ["629ba6a4-486d-4dbe-9ea1-4727a1e35567"] + }], + "urls": ["http://localhost:10101/"], + "plugins": [] +} \ No newline at end of file diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy index 46ff633cd..62fda2ed9 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy @@ -96,6 +96,9 @@ class DevConfig { }, new Role().with { name = 'ROLE_NONE' it + }, new Role().with { + name = 'ROLE_ENABLE' + it }] roles.each { roleRepository.save(it) @@ -207,4 +210,4 @@ class DevConfig { return it }) } -} +} \ 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 815360908..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 @@ -2,6 +2,7 @@ package edu.internet2.tier.shibboleth.admin.ui.service import com.google.common.base.Predicate import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityRoleWhiteListFilter @@ -20,8 +21,13 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMet import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.TemplateScheme import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.Refilterable +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import edu.internet2.tier.shibboleth.admin.util.OpenSamlChainingMetadataResolverUtil import groovy.util.logging.Slf4j import groovy.xml.DOMBuilder import groovy.xml.MarkupBuilder @@ -34,6 +40,7 @@ import org.opensaml.saml.metadata.resolver.filter.impl.NameIDFormatFilter import org.opensaml.saml.saml2.core.Attribute import org.opensaml.saml.saml2.metadata.EntityDescriptor import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service import org.w3c.dom.Document import javax.annotation.Nonnull @@ -45,11 +52,15 @@ import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBa import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver.ResourceType.SVN @Slf4j +@Service class JPAMetadataResolverServiceImpl implements MetadataResolverService { @Autowired private MetadataResolver metadataResolver + @Autowired + MetadataResolverConverterService metadataResolverConverterService + @Autowired private MetadataResolverRepository metadataResolverRepository @@ -62,155 +73,10 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { @Autowired private ShibUIConfiguration shibUIConfiguration - // TODO: enhance - @Override - void reloadFilters(String metadataResolverResourceId) { - OpenSamlChainingMetadataResolver chainingMetadataResolver = (OpenSamlChainingMetadataResolver) metadataResolver - MetadataResolver targetMetadataResolver = chainingMetadataResolver.getResolvers().find { - it.id == metadataResolverResourceId - } - edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver jpaMetadataResolver = metadataResolverRepository.findByResourceId(metadataResolverResourceId) - - if (targetMetadataResolver && targetMetadataResolver.getMetadataFilter() instanceof MetadataFilterChain) { - MetadataFilterChain metadataFilterChain = (MetadataFilterChain) targetMetadataResolver.getMetadataFilter() - - List metadataFilters = new ArrayList<>() - - // set up namespace protection - if (shibUIConfiguration.protectedAttributeNamespaces && shibUIConfiguration.protectedAttributeNamespaces.size() > 0 && targetMetadataResolver && jpaMetadataResolver.type in ['FileBackedHttpMetadataResolver', 'DynamicHttpMetadataResolver']) { - def target = new org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter() - target.attributeFilter = new ScriptedPredicate(new EvaluableScript(protectedNamespaceScript())) - metadataFilters.add(target) - } - - for (edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter metadataFilter : jpaMetadataResolver.getMetadataFilters()) { - if (metadataFilter instanceof EntityAttributesFilter) { - EntityAttributesFilter entityAttributesFilter = (EntityAttributesFilter) metadataFilter - - org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter target = new org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter() - Map, Collection> rules = new HashMap<>() - switch (entityAttributesFilter.getEntityAttributesFilterTarget().getEntityAttributesFilterTargetType()) { - case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY: - rules.put( - new EntityIdPredicate(entityAttributesFilter.getEntityAttributesFilterTarget().getValue()), - (List) (List) entityAttributesFilter.getAttributes() - ) - break - case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.CONDITION_SCRIPT: - rules.put(new ScriptedPredicate(new EvaluableScript(entityAttributesFilter.entityAttributesFilterTarget.value[0])), - (List) (List) entityAttributesFilter.getAttributes()) - break - case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.REGEX: - rules.put(new ScriptedPredicate(new EvaluableScript(generateJavaScriptRegexScript(entityAttributesFilter.entityAttributesFilterTarget.value[0]))), - (List) (List) entityAttributesFilter.getAttributes()) - break - default: - // do nothing, we'd have exploded elsewhere previously. - break - } - target.setRules(rules) - metadataFilters.add(target) - } - if (metadataFilter instanceof NameIdFormatFilter) { - NameIdFormatFilter nameIdFormatFilter = NameIdFormatFilter.cast(metadataFilter) - NameIDFormatFilter openSamlTargetFilter = new OpenSamlNameIdFormatFilter() - openSamlTargetFilter.removeExistingFormats = nameIdFormatFilter.removeExistingFormats == null ? false : nameIdFormatFilter.removeExistingFormats - Map, Collection> predicateRules = [:] - def type = nameIdFormatFilter.nameIdFormatFilterTarget.nameIdFormatFilterTargetType - def values = nameIdFormatFilter.nameIdFormatFilterTarget.value - switch (type) { - case ENTITY: - predicateRules[new EntityIdPredicate(values)] = nameIdFormatFilter.formats - break - case CONDITION_SCRIPT: - predicateRules[new ScriptedPredicate(new EvaluableScript(values[0]))] = nameIdFormatFilter.formats - break - case REGEX: - predicateRules[new ScriptedPredicate(new EvaluableScript(generateJavaScriptRegexScript(values[0])))] = nameIdFormatFilter.formats - break - default: - // do nothing, we'd have exploded elsewhere previously. - break - } - openSamlTargetFilter.rules = predicateRules - metadataFilters << openSamlTargetFilter - } - } - metadataFilterChain.setFilters(metadataFilters) - } - - if (targetMetadataResolver != null && targetMetadataResolver instanceof Refilterable) { - (Refilterable) targetMetadataResolver.refilter() - } else { - //TODO: Do something here if we need to refilter other non-Batch resolvers - log.warn("Target resolver is not a Refilterable resolver. Skipping refilter()") - } - } - - private String protectedNamespaceScript() { - return """(function (attribute) { - "use strict"; - var namespaces = [${shibUIConfiguration.protectedAttributeNamespaces.collect({"\"${it}\""}).join(', ')}]; - // check the parameter - if (attribute === null) { return true; } - for (var i in namespaces) { - if (attribute.getName().startsWith(namespaces[i])) { - return false; - } - } - return true; - }(input));""" - } - - private class ScriptedPredicate extends net.shibboleth.utilities.java.support.logic.ScriptedPredicate { - protected ScriptedPredicate(@Nonnull EvaluableScript theScript) { - super(theScript) - } - } + @Autowired + private UserService userService // TODO: enhance - @Override - Document generateConfiguration() { - // TODO: this can probably be a better writer - new StringWriter().withCloseable { writer -> - def xml = new MarkupBuilder(writer) - xml.omitEmptyAttributes = true - xml.omitNullAttributes = true - - xml.MetadataProvider(id: 'ShibbolethMetadata', - xmlns: 'urn:mace:shibboleth:2.0:metadata', - 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'xsi:type': 'ChainingMetadataProvider', - 'xsi:schemaLocation': 'urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd urn:mace:shibboleth:2.0:resource http://shibboleth.net/schema/idp/shibboleth-resource.xsd urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd' - ) { - - - resolversPositionOrderContainerService.allMetadataResolversInDefinedOrderOrUnordered.each { - edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr -> - //TODO: We do not currently marshall the internal incommon chaining resolver (with BaseMetadataResolver type) - if ((mr.type != 'BaseMetadataResolver') && (mr.enabled)) { - constructXmlNodeForResolver(mr, delegate) { - //TODO: enhance - def didNamespaceProtectionFilter = !(shibUIConfiguration.protectedAttributeNamespaces && shibUIConfiguration.protectedAttributeNamespaces.size() > 0) - def doNamespaceProtectionFilter = { def filter -> - if (mr.type in ['FileBackedMetadataResolver', 'DynamicHttpMetadataResolver'] && (filter == null || filter instanceof EntityAttributesFilter) && !didNamespaceProtectionFilter) { - constructXmlNodeForEntityAttributeNamespaceProtection(delegate) - didNamespaceProtectionFilter = true - } - } - mr.metadataFilters.each { edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter filter -> - doNamespaceProtectionFilter() - constructXmlNodeForFilter(filter, delegate) - } - doNamespaceProtectionFilter() - } - } - } - } - return DOMBuilder.newInstance().parseText(writer.toString()) - } - } - void constructXmlNodeForEntityAttributeNamespaceProtection(def markupBuilderDelegate) { markupBuilderDelegate.MetadataFilter('xsi:type': 'EntityAttributes') { AttributeFilterScript() { @@ -221,21 +87,6 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } - void constructXmlNodeForFilter(SignatureValidationFilter filter, def markupBuilderDelegate) { - if (filter.xmlShouldBeGenerated()) { - markupBuilderDelegate.MetadataFilter(id: filter.name, - 'xsi:type': 'SignatureValidation', - 'xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata', - 'requireSignedRoot': !filter.requireSignedRoot ?: null, - 'certificateFile': filter.certificateFile, - 'defaultCriteriaRef': filter.defaultCriteriaRef, - 'signaturePrevalidatorRef': filter.signaturePrevalidatorRef, - 'dynamicTrustedNamesStrategyRef': filter.dynamicTrustedNamesStrategyRef, - 'trustEngineRef': filter.trustEngineRef, - 'publicKey': filter.publicKey) - } - } - void constructXmlNodeForFilter(EntityAttributesFilter filter, def markupBuilderDelegate) { markupBuilderDelegate.MetadataFilter('xsi:type': 'EntityAttributes') { // TODO: enhance. currently this does weird things with namespaces @@ -274,12 +125,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } - private String generateJavaScriptRegexScript(String regex) { - return """ - "use strict"; - ${regex.startsWith('/') ? '' : '/'}${regex}${regex.endsWith('/') ? '' : '/'}.test(input.getEntityID());\n""" - } - + // TODO: enhance void constructXmlNodeForFilter(EntityRoleWhiteListFilter filter, def markupBuilderDelegate) { if (!filter.retainedRoles?.isEmpty()) { markupBuilderDelegate.MetadataFilter( @@ -294,15 +140,6 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } - void constructXmlNodeForFilter(RequiredValidUntilFilter filter, def markupBuilderDelegate) { - if (filter.xmlShouldBeGenerated()) { - markupBuilderDelegate.MetadataFilter( - 'xsi:type': 'RequiredValidUntil', - maxValidityInterval: filter.maxValidityInterval - ) - } - } - void constructXmlNodeForFilter(NameIdFormatFilter filter, def markupBuilderDelegate) { def type = filter.nameIdFormatFilterTarget.nameIdFormatFilterTargetType markupBuilderDelegate.MetadataFilter( @@ -341,30 +178,31 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } - void constructXmlNodeForResolver(FilesystemMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { - markupBuilderDelegate.MetadataProvider(id: resolver.xmlId, - 'xsi:type': 'FilesystemMetadataProvider', - metadataFile: resolver.metadataFile, - - requireValidMetadata: resolver.requireValidMetadata, - failFastInitialization: resolver.failFastInitialization, - sortKey: resolver.sortKey, - criterionPredicateRegistryRef: resolver.criterionPredicateRegistryRef, - useDefaultPredicateRegistry: resolver.useDefaultPredicateRegistry, - satisfyAnyPredicates: resolver.satisfyAnyPredicates, - - parserPoolRef: resolver.reloadableMetadataResolverAttributes?.parserPoolRef, - minRefreshDelay: resolver.reloadableMetadataResolverAttributes?.minRefreshDelay, - maxRefreshDelay: resolver.reloadableMetadataResolverAttributes?.maxRefreshDelay, - refreshDelayFactor: resolver.reloadableMetadataResolverAttributes?.refreshDelayFactor, - indexesRef: resolver.reloadableMetadataResolverAttributes?.indexesRef, - resolveViaPredicatesOnly: resolver.reloadableMetadataResolverAttributes?.resolveViaPredicatesOnly ?: null, - expirationWarningThreshold: resolver.reloadableMetadataResolverAttributes?.expirationWarningThreshold) { + void constructXmlNodeForFilter(RequiredValidUntilFilter filter, def markupBuilderDelegate) { + if (filter.xmlShouldBeGenerated()) { + markupBuilderDelegate.MetadataFilter( + 'xsi:type': 'RequiredValidUntil', + maxValidityInterval: filter.maxValidityInterval + ) + } + } - childNodes() + void constructXmlNodeForFilter(SignatureValidationFilter filter, def markupBuilderDelegate) { + if (filter.xmlShouldBeGenerated()) { + markupBuilderDelegate.MetadataFilter(id: filter.name, + 'xsi:type': 'SignatureValidation', + 'xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata', + 'requireSignedRoot': !filter.requireSignedRoot ?: null, + 'certificateFile': filter.certificateFile, + 'defaultCriteriaRef': filter.defaultCriteriaRef, + 'signaturePrevalidatorRef': filter.signaturePrevalidatorRef, + 'dynamicTrustedNamesStrategyRef': filter.dynamicTrustedNamesStrategyRef, + 'trustEngineRef': filter.trustEngineRef, + 'publicKey': filter.publicKey) } } + void constructXmlNodeForResolver(DynamicHttpMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { markupBuilderDelegate.MetadataProvider(id: resolver.xmlId, 'xsi:type': 'DynamicHTTPMetadataProvider', @@ -483,6 +321,30 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } + void constructXmlNodeForResolver(FilesystemMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { + markupBuilderDelegate.MetadataProvider(id: resolver.xmlId, + 'xsi:type': 'FilesystemMetadataProvider', + metadataFile: resolver.metadataFile, + + requireValidMetadata: resolver.requireValidMetadata, + failFastInitialization: resolver.failFastInitialization, + sortKey: resolver.sortKey, + criterionPredicateRegistryRef: resolver.criterionPredicateRegistryRef, + useDefaultPredicateRegistry: resolver.useDefaultPredicateRegistry, + satisfyAnyPredicates: resolver.satisfyAnyPredicates, + + parserPoolRef: resolver.reloadableMetadataResolverAttributes?.parserPoolRef, + minRefreshDelay: resolver.reloadableMetadataResolverAttributes?.minRefreshDelay, + maxRefreshDelay: resolver.reloadableMetadataResolverAttributes?.maxRefreshDelay, + refreshDelayFactor: resolver.reloadableMetadataResolverAttributes?.refreshDelayFactor, + indexesRef: resolver.reloadableMetadataResolverAttributes?.indexesRef, + resolveViaPredicatesOnly: resolver.reloadableMetadataResolverAttributes?.resolveViaPredicatesOnly ?: null, + expirationWarningThreshold: resolver.reloadableMetadataResolverAttributes?.expirationWarningThreshold) { + + childNodes() + } + } + void constructXmlNodeForResolver(LocalDynamicMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { markupBuilderDelegate.MetadataProvider(sourceDirectory: resolver.sourceDirectory, sourceManagerRef: resolver.sourceManagerRef, @@ -556,7 +418,191 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { childNodes() } + } + + public edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver findByResourceId(String resourceId) throws EntityNotFoundException { + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver result = metadataResolverRepository.findByResourceId(resourceId) + if (result == null ) { + throw new EntityNotFoundException("No Provider with resourceId[" + resourceId + "] was found") + } + return result + } + + @Override + Document generateConfiguration() { + // TODO: this can probably be a better writer + new StringWriter().withCloseable { writer -> + def xml = new MarkupBuilder(writer) + xml.omitEmptyAttributes = true + xml.omitNullAttributes = true + + xml.MetadataProvider(id: 'ShibbolethMetadata', + xmlns: 'urn:mace:shibboleth:2.0:metadata', + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:type': 'ChainingMetadataProvider', + 'xsi:schemaLocation': 'urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd urn:mace:shibboleth:2.0:resource http://shibboleth.net/schema/idp/shibboleth-resource.xsd urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd' + ) { + + + resolversPositionOrderContainerService.allMetadataResolversInDefinedOrderOrUnordered.each { + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr -> + //TODO: We do not currently marshall the internal incommon chaining resolver (with BaseMetadataResolver type) + if ((mr.type != 'BaseMetadataResolver') && (mr.enabled)) { + constructXmlNodeForResolver(mr, delegate) { + //TODO: enhance + def didNamespaceProtectionFilter = !(shibUIConfiguration.protectedAttributeNamespaces && shibUIConfiguration.protectedAttributeNamespaces.size() > 0) + def doNamespaceProtectionFilter = { def filter -> + if (mr.type in ['FileBackedMetadataResolver', 'DynamicHttpMetadataResolver'] && (filter == null || filter instanceof EntityAttributesFilter) && !didNamespaceProtectionFilter) { + constructXmlNodeForEntityAttributeNamespaceProtection(delegate) + didNamespaceProtectionFilter = true + } + } + mr.metadataFilters.each { edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter filter -> + doNamespaceProtectionFilter() + constructXmlNodeForFilter(filter, delegate) + } + doNamespaceProtectionFilter() + } + } + } + } + return DOMBuilder.newInstance().parseText(writer.toString()) + } + } + + private String generateJavaScriptRegexScript(String regex) { + return """ + "use strict"; + ${regex.startsWith('/') ? '' : '/'}${regex}${regex.endsWith('/') ? '' : '/'}.test(input.getEntityID());\n""" + } + + private String protectedNamespaceScript() { + return """(function (attribute) { + "use strict"; + var namespaces = [${shibUIConfiguration.protectedAttributeNamespaces.collect({ "\"${it}\"" }).join(', ')}]; + // check the parameter + if (attribute === null) { return true; } + for (var i in namespaces) { + if (attribute.getName().startsWith(namespaces[i])) { + return false; + } + } + return true; + }(input));""" + } + + @Override + void reloadFilters(String metadataResolverResourceId) { + OpenSamlChainingMetadataResolver chainingMetadataResolver = (OpenSamlChainingMetadataResolver) metadataResolver + MetadataResolver targetMetadataResolver = chainingMetadataResolver.getResolvers().find { + it.id == metadataResolverResourceId + } + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver jpaMetadataResolver = metadataResolverRepository.findByResourceId(metadataResolverResourceId) + + if (targetMetadataResolver && targetMetadataResolver.getMetadataFilter() instanceof MetadataFilterChain) { + MetadataFilterChain metadataFilterChain = (MetadataFilterChain) targetMetadataResolver.getMetadataFilter() + + List metadataFilters = new ArrayList<>() + + // set up namespace protection + if (shibUIConfiguration.protectedAttributeNamespaces && shibUIConfiguration.protectedAttributeNamespaces.size() > 0 && targetMetadataResolver && jpaMetadataResolver.type in ['FileBackedHttpMetadataResolver', 'DynamicHttpMetadataResolver']) { + def target = new org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter() + target.attributeFilter = new ScriptedPredicate(new EvaluableScript(protectedNamespaceScript())) + metadataFilters.add(target) + } + + for (edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter metadataFilter : jpaMetadataResolver.getMetadataFilters()) { + if (metadataFilter instanceof EntityAttributesFilter) { + EntityAttributesFilter entityAttributesFilter = (EntityAttributesFilter) metadataFilter + + org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter target = new org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter() + Map, Collection> rules = new HashMap<>() + switch (entityAttributesFilter.getEntityAttributesFilterTarget().getEntityAttributesFilterTargetType()) { + case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY: + rules.put( + new EntityIdPredicate(entityAttributesFilter.getEntityAttributesFilterTarget().getValue()), + (List) (List) entityAttributesFilter.getAttributes() + ) + break + case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.CONDITION_SCRIPT: + rules.put(new ScriptedPredicate(new EvaluableScript(entityAttributesFilter.entityAttributesFilterTarget.value[0])), + (List) (List) entityAttributesFilter.getAttributes()) + break + case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.REGEX: + rules.put(new ScriptedPredicate(new EvaluableScript(generateJavaScriptRegexScript(entityAttributesFilter.entityAttributesFilterTarget.value[0]))), + (List) (List) entityAttributesFilter.getAttributes()) + break + default: + // do nothing, we'd have exploded elsewhere previously. + break + } + target.setRules(rules) + metadataFilters.add(target) + } + if (metadataFilter instanceof NameIdFormatFilter) { + NameIdFormatFilter nameIdFormatFilter = NameIdFormatFilter.cast(metadataFilter) + NameIDFormatFilter openSamlTargetFilter = new OpenSamlNameIdFormatFilter() + openSamlTargetFilter.removeExistingFormats = nameIdFormatFilter.removeExistingFormats == null ? false : nameIdFormatFilter.removeExistingFormats + Map, Collection> predicateRules = [:] + def type = nameIdFormatFilter.nameIdFormatFilterTarget.nameIdFormatFilterTargetType + def values = nameIdFormatFilter.nameIdFormatFilterTarget.value + switch (type) { + case ENTITY: + predicateRules[new EntityIdPredicate(values)] = nameIdFormatFilter.formats + break + case CONDITION_SCRIPT: + predicateRules[new ScriptedPredicate(new EvaluableScript(values[0]))] = nameIdFormatFilter.formats + break + case REGEX: + predicateRules[new ScriptedPredicate(new EvaluableScript(generateJavaScriptRegexScript(values[0])))] = nameIdFormatFilter.formats + break + default: + // do nothing, we'd have exploded elsewhere previously. + break + } + openSamlTargetFilter.rules = predicateRules + metadataFilters << openSamlTargetFilter + } + } + metadataFilterChain.setFilters(metadataFilters) + } + + if (targetMetadataResolver != null && targetMetadataResolver instanceof Refilterable) { + (Refilterable) targetMetadataResolver.refilter() + } else { + //TODO: Do something here if we need to refilter other non-Batch resolvers + log.warn("Target resolver is not a Refilterable resolver. Skipping refilter()") + } + } + + public edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver updateMetadataResolverEnabledStatus(edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver updatedResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException { + if (!userService.currentUserCanEnable(updatedResolver)) { + throw new ForbiddenException("You do not have the permissions necessary to change the enable status of this filter.") + } + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver persistedResolver = metadataResolverRepository.save(updatedResolver) + + if (persistedResolver.getDoInitialization()) { + MetadataResolver openSamlRepresentation = null + try { + openSamlRepresentation = metadataResolverConverterService.convertToOpenSamlRepresentation(persistedResolver) + } catch (FileNotFoundException e) { + throw new MetadataFileNotFoundException("message.file-doesnt-exist") + } + try { + OpenSamlChainingMetadataResolverUtil.updateChainingMetadataResolver((OpenSamlChainingMetadataResolver) chainingMetadataResolver, openSamlRepresentation) + } + catch (Throwable e) { + throw new InitializationException(e); + } + } + + } + + private class ScriptedPredicate extends net.shibboleth.utilities.java.support.logic.ScriptedPredicate { + protected ScriptedPredicate(@Nonnull EvaluableScript theScript) { + super(theScript) + } } } \ No newline at end of file diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy index e007c62c3..16a4ed443 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy @@ -59,4 +59,4 @@ class UserBootstrap { } } } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java index c117b415f..c897a31b5 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java @@ -1,27 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration; -import javax.servlet.http.HttpServletRequest; - -import org.apache.lucene.analysis.Analyzer; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.support.ResourceBundleMessageSource; -import org.springframework.core.io.Resource; -import org.springframework.web.servlet.LocaleResolver; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; -import org.springframework.web.util.UrlPathHelper; - import com.fasterxml.jackson.databind.Module; - import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; @@ -36,30 +15,35 @@ 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.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.EntityIdsSearchService; -import edu.internet2.tier.shibboleth.admin.ui.service.EntityIdsSearchServiceImpl; -import edu.internet2.tier.shibboleth.admin.ui.service.EntityService; -import edu.internet2.tier.shibboleth.admin.ui.service.FileCheckingFileWritingService; -import edu.internet2.tier.shibboleth.admin.ui.service.FileWritingService; -import edu.internet2.tier.shibboleth.admin.ui.service.FilterService; -import edu.internet2.tier.shibboleth.admin.ui.service.FilterTargetService; -import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl; -import edu.internet2.tier.shibboleth.admin.ui.service.JPAFilterServiceImpl; -import edu.internet2.tier.shibboleth.admin.ui.service.JPAFilterTargetServiceImpl; -import edu.internet2.tier.shibboleth.admin.ui.service.JPAMetadataResolverServiceImpl; -import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; -import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolversPositionOrderContainerService; +import edu.internet2.tier.shibboleth.admin.ui.service.*; import edu.internet2.tier.shibboleth.admin.util.AttributeUtility; import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils; import edu.internet2.tier.shibboleth.admin.util.LuceneUtility; import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; +import org.apache.lucene.analysis.Analyzer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.core.io.Resource; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; +import org.springframework.web.util.UrlPathHelper; + +import javax.servlet.http.HttpServletRequest; @Configuration -@ComponentScan(basePackages="{ edu.internet2.tier.shibboleth.admin.ui.service }") -@EnableConfigurationProperties({ CustomPropertiesConfiguration.class, ShibUIConfiguration.class }) +@Import(SearchConfiguration.class) +@ComponentScan(basePackages = "{ edu.internet2.tier.shibboleth.admin.ui.service }") +@EnableConfigurationProperties({CustomPropertiesConfiguration.class, ShibUIConfiguration.class}) public class CoreShibUiConfiguration { @Bean public OpenSamlObjects openSamlObjects() { @@ -70,22 +54,12 @@ public OpenSamlObjects openSamlObjects() { public EntityService jpaEntityService() { return new JPAEntityServiceImpl(openSamlObjects()); } - - @Bean - public FilterService jpaFilterService() { - return new JPAFilterServiceImpl(); - } @Bean public FilterTargetService jpaFilterTargetService() { return new JPAFilterTargetServiceImpl(); } - @Bean - public MetadataResolverService metadataResolverService() { - return new JPAMetadataResolverServiceImpl(); - } - @Bean public AttributeUtility attributeUtility() { return new AttributeUtility(openSamlObjects()); @@ -221,18 +195,18 @@ public EntityDescriptorConversionUtils EntityDescriptorConverstionUtilsInit(Enti EntityDescriptorConversionUtils.setOpenSamlObjects(oso); return new EntityDescriptorConversionUtils(); } - + @Bean public GroupUpdatedEntityListener groupUpdatedEntityListener(OwnershipRepository repo) { GroupUpdatedEntityListener listener = new GroupUpdatedEntityListener(); listener.init(repo); return listener; } - + @Bean public UserUpdatedEntityListener userUpdatedEntityListener(OwnershipRepository repo, GroupsRepository groupRepo) { UserUpdatedEntityListener listener = new UserUpdatedEntityListener(); listener.init(repo, groupRepo); return listener; } -} +} \ 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 5e6e64b6c..9fc942dc8 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 @@ -44,7 +44,7 @@ public void onApplicationEvent(ContextRefreshedEvent event) { } @Transactional - private void doshibui_1740_migration() { + void doshibui_1740_migration() { groupService.ensureAdminGroupExists(); // do first // SHIBUI-1740: Adding admin group to all existing entity descriptors that do not have a group already. diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java index 3d66de957..8617bfa3b 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java @@ -9,7 +9,6 @@ import edu.internet2.tier.shibboleth.admin.ui.security.springsecurity.AdminUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -27,9 +26,8 @@ import org.springframework.security.web.firewall.StrictHttpFirewall; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import java.util.Collections; - import javax.transaction.Transactional; +import java.util.Collections; /** * Web security configuration. @@ -38,20 +36,32 @@ @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) public class WebSecurityConfig { - @Value("${shibui.logout-url:/dashboard}") - private String logoutUrl; + @Value("${shibui.roles.authenticated}") + private String[] acceptedAuthenticationRoles; @Value("${shibui.default-password:}") private String defaultPassword; + @Value("${shibui.logout-url:/dashboard}") + private String logoutUrl; + + @Autowired + private RoleRepository roleRepository; + + @Value("${shibui.default-rootuser:root}") + private String rootUser; + @Autowired private UserRepository userRepository; @Autowired private UserService userService; - - @Autowired - private RoleRepository roleRepository; + + @Bean + @Profile("!no-auth") + public AdminUserService adminUserService(UserRepository userRepository) { + return new AdminUserService(userRepository); + } private HttpFirewall allowUrlEncodedSlashHttpFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); @@ -60,8 +70,10 @@ private HttpFirewall allowUrlEncodedSlashHttpFirewall() { return firewall; } - private HttpFirewall defaultFirewall() { - return new DefaultHttpFirewall(); + @Bean + @Profile("!no-auth") + public AuditorAware defaultAuditorAware() { + return new DefaultAuditorAware(); } @Bean @@ -76,7 +88,7 @@ protected void configure(HttpSecurity http) throws Exception { .and() .authorizeRequests() .antMatchers("/unsecured/**/*").permitAll() - .anyRequest().hasAnyRole("USER", "ADMIN") + .anyRequest().hasAnyRole(acceptedAuthenticationRoles) .and() .exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> response.sendRedirect("/unsecured/error.html")) .and() @@ -92,9 +104,9 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); if (defaultPassword != null && !"".equals(defaultPassword)) { // TODO: yeah, this isn't good, but we gotta initialize this user for now - User adminUser = userRepository.findByUsername("root").orElseGet(() ->{ + User adminUser = userRepository.findByUsername(rootUser).orElseGet(() ->{ User u = new User(); - u.setUsername("root"); + u.setUsername(rootUser); u.setPassword(defaultPassword); u.setFirstName("admin"); u.setLastName("user"); @@ -127,16 +139,8 @@ public void configure(WebSecurity web) throws Exception { }; } - @Bean - @Profile("!no-auth") - public AuditorAware defaultAuditorAware() { - return new DefaultAuditorAware(); - } - - @Bean - @Profile("!no-auth") - public AdminUserService adminUserService(UserRepository userRepository) { - return new AdminUserService(userRepository); + private HttpFirewall defaultFirewall() { + return new DefaultHttpFirewall(); } @Bean @@ -157,5 +161,4 @@ public void configure(WebSecurity web) throws Exception { } }; } -} - +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java new file mode 100644 index 000000000..60705cd15 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java @@ -0,0 +1,65 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import javax.script.ScriptException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; +import edu.internet2.tier.shibboleth.admin.ui.service.FilterService; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; + +@RestController +@RequestMapping("/api/activate") +public class ActivateController { + + @Autowired + private EntityDescriptorService entityDescriptorService; + + @Autowired + private FilterService filterService; + + @Autowired + private MetadataResolverService metadataResolverService; + + @PatchMapping(path = "/entityDescriptor/{resourceId}/{mode}") + @Transactional + public ResponseEntity enableEntityDescriptor(@PathVariable String resourceId, @PathVariable String mode) throws EntityNotFoundException, ForbiddenException { + boolean status = "enable".equalsIgnoreCase(mode); + EntityDescriptorRepresentation edr = entityDescriptorService.updateEntityDescriptorEnabledStatus(resourceId, status); + return ResponseEntity.ok(edr); + } + + @PatchMapping(path = "/MetadataResolvers/{metadataResolverId}/Filter/{resourceId}/{mode}") + @Transactional + public ResponseEntity enableFilter(@PathVariable String metadataResolverId, @PathVariable String resourceId, @PathVariable String mode) throws EntityNotFoundException, ForbiddenException, ScriptException { + boolean status = "enable".equalsIgnoreCase(mode); + MetadataFilter persistedFilter = filterService.updateFilterEnabledStatus(metadataResolverId, resourceId, status); + return ResponseEntity.ok(persistedFilter); + } + + @PatchMapping("/MetadataResolvers/{resourceId}/{mode}") + @Transactional + public ResponseEntity enableProvider(@PathVariable String resourceId, @PathVariable String mode) throws EntityNotFoundException, ForbiddenException, MetadataFileNotFoundException, InitializationException { + boolean status = "enable".equalsIgnoreCase(mode); + MetadataResolver existingResolver = metadataResolverService.findByResourceId(resourceId); + existingResolver.setEnabled(status); + existingResolver = metadataResolverService.updateMetadataResolverEnabledStatus(existingResolver); + + return ResponseEntity.ok(existingResolver); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java new file mode 100644 index 000000000..0c766c53c --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java @@ -0,0 +1,48 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + +import javax.script.ScriptException; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException; + +@ControllerAdvice(assignableTypes = {ActivateController.class}) +public class ActivateExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler({ EntityNotFoundException.class }) + public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(HttpStatus.NOT_FOUND, e.getMessage())); + } + + @ExceptionHandler({ ForbiddenException.class }) + public ResponseEntity handleForbiddenAccess(ForbiddenException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(String.valueOf(HttpStatus.FORBIDDEN.value()), e.getMessage())); + } + + @ExceptionHandler({ InitializationException.class }) + public ResponseEntity handleInitializationException(InitializationException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), e.getMessage())); + } + + @ExceptionHandler({ MetadataFileNotFoundException.class }) + public ResponseEntity handleMetadataFileNotFoundException(MetadataFileNotFoundException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse(INTERNAL_SERVER_ERROR.toString(), e.getLocalizedMessage())); + } + + @ExceptionHandler({ ScriptException.class }) + public ResponseEntity handleScriptException(ScriptException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), e.getMessage())); + } + + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java index 8436a114e..be0d39c05 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 @@ -116,11 +116,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 { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ActivatableType.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ActivatableType.java new file mode 100644 index 000000000..8042d56d4 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ActivatableType.java @@ -0,0 +1,5 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +public enum ActivatableType { + ENTITY_DESCRIPTOR, METADATA_RESOLVER, FILTER +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java index a3acff06b..185b43918 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java @@ -33,11 +33,12 @@ import java.util.UUID; import java.util.stream.Collectors; +import static edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType.ENTITY_DESCRIPTOR; @Entity @EqualsAndHashCode(callSuper = true) @Audited -public class EntityDescriptor extends AbstractDescriptor implements org.opensaml.saml.saml2.metadata.EntityDescriptor, Ownable { +public class EntityDescriptor extends AbstractDescriptor implements org.opensaml.saml.saml2.metadata.EntityDescriptor, Ownable, IActivatable { @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "entitydesc_addlmetdatlocations_id") @OrderColumn @@ -47,7 +48,7 @@ public class EntityDescriptor extends AbstractDescriptor implements org.opensaml @OneToOne(cascade = CascadeType.ALL) @NotAudited private AffiliationDescriptor affiliationDescriptor; - + @OneToOne(cascade = CascadeType.ALL) @NotAudited private AttributeAuthorityDescriptor attributeAuthorityDescriptor; @@ -61,7 +62,7 @@ public class EntityDescriptor extends AbstractDescriptor implements org.opensaml private List contactPersons = new ArrayList<>(); private String entityID; - + private String localId; @OneToOne(cascade = CascadeType.ALL) @@ -70,7 +71,7 @@ public class EntityDescriptor extends AbstractDescriptor implements org.opensaml @Getter @Setter private String idOfOwner; - + @OneToOne(cascade = CascadeType.ALL) @NotAudited private PDPDescriptor pdpDescriptor; @@ -254,6 +255,10 @@ public void setEntityID(String entityID) { this.entityID = entityID; } + public void setEnabled(Boolean serviceEnabled) { + this.serviceEnabled = (serviceEnabled == null) ? false : serviceEnabled; + } + @Override public void setID(String id) { this.localId = id; @@ -296,12 +301,16 @@ public String toString() { .add("id", id) .toString(); } - + public String getObjectId() { return entityID; } - + public OwnableType getOwnableType() { return OwnableType.ENTITY_DESCRIPTOR; } -} + + @Override public ActivatableType getActivatableType() { + return ENTITY_DESCRIPTOR; + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IActivatable.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IActivatable.java new file mode 100644 index 000000000..84a31078a --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IActivatable.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +public interface IActivatable { + ActivatableType getActivatableType(); + + void setEnabled(Boolean enabled); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java index 9363f5711..548c97356 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java @@ -1,10 +1,9 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.filters; -import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.*; import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; +import edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType; +import edu.internet2.tier.shibboleth.admin.ui.domain.IActivatable; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -20,6 +19,8 @@ import javax.persistence.Transient; import java.util.UUID; +import static edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType.*; + /** * Domain class to store information about {@link org.opensaml.saml.metadata.resolver.filter.MetadataFilter} */ @@ -38,22 +39,27 @@ @JsonSubTypes.Type(value=NameIdFormatFilter.class, name="NameIDFormat")}) @Audited @AuditOverride(forClass = AbstractAuditable.class) -public abstract class MetadataFilter extends AbstractAuditable implements IConcreteMetadataFilterType { +public abstract class MetadataFilter extends AbstractAuditable implements IConcreteMetadataFilterType, IActivatable { - @JsonProperty("@type") - @Transient - String type; + private boolean filterEnabled; private String name; @Column(unique=true) private String resourceId = UUID.randomUUID().toString(); - private boolean filterEnabled; + @JsonProperty("@type") + @Transient + String type; @Transient private transient Integer version; + @JsonIgnore + public ActivatableType getActivatableType() { + return FILTER; + } + @JsonGetter("version") public int getVersion() { if (version != null && version != 0) { @@ -61,4 +67,8 @@ public int getVersion() { } return this.hashCode(); } -} + + public void setEnabled(Boolean serviceEnabled) { + this.filterEnabled = (serviceEnabled == null) ? false : serviceEnabled; + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java index f9347fab3..63a04d764 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java @@ -6,6 +6,9 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; +import edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType; +import static edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType.METADATA_RESOLVER; +import edu.internet2.tier.shibboleth.admin.ui.domain.IActivatable; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; import lombok.EqualsAndHashCode; @@ -43,7 +46,7 @@ @JsonSubTypes.Type(value = ResourceBackedMetadataResolver.class, name = "ResourceBackedMetadataResolver")}) @Audited @AuditOverride(forClass = AbstractAuditable.class) -public class MetadataResolver extends AbstractAuditable { +public class MetadataResolver extends AbstractAuditable implements IActivatable { @JsonProperty("@type") @Transient @@ -84,6 +87,28 @@ public class MetadataResolver extends AbstractAuditable { @Transient private Integer version; + public void addFilter(MetadataFilter metadataFilter) { + //To make sure that Spring Data auditing infrastructure recognizes update and "touched" modifiedDate + markAsModified(); + this.metadataFilters.add(metadataFilter); + } + + public void entityAttributesFilterIntoTransientRepresentation() { + //expose explicit API to call to convert into transient representation + //used in unit/integration tests where JPA's @PostLoad callback execution engine is not available + this.metadataFilters + .stream() + .filter(EntityAttributesFilter.class::isInstance) + .map(EntityAttributesFilter.class::cast) + .forEach(EntityAttributesFilter::intoTransientRepresentation); + } + + @Override + @JsonIgnore + public ActivatableType getActivatableType() { + return METADATA_RESOLVER; + } + public Boolean getDoInitialization() { return doInitialization == null ? false : doInitialization; } @@ -96,23 +121,7 @@ public int getVersion() { return this.hashCode(); } - public void addFilter(MetadataFilter metadataFilter) { - //To make sure that Spring Data auditing infrastructure recognizes update and "touched" modifiedDate - markAsModified(); - this.metadataFilters.add(metadataFilter); - } - public void markAsModified() { this.versionModifiedTimestamp = System.currentTimeMillis(); } - - public void entityAttributesFilterIntoTransientRepresentation() { - //expose explicit API to call to convert into transient representation - //used in unit/integration tests where JPA's @PostLoad callback execution engine is not available - this.metadataFilters - .stream() - .filter(EntityAttributesFilter.class::isInstance) - .map(EntityAttributesFilter.class::cast) - .forEach(EntityAttributesFilter::intoTransientRepresentation); - } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java index 6769aaa5f..4d0009523 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java @@ -4,4 +4,4 @@ public class EntityNotFoundException extends Exception { public EntityNotFoundException(String message) { super(message); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InitializationException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InitializationException.java new file mode 100644 index 000000000..f18483176 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InitializationException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class InitializationException extends Exception { + public InitializationException(Exception e) { + super(e); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java new file mode 100644 index 000000000..d576e0630 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java @@ -0,0 +1,74 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.controller; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IRolesService; + +@RestController +@RequestMapping("/api/admin/roles") +public class RolesController { + @Autowired + private IRolesService rolesService; + + @Secured("ROLE_ADMIN") + @PostMapping + @Transactional + public ResponseEntity create(@RequestBody Role role) throws RoleExistsConflictException { + Role result = rolesService.createRole(role); + return ResponseEntity.status(HttpStatus.CREATED).body(result); + } + + @Secured("ROLE_ADMIN") + @DeleteMapping("/{resourceId}") + @Transactional + public ResponseEntity delete(@PathVariable String resourceId) throws EntityNotFoundException, RoleDeleteException { + rolesService.deleteDefinition(resourceId); + return ResponseEntity.noContent().build(); + } + + @GetMapping + @Transactional(readOnly = true) + public ResponseEntity getAll() { + return ResponseEntity.ok(rolesService.findAll()); + } + + @GetMapping("/{resourceId}") + @Transactional(readOnly = true) + public ResponseEntity getOne(@PathVariable String resourceId) throws EntityNotFoundException { + Role role = rolesService.findByResourceId(resourceId); + return ResponseEntity.ok(role); + } + + @Secured("ROLE_ADMIN") + @PutMapping(path = {"/", "/{resourceId}" }) + @Transactional + public ResponseEntity update(@RequestBody Role incomingRoleDetail, @PathVariable Optional resourceId) throws EntityNotFoundException { + Role updateRole; + if (resourceId.isPresent()) { + updateRole = rolesService.findByResourceId(resourceId.get()); + } else { + updateRole = rolesService.findByResourceId(incomingRoleDetail.getResourceId()); + } + updateRole.setName(incomingRoleDetail.getName()); + Role result = rolesService.updateRole(updateRole); + return ResponseEntity.ok(result); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesExceptionHandler.java new file mode 100644 index 000000000..e4b840f1a --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesExceptionHandler.java @@ -0,0 +1,38 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.controller; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import edu.internet2.tier.shibboleth.admin.ui.controller.ErrorResponse; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; + +@ControllerAdvice(assignableTypes = {RolesController.class}) +public class RolesExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler({ EntityNotFoundException.class }) + public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(HttpStatus.NOT_FOUND, e.getMessage())); + } + + @ExceptionHandler({ RoleDeleteException.class }) + public ResponseEntity handleForbiddenAccess(RoleDeleteException e, WebRequest request) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/admin/role/{resourceId}").build().toUri()); + return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers) + .body(new ErrorResponse(String.valueOf(HttpStatus.CONFLICT.value()), e.getMessage())); + + } + + @ExceptionHandler({RoleExistsConflictException.class}) + public ResponseEntity handleRoleExistsConflictException(RoleExistsConflictException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponse(HttpStatus.CONFLICT, e.getMessage())); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleDeleteException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleDeleteException.java new file mode 100644 index 000000000..a7a35ab4f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleDeleteException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.exception; + +public class RoleDeleteException extends Exception { + public RoleDeleteException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleExistsConflictException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleExistsConflictException.java new file mode 100644 index 000000000..f5364c6ff --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleExistsConflictException.java @@ -0,0 +1,9 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.exception; + +public class RoleExistsConflictException extends Exception { + + public RoleExistsConflictException(String message) { + super(message); + } + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java index c56c403ab..7eb8718f3 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,13 +23,13 @@ @EntityListeners(GroupUpdatedEntityListener.class) @Entity(name = "user_groups") public class Group implements Owner { - public static final String DEFAULT_REGEX = "/(?:)/"; //non-capturing + public static final String DEFAULT_REGEX = "^.+$"; //everything @Transient @JsonIgnore public static Group ADMIN_GROUP; - @Column(name = "group_description", nullable = true) + @Column(name = "group_description") String description; @Transient @@ -78,7 +78,6 @@ public void registerLoader(ILazyLoaderHelper lazyLoaderHelper) { public Set getOwnedItems() { if (lazyLoaderHelper != null) { lazyLoaderHelper.loadOwnedItems(this); - lazyLoaderHelper = null; } return ownedItems; } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Ownable.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Ownable.java index c184b9679..557c92f58 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Ownable.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Ownable.java @@ -4,10 +4,10 @@ public interface Ownable { /** * @return representation of the id of the object. This is likely (but not limited to) the resource id of the object */ - public String getObjectId(); + String getObjectId(); /** * @return the OwnableType that describes the Ownable object */ - public OwnableType getOwnableType(); + OwnableType getOwnableType(); } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Owner.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Owner.java index ee7224335..4b9b28925 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Owner.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Owner.java @@ -4,10 +4,10 @@ public interface Owner { /** * @return representation of the id of the owner. This is likely (but not limited to) the resource id of the owner */ - public String getOwnerId(); + String getOwnerId(); /** * @return the type describing the owner */ - public OwnerType getOwnerType(); + OwnerType getOwnerType(); } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java index ea3048b32..ad9dd4844 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java @@ -1,6 +1,16 @@ package edu.internet2.tier.shibboleth.admin.ui.security.model; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ManyToMany; + import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -8,14 +18,6 @@ import lombok.Setter; import lombok.ToString; -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.ManyToMany; -import java.util.HashSet; -import java.util.Set; - /** * Models a basic administrative role concept in the system. * @@ -29,24 +31,27 @@ @ToString(exclude = "users") public class Role extends AbstractAuditable { - public Role(String name) { - this.name = name; - } - - public Role(String name, int rank) { - this.name = name; - this.rank = rank; - } - @Column(unique = true) private String name; @Column(name = "ROLE_RANK") private int rank; // 0=ADMIN, additional ranks are higher + @Column(name = "resource_id") + String resourceId = UUID.randomUUID().toString(); + //Ignore properties annotation here is to prevent stack overflow recursive error during JSON serialization @JsonIgnoreProperties("roles") @ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY) private Set users = new HashSet<>(); + public Role(String name) { + this.name = name; + } + + public Role(String name, int rank) { + this.name = name; + this.rank = rank; + } + } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/User.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/User.java index 81d9505c0..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 @@ -129,7 +129,6 @@ public String getRole() { public Set getUserGroups() { if (lazyLoaderHelper != null) { lazyLoaderHelper.loadGroups(this); - lazyLoaderHelper = null; } return userGroups; } 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 c6e56ec62..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 @@ -18,7 +18,7 @@ public class GroupUpdatedEntityListener implements ILazyLoaderHelper { * @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; } @@ -36,7 +36,6 @@ public synchronized void groupSavedOrFetched(Group group) { public void loadOwnedItems(Group group) { Set ownedItems = ownershipRepository.findAllByOwner(group); group.setOwnedItems(ownedItems); - group.registerLoader(null); // once loaded, remove the helper from the group } } \ 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 a53260486..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 @@ -22,7 +22,7 @@ public class UserUpdatedEntityListener implements ILazyLoaderHelper { * @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; } @@ -51,6 +51,5 @@ public void loadGroups(User user) { groups.add(userGroup); }); user.setGroups(groups); - user.setLazyLoaderHelper(null); } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepository.java index 7c3c6e7b1..daf3ce265 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepository.java @@ -8,11 +8,6 @@ public interface GroupsRepository extends JpaRepository { void deleteByResourceId(String resourceId); - - List findAll(); - + Group findByResourceId(String id); - - @SuppressWarnings("unchecked") - Group save(Group group); -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java index 120f2938e..fb77d0e9d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java @@ -12,5 +12,9 @@ */ public interface RoleRepository extends JpaRepository { + void deleteByResourceId(String resourceId); + Optional findByName(final String name); + + Optional findByResourceId(String resourceId); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java index 4749a5401..3a1dfd029 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 @@ -63,6 +63,7 @@ public void deleteDefinition(String resourceId) throws EntityNotFoundException, @Override public boolean doesStringMatchGroupPattern(String groupId, String uri) { Group group = find(groupId); + //@TODO change matching to rhino return Pattern.matches(group.getValidationRegex(), uri); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IRolesService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IRolesService.java new file mode 100644 index 000000000..26653d241 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IRolesService.java @@ -0,0 +1,22 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service; + +import java.util.List; + +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; + +public interface IRolesService { + + Role createRole(Role role) throws RoleExistsConflictException; + + Role updateRole(Role role) throws EntityNotFoundException; + + List findAll(); + + Role findByResourceId(String resourceId) throws EntityNotFoundException; + + void deleteDefinition(String resourceId) throws EntityNotFoundException, RoleDeleteException; + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/RolesServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/RolesServiceImpl.java new file mode 100644 index 000000000..cfbe57fb0 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/RolesServiceImpl.java @@ -0,0 +1,62 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service; + +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository; + +@Service +public class RolesServiceImpl implements IRolesService { + @Autowired + private RoleRepository roleRepository; + + @Override + public Role createRole(Role role) throws RoleExistsConflictException { + Optional found = roleRepository.findByName(role.getName()); + // If already defined, we don't want to create a new one, nor do we want this call update the definition + if (found.isPresent()) { + throw new RoleExistsConflictException( + String.format("Call update (PUT) to modify the role with name: [%s]", role.getName())); + } + return roleRepository.save(role); + } + + @Override + public void deleteDefinition(String resourceId) throws EntityNotFoundException, RoleDeleteException { + Optional found = roleRepository.findByResourceId(resourceId); + if (found.isPresent() && !found.get().getUsers().isEmpty()) { + throw new RoleDeleteException(String.format("Unable to delete role with resource id: [%s] - remove role from all users first", resourceId)); + } + roleRepository.deleteByResourceId(resourceId); + } + + @Override + public List findAll() { + return roleRepository.findAll(); + } + + @Override + public Role findByResourceId(String resourceId) throws EntityNotFoundException { + Optional found = roleRepository.findByResourceId(resourceId); + if (found.isEmpty()) { + throw new EntityNotFoundException(String.format("Unable to find role with resource id: [%s]", resourceId)); + } + return found.get(); + } + + @Override + public Role updateRole(Role role) throws EntityNotFoundException { + Optional found = roleRepository.findByName(role.getName()); + if (found.isEmpty()) { + throw new EntityNotFoundException(String.format("Unable to find role with name: [%s]", role.getName())); + } + return roleRepository.save(role); + } +} 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 f6ae4dfd8..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 @@ -1,18 +1,10 @@ package edu.internet2.tier.shibboleth.admin.ui.security.service; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; - -import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException; -import org.apache.commons.lang.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - +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.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; @@ -23,48 +15,89 @@ 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 static edu.internet2.tier.shibboleth.admin.ui.security.service.UserAccess.ADMIN; +import static edu.internet2.tier.shibboleth.admin.ui.security.service.UserAccess.GROUP; +import static edu.internet2.tier.shibboleth.admin.ui.security.service.UserAccess.NONE; import lombok.NoArgsConstructor; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; @Service @NoArgsConstructor public class UserService { @Autowired private IGroupService groupService; - + @Autowired private OwnershipRepository ownershipRepository; - + @Autowired private RoleRepository roleRepository; - + @Autowired private UserRepository userRepository; - + public UserService(IGroupService groupService, OwnershipRepository ownershipRepository, RoleRepository roleRepository, UserRepository userRepository) { this.groupService = groupService; this.ownershipRepository = ownershipRepository; this.roleRepository = roleRepository; this.userRepository = userRepository; } - + + public boolean currentUserCanEnable(IActivatable activatableObject) { + if (currentUserIsAdmin()) { return true; } + switch (activatableObject.getActivatableType()) { + case ENTITY_DESCRIPTOR: { + return currentUserHasExpectedRole(Arrays.asList("ROLE_ENABLE" )) && getCurrentUserGroup().getOwnerId().equals(((EntityDescriptor) activatableObject).getIdOfOwner()); + } + // Currently filters and providers dont have ownership, so we just look for the right role + case FILTER: + case METADATA_RESOLVER: + return currentUserHasExpectedRole(Arrays.asList("ROLE_ENABLE" )); + default: + return false; + } + } + + /** + * This basic logic assumes users only have a single role (despite users having a list of roles, we assume only 1 currently) + */ + private boolean currentUserHasExpectedRole(List acceptedRoles) { + User user = getCurrentUser(); + return acceptedRoles.contains(user.getRole()); + } + public boolean currentUserIsAdmin() { User user = getCurrentUser(); return user != null && user.getRole().equals("ROLE_ADMIN"); } - + @Transactional public void delete(String username) throws EntityNotFoundException, OwnershipConflictException { Optional userToRemove = userRepository.findByUsername(username); if (userToRemove.isEmpty()) throw new EntityNotFoundException("User does not exist"); - if (!ownershipRepository.findOwnedByUser(username).isEmpty()) throw new OwnershipConflictException("User ["+username+"] has ownership of entities in the system. Please remove all items before attemtping to delete the user."); - + if (!ownershipRepository.findOwnedByUser(username).isEmpty()) throw new OwnershipConflictException("User ["+username+"] has ownership of entities in the system. Please remove all items before attempting to delete the user."); + // ok, user exists and doesn't own anything in the system, so delete them // If the user is owned by anything, clear that first ownershipRepository.clearUsersGroups(username); - User user = userToRemove.get(); + User user = userToRemove.get(); userRepository.delete(user); } - + + public Optional findByUsername(String username) { + return userRepository.findByUsername(username); + } + public User getCurrentUser() { //TODO: Consider returning an Optional here User user = null; @@ -83,17 +116,17 @@ public User getCurrentUser() { public UserAccess getCurrentUserAccess() { User user = getCurrentUser(); if (user == null) { - return UserAccess.NONE; + return NONE; } if (user.getRole().equals("ROLE_ADMIN")) { - return UserAccess.ADMIN; + return ADMIN; } - if (user.getRole().equals("ROLE_USER")) { - return UserAccess.GROUP; + if (user.getRole().equals("ROLE_USER") || user.getRole().equals("ROLE_ENABLE")) { + return GROUP; } - return UserAccess.NONE; + return NONE; } - + public Group getCurrentUserGroup() { switch (getCurrentUserAccess()) { case ADMIN: @@ -102,16 +135,15 @@ public Group getCurrentUserGroup() { return getCurrentUser().getGroup(); } } - + public Set getUserRoles(String username) { Optional user = userRepository.findByUsername(username); HashSet result = new HashSet<>(); - if (user.isPresent() ) { - user.get().getRoles().forEach(role -> result.add(role.getName())); - } + user.ifPresent(value -> value.getRoles().forEach(role -> result.add(role.getName()))); return result; } + // @TODO - probably delegate this out to something plugable at some point public boolean isAuthorizedFor(Ownable ownableObject) { switch (getCurrentUserAccess()) { case ADMIN: // Pure admin is authorized to do anything @@ -129,7 +161,7 @@ public boolean isAuthorizedFor(Ownable ownableObject) { return false; } } - + /** * Creating users should always have a group. If the user isn't assigned to a group, create one based on their name. * If the user has the ADMIN role, they are always solely assigned to the admin group. @@ -164,7 +196,7 @@ public User save(User user) { if (g == null) { try { Group newGroup = ug; - Ownership o = ownershipRepository.saveAndFlush(new Ownership(newGroup, user)); + ownershipRepository.saveAndFlush(new Ownership(newGroup, user)); g = groupService.createGroup(newGroup); } catch (GroupExistsConflictException | InvalidGroupRegexException e) { @@ -177,14 +209,13 @@ public User save(User user) { } return userRepository.saveAndFlush(user); } - + /** * Given a user with a defined User.role, update the User.roles collection with that role. * * This currently exists because users should only ever have one role in the system at this time. However, user * roles are persisted as a set of roles (for future-proofing). Once we start allowing a user to have multiple roles, * this method and User.role can go away. - * @param user */ public void updateUserRole(User user) { if (StringUtils.isNotBlank(user.getRole())) { @@ -197,7 +228,7 @@ public void updateUserRole(User user) { throw new RuntimeException(String.format("User with username [%s] is defined with role [%s] which does not exist in the system!", user.getUsername(), user.getRole())); } } else { - throw new RuntimeException(String.format("User with username [%s] has no role defined and therefor cannot be updated!", user.getUsername())); + throw new RuntimeException(String.format("User with username [%s] has no role defined and therefore cannot be updated!", user.getUsername())); } } } \ No newline at end of file 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 0d070d43d..9928eefc4 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 @@ -70,7 +70,7 @@ EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepres * @return a list of EntityDescriptorRepresentations that a user has the rights to access */ List getAllRepresentationsBasedOnUserAccess() throws ForbiddenException; - + /** * Given a list of attributes, generate an AttributeReleaseList * @@ -113,4 +113,5 @@ EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRepresent */ void updateDescriptorFromRepresentation(final org.opensaml.saml.saml2.metadata.EntityDescriptor entityDescriptor, final EntityDescriptorRepresentation representation); + EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String resourceId, boolean status) throws EntityNotFoundException, ForbiddenException; } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java index 2ef9ab08e..6d752928b 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java @@ -1,7 +1,12 @@ package edu.internet2.tier.shibboleth.admin.ui.service; +import javax.script.ScriptException; + import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; /** * Main backend facade API that defines operations pertaining to manipulating {@link EntityAttributesFilter} objects. @@ -25,4 +30,6 @@ public interface FilterService { * @return FilterRepresentation front end representation */ FilterRepresentation createRepresentationFromFilter(final EntityAttributesFilter entityAttributesFilter); + + MetadataFilter updateFilterEnabledStatus(String metadataResolverId, String resourceId, boolean status) throws EntityNotFoundException, ForbiddenException, ScriptException; } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java index 4fbdbe004..db769acc3 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,7 +1,21 @@ 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.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.EntityIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; @@ -9,41 +23,56 @@ 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.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 - EntityDescriptorRepository entityDescriptorRepository; - + private EntityDescriptorRepository entityDescriptorRepository; + @Autowired IGroupService groupService; - + @Autowired private OpenSamlObjects openSamlObjects; - + @Autowired private OwnershipRepository ownershipRepository; @Autowired - UserService userService; - + private UserService userService; + private EntityDescriptor buildDescriptorFromRepresentation(final EntityDescriptor ed, final EntityDescriptorRepresentation representation) { ed.setEntityID(representation.getEntityId()); ed.setIdOfOwner(representation.getIdOfOwner()); @@ -77,14 +106,14 @@ public EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, EntityIdExistsException, InvalidPatternMatchException { return createNew(createRepresentationFromDescriptor(ed)); } - + @Override public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) throws ForbiddenException, EntityIdExistsException, InvalidPatternMatchException { if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { throw new ForbiddenException("You do not have the permissions necessary to enable this service."); } - + if (entityDescriptorRepository.findByEntityID(edRep.getEntityId()) != null) { throw new EntityIdExistsException(edRep.getEntityId()); } @@ -95,7 +124,10 @@ public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation e validateEntityIdAndACSUrls(edRep); EntityDescriptor ed = (EntityDescriptor) createDescriptorFromRepresentation(edRep); - ed.setIdOfOwner(ownerId); + ed.setIdOfOwner(userService.getCurrentUserGroup().getOwnerId()); + + ownershipRepository.deleteEntriesForOwnedObject(ed); + ownershipRepository.save(new Ownership(userService.getCurrentUserGroup(), ed)); return createRepresentationFromDescriptor(entityDescriptorRepository.save(ed)); } @@ -348,7 +380,7 @@ public List getAllRepresentationsBasedOnUserAcce @Override public List getAttributeReleaseListFromAttributeList(List attributeList) { if (attributeList == null) { - return new ArrayList(); + return new ArrayList<>(); } attributeList.removeIf(Objects::isNull); return ModelRepresentationConversions.getAttributeReleaseListFromAttributeList(attributeList); @@ -378,7 +410,7 @@ public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRe if (existingEd == null) { throw new EntityNotFoundException(String.format("The entity descriptor with entity id [%s] was not found for update.", edRep.getId())); } - if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { + 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())) { @@ -394,10 +426,17 @@ public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRe validateEntityIdAndACSUrls(edRep); updateDescriptorFromRepresentation(existingEd, edRep); - return createRepresentationFromDescriptor(entityDescriptorRepository.save(existingEd)); + existingEd = entityDescriptorRepository.save(existingEd); + ownershipRepository.deleteEntriesForOwnedObject(existingEd); + ownershipRepository.save(new Ownership(new Owner() { + public String getOwnerId() { return edRep.getIdOfOwner(); } + public OwnerType getOwnerType() { return OwnerType.GROUP; } + }, existingEd)); + return createRepresentationFromDescriptor(existingEd); } @Override + // This should be private, but we use it in a couple different test classes not sure we should keep... public void updateDescriptorFromRepresentation(org.opensaml.saml.saml2.metadata.EntityDescriptor entityDescriptor, EntityDescriptorRepresentation representation) { if (!(entityDescriptor instanceof EntityDescriptor)) { throw new UnsupportedOperationException("not yet implemented"); @@ -405,6 +444,20 @@ public void updateDescriptorFromRepresentation(org.opensaml.saml.saml2.metadata. buildDescriptorFromRepresentation((EntityDescriptor) entityDescriptor, representation); } + @Override + public EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String resourceId, boolean status) throws EntityNotFoundException, ForbiddenException { + EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); + if (ed == null) { + throw new EntityNotFoundException("Entity with resourceid[" + resourceId + "] was not found for update"); + } + if (!userService.currentUserCanEnable(ed)) { + throw new ForbiddenException("You do not have the permissions necessary to change the enable status of this entity descriptor."); + } + ed.setServiceEnabled(status); + ed = entityDescriptorRepository.save(ed); + return createRepresentationFromDescriptor(ed); + } + private void validateEntityIdAndACSUrls(EntityDescriptorRepresentation edRep) throws InvalidPatternMatchException { // Check the entity id first if (!groupService.doesStringMatchGroupPattern(edRep.getIdOfOwner(), edRep.getEntityId())) { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java index 0693b538a..c42bd7cad 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java @@ -1,13 +1,26 @@ package edu.internet2.tier.shibboleth.admin.ui.service; +import edu.internet2.tier.shibboleth.admin.ui.domain.IActivatable; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; + import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.interceptor.TransactionAspectSupport; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Optional; + +import javax.script.ScriptException; /** * Default implementation of {@link FilterService} @@ -15,19 +28,29 @@ * @since 1.0 * @author Bill Smith (wsmith@unicon.net) */ +@Service public class JPAFilterServiceImpl implements FilterService { - - private static final Logger LOGGER = LoggerFactory.getLogger(JPAFilterServiceImpl.class); - @Autowired EntityDescriptorService entityDescriptorService; - + @Autowired EntityService entityService; + @Autowired + FilterRepository filterRepository; + @Autowired FilterTargetService filterTargetService; + + @Autowired + private MetadataResolverRepository metadataResolverRepository; + + @Autowired + private MetadataResolverService metadataResolverService; + @Autowired + private UserService userService; + @Override public EntityAttributesFilter createFilterFromRepresentation(FilterRepresentation representation) { //TODO? use OpenSamlObjects.buildDefaultInstanceOfType(EntityAttributesFilter.class)? @@ -66,4 +89,52 @@ public FilterRepresentation createRepresentationFromFilter(EntityAttributesFilte representation.setVersion(entityAttributesFilter.hashCode()); return representation; } -} + + private void reloadFiltersAndHandleScriptException(String resolverResourceId) throws ScriptException { + try { + metadataResolverService.reloadFilters(resolverResourceId); + } catch (Throwable ex) { + //explicitly mark transaction for rollback when we get ScriptException as we call reloadFilters + //after persistence call. Then re-throw the exception with pertinent message + if (ex instanceof ScriptException) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + throw new ScriptException("Caught invalid script parsing error when reloading filters. Please fix the script data"); + } + } + } + + /** + * Logic taken directly from the MetadataFiltersController and then modified slightly. + */ + @Override + public MetadataFilter updateFilterEnabledStatus(String metadataResolverId, String resourceId, boolean status) + throws EntityNotFoundException, ForbiddenException, ScriptException { + + MetadataResolver metadataResolver = metadataResolverRepository.findByResourceId(metadataResolverId); + // Now we operate directly on the filter attached to MetadataResolver, + // Instead of fetching filter separately, to accommodate correct envers versioning with uni-directional one-to-many + Optional filterTobeUpdatedOptional = metadataResolver.getMetadataFilters().stream() + .filter(it -> it.getResourceId().equals(resourceId)).findFirst(); + if (filterTobeUpdatedOptional.isEmpty()) { + throw new EntityNotFoundException("Filter with resource id[" + resourceId + "] not found"); + } + + MetadataFilter filterTobeUpdated = filterTobeUpdatedOptional.get(); + + if (!userService.currentUserCanEnable(filterTobeUpdated)) { + throw new ForbiddenException("You do not have the permissions necessary to change the enable status of this filter."); + } + + filterTobeUpdated.setFilterEnabled(status); + MetadataFilter persistedFilter = filterRepository.save(filterTobeUpdated); + + // To support envers versioning from MetadataResolver side + metadataResolver.markAsModified(); + metadataResolverRepository.save(metadataResolver); + + // TODO: do we need to reload filters here? + reloadFiltersAndHandleScriptException(metadataResolver.getResourceId()); + + return persistedFilter; + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java index ee5f8cec3..2343206a7 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java @@ -22,6 +22,7 @@ import org.opensaml.saml.metadata.resolver.MetadataResolver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; import java.io.File; import java.io.FileNotFoundException; @@ -32,6 +33,7 @@ /** * @author Bill Smith (wsmith@unicon.net) */ +@Service public class MetadataResolverConverterServiceImpl implements MetadataResolverConverterService { @Autowired diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java index 376c34aa5..5fd205c20 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java @@ -2,8 +2,18 @@ import org.w3c.dom.Document; +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException; + public interface MetadataResolverService { - public void reloadFilters(String metadataResolverName); + public MetadataResolver findByResourceId(String resourceId) throws EntityNotFoundException; public Document generateConfiguration(); + + public void reloadFilters(String metadataResolverName); + + public MetadataResolver updateMetadataResolverEnabledStatus(MetadataResolver existingResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException; } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 83f2635e0..5b7b801f1 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -20,6 +20,7 @@ spring.datasource.platform=h2 spring.datasource.driverClassName=org.h2.Driver spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.h2.console.enabled=true +spring.h2.console.settings.web-allow-others=true # spring.jackson.default-property-inclusion=non_absent spring.jackson.default-property-inclusion=NON_NULL @@ -58,7 +59,10 @@ shibui.logout-url=/dashboard # spring.profiles.active=default -#shibui.default-password= +## Default root user can be set in application.yml or here - setting in both places can be undeterministic +## Default password must be set for the default user to be configured and setup +#shibui.default-password={noop}somepassword +shibui.default-rootuser=root shibui.metadata-sources-ui-schema-location=classpath:metadata-sources-ui-schema.json shibui.entity-attributes-filters-ui-schema-location=classpath:entity-attributes-filters-ui-schema.json @@ -87,7 +91,11 @@ shibui.mail.text-email-template-path-prefix=/mail/text/ shibui.mail.html.email-template-path-prefix=/mail/html/ shibui.mail.system-email-address=doNotReply@shibui.org -shibui.roles=ROLE_ADMIN,ROLE_USER,ROLE_NONE + +#ShibUIConfiguration slurps in these values and they are bootstrapped in on startup +shibui.roles=ROLE_ADMIN,ROLE_ENABLE,ROLE_USER,ROLE_NONE +#Authenticated access roles - used by Spring Security to allow access when authenticated +shibui.roles.authenticated=ADMIN,ENABLE,USER #In order to enable authentication via configured pac4j library (with external SAMl Idp, for example) #This property must be set to true and pac4j properties configured. For sample pac4j properties, see application.yml @@ -97,4 +105,4 @@ shibui.roles=ROLE_ADMIN,ROLE_USER,ROLE_NONE #This property must be set to true in order to enable posting stats to beacon endpoint. Furthermore, appropriate #environment variables must be set for beacon publisher to be used (the ones that are set when running shib-ui in #docker container -shibui.beacon-enabled=true +shibui.beacon-enabled=true \ No newline at end of file diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index e9301289a..74ae43689 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1,4 +1,8 @@ #shibui: +## Default password must be set for the default user to be configured and setup +# default-rootuser:root +## need to include the encoding for the password - be sure to quote the entire value as shown +# default-password: "{noop}foopassword" # pac4j-enabled: true # pac4j: # keystorePath: "/etc/shibui/samlKeystore.jks" diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 9100dd04c..54ad56fe2 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -66,6 +66,12 @@ action.add-attribute=Add Attribute action.custom-entity-attributes=Custom Entity Attributes action.groups=Groups action.source-group=Group +action.enable=Enable +action.disable=Disable + +action.add-new-role=Add new role +action.roles=Roles +action.source-role=Role value.enabled=Enabled value.disabled=Disabled @@ -100,7 +106,6 @@ value.dynamic-http-metadata-provider=DynamicHttpMetadataProvider value.entity-attributes-filter=EntityAttributes Filter value.spdescriptor=SPSSODescriptor value.attr-auth-descriptor=AttributeAuthorityDescriptor -value.local-dynamic-metadata-provider=LocalDynamicMetadataProvider value.md-query-protocol=MetadataQueryProtocol value.template=Template @@ -427,7 +432,6 @@ label.attribute-eduPersonUniqueId=eduPersonUniqueId label.attribute-employeeNumber=employeeNumber label.force-authn=Force AuthN -label.dynamic-attributes=Dynamic Attributes label.min-cache-duration=Min Cache Duration label.max-cache-duration=Max Cache Duration label.max-idle-entity-data=Max Idle Entity Data @@ -458,7 +462,6 @@ label.admin=Admin label.user-maintenance=User Maintenance label.user-id=UserId label.email=Email -label.role=Role label.delete=Delete? label.title=Title @@ -494,6 +497,16 @@ tooltip.url-validation-regex=URL validation regular expression message.user-role-admin-group=Cannot change group for ROLE_ADMIN users. +label.roles-management=Role Management +label.new-role=New Role +label.edit-role=Edit Role +label.role-name=Role Name +label.role-description=Role Description +label.role=Role + +message.delete-role-title=Delete Role? + +message.delete-role-body=You are requesting to delete a role. If you complete this process the role will be removed. This cannot be undone. Do you wish to continue? message.duration=Requires a valid ISO 8601 duration (ex. PT2D) message.delete-user-title=Delete User? @@ -690,4 +703,7 @@ tooltip.nameid-formats-value=Value tooltip.nameid-formats-type=Type tooltip.group-name=Group Name -tooltip.group-description=Group Description \ No newline at end of file +tooltip.group-description=Group Description + +tooltip.role-name=Role Name +tooltip.role-description=Role Description \ No newline at end of file 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 index 29206f86d..da12d8bc2 100644 --- 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 @@ -2,6 +2,9 @@ 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 @@ -17,6 +20,9 @@ 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=", @@ -26,10 +32,13 @@ import javax.persistence.EntityManager @ContextConfiguration(classes = [BaseDataJpaTestConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") -abstract class AbstractBaseDataJpaTest extends Specification implements ResetsDatabase { +abstract class AbstractBaseDataJpaTest extends Specification implements ResetsDatabaseTrait { @Autowired EntityManager entityManager + @Autowired + GroupsRepository groupRepository + @Autowired GroupServiceForTesting groupService @@ -74,6 +83,8 @@ abstract class AbstractBaseDataJpaTest extends Specification implements ResetsDa } createAdminUser() + GroupUpdatedEntityListener.init(ownershipRepository) + UserUpdatedEntityListener.init(ownershipRepository, groupRepository) } def cleanup() { 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 index c3657ef42..f3b2bc34e 100644 --- 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 @@ -1,8 +1,12 @@ 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 @@ -10,6 +14,7 @@ import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsReposito 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.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions import org.springframework.context.annotation.Bean @@ -18,9 +23,12 @@ 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" ]) +@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) { @@ -51,6 +59,16 @@ class BaseDataJpaTestConfiguration { 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() { @@ -59,6 +77,12 @@ class BaseDataJpaTestConfiguration { return result } + @Bean + @Primary + TestObjectGenerator testObjectGenerator (AttributeUtility attributeUtility, CustomPropertiesConfiguration customPropertiesConfiguration) { + return new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) + } + @Bean UserUpdatedEntityListener userUpdatedEntityListener(OwnershipRepository ownershipRepository, GroupsRepository groupRepo) { UserUpdatedEntityListener listener = new UserUpdatedEntityListener() diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/ResetDatabase.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/ResetsDatabaseTrait.groovy similarity index 97% rename from backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/ResetDatabase.groovy rename to backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/ResetsDatabaseTrait.groovy index ded212e42..d99fb85e7 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/ResetDatabase.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/ResetsDatabaseTrait.groovy @@ -2,7 +2,7 @@ package edu.internet2.tier.shibboleth.admin.ui import groovy.sql.Sql -trait ResetsDatabase { +trait ResetsDatabaseTrait { static final String H2_BACKUP_LOCATION = '/tmp/h2backup.sql' void dbsetup() { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy index 384cb9ce6..f46eb33d8 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy @@ -45,7 +45,7 @@ class TestConfiguration { final OpenSamlObjects openSamlObjects final MetadataResolverRepository metadataResolverRepository - final Logger logger = LoggerFactory.getLogger(TestConfiguration.class); + final Logger logger = LoggerFactory.getLogger(TestConfiguration.class) @Autowired private CustomEntityAttributeDefinitionRepository repository; @@ -91,12 +91,12 @@ class TestConfiguration { for (String entityId: this.getBackingStore().getIndexedDescriptors().keySet()) { Document document = new Document(); - document.add(new StringField("id", entityId, Field.Store.YES)); - document.add(new TextField("content", entityId, Field.Store.YES)); // TODO: change entityId to be content of entity descriptor block + document.add(new StringField("id", entityId, Field.Store.YES)) + document.add(new TextField("content", entityId, Field.Store.YES)) // TODO: change entityId to be content of entity descriptor block try { - indexWriter.addDocument(document); + indexWriter.addDocument(document) } catch (IOException e) { - logger.error(e.getMessage(), e); + logger.error(e.getMessage(), e) } } try { @@ -131,4 +131,4 @@ class TestConfiguration { return it } } -} +} \ No newline at end of file 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 f83ab1fb3..510e09e64 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 @@ -61,6 +61,12 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { @Autowired EntityService entityService + @Autowired + TestObjectGenerator generator + + @Autowired + ObjectMapper mapper + @Autowired OpenSamlObjects openSamlObjects @@ -68,8 +74,6 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { JPAEntityDescriptorServiceImpl jpaEntityDescriptorService RandomGenerator randomGenerator - TestObjectGenerator generator - def mapper def mockRestTemplate = Mock(RestTemplate) def mockMvc @@ -81,16 +85,14 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { @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) - generator = new TestObjectGenerator() randomGenerator = new RandomGenerator() - mapper = new ObjectMapper() controller = new EntityDescriptorController(versionService) controller.openSamlObjects = openSamlObjects 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 new file mode 100644 index 000000000..6462482d0 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorOwnershipIntegrationTests.groovy @@ -0,0 +1,153 @@ +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 +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 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 +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.security.test.context.support.WithMockUser +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 spock.lang.Stepwise +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.post +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +/** + * Test to recreate an issue discovered while trying to validate fixes for other bugs - SHIBUI-2033 + */ +@Stepwise +class EntityDescriptorOwnershipIntegrationTests extends AbstractBaseDataJpaTest { + @Autowired + EntityDescriptorRepository entityDescriptorRepository + + @Autowired + EntityService entityService + + @Autowired + OpenSamlObjects openSamlObjects + + @Autowired + JPAEntityDescriptorServiceImpl service + + def mockRestTemplate = Mock(RestTemplate) + + Group cuGroup = new Group().with { + it.name = "College Users" + it.resourceId = "cu-group" + it + } + + def mockMvc + + @Subject + def controller + + @Transactional + def setup() { + EntityDescriptorVersionService versionService = Mock() + controller = new EntityDescriptorController(versionService) + controller.openSamlObjects = openSamlObjects + controller.entityDescriptorService = service + 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") + userService.save(user) + + EntityDescriptorConversionUtils.setOpenSamlObjects(openSamlObjects) + EntityDescriptorConversionUtils.setEntityService(entityService) + } + + @WithMockAdmin + def "The test scenario"() { + when:"step 1 - create new group" + cuGroup = groupService.createGroup(cuGroup) + + then: + groupService.findAll().size() == 3 + + when: "step 2 - assign the user to new group" + User user = userRepository.findByUsername("someUser").get() + user.setGroup(cuGroup) + def updatedUser = userService.save(user) + + then: + updatedUser.getGroupId() == "cu-group" + ownershipRepository.findAllByOwner(cuGroup).size() == 1 + + when: "step 3 - create a new ED and then change its ownership to the cu group" + def expectedEntityId = 'https://shib' + def expectedSpName = 'sp1' + + def postedJsonBody = """ + { + "serviceProviderName": "$expectedSpName", + "entityId": "$expectedEntityId", + "organization": {}, + "serviceEnabled": false, + "current": false + } + """ + def result = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) + + then: + result.andExpect(status().isCreated()) + .andExpect(jsonPath("\$.entityId").value("https://shib")) + .andExpect(jsonPath("\$.serviceEnabled").value(false)) + .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) + + ownershipRepository.findAllByOwner(cuGroup).size() == 1 // someUser + ownershipRepository.findAllByOwner(Group.ADMIN_GROUP).size() == 2 // admin user + entity descriptor + + when: "step 4 - change ownership of the ED" + EntityDescriptor ed = entityDescriptorRepository.findByEntityID(expectedEntityId) + EntityDescriptorRepresentation edRep = service.createRepresentationFromDescriptor(ed) + edRep.setIdOfOwner(cuGroup.getOwnerId()) + service.update(edRep) + + then: + ownershipRepository.findAllByOwner(cuGroup).size() == 2 // someUser + entity descriptor + ownershipRepository.findAllByOwner(Group.ADMIN_GROUP).size() == 1 // admin user + } +} \ 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..8875dbf51 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorVersionControllerTests.groovy @@ -0,0 +1,180 @@ +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.configuration.CustomPropertiesConfiguration +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.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.JPAEntityServiceImpl +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.AttributeUtility +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import org.hamcrest.Matchers +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.jsonPath +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 + JPAEntityServiceImpl jpaEntityService(OpenSamlObjects openSamlObjects, AttributeUtility attributeUtility, + CustomPropertiesConfiguration customPropertiesConfiguration) { + return new JPAEntityServiceImpl(openSamlObjects, attributeUtility, customPropertiesConfiguration) + } + + @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/MetadataFiltersControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy index 14165b411..51c786429 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 @@ -4,9 +4,13 @@ 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.domain.exceptions.MetadataFileNotFoundException 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 @@ -85,6 +89,18 @@ class MetadataFiltersControllerTests extends AbstractBaseDataJpaTest { Document generateConfiguration() { return null } + + @Override + MetadataResolver updateMetadataResolverEnabledStatus(MetadataResolver existingResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException { + // This won't get called + return null + } + + @Override + MetadataResolver findByResourceId(String resourceId) throws EntityNotFoundException { + // This won't get called + return null + } }, chainingMetadataResolver: new OpenSamlChainingMetadataResolver().with { it.id = 'chain' diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy index a32b65f64..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 @@ -63,16 +63,18 @@ class MetadataResolversControllerIntegrationTests extends AbstractBaseDataJpaTes @Autowired AttributeUtility attributeUtility + @Autowired + MetadataResolversController controller + @Autowired CustomPropertiesConfiguration customPropertiesConfiguration @Autowired - MetadataResolversController controller + ObjectMapper mapper @Autowired MetadataResolverRepository metadataResolverRepository - ObjectMapper mapper TestObjectGenerator generator MockMvc mockMvc @@ -81,11 +83,6 @@ class MetadataResolversControllerIntegrationTests extends AbstractBaseDataJpaTes @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() @@ -376,20 +373,6 @@ class MetadataResolversControllerIntegrationTests extends AbstractBaseDataJpaTes return new DirectoryServiceImpl() } - @Bean - JPAMetadataResolverServiceImpl jpaMetadataResolverService(MetadataResolver metadataResolver, MetadataResolverRepository metadataResolverRepository, - OpenSamlObjects openSamlObjects, MetadataResolversPositionOrderContainerService resolversPositionOrderContainerService, - ShibUIConfiguration shibUIConfiguration) { - return new JPAMetadataResolverServiceImpl().with { - it.metadataResolver = metadataResolver - it.metadataResolverRepository = metadataResolverRepository - it.openSamlObjects = openSamlObjects - it.resolversPositionOrderContainerService = resolversPositionOrderContainerService - it.shibUIConfiguration = shibUIConfiguration - it - } - } - @Bean MetadataResolversController metadataResolversController(MetadataResolverRepository metadataResolverRepository, MetadataResolverValidationService metadataResolverValidationService, MetadataResolverService metadataResolverService, MetadataResolversPositionOrderContainerService positionOrderContainerService, 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 89a285ad8..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 @@ -6,24 +6,21 @@ import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration 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 org.springframework.beans.factory.annotation.Autowired class PolymorphicFiltersJacksonHandlingTests extends AbstractBaseDataJpaTest { - @Autowired - AttributeUtility attributeUtility @Autowired CustomPropertiesConfiguration customPropertiesConfiguration + @Autowired ObjectMapper mapper + + @Autowired TestObjectGenerator testObjectGenerator def setup() { - mapper = new ObjectMapper() - mapper.enable(SerializationFeature.INDENT_OUTPUT) - - testObjectGenerator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) + customPropertiesConfiguration.postConstruct() } @WithMockAdmin 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 f0df3716d..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 @@ -3,28 +3,20 @@ 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.AbstractBaseDataJpaTest -import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration 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.util.TestObjectGenerator -import edu.internet2.tier.shibboleth.admin.util.AttributeUtility import org.springframework.beans.factory.annotation.Autowired class PolymorphicResolversJacksonHandlingTests extends AbstractBaseDataJpaTest { @Autowired - AttributeUtility attributeUtility - - @Autowired - CustomPropertiesConfiguration customPropertiesConfiguration + TestObjectGenerator testObjectGenerator ObjectMapper mapper - TestObjectGenerator testObjectGenerator def setup() { mapper = new ObjectMapper() mapper.enable(SerializationFeature.INDENT_OUTPUT) - - testObjectGenerator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) } def "Correct polymorphic serialization of LocalDynamicMetadataResolver"() { @@ -233,113 +225,12 @@ class PolymorphicResolversJacksonHandlingTests extends AbstractBaseDataJpaTest { 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) 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 b07a304ab..4805dcaae 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 @@ -27,8 +27,13 @@ import org.springframework.web.util.NestedServletException import java.time.LocalDateTime import java.time.format.DateTimeFormatter -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* +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 @@ -140,12 +145,12 @@ class UsersControllerIntegrationTests extends AbstractBaseDataJpaTest { .andExpect(jsonPath("\$.[1].emailAddress").value("peter@institution.edu")) .andExpect(jsonPath("\$.[1].role").value("ROLE_USER")) .andExpect(jsonPath("\$.[1].groupId").value("nonadmin")) - .andExpect(jsonPath("\$.[1].userGroups.[0].validationRegex").value("/(?:)/")) + .andExpect(jsonPath("\$.[1].userGroups.[0].validationRegex").value(Group.DEFAULT_REGEX)) .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].userGroups.[0].validationRegex").value("/(?:)/")) + .andExpect(jsonPath("\$.[2].userGroups.[0].validationRegex").value(Group.DEFAULT_REGEX)) .andExpect(jsonPath("\$.[3].username").value("anonymousUser")) .andExpect(jsonPath("\$.[3].emailAddress").value("anon@institution.edu")) .andExpect(jsonPath("\$.[3].role").value("ROLE_ADMIN")) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy index 9916ae084..a3f223efb 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy @@ -7,15 +7,15 @@ import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl @Profile('test') class GroupServiceForTesting extends GroupServiceImpl { - public GroupServiceForTesting(GroupServiceImpl impl) { + GroupServiceForTesting(GroupServiceImpl impl) { this.groupRepository = impl.groupRepository this.ownershipRepository = impl.ownershipRepository } @Transactional - public void clearAllForTesting() { - groupRepository.deleteAll(); + void clearAllForTesting() { + groupRepository.deleteAll() ownershipRepository.clearAllOwnedByGroup() ensureAdminGroupExists() } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy index 26c7eb5f1..4c572e2ad 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy @@ -3,6 +3,7 @@ package edu.internet2.tier.shibboleth.admin.ui.service import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.configuration.JsonSchemaComponentsConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup import edu.internet2.tier.shibboleth.admin.ui.jsonschema.LowLevelJsonSchemaValidator import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.security.model.Group @@ -34,11 +35,6 @@ class AuxiliaryIntegrationTests extends Specification { def "SHIBUI-1723: after enabling saved entity descriptor, it should still have valid xml"() { given: - def group = new Group().with { - it.name = "foo" - it.resourceId = "foo" - it - } def entityDescriptor = openSamlObjects.unmarshalFromXml(this.class.getResource('/metadata/SHIBUI-1723-1.xml').bytes) as EntityDescriptor entityDescriptor.idOfOwner = "foo" @@ -52,7 +48,7 @@ class AuxiliaryIntegrationTests extends Specification { it } def json = objectMapper.writeValueAsString(entityDescriptorRepresentation) - def schemaUri = edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.metadataSourcesSchema(new JsonSchemaComponentsConfiguration().jsonSchemaResourceLocationRegistry(this.resourceLoader, this.objectMapper)).uri + def schemaUri = JsonSchemaLocationLookup.metadataSourcesSchema(new JsonSchemaComponentsConfiguration().jsonSchemaResourceLocationRegistry(this.resourceLoader, this.objectMapper)).uri when: LowLevelJsonSchemaValidator.validatePayloadAgainstSchema(new MockHttpInputMessage(json.bytes), schemaUri) @@ -60,4 +56,4 @@ class AuxiliaryIntegrationTests extends Specification { then: noExceptionThrown() } -} +} \ 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 f5230c410..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,14 +1,12 @@ package edu.internet2.tier.shibboleth.admin.ui.service import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest -import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration 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.repository.MetadataResolversPositionOrderContainerRepository import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator @@ -20,7 +18,7 @@ import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean import org.springframework.test.context.ContextConfiguration -import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.* +import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.generatedXmlIsTheSameAsExpectedXml @ContextConfiguration(classes = [IJPAMRSILocalConfig]) class IncommonJPAMetadataResolverServiceImplTests extends AbstractBaseDataJpaTest { @@ -96,25 +94,6 @@ class IncommonJPAMetadataResolverServiceImplTests extends AbstractBaseDataJpaTes @TestConfiguration private static class IJPAMRSILocalConfig { -// @Bean -// DirectoryService directoryService() { -// return new DirectoryServiceImpl() -// } - - @Bean - JPAMetadataResolverServiceImpl jpaMetadataResolverService(MetadataResolver metadataResolver, MetadataResolverRepository metadataResolverRepository, - OpenSamlObjects openSamlObjects, MetadataResolversPositionOrderContainerService resolversPositionOrderContainerService, - ShibUIConfiguration shibUIConfiguration) { - return new JPAMetadataResolverServiceImpl().with { - it.metadataResolver = metadataResolver - it.metadataResolverRepository = metadataResolverRepository - it.openSamlObjects = openSamlObjects - it.resolversPositionOrderContainerService = resolversPositionOrderContainerService - it.shibUIConfiguration = shibUIConfiguration - it - } - } - @Bean MetadataResolver metadataResolver(AttributeUtility attributeUtility, MetadataResolverRepository metadataResolverRepository) { def resolver = new ChainingMetadataResolver().with { 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 e2c8bc5bf..d80fd3f61 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 @@ -36,21 +36,24 @@ class JPAEntityDescriptorServiceImplTests extends AbstractBaseDataJpaTest { @Autowired EntityService entityService + @Autowired + ObjectMapper mapper + @Autowired OpenSamlObjects openSamlObjects @Autowired JPAEntityDescriptorServiceImpl service + @Autowired + TestObjectGenerator testObjectGenerator + RandomGenerator generator JacksonTester jacksonTester - ObjectMapper mapper = new ObjectMapper() - def testObjectGenerator def setup() { JacksonTester.initFields(this, mapper) generator = new RandomGenerator() - testObjectGenerator = new TestObjectGenerator() EntityDescriptorConversionUtils.openSamlObjects = openSamlObjects EntityDescriptorConversionUtils.entityService = entityService openSamlObjects.init() 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 5c4d192c2..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,11 +1,9 @@ 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.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.test.context.TestConfiguration import org.springframework.context.annotation.Bean @@ -18,20 +16,15 @@ import org.springframework.test.context.ContextConfiguration class JPAFilterServiceImplTests extends AbstractBaseDataJpaTest { @Autowired - AttributeUtility attributeUtility - - @Autowired - CustomPropertiesConfiguration customPropertiesConfiguration + JPAFilterServiceImpl filterService @Autowired - JPAFilterServiceImpl filterService + TestObjectGenerator testObjectGenerator RandomGenerator randomGenerator - TestObjectGenerator testObjectGenerator def setup() { randomGenerator = new RandomGenerator() - testObjectGenerator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) } def "createFilterFromRepresentation properly creates a filter from a representation"() { @@ -82,16 +75,6 @@ class JPAFilterServiceImplTests extends AbstractBaseDataJpaTest { @TestConfiguration private static class JPAFSIConfig { - @Bean - JPAFilterServiceImpl jpaFilterServiceImpl(EntityDescriptorService entityDescriptorService, EntityService entityService, FilterTargetService filterTargetService) { - return new JPAFilterServiceImpl().with { - it.entityDescriptorService = entityDescriptorService - it.entityService = entityService - it.filterTargetService = filterTargetService - it - } - } - @Bean JPAFilterTargetServiceImpl jpaFilterTargetService() { return new JPAFilterTargetServiceImpl() 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 32731c64a..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 @@ -18,7 +18,6 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSaml 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.util.TestObjectGenerator -import edu.internet2.tier.shibboleth.admin.util.AttributeUtility import groovy.xml.DOMBuilder import groovy.xml.MarkupBuilder import net.shibboleth.ext.spring.resource.ResourceHelper @@ -42,8 +41,6 @@ import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.generatedX @ContextConfiguration(classes=[ JPAMRSIConfig, PlaceholderResolverComponentsConfiguration ]) class JPAMetadataResolverServiceImplTests extends AbstractBaseDataJpaTest { - @Autowired - AttributeUtility attributeUtility @Autowired EntityService entityService @@ -66,13 +63,14 @@ class JPAMetadataResolverServiceImplTests extends AbstractBaseDataJpaTest { @Autowired ShibUIConfiguration shibUIConfiguration + @Autowired TestObjectGenerator testObjectGenerator + DOMBuilder domBuilder = DOMBuilder.newInstance() StringWriter writer = new StringWriter() MarkupBuilder markupBuilder def setup() { - testObjectGenerator = new TestObjectGenerator(attributeUtility) markupBuilder = new MarkupBuilder(writer) markupBuilder.omitNullAttributes = true markupBuilder.omitEmptyAttributes = true 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 ba7870566..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,27 @@ package edu.internet2.tier.shibboleth.admin.ui.util -import edu.internet2.tier.shibboleth.admin.ui.domain.* -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.* + +import edu.internet2.tier.shibboleth.admin.ui.domain.ContactPerson +import edu.internet2.tier.shibboleth.admin.ui.domain.Description +import edu.internet2.tier.shibboleth.admin.ui.domain.DisplayName +import edu.internet2.tier.shibboleth.admin.ui.domain.EmailAddress +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.Extensions +import edu.internet2.tier.shibboleth.admin.ui.domain.GivenName +import edu.internet2.tier.shibboleth.admin.ui.domain.InformationURL +import edu.internet2.tier.shibboleth.admin.ui.domain.KeyDescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.Logo +import edu.internet2.tier.shibboleth.admin.ui.domain.NameIDFormat +import edu.internet2.tier.shibboleth.admin.ui.domain.PrivacyStatementURL +import edu.internet2.tier.shibboleth.admin.ui.domain.SPSSODescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.SingleLogoutService +import edu.internet2.tier.shibboleth.admin.ui.domain.UIInfo +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.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.util.EntityDescriptorConversionUtils import org.opensaml.saml.common.xml.SAMLConstants 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 e91271c81..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 @@ -63,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 { @@ -109,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 { @@ -151,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 } @@ -180,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 } @@ -320,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 } @@ -405,7 +397,6 @@ class TestObjectGenerator { EntityAttributesFilterTarget entityAttributesFilterTarget = new EntityAttributesFilterTarget() entityAttributesFilterTarget.setEntityAttributesFilterTargetType(EntityAttributesFilterTargetType.ENTITY) entityAttributesFilterTarget.setSingleValue(buildEntityAttributesFilterTargetValueByType(EntityAttributesFilterTargetType.ENTITY)) - return entityAttributesFilterTarget } @@ -481,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)) @@ -507,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 } @@ -582,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 { @@ -630,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 diff --git a/backend/src/test/resources/conf/520.xml b/backend/src/test/resources/conf/520.xml index 50efab7e9..57f86a334 100644 --- a/backend/src/test/resources/conf/520.xml +++ b/backend/src/test/resources/conf/520.xml @@ -9,3 +9,4 @@ metadataFile="metadata/metadata.xml" xsi:type="FilesystemMetadataProvider"/> + \ 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 c379690c5..2d89d3151 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,69 +1,61 @@ package net.unicon.shibui.pac4j; -import com.fasterxml.jackson.databind.ObjectMapper; -import edu.internet2.tier.shibboleth.admin.ui.controller.ErrorResponse; +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; 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.service.EmailService; - +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; import org.pac4j.core.context.JEEContext; import org.pac4j.core.context.session.JEESessionStore; import org.pac4j.core.matching.matcher.Matcher; import org.pac4j.core.profile.CommonProfile; -import org.pac4j.saml.profile.SAML2Profile; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.bcrypt.BCrypt; import javax.mail.MessagingException; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; +import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.transaction.Transactional; - import java.io.IOException; -import java.util.List; -import java.util.Optional; - -import lombok.extern.slf4j.Slf4j; +import java.util.*; @Slf4j public class AddNewUserFilter implements Filter { private static final String ROLE_NONE = "ROLE_NONE"; private Optional emailService; + private IGroupService groupService; + private Matcher matcher; private Pac4jConfigurationProperties pac4jConfigurationProperties; private RoleRepository roleRepository; private Pac4jConfigurationProperties.SimpleProfileMapping simpleProfileMapping; - private UserRepository userRepository; - private Matcher matcher; + private UserService userService; - public AddNewUserFilter(Pac4jConfigurationProperties pac4jConfigurationProperties, UserRepository userRepository, RoleRepository roleRepository, Matcher matcher, Optional emailService) { - this.userRepository = userRepository; + public AddNewUserFilter(Pac4jConfigurationProperties pac4jConfigurationProperties, UserService userService, RoleRepository roleRepository, Matcher matcher, IGroupService groupService, Optional emailService) { + this.userService = userService; this.roleRepository = roleRepository; this.emailService = emailService; this.pac4jConfigurationProperties = pac4jConfigurationProperties; this.matcher = matcher; + this.groupService = groupService; simpleProfileMapping = this.pac4jConfigurationProperties.getSimpleProfileMapping(); } @Transactional - private User buildAndPersistNewUserFromProfile(CommonProfile profile) { + User buildAndPersistNewUserFromProfile(CommonProfile profile) { Optional noRole = roleRepository.findByName(ROLE_NONE); Role newUserRole; if (noRole.isEmpty()) { newUserRole = new Role(ROLE_NONE); - newUserRole = roleRepository.save(newUserRole); + roleRepository.save(newUserRole); } newUserRole = noRole.get(); @@ -74,7 +66,23 @@ private User buildAndPersistNewUserFromProfile(CommonProfile profile) { user.setFirstName(profile.getFirstName()); user.setLastName(profile.getFamilyName()); user.setEmailAddress(profile.getEmail()); - User persistedUser = userRepository.save(user); + + // get profile attribute for groups + Object obj = profile.getAttribute(simpleProfileMapping.getGroups()); + if (obj != null) { + final ArrayList groupNames = new ArrayList<>(); + if (obj instanceof String) { + groupNames.add(obj.toString()); + } + if (obj instanceof List) { + ((List)obj).forEach(val -> groupNames.add(val.toString())); + } + if (!groupNames.isEmpty()) { + user.setUserGroups(findOrCreateGroups(groupNames)); + } + } + + User persistedUser = userService.save(user); if (log.isDebugEnabled()) { log.debug("Persisted new user:\n" + user); } @@ -97,7 +105,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha if (profile != null) { String username = profile.getUsername(); if (username != null) { - Optional persistedUser = userRepository.findByUsername(username); + Optional persistedUser = userService.findByUsername(username); User user; if (persistedUser.isEmpty()) { user = buildAndPersistNewUserFromProfile(profile); @@ -122,7 +130,26 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } } + private Set findOrCreateGroups(ArrayList groupNames) { + final HashSet result = new HashSet<>(); + groupNames.forEach(name -> { + Group g = groupService.find(name); + if (g == null) { + g = new Group(); + g.setResourceId(name); + g.setName(name); + try { + groupService.createGroup(g); + } + catch (GroupExistsConflictException | InvalidGroupRegexException shouldntHappen) { + } + } + result.add(g); + }); + return result; + } + @Override public void init(FilterConfig filterConfig) throws ServletException { } -} +} \ No newline at end of file diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/BetterSAML2Profile.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/BetterSAML2Profile.java index 17a5edd64..38ec220f3 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/BetterSAML2Profile.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/BetterSAML2Profile.java @@ -1,10 +1,10 @@ package net.unicon.shibui.pac4j; -import org.pac4j.saml.profile.SAML2Profile; - import net.unicon.shibui.pac4j.Pac4jConfigurationProperties.SimpleProfileMapping; +import org.pac4j.saml.profile.SAML2Profile; import java.util.Collection; +import java.util.List; public class BetterSAML2Profile extends SAML2Profile { private SimpleProfileMapping profileMapping; @@ -28,6 +28,10 @@ public String getFirstName() { return (String) getAttribute(profileMapping.getFirstName()); } + public List getGroups() { + return (List) getAttribute(profileMapping.getGroups()); + } + @Override public String getUsername() { Object username = getAttribute(profileMapping.getUsername()); @@ -38,4 +42,4 @@ public String getUsername() { } } -} +} \ No newline at end of file diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java index 5e173f151..39f12035b 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java @@ -1,5 +1,9 @@ package net.unicon.shibui.pac4j; +import com.google.common.collect.Lists; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.pac4j.core.client.Clients; import org.pac4j.core.config.Config; @@ -17,7 +21,6 @@ import org.pac4j.saml.config.SAML2Configuration; import org.pac4j.saml.credentials.authenticator.SAML2Authenticator; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.ErrorPageRegistrar; @@ -26,12 +29,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; -import com.google.common.collect.Lists; - -import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; -import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; -import lombok.extern.slf4j.Slf4j; - /** * Configuration setup here following readme from - https://github.com/pac4j/spring-security-pac4j/tree/5.0.x * NOTE: matchers are now done as part of the config and have been moved over from the WebSecurity.java class of this package @@ -85,7 +82,6 @@ public Config config(final Pac4jConfigurationProperties pac4jConfigProps, //saml2Config.setSpLogoutRequestBindingType(pac4jConfigProps.getSpLogoutRequestBindingType()); final SAML2Client saml2Client = new SAML2Client(saml2Config); - saml2Client.setName("Saml2Client"); saml2Client.addAuthorizationGenerator(saml2ModelAuthorizationGenerator); SAML2Authenticator saml2Authenticator = new SAML2Authenticator(saml2Config.getAttributeAsId(), saml2Config.getMappedAttributes()); saml2Authenticator.setProfileDefinition(new CommonProfileDefinition(p -> new BetterSAML2Profile(pac4jConfigProps.getSimpleProfileMapping()))); @@ -110,7 +106,7 @@ public void validate(Credentials credentials, WebContext context, SessionStore s throw new CredentialsException("Invalid Credentials object generated by HeaderClient"); } final CommonProfile profile = new CommonProfile(); - String token = ((TokenCredentials)credentials).getToken(); + String token = ((TokenCredentials)credentials).getToken(); profile.setId(token); profile.addAttribute("username", token); profile.setRoles(userService.getUserRoles(token)); @@ -134,4 +130,4 @@ private void registerErrorPages(ErrorPageRegistry registry) { registry.addErrorPages(new ErrorPage(HttpStatus.UNAUTHORIZED, "/unsecured/error.html")); registry.addErrorPages(new ErrorPage(HttpStatus.FORBIDDEN, "/unsecured/error.html")); } -} +} \ No newline at end of file diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java index 977164958..41f65d43b 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java @@ -1,16 +1,12 @@ package net.unicon.shibui.pac4j; -import javax.servlet.Filter; - -import org.pac4j.springframework.security.web.CallbackFilter; +import lombok.Getter; +import lombok.Setter; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.stereotype.Component; -import lombok.Getter; -import lombok.Setter; - @Component @ConfigurationProperties(prefix = "shibui.pac4j") @EnableConfigurationProperties @@ -40,8 +36,8 @@ public class Pac4jConfigurationProperties { public static class SimpleProfileMapping { private String email; private String firstName; + private String groups; private String lastName; private String username; - } -} - + } +} \ No newline at end of file diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java index b16af86ba..ac7633afa 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java @@ -2,16 +2,13 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.auto.EmailConfiguration; 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.service.EmailService; - -import org.apache.commons.lang3.StringUtils; import org.pac4j.core.config.Config; import org.pac4j.core.matching.matcher.Matcher; -import org.pac4j.core.matching.matcher.PathMatcher; import org.pac4j.springframework.security.web.CallbackFilter; import org.pac4j.springframework.security.web.SecurityFilter; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -24,11 +21,9 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.firewall.StrictHttpFirewall; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; - -import java.util.Optional; import javax.servlet.Filter; +import java.util.Optional; @Configuration @AutoConfigureOrder(-1) @@ -36,39 +31,43 @@ @AutoConfigureAfter(EmailConfiguration.class) public class WebSecurity { @Bean("webSecurityConfig") - public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter(final Config config, UserRepository userRepository, + public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter(final Config config, UserService userService, RoleRepository roleRepository, Optional emailService, - Pac4jConfigurationProperties pac4jConfigurationProperties) { - return new Pac4jWebSecurityConfigurerAdapter(config, userRepository, roleRepository, emailService, + Pac4jConfigurationProperties pac4jConfigurationProperties, IGroupService groupService) { + return new Pac4jWebSecurityConfigurerAdapter(config, userService, roleRepository, emailService, groupService, pac4jConfigurationProperties); } @Order(100) public static class Pac4jWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { private final Config config; - private UserRepository userRepository; - private RoleRepository roleRepository; private Optional emailService; + private IGroupService groupService; private Pac4jConfigurationProperties pac4jConfigurationProperties; + private RoleRepository roleRepository; + private UserService userService; - public Pac4jWebSecurityConfigurerAdapter(final Config config, UserRepository userRepository, RoleRepository roleRepository, - Optional emailService, Pac4jConfigurationProperties pac4jConfigurationProperties) { + public Pac4jWebSecurityConfigurerAdapter(final Config config, UserService userService, RoleRepository roleRepository, + Optional emailService, IGroupService groupService, Pac4jConfigurationProperties pac4jConfigurationProperties) { this.config = config; - this.userRepository = userRepository; + this.userService = userService; this.roleRepository = roleRepository; this.emailService = emailService; + this.groupService = groupService; this.pac4jConfigurationProperties = pac4jConfigurationProperties; } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/unsecured/**/*").permitAll(); - + + final SecurityFilter securityFilter = new SecurityFilter(this.config, "Saml2Client"); + // add filter based on auth type http.antMatcher("/**").addFilterBefore(getFilter(config, pac4jConfigurationProperties.getTypeOfAuth()), BasicAuthenticationFilter.class); - + http.antMatcher("/**").addFilterBefore(securityFilter, BasicAuthenticationFilter.class); // add the new user filter - http.addFilterAfter(new AddNewUserFilter(pac4jConfigurationProperties, userRepository, roleRepository, getPathMatcher("exclude-paths-matcher") , emailService), SecurityFilter.class); + http.addFilterAfter(new AddNewUserFilter(pac4jConfigurationProperties, userService, roleRepository, getPathMatcher("exclude-paths-matcher"), groupService, emailService), SecurityFilter.class); http.exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> response.sendRedirect("/unsecured/error.html")); http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS); @@ -110,4 +109,4 @@ public void configure(org.springframework.security.config.annotation.web.builder public AuditorAware pac4jAuditorAware() { return new Pac4jAuditorAware(); } -} +} \ No newline at end of file diff --git a/pac4j-module/src/main/resources/application.yml b/pac4j-module/src/main/resources/application.yml index ebba4fcd9..4dd28dc1f 100644 --- a/pac4j-module/src/main/resources/application.yml +++ b/pac4j-module/src/main/resources/application.yml @@ -1,7 +1,9 @@ shibui: + pac4j-enabled: true pac4j: simpleProfileMapping: username: urn:oid:0.9.2342.19200300.100.1.3 firstName: givenName lastName: sn - email: mail \ No newline at end of file + email: mail + groups: urn:oid:1.3.6.1.4.1.5923.1.5.1.1 # attributeId - isMemberOf \ No newline at end of file diff --git a/pac4j-module/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy b/pac4j-module/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy new file mode 100644 index 000000000..7773c2d20 --- /dev/null +++ b/pac4j-module/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy @@ -0,0 +1,17 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service + +import org.springframework.transaction.annotation.Transactional + +class GroupServiceForTesting extends GroupServiceImpl { + public GroupServiceForTesting(GroupServiceImpl impl) { + this.groupRepository = impl.groupRepository + this.ownershipRepository = impl.ownershipRepository + } + + @Transactional + public void clearAllForTesting() { + groupRepository.deleteAll(); + ownershipRepository.clearAllOwnedByGroup() + ensureAdminGroupExists() + } +} \ No newline at end of file diff --git a/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterMockTests.groovy b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterMockTests.groovy new file mode 100644 index 000000000..1f729beb7 --- /dev/null +++ b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterMockTests.groovy @@ -0,0 +1,101 @@ +package net.unicon.shibui.pac4j + +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import edu.internet2.tier.shibboleth.admin.ui.service.EmailService + +import org.pac4j.core.matching.matcher.PathMatcher +import org.pac4j.saml.profile.SAML2Profile +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder +import spock.lang.Specification +import spock.lang.Subject + +import javax.servlet.FilterChain +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@SpringBootTest(classes = [Pac4jConfigurationProperties]) +@EnableConfigurationProperties([Pac4jConfigurationProperties]) +class AddNewUserFilterMockTests extends Specification { + + UserService userService = Mock() + RoleRepository roleRepository = Mock() + EmailService emailService = Mock() + IGroupService groupService = Mock() + + HttpServletRequest request = Mock() + HttpServletResponse response = Mock() + FilterChain chain = Mock() + + SecurityContext securityContext = Mock() + Authentication authentication = Mock() + SAML2Profile saml2Profile = Mock() + + @Autowired + Pac4jConfigurationProperties pac4jConfigurationProperties + + Pac4jConfigurationProperties.SimpleProfileMapping profileMapping + + @Subject + AddNewUserFilter addNewUserFilter + + def setup() { + SecurityContextHolder.setContext(securityContext) + securityContext.getAuthentication() >> authentication + authentication.getPrincipal() >> saml2Profile + + addNewUserFilter = new AddNewUserFilter(pac4jConfigurationProperties, userService, roleRepository, new PathMatcher(), groupService, Optional.of(emailService)) + profileMapping = pac4jConfigurationProperties.simpleProfileMapping + } + + def "new users are redirected"() { + given: + ['Username': 'newUser', + 'FirstName': 'New', + 'LastName': 'User', + 'Email': 'newuser@institution.edu'].each { key, value -> + saml2Profile.getAttribute(profileMapping."get${key}"()) >> [value] + } + saml2Profile.getUsername() >> "newUser" + userService.findByUsername('newUser') >> Optional.empty() + roleRepository.findByName('ROLE_NONE') >> Optional.of(new Role('ROLE_NONE')) + + when: + addNewUserFilter.doFilter(request, response, chain) + + then: + 0 * roleRepository.save(_) + 1 * userService.save(_ as User) >> { User user -> user } + 1 * emailService.sendNewUserMail('newUser') + 1 * response.sendRedirect("/unsecured/error.html") + } + + def "existing users are not redirected"() { + given: + saml2Profile.getUsername() >> "existingUser" + userService.findByUsername('existingUser') >> Optional.of(new User().with { + it.username = 'existingUser' + it.roles = [new Role('ROLE_USER')] + it + }) + + when: + addNewUserFilter.doFilter(request, response, chain) + + then: + 0 * roleRepository.save(_) + 0 * userService.save(_) + 1 * chain.doFilter(_, _) + } +} \ No newline at end of file diff --git a/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy index d15b70f0a..9b8f6131d 100644 --- a/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy +++ b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy @@ -2,37 +2,58 @@ package net.unicon.shibui.pac4j 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.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.service.EmailService - +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceForTesting +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import org.pac4j.core.matching.matcher.PathMatcher -import org.pac4j.core.profile.CommonProfile import org.pac4j.saml.profile.SAML2Profile import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.context.properties.EnableConfigurationProperties -import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +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.test.annotation.DirtiesContext +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.annotation.Transactional import spock.lang.Specification import spock.lang.Subject import javax.servlet.FilterChain -import javax.servlet.ServletRequest import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse -/** - * @author Bill Smith (wsmith@unicon.net) - */ -@SpringBootTest(classes = [Pac4jConfigurationProperties]) +@DataJpaTest +@ContextConfiguration(classes=[Pac4JTestingConfig]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@DirtiesContext @EnableConfigurationProperties([Pac4jConfigurationProperties]) class AddNewUserFilterTests extends Specification { + @Subject + AddNewUserFilter addNewUserFilter + + @Autowired + GroupServiceForTesting groupService - UserRepository userRepository = Mock() - RoleRepository roleRepository = Mock() - EmailService emailService = Mock() + @Autowired + OwnershipRepository ownershipRepository + + @Autowired + RoleRepository roleRepository + + @Autowired + Pac4jConfigurationProperties pac4jConfigurationProperties + + @Autowired + UserRepository userRepository + + @Autowired + UserService userService HttpServletRequest request = Mock() HttpServletResponse response = Mock() @@ -42,24 +63,42 @@ class AddNewUserFilterTests extends Specification { Authentication authentication = Mock() SAML2Profile saml2Profile = Mock() - @Autowired - Pac4jConfigurationProperties pac4jConfigurationProperties - Pac4jConfigurationProperties.SimpleProfileMapping profileMapping - @Subject - AddNewUserFilter addNewUserFilter - + @Transactional def setup() { SecurityContextHolder.setContext(securityContext) securityContext.getAuthentication() >> authentication authentication.getPrincipal() >> saml2Profile - addNewUserFilter = new AddNewUserFilter(pac4jConfigurationProperties, userRepository, roleRepository, new PathMatcher(), Optional.of(emailService)) + addNewUserFilter = new AddNewUserFilter(pac4jConfigurationProperties, userService, roleRepository, new PathMatcher(), groupService, Optional.empty()) profileMapping = pac4jConfigurationProperties.simpleProfileMapping + + userRepository.findAll().forEach { + userService.delete(it.getUsername()) + } + userRepository.flush() + + roleRepository.deleteAll() + roleRepository.flush() + groupService.clearAllForTesting() //leaves us just the admingroup + + 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) + } } - def "new users are redirected"() { + def "new user created"() { given: ['Username': 'newUser', 'FirstName': 'New', @@ -68,34 +107,34 @@ class AddNewUserFilterTests extends Specification { saml2Profile.getAttribute(profileMapping."get${key}"()) >> [value] } saml2Profile.getUsername() >> "newUser" - userRepository.findByUsername('newUser') >> Optional.empty() - roleRepository.findByName('ROLE_NONE') >> Optional.of(new Role('ROLE_NONE')) when: addNewUserFilter.doFilter(request, response, chain) then: - 0 * roleRepository.save(_) - 1 * userRepository.save(_ as User) >> { User user -> user } - 1 * emailService.sendNewUserMail('newUser') 1 * response.sendRedirect("/unsecured/error.html") + User user = userRepository.findByUsername("newUser").get() + user.getGroupId() == "newUser" } - def "existing users are not redirected"() { + def "new user created with group - assumes saml2profile got property for groups"() { given: - saml2Profile.getUsername() >> "existingUser" - userRepository.findByUsername('existingUser') >> Optional.of(new User().with { - it.username = 'existingUser' - it.roles = [new Role('ROLE_USER')] - it - }) + ['Username': 'newUser', + 'FirstName': 'New', + 'LastName': 'User', + 'Email': 'newuser@institution.edu', + 'Groups':'AAAGroup' + ].each { key, value -> + saml2Profile.getAttribute(profileMapping."get${key}"()) >> [value] + } + saml2Profile.getUsername() >> "newUser" when: addNewUserFilter.doFilter(request, response, chain) then: - 0 * roleRepository.save(_) - 0 * userRepository.save(_) - 1 * chain.doFilter(_, _) + 1 * response.sendRedirect("/unsecured/error.html") + User user = userRepository.findByUsername("newUser").get() + user.getGroupId() == "AAAGroup" } -} +} \ No newline at end of file diff --git a/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/Pac4JTestingConfig.groovy b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/Pac4JTestingConfig.groovy new file mode 100644 index 000000000..552aaca04 --- /dev/null +++ b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/Pac4JTestingConfig.groovy @@ -0,0 +1,52 @@ +package net.unicon.shibui.pac4j + +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.GroupServiceImpl +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 +import org.springframework.context.annotation.Primary + +@Configuration +class Pac4JTestingConfig { + @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 + } + + @Bean + @Primary + GroupUpdatedEntityListener groupUpdatedEntityListener(OwnershipRepository repo) { + GroupUpdatedEntityListener listener = new GroupUpdatedEntityListener() + listener.init(repo) + return listener + } + + @Bean + @Primary + UserUpdatedEntityListener userUpdatedEntityListener(OwnershipRepository repo, GroupsRepository groupRepo) { + UserUpdatedEntityListener listener = new UserUpdatedEntityListener() + listener.init(repo, groupRepo) + return listener + } + + @Bean + @Primary + UserService userService(IGroupService groupService, OwnershipRepository ownershipRepository, RoleRepository roleRepository, UserRepository userRepository) { + return new UserService(groupService, ownershipRepository, roleRepository, userRepository) + } +} \ No newline at end of file diff --git a/ui/public/assets/schema/roles/role.json b/ui/public/assets/schema/roles/role.json new file mode 100644 index 000000000..8145fae88 --- /dev/null +++ b/ui/public/assets/schema/roles/role.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "title": "label.role-name", + "description": "tooltip.role-name", + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } +} \ No newline at end of file diff --git a/ui/src/app/App.js b/ui/src/app/App.js index 2fb4797b6..e52a45f4a 100644 --- a/ui/src/app/App.js +++ b/ui/src/app/App.js @@ -26,6 +26,7 @@ import { NewProvider } from './metadata/new/NewProvider'; import { Filter } from './metadata/Filter'; import { Contention } from './metadata/contention/ContentionContext'; import { SessionModal } from './core/user/SessionModal'; +import { Roles } from './admin/Roles'; import Button from 'react-bootstrap/Button'; import { Groups } from './admin/Groups'; @@ -66,7 +67,7 @@ function App() { {(message, confirm, confirmCallback, setConfirm, getConfirmation) => - +
@@ -80,6 +81,7 @@ function App() { + diff --git a/ui/src/app/admin/Roles.js b/ui/src/app/admin/Roles.js new file mode 100644 index 000000000..08daed90d --- /dev/null +++ b/ui/src/app/admin/Roles.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { Switch, Route, useRouteMatch, Redirect } from 'react-router-dom'; +import { RolesProvider } from './hoc/RolesProvider'; +import { NewRole } from './container/NewRole'; +import { EditRole } from './container/EditRole'; +import { RoleList } from './container/RoleList'; + +export function Roles() { + + let { path } = useRouteMatch(); + + return ( + <> + + + + {(roles, onDelete) => + + } + + } /> + + + } /> + + + } /> + + + + ); +} \ No newline at end of file diff --git a/ui/src/app/admin/component/RoleForm.js b/ui/src/app/admin/component/RoleForm.js new file mode 100644 index 000000000..074cb5b09 --- /dev/null +++ b/ui/src/app/admin/component/RoleForm.js @@ -0,0 +1,68 @@ +import React from 'react'; +import Button from 'react-bootstrap/Button'; +import Form from '@rjsf/bootstrap-4'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSpinner, faSave } from '@fortawesome/free-solid-svg-icons'; +import Translate from '../../i18n/components/translate'; + +import { useRoleUiSchema } from '../hooks'; +import { fields, widgets } from '../../form/component'; +import { templates } from '../../form/component'; +import { FormContext, setFormDataAction, setFormErrorAction } from '../../form/FormManager'; + +function ErrorListTemplate() { + return (<>); +} + +export function RoleForm({ role = {}, errors = [], loading = false, schema, onSave, onCancel }) { + + const { dispatch } = React.useContext(FormContext); + const onChange = ({ formData, errors }) => { + dispatch(setFormDataAction(formData)); + dispatch(setFormErrorAction(errors)); + }; + + const uiSchema = useRoleUiSchema(); + + return (<> +
+
+ + + + +
+
+
+
+
onChange(form)} + schema={schema} + uiSchema={uiSchema} + FieldTemplate={templates.FieldTemplate} + ObjectFieldTemplate={templates.ObjectFieldTemplate} + ArrayFieldTemplate={templates.ArrayFieldTemplate} + fields={fields} + widgets={widgets} + liveValidate={true} + ErrorList={ErrorListTemplate}> + <> +
+
+
+
+ ) +} +/**/ \ No newline at end of file diff --git a/ui/src/app/admin/component/UserMaintenance.js b/ui/src/app/admin/component/UserMaintenance.js index c4542438f..e9a7b8c33 100644 --- a/ui/src/app/admin/component/UserMaintenance.js +++ b/ui/src/app/admin/component/UserMaintenance.js @@ -23,10 +23,10 @@ export default function UserMaintenance({ users, roles, loading, onDeleteUser, o UserId - Name + Name Email - Role - Group + Role + Group Delete? @@ -36,10 +36,10 @@ export default function UserMaintenance({ users, roles, loading, onDeleteUser, o {users.map((user, idx) => - {user.username} - {user.firstName} {user.lastName} - {user.emailAddress} - + {user.username} + {user.firstName} {user.lastName} + {user.emailAddress} + changeSourceGroup(model, event.target.value)} - value={model.idOfOwner} - disabled={loadingGroups} - disablevalidation="true"> - - {groups.map((g, ridx) => ( - - ))} - - - - } - + + + + } + } {children} - +

Enabled @@ -90,8 +90,8 @@ export function MetadataHeader ({ showGroup, model, current = true, enabled = tr Current

- + ); -} \ No newline at end of file +} diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationList.js b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationList.js index 521055c12..74b879698 100644 --- a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationList.js +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationList.js @@ -2,43 +2,49 @@ import React from 'react'; import { Ordered } from '../../../../dashboard/component/Ordered'; import { Translate } from '../../../../i18n/components/translate'; -import { MetadataFiltersContext } from './MetadataFilters'; +import { MetadataFilters } from './MetadataFilters'; import { MetadataFilterConfigurationListItem } from './MetadataFilterConfigurationListItem'; +import { MetadataFilterTypes } from '..'; -export function MetadataFilterConfigurationList ({provider, onDelete, editable = true}) { - const filters = React.useContext(MetadataFiltersContext); +export function MetadataFilterConfigurationList ({provider, editable = true}) { return ( - - {(ordered, first, last, onOrderUp, onOrderDown) => - <> - {ordered.length > 0 && -
    - {ordered.map((filter, i) => -
  • - onDelete(filter.resourceId)} - /> -
  • - )} -
- } - { filters && filters.length < 1 && -
-

No Filters

-

No filters have been added to this Metadata Provider

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

No Filters

+

No filters have been added to this Metadata Provider

+
+ } + + } +
} -
+ ); } \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js index 4601db323..3c336a27e 100644 --- a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js @@ -3,6 +3,7 @@ import React from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faArrowCircleDown, faArrowCircleUp, faChevronUp, faEdit, faTrash } from '@fortawesome/free-solid-svg-icons'; import Button from 'react-bootstrap/Button'; +import Form from 'react-bootstrap/Form'; import { Translate } from '../../../../i18n/components/translate'; import { Link } from 'react-router-dom'; @@ -11,7 +12,7 @@ import { MetadataConfiguration } from '../../../component/MetadataConfiguration' import { useMetadataConfiguration } from '../../../hooks/configuration'; import useFetch from 'use-http'; -export function MetadataFilterConfigurationListItem ({ filter, isLast, isFirst, onOrderUp, onOrderDown, editable, onRemove, index }) { +export function MetadataFilterConfigurationListItem ({ filter, isLast, isFirst, onOrderUp, onOrderDown, onEnable, editable, onRemove, loading, index }) { const [open, setOpen] = React.useState(false); const definition = React.useMemo(() => getDefinition(filter['@type'], ), [filter]); @@ -49,10 +50,15 @@ export function MetadataFilterConfigurationListItem ({ filter, isLast, isFirst, } { filter['@type'] } - - - - + + } + checked={filter.filterEnabled} + disabled={loading} + onChange={({ target: { checked } }) => onEnable(filter, checked)} /> + {filter.disabled && } + {open && diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilterEditorList.js b/ui/src/app/metadata/domain/filter/component/MetadataFilterEditorList.js index f5b23aa23..befeef819 100644 --- a/ui/src/app/metadata/domain/filter/component/MetadataFilterEditorList.js +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilterEditorList.js @@ -9,7 +9,7 @@ import { faArrowCircleDown, faArrowCircleUp, faEdit, faTrash } from '@fortawesom import { Ordered } from '../../../../dashboard/component/Ordered'; import { Translate } from '../../../../i18n/components/translate'; -export function MetadataFilterEditorList ({provider, filters, onDelete, onUpdate, loading}) { +export function MetadataFilterEditorList ({provider, filters, onDelete, onUpdate, onEnable, loading}) { return ( @@ -51,7 +51,7 @@ export function MetadataFilterEditorList ({provider, filters, onDelete, onUpdate label={Toggle this switch element} checked={filter.filterEnabled} disabled={loading} - onChange={() => onUpdate({ ...filter, filterEnabled: !filter.filterEnabled })} /> + onChange={({target: { checked }}) => onEnable(filter, checked)} /> {filter.disabled && } diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilters.js b/ui/src/app/metadata/domain/filter/component/MetadataFilters.js index 4e5ff8a98..d8a1d8242 100644 --- a/ui/src/app/metadata/domain/filter/component/MetadataFilters.js +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilters.js @@ -1,15 +1,22 @@ import React from 'react'; -import { useMetadataFilters } from '../../../hooks/api'; +import { useMetadataFilters, useFilterActivator } from '../../../hooks/api'; import { DeleteConfirmation } from '../../../../core/components/DeleteConfirmation'; +import { NotificationContext, createNotificationAction } from '../../../../notifications/hoc/Notifications'; export const MetadataFiltersContext = React.createContext(); export function MetadataFilters ({ providerId, types = [], filters, children }) { + const { dispatch } = React.useContext(NotificationContext); + const { put, del, get, response, loading } = useMetadataFilters(providerId, { cachePolicy: 'no-cache' }); + const { patch } = useFilterActivator(providerId, { + cachePolicy: 'no-cache' + }); + const [filterData, setFilterData] = React.useState([]); async function loadFilters(id) { @@ -23,12 +30,31 @@ export function MetadataFilters ({ providerId, types = [], filters, children }) await put(`/${filter.resourceId}`, filter); if (response.ok) { loadFilters(providerId); + dispatch(createNotificationAction( + `Metadata Filter has been updated.` + )); + } + } + + async function enableFilter(filter, enabled) { + await patch(`/${filter.resourceId}/${enabled ? 'enable' : 'disable'}`, { + ...filter, + filterEnabled: enabled + }); + if (response.ok) { + dispatch(createNotificationAction( + `Metadata Filter has been ${enabled ? 'enabled' : 'disabled'}.` + )); + loadFilters(providerId); } } async function deleteFilter(filterId) { await del(`/${filterId}`); if (response.ok) { + dispatch(createNotificationAction( + `Metadata Filter has been deleted.` + )); loadFilters(); } } @@ -50,9 +76,9 @@ export function MetadataFilters ({ providerId, types = [], filters, children }) {(block) => - {children(filterData, onUpdate, (id) => block(() => onDelete(id)), loading)} + {children(filterData, onUpdate, (id) => block(() => onDelete(id)), enableFilter, loading)} } ); -} \ No newline at end of file +} diff --git a/ui/src/app/metadata/domain/filter/definition/EntityAttributesFilterDefinition.js b/ui/src/app/metadata/domain/filter/definition/EntityAttributesFilterDefinition.js index 8ece90ecc..7ef0e08a9 100644 --- a/ui/src/app/metadata/domain/filter/definition/EntityAttributesFilterDefinition.js +++ b/ui/src/app/metadata/domain/filter/definition/EntityAttributesFilterDefinition.js @@ -75,7 +75,6 @@ export const EntityAttributesFilterEditor= { 'name', '@type', 'resourceId', - 'filterEnabled', 'entityAttributesFilterTarget' ] }, diff --git a/ui/src/app/metadata/domain/filter/definition/NameIdFilterDefinition.js b/ui/src/app/metadata/domain/filter/definition/NameIdFilterDefinition.js index f05ecd703..358b51b34 100644 --- a/ui/src/app/metadata/domain/filter/definition/NameIdFilterDefinition.js +++ b/ui/src/app/metadata/domain/filter/definition/NameIdFilterDefinition.js @@ -40,7 +40,6 @@ export const NameIDFilterEditor = { index: 1, fields: [ 'name', - 'filterEnabled', '@type', 'resourceId', 'nameIdFormatFilterTarget' diff --git a/ui/src/app/metadata/domain/provider/component/ProviderList.js b/ui/src/app/metadata/domain/provider/component/ProviderList.js index 86e0d83ea..e97b8c4ce 100644 --- a/ui/src/app/metadata/domain/provider/component/ProviderList.js +++ b/ui/src/app/metadata/domain/provider/component/ProviderList.js @@ -2,14 +2,21 @@ import React from 'react'; import { Link } from 'react-router-dom'; import Badge from 'react-bootstrap/Badge'; import Button from 'react-bootstrap/Button'; +import Form from 'react-bootstrap/Form'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faChevronCircleDown, faChevronCircleUp } from '@fortawesome/free-solid-svg-icons'; import FormattedDate from '../../../../core/components/FormattedDate'; import Translate from '../../../../i18n/components/translate'; import { Scroller } from '../../../../dashboard/component/Scroller'; +import { useIsAdmin } from '../../../../core/user/UserContext'; +import { useTranslator } from '../../../../i18n/hooks'; + +export function ProviderList({ entities, reorder = true, first, last, onEnable, onOrderUp, onOrderDown }) { + + const isAdmin = useIsAdmin(); + const translator = useTranslator(); -export function ProviderList({ entities, reorder = true, first, last, onOrderUp, onOrderDown }) { return ( {(limited) =>
@@ -61,10 +68,24 @@ export function ProviderList({ entities, reorder = true, first, last, onOrderUp, { provider['@type'] } { provider.createdBy } - - - - + + + {onEnable && isAdmin ? + onEnable(provider, checked)} + checked={provider.enabled} + > + + : + + + + } + )} diff --git a/ui/src/app/metadata/domain/provider/definition/DynamicHttpMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/definition/DynamicHttpMetadataProviderDefinition.js index 901a613c8..84b694e9a 100644 --- a/ui/src/app/metadata/domain/provider/definition/DynamicHttpMetadataProviderDefinition.js +++ b/ui/src/app/metadata/domain/provider/definition/DynamicHttpMetadataProviderDefinition.js @@ -70,9 +70,7 @@ export const DynamicHttpMetadataProviderWizard = { label: 'label.finished', index: 5, initialValues: [], - fields: [ - 'enabled' - ] + fields: [] } ], uiSchema: defaultsDeep({ @@ -200,7 +198,6 @@ export const DynamicHttpMetadataProviderEditor = { '@type', 'xmlId', 'metadataRequestURLConstructionScheme', - 'enabled', 'requireValidMetadata', 'failFastInitialization' ] diff --git a/ui/src/app/metadata/domain/provider/definition/FileBackedHttpMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/definition/FileBackedHttpMetadataProviderDefinition.js index 489b24cc8..46a667923 100644 --- a/ui/src/app/metadata/domain/provider/definition/FileBackedHttpMetadataProviderDefinition.js +++ b/ui/src/app/metadata/domain/provider/definition/FileBackedHttpMetadataProviderDefinition.js @@ -68,9 +68,7 @@ export const FileBackedHttpMetadataProviderWizard = { label: 'label.finished', index: 5, initialValues: [], - fields: [ - 'enabled' - ] + fields: [] } ], uiSchema: defaultsDeep({ @@ -84,12 +82,6 @@ export const FileBackedHttpMetadataProviderWizard = { '@type' ] }, - { - size: 8, - fields: [ - 'enabled' - ] - }, { size: 8, fields: [ @@ -197,7 +189,6 @@ export const FileBackedHttpMetadataProviderEditor = { fields: [ 'name', '@type', - 'enabled', 'xmlId', 'metadataURL', 'initializeFromBackupFile', diff --git a/ui/src/app/metadata/domain/provider/definition/FileSystemMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/definition/FileSystemMetadataProviderDefinition.js index 8738ffad2..57d3447af 100644 --- a/ui/src/app/metadata/domain/provider/definition/FileSystemMetadataProviderDefinition.js +++ b/ui/src/app/metadata/domain/provider/definition/FileSystemMetadataProviderDefinition.js @@ -35,9 +35,7 @@ export const FileSystemMetadataProviderWizard = { label: 'label.finished', index: 4, initialValues: [], - fields: [ - 'enabled' - ] + fields: [] } ], uiSchema: defaultsDeep({ @@ -51,12 +49,6 @@ export const FileSystemMetadataProviderWizard = { '@type' ] }, - { - size: 8, - fields: [ - 'enabled' - ] - }, { size: 8, fields: [ @@ -115,7 +107,6 @@ export const FileSystemMetadataProviderEditor = { 'xmlId', '@type', 'metadataFile', - 'enabled', 'doInitialization' ], override: { @@ -140,7 +131,6 @@ export const FileSystemMetadataProviderEditor = { type: 'group-lg', class: ['col-12'], fields: [ - 'enabled', 'xmlId', 'metadataFile', 'doInitialization' diff --git a/ui/src/app/metadata/domain/provider/definition/LocalDynamicMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/definition/LocalDynamicMetadataProviderDefinition.js index 339c0c606..eea5d3541 100644 --- a/ui/src/app/metadata/domain/provider/definition/LocalDynamicMetadataProviderDefinition.js +++ b/ui/src/app/metadata/domain/provider/definition/LocalDynamicMetadataProviderDefinition.js @@ -35,9 +35,7 @@ export const LocalDynamicMetadataProviderWizard = { label: 'label.finished', index: 4, initialValues: [], - fields: [ - 'enabled' - ] + fields: [] } ], uiSchema: defaultsDeep({ @@ -51,12 +49,6 @@ export const LocalDynamicMetadataProviderWizard = { '@type' ] }, - { - size: 8, - fields: [ - 'enabled' - ] - }, { size: 8, fields: [ @@ -125,7 +117,6 @@ export const LocalDynamicMetadataProviderEditor = { fields: [ 'name', '@type', - 'enabled', 'xmlId', 'sourceDirectory', ], @@ -150,7 +141,6 @@ export const LocalDynamicMetadataProviderEditor = { type: 'group-lg', class: ['col-12'], fields: [ - 'enabled', 'xmlId', 'sourceDirectory', ] diff --git a/ui/src/app/metadata/domain/source/component/SourceList.js b/ui/src/app/metadata/domain/source/component/SourceList.js index bead9da9b..792c16394 100644 --- a/ui/src/app/metadata/domain/source/component/SourceList.js +++ b/ui/src/app/metadata/domain/source/component/SourceList.js @@ -3,29 +3,29 @@ import { Link } from 'react-router-dom'; import Badge from 'react-bootstrap/Badge'; import Popover from 'react-bootstrap/Popover'; import Button from 'react-bootstrap/Button'; +import Form from 'react-bootstrap/Form'; import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTrash, faCheck } from '@fortawesome/free-solid-svg-icons'; +import { faTrash } from '@fortawesome/free-solid-svg-icons'; import FormattedDate from '../../../../core/components/FormattedDate'; import Translate from '../../../../i18n/components/translate'; import { Scroller } from '../../../../dashboard/component/Scroller'; -import { DeleteSourceConfirmation } from './DeleteSourceConfirmation'; -import { useIsAdmin } from '../../../../core/user/UserContext'; +import { useTranslator } from '../../../../i18n/hooks'; +import { useCanEnable, useIsAdmin } from '../../../../core/user/UserContext'; import { GroupsProvider } from '../../../../admin/hoc/GroupsProvider'; export default function SourceList({ entities, onDelete, onEnable, onChangeGroup }) { + const translator = useTranslator(); const isAdmin = useIsAdmin(); + const canEnable = useCanEnable(); return ( - - {(onDeleteSource) => - + {(limited) =>
- @@ -33,82 +33,84 @@ export default function SourceList({ entities, onDelete, onEnable, onChangeGroup - + {isAdmin && onChangeGroup && } - {onDeleteSource && } + {onDelete && isAdmin && } {limited.map((source, idx) => - - - - - + - {isAdmin && onChangeGroup && - + } - {onDeleteSource && - } @@ -117,8 +119,6 @@ export default function SourceList({ entities, onDelete, onEnable, onChangeGroup
Entity ID Author Created DateEnabledEnabledGroup
+ {source.serviceProviderName} + {source.entityId} + {source.createdBy} - {onEnable ? - + + + {onEnable && canEnable ? + onEnable(source, checked)} + checked={source.serviceEnabled} + > + : } + - - {(groups, removeGroup, loadingGroups) => - - - - - } - - + + {(groups, removeGroup, loadingGroups) => + + + + + } + + + {onDelete && isAdmin && + A metadata source must be disabled before it can be deleted. }> - - - + + +
} -
- } -
+ ); } diff --git a/ui/src/app/metadata/domain/source/definition/SourceDefinition.js b/ui/src/app/metadata/domain/source/definition/SourceDefinition.js index 40a331bae..00b2dbb64 100644 --- a/ui/src/app/metadata/domain/source/definition/SourceDefinition.js +++ b/ui/src/app/metadata/domain/source/definition/SourceDefinition.js @@ -106,7 +106,6 @@ export const SourceBase = { fields: [ 'serviceProviderName', 'entityId', - 'serviceEnabled', 'organization' ] }, @@ -308,7 +307,6 @@ export const SourceEditor = { fields: [ 'serviceProviderName', 'entityId', - 'serviceEnabled', 'organization', 'contacts' ] @@ -441,9 +439,7 @@ export const SourceWizard = { }, { size: 6, - fields: [ - 'serviceEnabled' - ] + fields: [] } ] } @@ -527,9 +523,7 @@ export const SourceWizard = { index: 10, id: 'summary', label: 'label.finished', - fields: [ - 'serviceEnabled' - ] + fields: [] } ] } \ No newline at end of file diff --git a/ui/src/app/metadata/editor/MetadataFilterList.js b/ui/src/app/metadata/editor/MetadataFilterList.js index 9b71f6ee5..699b1c400 100644 --- a/ui/src/app/metadata/editor/MetadataFilterList.js +++ b/ui/src/app/metadata/editor/MetadataFilterList.js @@ -90,12 +90,13 @@ export function MetadataFilterList() {
{definition && schema && current && - {(filters, onUpdate, onDelete, loading) => + {(filters, onUpdate, onDelete, onEnable, loading) => } diff --git a/ui/src/app/metadata/hoc/MetadataSelector.js b/ui/src/app/metadata/hoc/MetadataSelector.js index acdf115be..b81db74b2 100644 --- a/ui/src/app/metadata/hoc/MetadataSelector.js +++ b/ui/src/app/metadata/hoc/MetadataSelector.js @@ -24,7 +24,7 @@ export function MetadataSelector({ children, ...props }) { const { get, response } = useMetadataEntity(type); - const [metadata, setMetadata] = React.useState([]); + const [metadata, setMetadata] = React.useState(); async function loadMetadata(id) { const source = await get(`/${id}`); @@ -44,7 +44,7 @@ export function MetadataSelector({ children, ...props }) { {type && {metadata && metadata.version && - {children(metadata)} + {children(metadata, reload)} } } @@ -60,4 +60,4 @@ export function useMetadataLoader() { return React.useContext(MetadataLoaderContext); } -export default MetadataSelector; \ No newline at end of file +export default MetadataSelector; diff --git a/ui/src/app/metadata/hooks/api.js b/ui/src/app/metadata/hooks/api.js index cc2afa9c7..49e11f338 100644 --- a/ui/src/app/metadata/hooks/api.js +++ b/ui/src/app/metadata/hooks/api.js @@ -132,6 +132,18 @@ export function useMetadataUpdater (path, current) { } } +export function useMetadataActivator(type, opts = { + cachePolicy: 'no-cache' +}) { + return useFetch(`${API_BASE_PATH}/activate/${type === 'source' ? 'entityDescriptor' : 'MetadataResolvers'}/`, opts); +} + +export function useFilterActivator(providerId, opts = { + cachePolicy: 'no-cache' +}) { + return useFetch(`${API_BASE_PATH}/activate${getMetadataPath('provider')}/${providerId}/Filter`, opts); +} + export function useMetadataAttributes (opts = {}, onMount) { // return useFetch(`${API_BASE_PATH}/custom/entity/attributes`, opts, onMount); diff --git a/ui/src/app/metadata/hooks/schema.js b/ui/src/app/metadata/hooks/schema.js index ecde47f05..5083559b1 100644 --- a/ui/src/app/metadata/hooks/schema.js +++ b/ui/src/app/metadata/hooks/schema.js @@ -1,5 +1,4 @@ import React from 'react'; -import { useIsAdmin } from '../../core/user/UserContext'; export const fillInRootProperties = (keys, ui) => keys.reduce((sch, key, idx) => { if (!sch.hasOwnProperty(key)) { @@ -31,21 +30,7 @@ export function useUiSchema(definition, schema, current, locked = true) { } ), [mapped, step.locked, locked]); - const isAdmin = useIsAdmin(); - - const hideEnableFromNonAdmins = React.useMemo(() => { - if (!isAdmin) { - return { - ...isLocked, - serviceEnabled: { - 'ui:widget': 'hidden' - } - }; - } - return isLocked; - }, [isAdmin, isLocked]); - - return { uiSchema: hideEnableFromNonAdmins, step}; + return { uiSchema: isLocked, step}; } diff --git a/ui/src/app/metadata/view/MetadataOptions.js b/ui/src/app/metadata/view/MetadataOptions.js index 38bd4a951..9bc8d2f1c 100644 --- a/ui/src/app/metadata/view/MetadataOptions.js +++ b/ui/src/app/metadata/view/MetadataOptions.js @@ -1,5 +1,5 @@ import React from 'react'; -import { faArrowDown, faArrowUp, faHistory, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { faArrowDown, faArrowUp, faHistory, faPlus, faToggleOff, faToggleOn, faTrash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Link, useHistory, useParams } from 'react-router-dom'; import Button from 'react-bootstrap/Button'; @@ -14,15 +14,15 @@ import { MetadataDefinitionContext, MetadataSchemaContext } from '../hoc/Metadat import { useMetadataConfiguration } from '../hooks/configuration'; import { MetadataViewToggle } from '../component/MetadataViewToggle'; -import { DeleteSourceConfirmation } from '../domain/source/component/DeleteSourceConfirmation'; +import { MetadataActions } from '../../admin/container/MetadataActions'; import { MetadataFilters } from '../domain/filter/component/MetadataFilters'; import { MetadataFilterConfigurationList } from '../domain/filter/component/MetadataFilterConfigurationList'; import { MetadataFilterTypes } from '../domain/filter'; import { useMetadataSchema } from '../hooks/schema'; import { FilterableProviders } from '../domain/provider'; +import { useCanEnable, useIsAdmin } from '../../core/user/UserContext'; - -export function MetadataOptions () { +export function MetadataOptions ({reload}) { const metadata = React.useContext(MetadataObjectContext); const definition = React.useContext(MetadataDefinitionContext); @@ -50,9 +50,14 @@ export function MetadataOptions () { const canFilter = FilterableProviders.indexOf(definition.type) > -1; + const enabled = type === 'source' ? metadata.serviceEnabled : metadata.enabled; + + const canEnable = useCanEnable(); + const isAdmin = useIsAdmin(); + return ( - - {(onDeleteSource) => + + {(enable, remove) => <>
@@ -111,4 +85,4 @@ export function MetadataProviderWizard({onRestart}) { } ); -} \ No newline at end of file +} diff --git a/ui/src/app/metadata/wizard/MetadataSourceWizard.js b/ui/src/app/metadata/wizard/MetadataSourceWizard.js index d9ede4e9d..6973f1e9c 100644 --- a/ui/src/app/metadata/wizard/MetadataSourceWizard.js +++ b/ui/src/app/metadata/wizard/MetadataSourceWizard.js @@ -21,7 +21,7 @@ import { checkChanges } from '../hooks/utility'; import { useUserGroup } from '../../core/user/UserContext'; -export function MetadataSourceWizard ({ onShowNav }) { +export function MetadataSourceWizard ({ onShowNav, onSave, block }) { const { post, loading, response } = useMetadataEntity('source'); const history = useHistory(); @@ -52,39 +52,20 @@ export function MetadataSourceWizard ({ onShowNav }) { const onChange = (changes) => { formDispatch(setFormDataAction(changes.formData)); formDispatch(setFormErrorAction(changes.errors)); - setBlocking(checkChanges(metadata, changes.formData)); + block(checkChanges(metadata, changes.formData)); }; const onEditFromSummary = (idx) => { wizardDispatch(setWizardIndexAction(idx)); }; - const onBlur = (form) => { - // console.log(form); - } - - async function save () { - const body = removeNull(metadata, true); - await post('', body); - if (response.ok) { - setBlocking(false); - history.push('/'); - } - } - - const [blocking, setBlocking] = React.useState(false); + const save = () => onSave(definition.parser(metadata)); const validator = useMetadataDefinitionValidator(data, null, group); const warnings = definition.warnings && definition.warnings(metadata); return ( <> - - `message.unsaved-editor` - } - />
0 || loading } saving={loading} /> @@ -111,7 +92,6 @@ export function MetadataSourceWizard ({ onShowNav }) { schema={schema || {}} current={current} onChange={onChange} - onBlur={onBlur} validator={validator} />
@@ -126,4 +106,4 @@ export function MetadataSourceWizard ({ onShowNav }) { } ); -} \ No newline at end of file +} diff --git a/ui/src/app/metadata/wizard/MetadataWizardForm.js b/ui/src/app/metadata/wizard/MetadataWizardForm.js index c508bd267..58000c605 100644 --- a/ui/src/app/metadata/wizard/MetadataWizardForm.js +++ b/ui/src/app/metadata/wizard/MetadataWizardForm.js @@ -12,7 +12,7 @@ function ErrorListTemplate () { return (<>); } -export function MetadataWizardForm ({ metadata, definition, schema, current, onChange, onBlur = false, validator }) { +export function MetadataWizardForm ({ metadata, definition, schema, current, onChange, onBlur = () => {}, validator }) { const {uiSchema} = useUiSchema(definition, schema, current); diff --git a/ui/src/testing/sourceSchema.js b/ui/src/testing/sourceSchema.js index 93262c5f4..c723fd548 100644 --- a/ui/src/testing/sourceSchema.js +++ b/ui/src/testing/sourceSchema.js @@ -1,3 +1,3 @@ -const SCHEMA = { "type": "object", "required": ["serviceProviderName", "entityId"], "properties": { "serviceProviderName": { "title": "label.service-provider-name", "description": "tooltip.service-provider-name", "type": "string", "minLength": 1, "maxLength": 255 }, "entityId": { "title": "label.entity-id", "description": "tooltip.entity-id", "type": "string", "minLength": 1, "maxLength": 255 }, "serviceEnabled": { "title": "label.enable-this-service", "description": "tooltip.enable-this-service-upon-saving", "type": "boolean", "default": false }, "organization": { "$ref": "#/definitions/Organization" }, "contacts": { "title": "label.contact-information", "description": "tooltip.contact-information", "type": "array", "items": { "$ref": "#/definitions/Contact" } }, "mdui": { "$ref": "#/definitions/MDUI" }, "securityInfo": { "type": "object", "widget": { "id": "fieldset" }, "dependencies": { "authenticationRequestsSigned": { "oneOf": [{ "properties": { "authenticationRequestsSigned": { "enum": [true] }, "x509Certificates": { "minItems": 1 } } }, { "properties": { "authenticationRequestsSigned": { "enum": [false] }, "x509Certificates": { "minItems": 0 } } }] } }, "properties": { "x509CertificateAvailable": { "type": "boolean", "default": true }, "authenticationRequestsSigned": { "title": "label.authentication-requests-signed", "description": "tooltip.authentication-requests-signed", "type": "boolean", "enumNames": ["value.true", "value.false"] }, "wantAssertionsSigned": { "title": "label.want-assertions-signed", "description": "tooltip.want-assertions-signed", "type": "boolean", "enumNames": ["value.true", "value.false"] }, "x509Certificates": { "title": "label.x509-certificates", "type": "array", "items": { "$ref": "#/definitions/Certificate" } } } }, "assertionConsumerServices": { "title": "label.assertion-consumer-service-endpoints", "description": "", "type": "array", "items": { "$ref": "#/definitions/AssertionConsumerService" } }, "serviceProviderSsoDescriptor": { "type": "object", "properties": { "protocolSupportEnum": { "title": "label.protocol-support-enumeration", "description": "tooltip.protocol-support-enumeration", "type": "string", "widget": { "id": "select" }, "oneOf": [{ "enum": ["SAML 2"], "description": "SAML 2" }, { "enum": ["SAML 1.1"], "description": "SAML 1.1" }] }, "nameIdFormats": { "$ref": "#/definitions/nameIdFormats" } }, "dependencies": { "nameIdFormats": ["protocolSupportEnum"] } }, "logoutEndpoints": { "title": "label.logout-endpoints", "description": "tooltip.logout-endpoints", "type": "array", "items": { "$ref": "#/definitions/LogoutEndpoint" } }, "relyingPartyOverrides": { "type": "object", "properties": { "signAssertion": { "title": "label.sign-the-assertion", "description": "tooltip.sign-assertion", "type": "boolean", "default": false }, "dontSignResponse": { "title": "label.dont-sign-the-response", "description": "tooltip.dont-sign-response", "type": "boolean", "default": false }, "turnOffEncryption": { "title": "label.turn-off-encryption-of-response", "description": "tooltip.turn-off-encryption", "type": "boolean", "default": false }, "useSha": { "title": "label.use-sha1-signing-algorithm", "description": "tooltip.usa-sha-algorithm", "type": "boolean", "default": false }, "ignoreAuthenticationMethod": { "title": "label.ignore-any-sp-requested-authentication-method", "description": "tooltip.ignore-auth-method", "type": "boolean", "default": false }, "omitNotBefore": { "title": "label.omit-not-before-condition", "description": "tooltip.omit-not-before-condition", "type": "boolean", "default": false }, "responderId": { "title": "label.responder-id", "description": "tooltip.responder-id", "type": "string", "default": "" }, "nameIdFormats": { "$ref": "#/definitions/nameIdFormats" }, "authenticationMethods": { "$ref": "#/definitions/authenticationMethods" }, "forceAuthn": { "title": "label.force-authn", "description": "tooltip.force-authn", "type": "boolean", "default": false } } }, "attributeRelease": { "type": "array", "title": "label.attribute-release", "description": "Attribute release table - select the attributes you want to release (default unchecked)", "items": { "type": "string", "enum": ["eduPersonPrincipalName", "uid", "mail", "surname", "givenName", "eduPersonAffiliation", "eduPersonScopedAffiliation", "eduPersonPrimaryAffiliation", "eduPersonEntitlement", "eduPersonAssurance", "eduPersonUniqueId", "employeeNumber"] }, "uniqueItems": true } }, "definitions": { "Contact": { "type": "object", "required": ["name", "type", "emailAddress"], "properties": { "name": { "title": "label.contact-name", "description": "tooltip.contact-name", "type": "string", "minLength": 1, "maxLength": 255 }, "type": { "title": "label.contact-type", "description": "tooltip.contact-type", "type": "string", "widget": "select", "minLength": 1, "oneOf": [{ "enum": ["support"], "description": "value.support" }, { "enum": ["technical"], "description": "value.technical" }, { "enum": ["administrative"], "description": "value.administrative" }, { "enum": ["other"], "description": "value.other" }] }, "emailAddress": { "title": "label.contact-email-address", "description": "tooltip.contact-email", "type": "string", "pattern": "^(mailto:)?(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$", "minLength": 1, "maxLength": 255 } } }, "Certificate": { "type": "object", "required": ["type", "value"], "properties": { "name": { "title": "label.certificate-name-display-only", "description": "tooltip.certificate-name", "type": "string", "maxLength": 255 }, "type": { "title": "label.certificate-type", "type": "string", "widget": { "id": "radio", "class": "form-check-inline" }, "oneOf": [{ "enum": ["signing"], "description": "value.signing" }, { "enum": ["encryption"], "description": "value.encryption" }, { "enum": ["both"], "description": "value.both" }] }, "value": { "title": "label.certificate", "description": "tooltip.certificate", "type": "string", "widget": "textarea", "minLength": 1 } } }, "AssertionConsumerService": { "type": "object", "required": ["locationUrl", "binding"], "properties": { "locationUrl": { "title": "label.assertion-consumer-service-location", "description": "tooltip.assertion-consumer-service-location", "type": "string", "widget": { "id": "string", "help": "message.valid-url" }, "minLength": 1, "maxLength": 255 }, "binding": { "title": "label.assertion-consumer-service-location-binding", "description": "tooltip.assertion-consumer-service-location-binding", "type": "string", "widget": "select", "oneOf": [{ "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:PAOS"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:PAOS" }, { "enum": ["urn:oasis:names:tc:SAML:1.0:profiles:browser-post"], "description": "urn:oasis:names:tc:SAML:1.0:profiles:browser-post" }, { "enum": ["urn:oasis:names:tc:SAML:1.0:profiles:artifact-01"], "description": "urn:oasis:names:tc:SAML:1.0:profiles:artifact-01" }] }, "makeDefault": { "title": "label.mark-as-default", "description": "tooltip.mark-as-default", "type": "boolean" } } }, "LogoutEndpoint": { "description": "tooltip.new-endpoint", "type": "object", "fieldsets": [{ "fields": ["url", "bindingType"] }], "required": ["url", "bindingType"], "properties": { "url": { "title": "label.url", "description": "tooltip.url", "type": "string", "minLength": 1, "maxLength": 255 }, "bindingType": { "title": "label.binding-type", "description": "tooltip.binding-type", "type": "string", "widget": "select", "oneOf": [{ "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:SOAP"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" }] } } }, "MDUI": { "type": "object", "widget": { "id": "fieldset" }, "fieldsets": [{ "type": "group", "fields": ["displayName", "informationUrl", "description"] }, { "type": "group", "fields": ["privacyStatementUrl", "logoUrl", "logoWidth", "logoHeight"] }], "properties": { "displayName": { "title": "label.display-name", "description": "tooltip.mdui-display-name", "type": "string", "minLength": 1, "maxLength": 255 }, "informationUrl": { "title": "label.information-url", "description": "tooltip.mdui-information-url", "type": "string", "minLength": 1, "maxLength": 255 }, "privacyStatementUrl": { "title": "label.privacy-statement-url", "description": "tooltip.mdui-privacy-statement-url", "type": "string", "minLength": 1, "maxLength": 255 }, "description": { "title": "label.description", "description": "tooltip.mdui-description", "type": "string", "widget": { "id": "textarea" }, "minLength": 1, "maxLength": 255 }, "logoUrl": { "title": "label.logo-url", "description": "tooltip.mdui-logo-url", "type": "string", "minLength": 1, "maxLength": 255 }, "logoHeight": { "title": "label.logo-height", "description": "tooltip.mdui-logo-height", "minimum": 0, "type": "integer" }, "logoWidth": { "title": "label.logo-width", "description": "tooltip.mdui-logo-width", "minimum": 0, "type": "integer" } } }, "Organization": { "type": "object", "properties": { "name": { "title": "label.organization-name", "description": "tooltip.organization-name", "type": "string", "minLength": 1, "maxLength": 255 }, "displayName": { "title": "label.organization-display-name", "description": "tooltip.organization-display-name", "type": "string", "minLength": 1, "maxLength": 255 }, "url": { "title": "label.organization-url", "description": "tooltip.organization-url", "type": "string", "minLength": 1, "maxLength": 255 } }, "dependencies": { "name": { "required": ["displayName", "url"] }, "displayName": { "required": ["name", "url"] }, "url": { "required": ["name", "displayName"] } } }, "nameIdFormats": { "title": "label.nameid-format-to-send", "description": "tooltip.nameid-format", "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 255, "examples": ["urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"] } }, "authenticationMethods": { "title": "label.authentication-methods-to-use", "description": "tooltip.authentication-methods-to-use", "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 255, "examples": ["https://refeds.org/profile/mfa", "urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken", "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"] } } } }; +const SCHEMA = { "type": "object", "required": ["serviceProviderName", "entityId"], "properties": { "serviceProviderName": { "title": "label.service-provider-name", "description": "tooltip.service-provider-name", "type": "string", "minLength": 1, "maxLength": 255 }, "entityId": { "title": "label.entity-id", "description": "tooltip.entity-id", "type": "string", "minLength": 1, "maxLength": 255 }, "organization": { "$ref": "#/definitions/Organization" }, "contacts": { "title": "label.contact-information", "description": "tooltip.contact-information", "type": "array", "items": { "$ref": "#/definitions/Contact" } }, "mdui": { "$ref": "#/definitions/MDUI" }, "securityInfo": { "type": "object", "widget": { "id": "fieldset" }, "dependencies": { "authenticationRequestsSigned": { "oneOf": [{ "properties": { "authenticationRequestsSigned": { "enum": [true] }, "x509Certificates": { "minItems": 1 } } }, { "properties": { "authenticationRequestsSigned": { "enum": [false] }, "x509Certificates": { "minItems": 0 } } }] } }, "properties": { "x509CertificateAvailable": { "type": "boolean", "default": true }, "authenticationRequestsSigned": { "title": "label.authentication-requests-signed", "description": "tooltip.authentication-requests-signed", "type": "boolean", "enumNames": ["value.true", "value.false"] }, "wantAssertionsSigned": { "title": "label.want-assertions-signed", "description": "tooltip.want-assertions-signed", "type": "boolean", "enumNames": ["value.true", "value.false"] }, "x509Certificates": { "title": "label.x509-certificates", "type": "array", "items": { "$ref": "#/definitions/Certificate" } } } }, "assertionConsumerServices": { "title": "label.assertion-consumer-service-endpoints", "description": "", "type": "array", "items": { "$ref": "#/definitions/AssertionConsumerService" } }, "serviceProviderSsoDescriptor": { "type": "object", "properties": { "protocolSupportEnum": { "title": "label.protocol-support-enumeration", "description": "tooltip.protocol-support-enumeration", "type": "string", "widget": { "id": "select" }, "oneOf": [{ "enum": ["SAML 2"], "description": "SAML 2" }, { "enum": ["SAML 1.1"], "description": "SAML 1.1" }] }, "nameIdFormats": { "$ref": "#/definitions/nameIdFormats" } }, "dependencies": { "nameIdFormats": ["protocolSupportEnum"] } }, "logoutEndpoints": { "title": "label.logout-endpoints", "description": "tooltip.logout-endpoints", "type": "array", "items": { "$ref": "#/definitions/LogoutEndpoint" } }, "relyingPartyOverrides": { "type": "object", "properties": { "signAssertion": { "title": "label.sign-the-assertion", "description": "tooltip.sign-assertion", "type": "boolean", "default": false }, "dontSignResponse": { "title": "label.dont-sign-the-response", "description": "tooltip.dont-sign-response", "type": "boolean", "default": false }, "turnOffEncryption": { "title": "label.turn-off-encryption-of-response", "description": "tooltip.turn-off-encryption", "type": "boolean", "default": false }, "useSha": { "title": "label.use-sha1-signing-algorithm", "description": "tooltip.usa-sha-algorithm", "type": "boolean", "default": false }, "ignoreAuthenticationMethod": { "title": "label.ignore-any-sp-requested-authentication-method", "description": "tooltip.ignore-auth-method", "type": "boolean", "default": false }, "omitNotBefore": { "title": "label.omit-not-before-condition", "description": "tooltip.omit-not-before-condition", "type": "boolean", "default": false }, "responderId": { "title": "label.responder-id", "description": "tooltip.responder-id", "type": "string", "default": "" }, "nameIdFormats": { "$ref": "#/definitions/nameIdFormats" }, "authenticationMethods": { "$ref": "#/definitions/authenticationMethods" }, "forceAuthn": { "title": "label.force-authn", "description": "tooltip.force-authn", "type": "boolean", "default": false } } }, "attributeRelease": { "type": "array", "title": "label.attribute-release", "description": "Attribute release table - select the attributes you want to release (default unchecked)", "items": { "type": "string", "enum": ["eduPersonPrincipalName", "uid", "mail", "surname", "givenName", "eduPersonAffiliation", "eduPersonScopedAffiliation", "eduPersonPrimaryAffiliation", "eduPersonEntitlement", "eduPersonAssurance", "eduPersonUniqueId", "employeeNumber"] }, "uniqueItems": true } }, "definitions": { "Contact": { "type": "object", "required": ["name", "type", "emailAddress"], "properties": { "name": { "title": "label.contact-name", "description": "tooltip.contact-name", "type": "string", "minLength": 1, "maxLength": 255 }, "type": { "title": "label.contact-type", "description": "tooltip.contact-type", "type": "string", "widget": "select", "minLength": 1, "oneOf": [{ "enum": ["support"], "description": "value.support" }, { "enum": ["technical"], "description": "value.technical" }, { "enum": ["administrative"], "description": "value.administrative" }, { "enum": ["other"], "description": "value.other" }] }, "emailAddress": { "title": "label.contact-email-address", "description": "tooltip.contact-email", "type": "string", "pattern": "^(mailto:)?(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$", "minLength": 1, "maxLength": 255 } } }, "Certificate": { "type": "object", "required": ["type", "value"], "properties": { "name": { "title": "label.certificate-name-display-only", "description": "tooltip.certificate-name", "type": "string", "maxLength": 255 }, "type": { "title": "label.certificate-type", "type": "string", "widget": { "id": "radio", "class": "form-check-inline" }, "oneOf": [{ "enum": ["signing"], "description": "value.signing" }, { "enum": ["encryption"], "description": "value.encryption" }, { "enum": ["both"], "description": "value.both" }] }, "value": { "title": "label.certificate", "description": "tooltip.certificate", "type": "string", "widget": "textarea", "minLength": 1 } } }, "AssertionConsumerService": { "type": "object", "required": ["locationUrl", "binding"], "properties": { "locationUrl": { "title": "label.assertion-consumer-service-location", "description": "tooltip.assertion-consumer-service-location", "type": "string", "widget": { "id": "string", "help": "message.valid-url" }, "minLength": 1, "maxLength": 255 }, "binding": { "title": "label.assertion-consumer-service-location-binding", "description": "tooltip.assertion-consumer-service-location-binding", "type": "string", "widget": "select", "oneOf": [{ "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:PAOS"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:PAOS" }, { "enum": ["urn:oasis:names:tc:SAML:1.0:profiles:browser-post"], "description": "urn:oasis:names:tc:SAML:1.0:profiles:browser-post" }, { "enum": ["urn:oasis:names:tc:SAML:1.0:profiles:artifact-01"], "description": "urn:oasis:names:tc:SAML:1.0:profiles:artifact-01" }] }, "makeDefault": { "title": "label.mark-as-default", "description": "tooltip.mark-as-default", "type": "boolean" } } }, "LogoutEndpoint": { "description": "tooltip.new-endpoint", "type": "object", "fieldsets": [{ "fields": ["url", "bindingType"] }], "required": ["url", "bindingType"], "properties": { "url": { "title": "label.url", "description": "tooltip.url", "type": "string", "minLength": 1, "maxLength": 255 }, "bindingType": { "title": "label.binding-type", "description": "tooltip.binding-type", "type": "string", "widget": "select", "oneOf": [{ "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:SOAP"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP" }, { "enum": ["urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"], "description": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" }] } } }, "MDUI": { "type": "object", "widget": { "id": "fieldset" }, "fieldsets": [{ "type": "group", "fields": ["displayName", "informationUrl", "description"] }, { "type": "group", "fields": ["privacyStatementUrl", "logoUrl", "logoWidth", "logoHeight"] }], "properties": { "displayName": { "title": "label.display-name", "description": "tooltip.mdui-display-name", "type": "string", "minLength": 1, "maxLength": 255 }, "informationUrl": { "title": "label.information-url", "description": "tooltip.mdui-information-url", "type": "string", "minLength": 1, "maxLength": 255 }, "privacyStatementUrl": { "title": "label.privacy-statement-url", "description": "tooltip.mdui-privacy-statement-url", "type": "string", "minLength": 1, "maxLength": 255 }, "description": { "title": "label.description", "description": "tooltip.mdui-description", "type": "string", "widget": { "id": "textarea" }, "minLength": 1, "maxLength": 255 }, "logoUrl": { "title": "label.logo-url", "description": "tooltip.mdui-logo-url", "type": "string", "minLength": 1, "maxLength": 255 }, "logoHeight": { "title": "label.logo-height", "description": "tooltip.mdui-logo-height", "minimum": 0, "type": "integer" }, "logoWidth": { "title": "label.logo-width", "description": "tooltip.mdui-logo-width", "minimum": 0, "type": "integer" } } }, "Organization": { "type": "object", "properties": { "name": { "title": "label.organization-name", "description": "tooltip.organization-name", "type": "string", "minLength": 1, "maxLength": 255 }, "displayName": { "title": "label.organization-display-name", "description": "tooltip.organization-display-name", "type": "string", "minLength": 1, "maxLength": 255 }, "url": { "title": "label.organization-url", "description": "tooltip.organization-url", "type": "string", "minLength": 1, "maxLength": 255 } }, "dependencies": { "name": { "required": ["displayName", "url"] }, "displayName": { "required": ["name", "url"] }, "url": { "required": ["name", "displayName"] } } }, "nameIdFormats": { "title": "label.nameid-format-to-send", "description": "tooltip.nameid-format", "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 255, "examples": ["urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"] } }, "authenticationMethods": { "title": "label.authentication-methods-to-use", "description": "tooltip.authentication-methods-to-use", "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 255, "examples": ["https://refeds.org/profile/mfa", "urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken", "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"] } } } }; export default SCHEMA; \ No newline at end of file diff --git a/ui/src/testing/uiSchema.js b/ui/src/testing/uiSchema.js index eb557bcf2..c1af6f4fa 100644 --- a/ui/src/testing/uiSchema.js +++ b/ui/src/testing/uiSchema.js @@ -11,7 +11,6 @@ const schema = { "fields": [ "serviceProviderName", "entityId", - "serviceEnabled", "organization" ] }, @@ -208,7 +207,6 @@ const schema = { }, "serviceProviderName": {}, "entityId": {}, - "serviceEnabled": {}, "organization": {}, "ui:disabled": false };