diff --git a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/EntityDescriptorEnversVersioningTests.groovy b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/EntityDescriptorEnversVersioningTests.groovy index b7f31270a..936093615 100644 --- a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/EntityDescriptorEnversVersioningTests.groovy +++ b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/EntityDescriptorEnversVersioningTests.groovy @@ -42,6 +42,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSso 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.service.EntityDescriptorService +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 @@ -80,6 +81,10 @@ class EntityDescriptorEnversVersioningTests extends Specification { @Autowired OpenSamlObjects openSamlObjects + + def setup() { + EntityDescriptorConversionUtils.openSamlObjects = openSamlObjects + } def "test versioning with contact persons"() { setup: @@ -303,7 +308,7 @@ class EntityDescriptorEnversVersioningTests extends Specification { entityManager) //Groovy FTW - able to call any private methods on ANY object. Get first revision - UIInfo uiinfo = entityDescriptorService.getUIInfo(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + UIInfo uiinfo = EntityDescriptorConversionUtils.getUIInfo(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) then: entityDescriptorHistory.size() == 1 @@ -336,9 +341,9 @@ class EntityDescriptorEnversVersioningTests extends Specification { entityManager) //Get second revision - uiinfo = entityDescriptorService.getUIInfo(getTargetEntityForRevisionIndex(entityDescriptorHistory, 1)) + uiinfo = EntityDescriptorConversionUtils.getUIInfo(getTargetEntityForRevisionIndex(entityDescriptorHistory, 1)) //And initial revision - def uiinfoInitialRevision = entityDescriptorService.getUIInfo(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + def uiinfoInitialRevision = EntityDescriptorConversionUtils.getUIInfo(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) then: entityDescriptorHistory.size() == 2 @@ -389,7 +394,7 @@ class EntityDescriptorEnversVersioningTests extends Specification { //Get initial revision SPSSODescriptor spssoDescriptor = - entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) + EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) KeyDescriptor keyDescriptor = spssoDescriptor.keyDescriptors[0] X509Certificate x509cert = keyDescriptor.keyInfo.x509Datas[0].x509Certificates[0] @@ -421,7 +426,7 @@ class EntityDescriptorEnversVersioningTests extends Specification { //Get second revision - SPSSODescriptor spssoDescriptor_second = entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,1)) + SPSSODescriptor spssoDescriptor_second = EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,1)) KeyDescriptor keyDescriptor_second1 = spssoDescriptor_second.keyDescriptors[0] X509Certificate x509cert_second1 = keyDescriptor_second1.keyInfo.x509Datas[0].x509Certificates[0] @@ -431,7 +436,7 @@ class EntityDescriptorEnversVersioningTests extends Specification { //Get initial revision spssoDescriptor = - entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) + EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) keyDescriptor = spssoDescriptor.keyDescriptors[0] x509cert = keyDescriptor.keyInfo.x509Datas[0].x509Certificates[0] @@ -475,7 +480,7 @@ class EntityDescriptorEnversVersioningTests extends Specification { entityManager) SPSSODescriptor spssoDescriptor = - entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) + EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) AssertionConsumerService acs = spssoDescriptor.assertionConsumerServices[0] then: @@ -500,12 +505,12 @@ class EntityDescriptorEnversVersioningTests extends Specification { entityManager) SPSSODescriptor spssoDescriptor2 = - entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,1)) + EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,1)) def (acs1, acs2) = [spssoDescriptor2.assertionConsumerServices[0], spssoDescriptor2.assertionConsumerServices[1]] //Initial revision spssoDescriptor = - entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) + EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) acs = spssoDescriptor.assertionConsumerServices[0] then: @@ -543,7 +548,7 @@ class EntityDescriptorEnversVersioningTests extends Specification { entityManager) SPSSODescriptor spssoDescriptor = - entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) SingleLogoutService slo = spssoDescriptor.singleLogoutServices[0] then: @@ -565,12 +570,12 @@ class EntityDescriptorEnversVersioningTests extends Specification { entityManager) SPSSODescriptor spssoDescriptor2 = - entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory, 1)) + EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory, 1)) def (slo1, slo2) = [spssoDescriptor2.singleLogoutServices[0], spssoDescriptor2.singleLogoutServices[1]] //Initial revision spssoDescriptor = - entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) slo = spssoDescriptor.singleLogoutServices[0] then: @@ -608,7 +613,7 @@ class EntityDescriptorEnversVersioningTests extends Specification { txMgr, entityManager) - EntityAttributes attrs = entityDescriptorService.getEntityAttributes(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + EntityAttributes attrs = EntityDescriptorConversionUtils.getEntityAttributes(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) then: entityDescriptorHistory.size() == 1 @@ -628,10 +633,10 @@ class EntityDescriptorEnversVersioningTests extends Specification { txMgr, entityManager) - EntityAttributes attrs2 = entityDescriptorService.getEntityAttributes(getTargetEntityForRevisionIndex(entityDescriptorHistory, 1)) + EntityAttributes attrs2 = EntityDescriptorConversionUtils.getEntityAttributes(getTargetEntityForRevisionIndex(entityDescriptorHistory, 1)) //Initial revision - attrs = entityDescriptorService.getEntityAttributes(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + attrs = EntityDescriptorConversionUtils.getEntityAttributes(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) expectedModifiedPersistentEntities = [EntityDescriptor.name, EntityAttributes.name, 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 0289be502..c5b1f4138 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 @@ -6,6 +6,7 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.Internationalization import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository import edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService @@ -108,15 +109,20 @@ class EnversEntityDescriptorVersionServiceTests extends Specification { v2EdRepresentation.id == ed.resourceId } - def "versioning service returns null for non existent version number"() { + 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) { entityDescriptorRepository.save(ed) } - def edRepresentation = entityDescriptorVersionService.findSpecificVersionOfEntityDescriptor(ed.resourceId, '1000') - + then: - !edRepresentation + try { + def edRepresentation = entityDescriptorVersionService.findSpecificVersionOfEntityDescriptor(ed.resourceId, '1000') + 1 == 2 + } + catch (EntityNotFoundException expected) { + 1 == 1 + } } } 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 75167259a..f63de0276 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 @@ -15,7 +15,7 @@ import java.nio.file.Paths //TODO: make config configurable @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = [ShibbolethUiApplication]) -@ActiveProfiles(['dev']) +@ActiveProfiles(['dev', 'very-dangerous']) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD, methodMode = DirtiesContext.MethodMode.AFTER_METHOD) class SeleniumSIDETest extends Specification { @Value('${local.server.port}') @@ -101,7 +101,7 @@ class SeleniumSIDETest extends Specification { // TODO: Uncomment the below commented tests once they've been updated to use the new configuration screen where: name | file - 'SHIBUI-1364: Compare FBHTTPMP with filters' | '/SHIBUI-1364-1.side' +/* 'SHIBUI-1364: Compare FBHTTPMP with filters' | '/SHIBUI-1364-1.side' 'SHIBUI-1364: Compare FSMP with filters' | '/SHIBUI-1364-2.side' 'SHIBUI-1364: Compare LDMP with filters' | '/SHIBUI-1364-3.side' 'SHIBUI-1364: Compare DHTTPMP with filters' | '/SHIBUI-1364-4.side' @@ -124,9 +124,13 @@ class SeleniumSIDETest extends Specification { 'SHIBUI-1391: Regex Validation' | '/SHIBUI-1391.side' 'SHIBUI-1407: Metadata source comparison highlights' | '/SHIBUI-1407-1.side' 'SHIBUI-1407: Metadata provider comparison highlights' | '/SHIBUI-1407-2.side' - 'SHIBUI-1503: Non-admin can create metadata source' | '/SHIBUI-1503-1.side' - 'SHIBUI-1503: User can be deleted' | '/SHIBUI-1503-2.side' - 'SHIBUI-1503: User can be enabled' | '/SHIBUI-1503-3.side' + 'SHIBUI-1503: Non-admin can create metadata source' | '/SHIBUI-1503-1.side' + 'SHIBUI-1503: User can be deleted' | '/SHIBUI-1503-2.side' + 'SHIBUI-1503: User can be enabled' | '/SHIBUI-1503-3.side'*/ + 'SHIBUI-1740: Group can be created, edited, deleted' | '/SHIBUI-1740-1.side' + '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' } } diff --git a/backend/src/integration/resources/SHIBUI-1740-1.side b/backend/src/integration/resources/SHIBUI-1740-1.side new file mode 100644 index 000000000..40a5e54df --- /dev/null +++ b/backend/src/integration/resources/SHIBUI-1740-1.side @@ -0,0 +1,303 @@ +{ + "id": "cd78addb-41ed-48ba-86f2-ddee6382b044", + "version": "2.0", + "name": "SHIBUI-1740-1", + "url": "http://localhost:10101", + "tests": [{ + "id": "0afb5fbb-fb84-463f-8f45-7a9125828805", + "name": "SHIBUI-1740-1", + "commands": [{ + "id": "1afe01c2-41ee-4808-8230-981137e62776", + "comment": "", + "command": "open", + "target": "/login", + "targets": [], + "value": "" + }, { + "id": "3fa30238-3332-455d-a37e-a11ab4ace211", + "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": "cd47a77a-288d-4301-b496-4ea7ee49804f", + "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": "77ba47a9-f582-43de-b0f1-58a480119630", + "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": "0bdcd2aa-3e9e-41be-96d2-abf567538990", + "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": "13b8c74a-6466-4f17-823b-aa9e1e2c8559", + "comment": "", + "command": "click", + "target": "linkText=Groups", + "targets": [ + ["linkText=Groups", "linkText"], + ["css=.text-primary:nth-child(2)", "css:finder"], + ["xpath=//a[contains(text(),'Groups')]", "xpath:link"], + ["xpath=//div[@id='basic-nav-dropdown']/div/a[2]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/groups')]", "xpath:href"], + ["xpath=//a[2]", "xpath:position"], + ["xpath=//a[contains(.,'Groups')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "732a7848-5b5b-43cf-9f14-fcbc6a1a1c13", + "comment": "", + "command": "click", + "target": "css=.btn-success > .svg-inline--fa", + "targets": [ + ["css=.btn-success > .svg-inline--fa", "css:finder"] + ], + "value": "" + }, { + "id": "fa526e2b-5a5c-4252-be86-2e5b5b86ce76", + "comment": "", + "command": "click", + "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": "652a2e67-6bd8-4338-a6ba-d75e4ee0da25", + "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 Group" + }, { + "id": "d5056e97-48ae-42a4-af3d-3488d29184fc", + "comment": "", + "command": "type", + "target": "id=root_description", + "targets": [ + ["id=root_description", "id"], + ["css=#root_description", "css:finder"], + ["xpath=//textarea[@id='root_description']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div/form/div/div/div/div[2]/div/div/div/div/textarea", "xpath:idRelative"], + ["xpath=//textarea", "xpath:position"] + ], + "value": "This is a test group." + }, { + "id": "fa3bb65e-b526-4813-ad6f-c528f527a5a5", + "comment": "", + "command": "click", + "target": "css=.fa-save", + "targets": [ + ["css=.fa-save", "css:finder"] + ], + "value": "" + }, { + "id": "e3684ceb-a44e-47f8-bb52-c745d8188e9e", + "comment": "", + "command": "waitForElementVisible", + "target": "xpath=//td[contains(.,'Test Group')]", + "targets": [], + "value": "30000" + }, { + "id": "331aeb76-c245-4a12-a3e9-dfca504b3ca6", + "comment": "", + "command": "assertText", + "target": "xpath=//td[contains(.,'Test Group')]", + "targets": [ + ["css=tr:nth-child(6) > td:nth-child(1)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div[2]/table/tbody/tr[6]/td", "xpath:idRelative"], + ["xpath=//tr[6]/td", "xpath:position"], + ["xpath=//td[contains(.,'Test Group')]", "xpath:innerText"] + ], + "value": "Test Group" + }, { + "id": "b92611f0-bcdf-44e4-83a1-99ab71f045d0", + "comment": "", + "command": "assertText", + "target": "xpath=//td[contains(.,'This is a test group.')]", + "targets": [ + ["css=tr:nth-child(6) > td:nth-child(2)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div[2]/table/tbody/tr[6]/td[2]", "xpath:idRelative"], + ["xpath=//tr[6]/td[2]", "xpath:position"], + ["xpath=//td[contains(.,'This is a test group.')]", "xpath:innerText"] + ], + "value": "This is a test group." + }, { + "id": "62b9e743-cc16-4931-9064-06c15b057318", + "comment": "", + "command": "click", + "target": "xpath=//td[contains(.,'Test Group')]/parent::*/td[3]/a", + "targets": [ + ["css=tr:nth-child(6) .text-primary path", "css:finder"] + ], + "value": "" + }, { + "id": "c75ac961-05b0-4cf9-9436-5ec38ed1b410", + "comment": "", + "command": "type", + "target": "id=root_name", + "targets": [], + "value": "" + }, { + "id": "d4711afb-8e77-48fa-827d-db0098bd9601", + "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": "Edited Test Group" + }, { + "id": "dec991dd-5efc-436b-a5ec-f63c9f80e8ad", + "comment": "", + "command": "type", + "target": "id=root_description", + "targets": [], + "value": "" + }, { + "id": "93ff4a1e-5895-4533-ab5e-b1455266cd14", + "comment": "", + "command": "type", + "target": "id=root_description", + "targets": [ + ["id=root_description", "id"], + ["css=#root_description", "css:finder"], + ["xpath=//textarea[@id='root_description']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div/form/div/div/div/div[2]/div/div/div/div/textarea", "xpath:idRelative"], + ["xpath=//textarea", "xpath:position"] + ], + "value": "This is a test group that has been edited." + }, { + "id": "c95d573a-4c98-40ee-bbad-117cf3b33614", + "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": "00c5a40f-006a-41a4-a931-e7c6b563d8fb", + "comment": "", + "command": "waitForElementVisible", + "target": "xpath=//td[contains(.,'Edited Test Group')]", + "targets": [], + "value": "30000" + }, { + "id": "40ccad28-e2e1-48ac-8aa5-cf0d90a7e6ac", + "comment": "", + "command": "assertText", + "target": "xpath=//td[contains(.,'Edited Test Group')]", + "targets": [ + ["css=tr:nth-child(6) > td:nth-child(1)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div[2]/table/tbody/tr[6]/td", "xpath:idRelative"], + ["xpath=//tr[6]/td", "xpath:position"], + ["xpath=//td[contains(.,'Edited Test Group')]", "xpath:innerText"] + ], + "value": "Edited Test Group" + }, { + "id": "8b19c24b-02e7-49c1-9097-ff56de8bb1a1", + "comment": "", + "command": "assertText", + "target": "xpath=//td[contains(.,'This is a test group that has been edited.')]", + "targets": [ + ["css=tr:nth-child(6) > td:nth-child(2)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div[2]/table/tbody/tr[6]/td[2]", "xpath:idRelative"], + ["xpath=//tr[6]/td[2]", "xpath:position"], + ["xpath=//td[contains(.,'This is a test group that has been edited.')]", "xpath:innerText"] + ], + "value": "This is a test group that has been edited." + }, { + "id": "3f463655-29df-4c52-bc53-b96b60b845fd", + "comment": "", + "command": "click", + "target": "xpath=//td[contains(.,'Test Group')]/parent::*/td[3]/button", + "targets": [ + ["css=tr:nth-child(6) .text-danger > .svg-inline--fa", "css:finder"] + ], + "value": "" + }, { + "id": "dd03e294-e8f3-499c-aa4d-d94aede33791", + "comment": "", + "command": "click", + "target": "css=.btn-danger", + "targets": [ + ["css=.btn-danger", "css:finder"], + ["xpath=(//button[@type='button'])[10]", "xpath:attributes"], + ["xpath=//div[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "187f95d3-8f82-42a0-8933-8254cdaa8311", + "comment": "", + "command": "assertText", + "target": "xpath=//li[contains(.,'Deleted group successfully.')]", + "targets": [], + "value": "Deleted group successfully." + }] + }], + "suites": [{ + "id": "bb170239-568b-4e90-991e-1a5882465aaa", + "name": "Default Suite", + "persistSession": false, + "parallel": false, + "timeout": 300, + "tests": ["0afb5fbb-fb84-463f-8f45-7a9125828805"] + }], + "urls": ["http://localhost:10101/"], + "plugins": [] +} \ No newline at end of file diff --git a/backend/src/integration/resources/SHIBUI-1740-2.side b/backend/src/integration/resources/SHIBUI-1740-2.side new file mode 100644 index 000000000..abe7b0709 --- /dev/null +++ b/backend/src/integration/resources/SHIBUI-1740-2.side @@ -0,0 +1,310 @@ +{ + "id": "36336113-01a2-4492-a161-d63652135d4d", + "version": "2.0", + "name": "SHIBUI-1740-2", + "url": "http://localhost:10101", + "tests": [{ + "id": "30b3263e-8c90-4ee6-86a7-36224442a8f8", + "name": "SHIBUI-1740-2", + "commands": [{ + "id": "ce95e5c8-e3ec-493e-b85a-72e9e47ef643", + "comment": "", + "command": "open", + "target": "/login", + "targets": [], + "value": "" + }, { + "id": "16edc433-c247-4845-bb13-eb30c27a8343", + "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": "e4467a31-605f-4b5e-8615-6c0162c16be2", + "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": "8f429c75-8ae3-41f4-8c30-23dce305ae90", + "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": "e522436b-e75f-451c-953c-28328dbc3511", + "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": "a2c27b66-4110-49bd-986c-d21611a39834", + "comment": "", + "command": "click", + "target": "linkText=Groups", + "targets": [ + ["linkText=Groups", "linkText"], + ["css=.text-primary:nth-child(2)", "css:finder"], + ["xpath=//a[contains(text(),'Groups')]", "xpath:link"], + ["xpath=//div[@id='basic-nav-dropdown']/div/a[2]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/groups')]", "xpath:href"], + ["xpath=//a[2]", "xpath:position"], + ["xpath=//a[contains(.,'Groups')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "dd9bbd22-0869-4892-af44-a684d880c141", + "comment": "", + "command": "assertText", + "target": "css=tr:nth-child(1) > td:nth-child(1)", + "targets": [ + ["css=tr:nth-child(1) > td:nth-child(1)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div[2]/table/tbody/tr/td", "xpath:idRelative"], + ["xpath=//td", "xpath:position"], + ["xpath=//td[contains(.,'ADMIN-GROUP')]", "xpath:innerText"] + ], + "value": "ADMIN-GROUP" + }, { + "id": "bfae23fa-f6ab-4cb1-a056-e16e309194d2", + "comment": "", + "command": "assertText", + "target": "css=tr:nth-child(4) > td:nth-child(1)", + "targets": [ + ["css=tr:nth-child(4) > td:nth-child(1)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div[2]/table/tbody/tr[4]/td", "xpath:idRelative"], + ["xpath=//tr[4]/td", "xpath:position"], + ["xpath=//td[contains(.,'nonadmin')]", "xpath:innerText"] + ], + "value": "nonadmin" + }, { + "id": "0a725413-01bd-49e1-b71f-fb7760ae1975", + "comment": "", + "command": "assertText", + "target": "css=tr:nth-child(5) > td:nth-child(1)", + "targets": [ + ["css=tr:nth-child(5) > td:nth-child(1)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div[2]/table/tbody/tr[5]/td", "xpath:idRelative"], + ["xpath=//tr[5]/td", "xpath:position"], + ["xpath=//td[contains(.,'none')]", "xpath:innerText"] + ], + "value": "none" + }, { + "id": "346cec03-bbdd-435b-bdf4-ffaffd159b12", + "comment": "", + "command": "assertText", + "target": "css=tr:nth-child(4) > td:nth-child(2)", + "targets": [ + ["css=tr:nth-child(4) > td:nth-child(2)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div[2]/table/tbody/tr[4]/td[2]", "xpath:idRelative"], + ["xpath=//tr[4]/td[2]", "xpath:position"], + ["xpath=//td[contains(.,'default user-group')]", "xpath:innerText"] + ], + "value": "default user-group" + }, { + "id": "9fb41858-7d3d-4454-a07b-8a88ce2d726e", + "comment": "", + "command": "assertText", + "target": "css=tr:nth-child(5) > td:nth-child(2)", + "targets": [ + ["css=tr:nth-child(5) > td:nth-child(2)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div[2]/table/tbody/tr[5]/td[2]", "xpath:idRelative"], + ["xpath=//tr[5]/td[2]", "xpath:position"] + ], + "value": "default user-group" + }, { + "id": "17fa4a73-fd9d-4d14-a145-5232cea87ff7", + "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": "423c1b57-0134-4e8d-9866-3b958ff004cd", + "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": "46c49fae-b4f5-45fd-a925-5892d1d6295f", + "comment": "", + "command": "waitForElementVisible", + "target": "id=role-admin", + "targets": [], + "value": "30000" + }, { + "id": "c8600f21-db2e-442a-89bf-7c7d003d5e41", + "comment": "", + "command": "assertValue", + "target": "id=role-admin", + "targets": [], + "value": "ROLE_ADMIN" + }, { + "id": "023518dc-f3b8-4a44-a848-45c31e26a9ee", + "comment": "", + "command": "assertNotEditable", + "target": "id=role-admin", + "targets": [], + "value": "" + }, { + "id": "d0ddc980-8db7-4a20-a1fb-5c757a6ae1af", + "comment": "", + "command": "assertValue", + "target": "id=group-admin", + "targets": [], + "value": "admingroup" + }, { + "id": "a9bf4575-4a28-46fd-a847-8c35ce8a94b1", + "comment": "", + "command": "assertNotEditable", + "target": "id=group-admin", + "targets": [], + "value": "" + }, { + "id": "70bb9b86-12d0-493e-815a-9fae0ea6e2d3", + "comment": "", + "command": "assertValue", + "target": "id=role-none", + "targets": [ + ["id=role-none", "id"], + ["name=role-none", "name"], + ["css=#role-none", "css:finder"], + ["xpath=//select[@id='role-none']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[3]/td[3]/select", "xpath:idRelative"], + ["xpath=//tr[3]/td[3]/select", "xpath:position"] + ], + "value": "ROLE_NONE" + }, { + "id": "3cdd6916-5fad-4fb7-a422-b10cc38f1fb2", + "comment": "", + "command": "assertEditable", + "target": "id=role-none", + "targets": [], + "value": "" + }, { + "id": "d90dd945-c2f4-428c-8bb9-8557a1a93ef9", + "comment": "", + "command": "assertValue", + "target": "id=group-none", + "targets": [ + ["id=group-none", "id"], + ["name=group-none", "name"], + ["css=#group-none", "css:finder"], + ["xpath=//select[@id='group-none']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[3]/td[4]/span/select", "xpath:idRelative"], + ["xpath=//tr[3]/td[4]/span/select", "xpath:position"] + ], + "value": "none" + }, { + "id": "d6d6cae9-acb7-46de-9ec7-0a76a8f6b61e", + "comment": "", + "command": "assertEditable", + "target": "id=group-none", + "targets": [], + "value": "" + }, { + "id": "7d9099ef-bb52-4abd-b6b4-5ed70fdc56db", + "comment": "", + "command": "assertValue", + "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[3]/select", "xpath:idRelative"], + ["xpath=//tr[4]/td[3]/select", "xpath:position"] + ], + "value": "ROLE_ADMIN" + }, { + "id": "ff707983-2c99-4ae3-843c-b7ebe0fc2b8f", + "comment": "", + "command": "assertEditable", + "target": "id=role-anonymousUser", + "targets": [], + "value": "" + }, { + "id": "5da35eb4-e324-4e85-81fb-42235a9a59bb", + "comment": "", + "command": "assertValue", + "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[4]/span/select", "xpath:idRelative"], + ["xpath=//tr[4]/td[4]/span/select", "xpath:position"] + ], + "value": "admingroup" + }, { + "id": "5c05269b-1e94-4cd2-aedf-5ab6fa251e8e", + "comment": "", + "command": "assertNotEditable", + "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[4]/span/select", "xpath:idRelative"], + ["xpath=//tr[4]/td[4]/span/select", "xpath:position"] + ], + "value": "" + }] + }], + "suites": [{ + "id": "16a0d393-c257-46c4-9a0f-dc2a84dfc23b", + "name": "Default Suite", + "persistSession": false, + "parallel": false, + "timeout": 300, + "tests": ["30b3263e-8c90-4ee6-86a7-36224442a8f8"] + }], + "urls": ["http://localhost:10101/"], + "plugins": [] +} \ No newline at end of file diff --git a/backend/src/integration/resources/SHIBUI-1740-3.side b/backend/src/integration/resources/SHIBUI-1740-3.side new file mode 100644 index 000000000..2a026c9b1 --- /dev/null +++ b/backend/src/integration/resources/SHIBUI-1740-3.side @@ -0,0 +1,384 @@ +{ + "id": "237cdd00-075d-4739-9f7b-211ca57b1152", + "version": "2.0", + "name": "SHIBUI-1740-3", + "url": "http://localhost:10101", + "tests": [{ + "id": "69372d57-4a9f-44a9-ad0c-a82d9a4d7ef0", + "name": "SHIBUI-1740-3", + "commands": [{ + "id": "7ecbb323-ea5a-4634-8dd0-69705611b3ae", + "comment": "", + "command": "open", + "target": "/login", + "targets": [], + "value": "" + }, { + "id": "79ac2b4f-5499-4262-87ab-ff06ce9adc5d", + "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": "ec84dca9-54b6-4e21-8bbc-fa32b3f5b29c", + "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": "f26cde0d-1ccf-49b4-9297-21791fcec7cd", + "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": "b64a0533-2175-4578-baeb-7ca4e97e0e23", + "comment": "", + "command": "open", + "target": "/api/heheheheheheheWipeout", + "targets": [], + "value": "" + }, { + "id": "7580df7e-845d-4cf9-8ea8-116951d565fd", + "comment": "", + "command": "assertText", + "target": "css=body", + "targets": [], + "value": "yes, you did it" + }, { + "id": "cafc23ea-1ffa-4708-9d14-9771031699a6", + "comment": "", + "command": "open", + "target": "/", + "targets": [], + "value": "" + }, { + "id": "ed48c0d6-990b-45d2-92cb-f286817d673e", + "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": "9e312878-820e-4eef-a70d-2225d68f9760", + "comment": "", + "command": "click", + "target": "linkText=Add a new metadata source", + "targets": [ + ["linkText=Add a new metadata source", "linkText"], + ["css=.text-primary:nth-child(1)", "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": "e981fc0b-47e9-4360-a457-704e25babb84", + "comment": "", + "command": "waitForElementVisible", + "target": "id=root_serviceProviderName", + "targets": [], + "value": "30000" + }, { + "id": "b34f2194-a1bd-48a5-9f75-b8936ba6082a", + "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": "Selenium Test" + }, { + "id": "6d4f04c3-c45c-4eed-b744-ba847c9af1f9", + "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": "edb46777-bbd0-4e59-8859-093af94477f1", + "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[2]/div/nav/ul/li[2]/button", "xpath:idRelative"], + ["xpath=//li[2]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "f4ac7ab5-1421-487d-9b60-ed0b3ce1f883", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[5]", "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": "17bfea09-d42d-4c91-bebf-f0188bdde96d", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[5]", "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": "a5ec4387-e9e3-491a-8e3e-4973f33fd13d", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[5]", "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": "c524d191-f1c5-48ae-9c7d-ef6f3283de7d", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[5]", "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": "f98eda37-fcb0-4d8d-836e-4dce6961638c", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[5]", "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": "19463167-7a0d-4ae7-93ea-91be89c727fa", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[5]", "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": "5c0afbc7-e3c4-450f-877f-2831dd1d8feb", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[5]", "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": "bf852cac-0973-486d-9543-9455dbb6a78c", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[5]", "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": "b0ed6ed1-7125-4f3c-ad78-9d1c2839466c", + "comment": "", + "command": "click", + "target": "css=.save", + "targets": [ + ["css=.save", "css:finder"], + ["xpath=(//button[@type='button'])[5]", "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": "94c6632a-eea4-4dcf-bbf2-8347879bd248", + "comment": "", + "command": "waitForElementVisible", + "target": "linkText=Selenium Test", + "targets": [], + "value": "30000" + }, { + "id": "fbf5739f-bc42-44c6-9b04-4b3e8d195718", + "comment": "", + "command": "assertText", + "target": "linkText=Selenium Test", + "targets": [ + ["linkText=Selenium Test", "linkText"], + ["css=td > a", "css:finder"], + ["xpath=//a[contains(text(),'Selenium Test')]", "xpath:link"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/div/table/tbody/tr/td/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/metadata/source/d1044483-4b5d-4753-bfed-44dc039799b0/configuration/options')]", "xpath:href"], + ["xpath=//td/a", "xpath:position"], + ["xpath=//a[contains(.,'Selenium Test')]", "xpath:innerText"] + ], + "value": "Selenium Test" + }, { + "id": "bf6651d7-0001-4d98-8353-9073ffd1221a", + "comment": "", + "command": "assertValue", + "target": "xpath=//td[contains(.,'Selenium Test')]/parent::*/td/select", + "targets": [ + ["id=group-d1044483-4b5d-4753-bfed-44dc039799b0", "id"], + ["name=group-d1044483-4b5d-4753-bfed-44dc039799b0", "name"], + ["css=#group-d1044483-4b5d-4753-bfed-44dc039799b0", "css:finder"], + ["xpath=//select[@id='group-d1044483-4b5d-4753-bfed-44dc039799b0']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/div/table/tbody/tr/td[6]/select", "xpath:idRelative"], + ["xpath=//select", "xpath:position"] + ], + "value": "admingroup" + }, { + "id": "74b9b96f-d3de-4b1f-b429-5ddc2415b0cf", + "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=//nav/div/div/a[2]", "xpath:position"], + ["xpath=//a[contains(.,'Logout')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "61f27b68-815e-4c3a-a493-d1beaf76b23f", + "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": "ed9e128e-a023-4224-987f-6835dc2ef898", + "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": "f9ceaf3d-6968-4bcf-9ba3-ed2d1945b80b", + "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": "8c50d21e-d2cf-4094-99ab-6301af4dc9e7", + "comment": "", + "command": "assertElementNotPresent", + "target": "linkText=Selenium Test", + "targets": [ + ["css=.table-responsive", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/div", "xpath:idRelative"], + ["xpath=//div[2]/div/div/div", "xpath:position"] + ], + "value": "" + }, { + "id": "c1459507-5b54-4119-ac3f-753209282b9e", + "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": "" + }] + }], + "suites": [{ + "id": "9c39a96d-d969-4bd7-bba4-074f847f14d0", + "name": "Default Suite", + "persistSession": false, + "parallel": false, + "timeout": 300, + "tests": ["69372d57-4a9f-44a9-ad0c-a82d9a4d7ef0"] + }], + "urls": ["http://localhost:10101/"], + "plugins": [] +} \ No newline at end of file diff --git a/backend/src/integration/resources/SHIBUI-1740-4.side b/backend/src/integration/resources/SHIBUI-1740-4.side new file mode 100644 index 000000000..9c2270371 --- /dev/null +++ b/backend/src/integration/resources/SHIBUI-1740-4.side @@ -0,0 +1,585 @@ +{ + "id": "c0240daf-8c07-479d-b3bf-1462feca6404", + "version": "2.0", + "name": "SHIBUI-1740-4", + "url": "http://localhost:10101", + "tests": [{ + "id": "6932d7f8-1e1b-4c60-9a8b-09769eb17b62", + "name": "SHIBUI-1740-4", + "commands": [{ + "id": "916f36cc-eaee-4ab1-b14b-90caf6aa46cc", + "comment": "", + "command": "open", + "target": "/login", + "targets": [], + "value": "" + }, { + "id": "2fdfe733-4a6d-4189-bfe2-a588e6b95400", + "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": "2ae3301d-5ba7-4eef-b765-64b3c7b61924", + "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": "4d329c94-2200-44d2-8861-6f3b9e739fd7", + "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": "76210fb2-ebed-430b-bc73-7c114fef2c34", + "comment": "", + "command": "open", + "target": "/api/heheheheheheheWipeout", + "targets": [], + "value": "" + }, { + "id": "2efd32dd-9894-4b6c-8730-5337c11b512a", + "comment": "", + "command": "assertText", + "target": "css=body", + "targets": [], + "value": "yes, you did it" + }, { + "id": "6ba7a536-1307-4321-a4f4-009d3715afa0", + "comment": "", + "command": "open", + "target": "/", + "targets": [], + "value": "" + }, { + "id": "15f111a8-69c3-48a1-921e-6f73d2bb5722", + "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": "72231d87-3efa-4e65-af79-11b9e7dea105", + "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": "84633f12-7439-42be-a9a0-081e4c6d563c", + "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": "8168f80c-3ca7-45ec-9a9a-9cbfe320480e", + "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": "6fb9efa3-c73e-4d27-82e6-28b25bf4fb60", + "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": "ee100ee8-d619-4d23-8cbc-ef9fa0e4b974", + "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": "545dab13-5336-445b-ad72-88caf5c7972a", + "comment": "", + "command": "waitForElementPresent", + "target": "id=root_serviceProviderName", + "targets": [], + "value": "30000" + }, { + "id": "df2df867-aa22-4c76-8006-c2decea70eb5", + "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": "Selenium Test" + }, { + "id": "2fac3de0-9e9c-4bb0-b2bd-69283521a91a", + "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": "c142b6f2-dbb1-492a-9273-018f6fba5b59", + "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": "4bbbf2de-2232-4145-821a-db2f4a982c0c", + "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": "a05df50e-1c75-4766-9730-b48bea0e1106", + "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": "f2805304-0121-41ef-9930-8fe8817931e3", + "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": "ed95baf0-3802-4811-8ec0-3ac9ea1a16cf", + "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": "5da3884b-dafe-4dba-99de-13386e714fc6", + "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": "e1a8ef33-1843-4bf7-ac1a-fee969e65587", + "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": "359fe812-44fb-45c8-a7f5-95f9793a669e", + "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": "1bb86278-c815-4367-a431-8e4c7067c10e", + "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": "8cabfe20-0f79-456c-a0ab-5dd7126da79b", + "comment": "", + "command": "click", + "target": "css=.save", + "targets": [ + ["css=.direction:nth-child(2)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button/span[2]", "xpath:idRelative"], + ["xpath=//li[3]/button/span[2]", "xpath:position"] + ], + "value": "" + }, { + "id": "17150592-3f1e-44d2-96eb-c170083c50c1", + "comment": "", + "command": "waitForElementPresent", + "target": "linkText=Selenium Test", + "targets": [], + "value": "30000" + }, { + "id": "fa8f0af2-5777-4e1f-bf2f-52911c900a2b", + "comment": "", + "command": "assertText", + "target": "linkText=Selenium Test", + "targets": [ + ["linkText=Selenium Test", "linkText"], + ["css=td > a", "css:finder"], + ["xpath=//a[contains(text(),'Selenium Test')]", "xpath:link"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/div/table/tbody/tr/td/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/metadata/source/3d88c306-8e8f-45e8-a7b9-a47d4a45468e/configuration/options')]", "xpath:href"], + ["xpath=//td/a", "xpath:position"], + ["xpath=//a[contains(.,'Selenium Test')]", "xpath:innerText"] + ], + "value": "Selenium Test" + }, { + "id": "3e1f32d0-3d4d-4dac-95fd-765337b75e44", + "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": "8b813120-1d11-490b-8311-f7bfb7d81fbf", + "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": "9540861b-6d58-4590-b3f5-588bf4c345c7", + "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": "98b2b40f-f30c-42ad-8a20-55a570026034", + "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": "f224e859-0e06-45d9-996e-969592d60407", + "comment": "", + "command": "waitForElementPresent", + "target": "linkText=Selenium Test", + "targets": [], + "value": "30000" + }, { + "id": "3a33f990-3c63-4446-b4e3-2694ea13a5da", + "comment": "", + "command": "assertText", + "target": "linkText=Selenium Test", + "targets": [ + ["linkText=Selenium Test", "linkText"], + ["css=td > a", "css:finder"], + ["xpath=//a[contains(text(),'Selenium Test')]", "xpath:link"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/div/table/tbody/tr/td/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/metadata/source/3d88c306-8e8f-45e8-a7b9-a47d4a45468e/configuration/options')]", "xpath:href"], + ["xpath=//td/a", "xpath:position"], + ["xpath=//a[contains(.,'Selenium Test')]", "xpath:innerText"] + ], + "value": "Selenium Test" + }, { + "id": "1a1a7dd2-9442-48be-b6a2-f4868dc28c82", + "comment": "", + "command": "assertValue", + "target": "xpath=//td[contains(.,'Selenium Test')]/parent::*/td/select", + "targets": [ + ["id=group-3d88c306-8e8f-45e8-a7b9-a47d4a45468e", "id"], + ["name=group-3d88c306-8e8f-45e8-a7b9-a47d4a45468e", "name"], + ["css=#group-3d88c306-8e8f-45e8-a7b9-a47d4a45468e", "css:finder"], + ["xpath=//select[@id='group-3d88c306-8e8f-45e8-a7b9-a47d4a45468e']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/div/table/tbody/tr/td[6]/select", "xpath:idRelative"], + ["xpath=//select", "xpath:position"] + ], + "value": "nonadmin" + }, { + "id": "a0b10e29-e4f7-4249-bf75-ceb073a25a0f", + "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": "9a8d86eb-636c-4f84-9f76-07dd9ed5f3a9", + "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[3]/select", "xpath:idRelative"], + ["xpath=//tr[4]/td[3]/select", "xpath:position"] + ], + "value": "" + }, { + "id": "712a05ee-e300-472a-a3d0-529dcecf8e3b", + "comment": "", + "command": "select", + "target": "id=role-anonymousUser", + "targets": [], + "value": "label=ROLE_USER" + }, { + "id": "a001ad79-c8ef-4308-a0d7-73843385621d", + "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[4]/span/select", "xpath:idRelative"], + ["xpath=//tr[4]/td[4]/span/select", "xpath:position"] + ], + "value": "" + }, { + "id": "4f1dc865-c72b-4c75-99c7-2ff1257812db", + "comment": "", + "command": "select", + "target": "id=group-anonymousUser", + "targets": [], + "value": "label=A1" + }, { + "id": "8913d9c6-7cca-42cc-abd5-bd1b014e1853", + "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": "9519fd33-870b-4afb-ac1d-ac222bcad784", + "comment": "", + "command": "click", + "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": "" + }, { + "id": "8b6995bd-94bf-4ba2-991e-98e046b4d0a1", + "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": "f324931b-d410-4481-a45b-aa6fd9577635", + "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": "ab1de974-3c1f-4687-b2d3-5b81c8a269a5", + "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": "6bb7f95e-0ee8-4e09-b9bd-46a9f3763455", + "comment": "", + "command": "assertElementNotPresent", + "target": "linkText=Selenium Test", + "targets": [ + ["css=.table-responsive", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/div", "xpath:idRelative"], + ["xpath=//div[2]/div/div/div", "xpath:position"] + ], + "value": "" + }, { + "id": "1f4dfb86-8969-4f2a-a8f1-0873a501f670", + "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": "" + }] + }], + "suites": [{ + "id": "13b2104f-1386-474b-8478-566db394923b", + "name": "Default Suite", + "persistSession": false, + "parallel": false, + "timeout": 300, + "tests": ["6932d7f8-1e1b-4c60-9a8b-09769eb17b62"] + }], + "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 9e97395db..46ff633cd 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 @@ -12,12 +12,17 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ReloadableMetadat import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group import edu.internet2.tier.shibboleth.admin.ui.security.model.Role import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions +import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Profile import org.springframework.stereotype.Component @@ -28,30 +33,59 @@ import javax.annotation.PostConstruct @Component @Profile('dev') class DevConfig { - private final UserRepository adminUserRepository - private final RoleRepository roleRepository - - private final MetadataResolverRepository metadataResolverRepository private final EntityDescriptorRepository entityDescriptorRepository - + private final GroupsRepository groupsRepository + private final MetadataResolverRepository metadataResolverRepository private final OpenSamlObjects openSamlObjects + private final RoleRepository roleRepository + private final UserRepository userRepository + + @Autowired + private UserService userService DevConfig(UserRepository adminUserRepository, + GroupsRepository groupsRepository, MetadataResolverRepository metadataResolverRepository, RoleRepository roleRepository, EntityDescriptorRepository entityDescriptorRepository, - OpenSamlObjects openSamlObjects) { + OpenSamlObjects openSamlObjects, + IGroupService groupService) { - this.adminUserRepository = adminUserRepository + this.userRepository = adminUserRepository this.metadataResolverRepository = metadataResolverRepository this.roleRepository = roleRepository this.entityDescriptorRepository = entityDescriptorRepository this.openSamlObjects = openSamlObjects + this.groupsRepository = groupsRepository + + groupService.ensureAdminGroupExists() } @Transactional @PostConstruct - void createDevUsers() { + void createDevUsersAndGroups() { + def groups = [ + new Group().with { + it.name = "A1" + it.description = "AAA Group" + it.resourceId = "AAA" + it + }, + new Group().with { + it.name = "B1" + it.description = "BBB Group" + it.resourceId = "BBB" + it + }] + groups.each { + try { + groupsRepository.save(it) + } catch (Throwable e) { + // Must already exist (from a unit test) + } + } + groupsRepository.flush() + if (roleRepository.count() == 0) { def roles = [new Role().with { name = 'ROLE_ADMIN' @@ -68,7 +102,7 @@ class DevConfig { } } roleRepository.flush() - if (adminUserRepository.count() == 0) { + if (userRepository.count() == 0) { def users = [new User().with { username = 'admin' password = '{noop}adminpass' @@ -103,40 +137,11 @@ class DevConfig { it }] users.each { - adminUserRepository.save(it) + userService.save(it) } - adminUserRepository.flush() } } - @Transactional - @Profile('fbhmr') - @Bean - MetadataResolver fbhmr(ModelRepresentationConversions modelRepresentationConversions) { - return this.metadataResolverRepository.save(new FileBackedHttpMetadataResolver().with { - enabled = true - xmlId = 'test-fbhmr' - name = 'test-fbhmr' - metadataURL = 'http://md.incommon.org/InCommon/InCommon-metadata.xml' - backingFile = '%{idp.home}/test-fbhmr.xml' - reloadableMetadataResolverAttributes = new ReloadableMetadataResolverAttributes() - httpMetadataResolverAttributes = new HttpMetadataResolverAttributes() - it.metadataFilters.add(new EntityAttributesFilter().with { - it.name = 'test' - it.filterEnabled = true - it.entityAttributesFilterTarget = new EntityAttributesFilterTarget().with { - it.entityAttributesFilterTargetType = EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY - it.value = ["https://carmenwiki.osu.edu/shibboleth"] - return it - } - it.attributeRelease = ['eduPersonPrincipalName', 'givenName', 'surname', 'mail'] - it.relyingPartyOverrides = null - return it - }) - return it - }) - } - @Profile('dhmr') @Transactional @Bean @@ -174,4 +179,32 @@ class DevConfig { it }) } + + @Transactional + @Profile('fbhmr') + @Bean + MetadataResolver fbhmr(ModelRepresentationConversions modelRepresentationConversions) { + return this.metadataResolverRepository.save(new FileBackedHttpMetadataResolver().with { + enabled = true + xmlId = 'test-fbhmr' + name = 'test-fbhmr' + metadataURL = 'http://md.incommon.org/InCommon/InCommon-metadata.xml' + backingFile = '%{idp.home}/test-fbhmr.xml' + reloadableMetadataResolverAttributes = new ReloadableMetadataResolverAttributes() + httpMetadataResolverAttributes = new HttpMetadataResolverAttributes() + it.metadataFilters.add(new EntityAttributesFilter().with { + it.name = 'test' + it.filterEnabled = true + it.entityAttributesFilterTarget = new EntityAttributesFilterTarget().with { + it.entityAttributesFilterTargetType = EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY + it.value = ["https://carmenwiki.osu.edu/shibboleth"] + return it + } + it.attributeRelease = ['eduPersonPrincipalName', 'givenName', 'surname', 'mail'] + it.relyingPartyOverrides = null + return it + }) + return it + }) + } } 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 4ca5b435b..e007c62c3 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 @@ -6,6 +6,8 @@ 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 groovy.util.logging.Slf4j import org.springframework.boot.context.event.ApplicationStartedEvent import org.springframework.context.event.EventListener @@ -19,11 +21,14 @@ class UserBootstrap { private final ShibUIConfiguration shibUIConfiguration private final UserRepository userRepository private final RoleRepository roleRepository + private final UserService userService - UserBootstrap(ShibUIConfiguration shibUIConfiguration, UserRepository userRepository, RoleRepository roleRepository) { + UserBootstrap(ShibUIConfiguration shibUIConfiguration, UserRepository userRepository, RoleRepository roleRepository, UserService userService, IGroupService groupService) { this.shibUIConfiguration = shibUIConfiguration this.userRepository = userRepository this.roleRepository = roleRepository + this.userService = userService + groupService.ensureAdminGroupExists() } @Transactional @@ -50,7 +55,7 @@ class UserBootstrap { it.emailAddress = email it } - userRepository.saveAndFlush(user) + userService.save(user) } } } 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 da33c53f8..c117b415f 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,19 +1,44 @@ 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; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolversPositionOrderContainerRepository; import edu.internet2.tier.shibboleth.admin.ui.scheduled.EntityDescriptorFilesScheduledTasks; import edu.internet2.tier.shibboleth.admin.ui.scheduled.MetadataProvidersScheduledTasks; +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.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.EntityDescriptorService; 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; @@ -21,7 +46,6 @@ 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.JPAEntityDescriptorServiceImpl; 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; @@ -29,33 +53,14 @@ import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolversPositionOrderContainerService; 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.slf4j.Logger; -import org.slf4j.LoggerFactory; -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.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 javax.servlet.http.HttpServletRequest; @Configuration -@EnableConfigurationProperties({CustomPropertiesConfiguration.class, ShibUIConfiguration.class}) +@ComponentScan(basePackages="{ edu.internet2.tier.shibboleth.admin.ui.service }") +@EnableConfigurationProperties({ CustomPropertiesConfiguration.class, ShibUIConfiguration.class }) public class CoreShibUiConfiguration { - private static final Logger logger = LoggerFactory.getLogger(CoreShibUiConfiguration.class); - @Bean public OpenSamlObjects openSamlObjects() { return new OpenSamlObjects(); @@ -65,12 +70,7 @@ public OpenSamlObjects openSamlObjects() { public EntityService jpaEntityService() { return new JPAEntityServiceImpl(openSamlObjects()); } - - @Bean - public EntityDescriptorService jpaEntityDescriptorService(UserService userService) { - return new JPAEntityDescriptorServiceImpl(openSamlObjects(), jpaEntityService(), userService); - } - + @Bean public FilterService jpaFilterService() { return new JPAFilterServiceImpl(); @@ -99,13 +99,18 @@ public AttributeUtility attributeUtility() { @Bean @ConditionalOnProperty(name = "shibui.metadata-dir") - public EntityDescriptorFilesScheduledTasks entityDescriptorFilesScheduledTasks(EntityDescriptorRepository entityDescriptorRepository, @Value("${shibui.metadata-dir}") final String metadataDir) { - return new EntityDescriptorFilesScheduledTasks(metadataDir, entityDescriptorRepository, openSamlObjects(), fileWritingService()); + public EntityDescriptorFilesScheduledTasks entityDescriptorFilesScheduledTasks( + EntityDescriptorRepository entityDescriptorRepository, + @Value("${shibui.metadata-dir}") final String metadataDir) { + return new EntityDescriptorFilesScheduledTasks(metadataDir, entityDescriptorRepository, openSamlObjects(), + fileWritingService()); } @Bean @ConditionalOnProperty(name = "shibui.metadataProviders.target") - public MetadataProvidersScheduledTasks metadataProvidersScheduledTasks(@Value("${shibui.metadataProviders.target}") final Resource resource, final MetadataResolverService metadataResolverService) { + public MetadataProvidersScheduledTasks metadataProvidersScheduledTasks( + @Value("${shibui.metadataProviders.target}") final Resource resource, + final MetadataResolverService metadataResolverService) { return new MetadataProvidersScheduledTasks(resource, metadataResolverService, fileWritingService()); } @@ -124,7 +129,8 @@ public LocaleChangeInterceptor localeChangeInterceptor() { /** * A WebMvcConfigurer that won't mangle the path for the entities endpoint. * - * inspired by [ https://stackoverflow.com/questions/13482020/encoded-slash-2f-with-spring-requestmapping-path-param-gives-http-400 ] + * inspired by [ + * https://stackoverflow.com/questions/13482020/encoded-slash-2f-with-spring-requestmapping-path-param-gives-http-400 ] * * @return configurer */ @@ -166,10 +172,9 @@ public void addInterceptors(InterceptorRegistry registry) { } @Bean - public MetadataResolversPositionOrderContainerService - metadataResolversPositionOrderContainerService(MetadataResolversPositionOrderContainerRepository - positionOrderContainerRepository, - MetadataResolverRepository resolverRepository) { + public MetadataResolversPositionOrderContainerService metadataResolversPositionOrderContainerService( + MetadataResolversPositionOrderContainerRepository positionOrderContainerRepository, + MetadataResolverRepository resolverRepository) { return new DefaultMetadataResolversPositionOrderContainerService(positionOrderContainerRepository, resolverRepository); @@ -201,12 +206,33 @@ public ModelRepresentationConversions modelRepresentationConversions() { } @Bean - public UserService userService(RoleRepository roleRepository, UserRepository userRepository) { - return new UserService(roleRepository, userRepository); + public UserService userService(IGroupService groupService, OwnershipRepository ownershipRepository, RoleRepository roleRepository, UserRepository userRepository) { + return new UserService(groupService, ownershipRepository, roleRepository, userRepository); } @Bean public FileWritingService fileWritingService() { return new FileCheckingFileWritingService(); } + + @Bean + public EntityDescriptorConversionUtils EntityDescriptorConverstionUtilsInit(EntityService entityService, OpenSamlObjects oso) { + EntityDescriptorConversionUtils.setEntityService(entityService); + 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; + } } 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 new file mode 100644 index 000000000..f14d35763 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/MigrationTasksContextLoadedListener.java @@ -0,0 +1,67 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration.auto; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository; +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; + +/** + * After the context loads, do any needed migration tasks + */ +@Component +public class MigrationTasksContextLoadedListener implements ApplicationListener { + @Autowired + private EntityDescriptorRepository entityDescriptorRepository; + + @Autowired + private IGroupService groupService; + + @Autowired + private OwnershipRepository ownershipRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserService userService; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + doshibui_1740_migration(); // do first + } + + @Transactional + private 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. + // the ADMIN_GROUP has already been setup (just above) + try { + entityDescriptorRepository.findAllByIdOfOwnerIsNull().forEach(ed -> { + ed.setIdOfOwner(Group.ADMIN_GROUP.getOwnerId()); + ed = entityDescriptorRepository.saveAndFlush(ed); + ownershipRepository.saveAndFlush(new Ownership(Group.ADMIN_GROUP, ed)); + }); + } + catch (NullPointerException e) { + // This block was added due to a number of mock test where NPEs happened. Rather than wire more mock junk + // into tests that are only trying to compensate for this migration, this is here + } + + userRepository.findAll().forEach(user -> { + if (user.getGroupId() == null) { + userService.save(user); // this will ensure group is set as the default user group + } + }); + + } +} 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 f75f323be..3d66de957 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 @@ -5,6 +5,7 @@ import edu.internet2.tier.shibboleth.admin.ui.security.model.User; import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository; import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import edu.internet2.tier.shibboleth.admin.ui.security.springsecurity.AdminUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -28,10 +29,10 @@ import java.util.Collections; +import javax.transaction.Transactional; + /** * Web security configuration. - *

- * Workaround for slashes in URL from [https://stackoverflow.com/questions/48453980/spring-5-0-3-requestrejectedexception-the-request-was-rejected-because-the-url] */ @Configuration @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) @@ -46,6 +47,9 @@ public class WebSecurityConfig { @Autowired private UserRepository userRepository; + @Autowired + private UserService userService; + @Autowired private RoleRepository roleRepository; @@ -82,6 +86,7 @@ protected void configure(HttpSecurity http) throws Exception { } @Override + @Transactional protected void configure(AuthenticationManagerBuilder auth) throws Exception { // TODO: more configurable authentication PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); @@ -100,10 +105,10 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { }); u.setRoles(Collections.singleton(adminRole)); u.setEmailAddress("admin@localhost"); - return userRepository.saveAndFlush(u); + return userService.save(u); }); adminUser.setPassword(defaultPassword); - userRepository.saveAndFlush(adminUser); + userService.save(adminUser); auth .inMemoryAuthentication() diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/DangerController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/DangerController.java index fe0c2d41a..532731bfc 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/DangerController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/DangerController.java @@ -1,9 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; -import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; -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.repository.MetadataResolversPositionOrderContainerRepository; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -11,26 +8,55 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; +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.repository.MetadataResolversPositionOrderContainerRepository; +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.service.EntityDescriptorService; +import lombok.extern.slf4j.Slf4j; + @Controller @RequestMapping(value = "/api/heheheheheheheWipeout") @Profile("very-dangerous") +@Slf4j public class DangerController { - private final MetadataResolverRepository metadataResolverRepository; - private final EntityDescriptorRepository entityDescriptorRepository; - private final FilterRepository filterRepository; - private final MetadataResolversPositionOrderContainerRepository metadataResolversPositionOrderContainerRepository; - - public DangerController(final MetadataResolverRepository metadataResolverRepository, final EntityDescriptorRepository entityDescriptorRepository, final FilterRepository filterRepository, final MetadataResolversPositionOrderContainerRepository metadataResolversPositionOrderContainerRepository) { - this.metadataResolverRepository = metadataResolverRepository; - this.entityDescriptorRepository = entityDescriptorRepository; - this.filterRepository = filterRepository; - this.metadataResolversPositionOrderContainerRepository = metadataResolversPositionOrderContainerRepository; - } + @Autowired + private EntityDescriptorService entityDescriptorService; + + @Autowired + private EntityDescriptorRepository edRepo; + + @Autowired + private FilterRepository filterRepository; + + @Autowired + private IGroupService groupService; + + @Autowired + private MetadataResolverRepository metadataResolverRepository; + + @Autowired + private MetadataResolversPositionOrderContainerRepository metadataResolversPositionOrderContainerRepository; + @Autowired + private OwnershipRepository ownershipRepository; + @Transactional @GetMapping public ResponseEntity wipeOut() { - this.entityDescriptorRepository.deleteAll(); + edRepo.findAll().forEach(ed -> { + try { + ed.setServiceEnabled(false); + edRepo.save(ed); + ownershipRepository.deleteEntriesForOwnedObject(ed); + entityDescriptorService.delete(ed.getResourceId()); + } + catch (Throwable e) { + System.out.println("@@@@@@ error deleting" + e.getMessage()); + } + }); this.metadataResolverRepository.deleteAll(); this.filterRepository.deleteAll(); this.metadataResolversPositionOrderContainerRepository.deleteAll(); 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 914ae8d1b..f8e28fc75 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 @@ -3,15 +3,18 @@ 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.domain.versioning.Version; +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; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.security.model.User; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionService; +import lombok.extern.slf4j.Slf4j; + import org.opensaml.core.xml.io.MarshallingException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.http.HttpHeaders; @@ -20,6 +23,7 @@ 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.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -32,286 +36,139 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import javax.annotation.PostConstruct; + import java.net.URI; +import java.util.ConcurrentModificationException; import java.util.List; import java.util.stream.Collectors; @RestController @RequestMapping("/api") +@Slf4j public class EntityDescriptorController { - + static URI getResourceUriFor(String resourceId) { + return ServletUriComponentsBuilder + .fromCurrentServletMapping().path("/api/EntityDescriptor") + .pathSegment(resourceId) + .build() + .toUri(); + } + @Autowired - private EntityDescriptorRepository entityDescriptorRepository; + private EntityDescriptorService entityDescriptorService; @Autowired private OpenSamlObjects openSamlObjects; - @Autowired - private EntityDescriptorService entityDescriptorService; + private RestTemplate restTemplate; @Autowired RestTemplateBuilder restTemplateBuilder; - private UserService userService; - - private RestTemplate restTemplate; - private EntityDescriptorVersionService versionService; - private static Logger LOGGER = LoggerFactory.getLogger(EntityDescriptorController.class); - - public EntityDescriptorController(UserService userService, EntityDescriptorVersionService versionService) { - this.userService = userService; + public EntityDescriptorController(EntityDescriptorVersionService versionService) { this.versionService = versionService; } - @PostConstruct - public void initRestTemplate() { - this.restTemplate = restTemplateBuilder.build(); - } - @PostMapping("/EntityDescriptor") @Transactional - public ResponseEntity create(@RequestBody EntityDescriptorRepresentation edRepresentation) { - final String entityId = edRepresentation.getEntityId(); - - ResponseEntity entityDescriptorEnablingDeniedResponse = entityDescriptorEnablePermissionsCheck(edRepresentation.isServiceEnabled()); - if (entityDescriptorEnablingDeniedResponse != null) { - return entityDescriptorEnablingDeniedResponse; - } - - ResponseEntity existingEntityDescriptorConflictResponse = existingEntityDescriptorCheck(entityId); - if (existingEntityDescriptorConflictResponse != null) { - return existingEntityDescriptorConflictResponse; - } - - EntityDescriptor ed = (EntityDescriptor) entityDescriptorService.createDescriptorFromRepresentation(edRepresentation); - - EntityDescriptor persistedEd = entityDescriptorRepository.save(ed); - edRepresentation.setId(persistedEd.getResourceId()); - edRepresentation.setCreatedDate(persistedEd.getCreatedDate()); - return ResponseEntity.created(getResourceUriFor(persistedEd)).body(entityDescriptorService.createRepresentationFromDescriptor(persistedEd)); + public ResponseEntity create(@RequestBody EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityIdExistsException { + EntityDescriptorRepresentation persistedEd = entityDescriptorService.createNew(edRepresentation); + return ResponseEntity.created(getResourceUriFor(persistedEd.getId())).body(persistedEd); } - @PostMapping(value = "/EntityDescriptor", consumes = "application/xml") + @Secured("ROLE_ADMIN") + @DeleteMapping(value = "/EntityDescriptor/{resourceId}") @Transactional - public ResponseEntity upload(@RequestBody byte[] entityDescriptorXml, @RequestParam String spName) throws Exception { - return handleUploadingEntityDescriptorXml(entityDescriptorXml, spName); + public ResponseEntity deleteOne(@PathVariable String resourceId) throws ForbiddenException, EntityNotFoundException { + entityDescriptorService.delete(resourceId); + return ResponseEntity.noContent().build(); } - @PostMapping(value = "/EntityDescriptor", consumes = "application/x-www-form-urlencoded") + @GetMapping("/EntityDescriptors") @Transactional - public ResponseEntity upload(@RequestParam String metadataUrl, @RequestParam String spName) throws Exception { - try { - byte[] xmlContents = this.restTemplate.getForObject(metadataUrl, byte[].class); - return handleUploadingEntityDescriptorXml(xmlContents, spName); - } catch (Throwable e) { - LOGGER.error("Error fetching XML metadata from the provided URL: [{}]. The error is: {}", metadataUrl, e); - LOGGER.error(e.getMessage(), e); - return ResponseEntity - .badRequest() - .body(String.format("Error fetching XML metadata from the provided URL. Error: %s", e.getMessage())); - } + public ResponseEntity getAll() throws ForbiddenException { + return ResponseEntity.ok(entityDescriptorService.getAllRepresentationsBasedOnUserAccess()); } - @PutMapping("/EntityDescriptor/{resourceId}") + @GetMapping("/EntityDescriptor/{resourceId}/Versions") @Transactional - public ResponseEntity update(@RequestBody EntityDescriptorRepresentation edRepresentation, @PathVariable String resourceId) { - User currentUser = userService.getCurrentUser(); - EntityDescriptor existingEd = entityDescriptorRepository.findByResourceId(resourceId); - if (existingEd == null) { - return ResponseEntity.notFound().build(); - } else { - if (currentUser != null && (currentUser.getRole().equals("ROLE_ADMIN") || currentUser.getUsername().equals(existingEd.getCreatedBy()))) { - if (!existingEd.isServiceEnabled()) { - ResponseEntity entityDescriptorEnablingDeniedResponse = entityDescriptorEnablePermissionsCheck(edRepresentation.isServiceEnabled()); - if (entityDescriptorEnablingDeniedResponse != null) { - return entityDescriptorEnablingDeniedResponse; - } - } - - // Verify we're the only one attempting to update the EntityDescriptor - if (edRepresentation.getVersion() != existingEd.hashCode()) { - return new ResponseEntity(HttpStatus.CONFLICT); - } - - entityDescriptorService.updateDescriptorFromRepresentation(existingEd, edRepresentation); - existingEd = entityDescriptorRepository.save(existingEd); - - return ResponseEntity.ok().body(entityDescriptorService.createRepresentationFromDescriptor(existingEd)); - } else { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, - "You are not authorized to perform the requested operation.")); - } - } + public ResponseEntity getAllVersions(@PathVariable String resourceId) 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.findVersionsForEntityDescriptor(ed.getResourceId())); } - @GetMapping("/EntityDescriptors") - @Transactional(readOnly = true) - public ResponseEntity getAll() { - User currentUser = userService.getCurrentUser(); - if (currentUser != null) { - if (currentUser.getRole().equals("ROLE_ADMIN")) { - return ResponseEntity.ok(entityDescriptorRepository.findAllStreamByCustomQuery() - .map(ed -> entityDescriptorService.createRepresentationFromDescriptor(ed)) - .collect(Collectors.toList())); - } else { - return ResponseEntity.ok(entityDescriptorRepository.findAllStreamByCreatedBy(currentUser.getUsername()) - .map(ed -> entityDescriptorService.createRepresentationFromDescriptor(ed)) - .collect(Collectors.toList())); - } - } else { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, - "You are not authorized to perform the requested operation.")); - } + @Secured("ROLE_ADMIN") + @Transactional + @GetMapping(value = "/EntityDescriptor/disabledNonAdmin") + public Iterable getDisabledAndNotOwnedByAdmin() throws ForbiddenException { + return entityDescriptorService.getAllDisabledAndNotOwnedByAdmin(); } @GetMapping("/EntityDescriptor/{resourceId}") - @Transactional(readOnly = true) - public ResponseEntity getOne(@PathVariable String resourceId) { - User currentUser = userService.getCurrentUser(); - EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); - if (ed == null) { - return ResponseEntity.notFound().build(); - } else { - if (currentUser != null && (currentUser.getRole().equals("ROLE_ADMIN") || currentUser.getUsername().equals(ed.getCreatedBy()))) { - EntityDescriptorRepresentation edr = entityDescriptorService.createRepresentationFromDescriptor(ed); - return ResponseEntity.ok(edr); - } else { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, - "You are not authorized to perform the requested operation.")); - } - } + @Transactional + public ResponseEntity getOne(@PathVariable String resourceId) throws EntityNotFoundException, ForbiddenException { + return ResponseEntity.ok(entityDescriptorService + .createRepresentationFromDescriptor(entityDescriptorService.getEntityDescriptorByResourceId(resourceId))); } @GetMapping(value = "/EntityDescriptor/{resourceId}", produces = "application/xml") - @Transactional(readOnly = true) - public ResponseEntity getOneXml(@PathVariable String resourceId) throws MarshallingException { - User currentUser = userService.getCurrentUser(); - EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); - if (ed == null) { - return ResponseEntity.notFound().build(); - } else { - if (currentUser != null && (currentUser.getRole().equals("ROLE_ADMIN") || currentUser.getUsername().equals(ed.getCreatedBy()))) { - final String xml = this.openSamlObjects.marshalToXmlString(ed); - return ResponseEntity.ok(xml); - } else { - return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); - } - } - } - - @Transactional(readOnly = true) - @GetMapping(value = "/EntityDescriptor/disabledNonAdmin") - public Iterable getDisabledAndNotOwnedByAdmin() { - return entityDescriptorRepository.findAllDisabledAndNotOwnedByAdmin() - .map(ed -> entityDescriptorService.createRepresentationFromDescriptor(ed)) - .collect(Collectors.toList()); - } - - @Secured("ROLE_ADMIN") - @DeleteMapping(value = "/EntityDescriptor/{resourceId}") @Transactional - public ResponseEntity deleteOne(@PathVariable String resourceId) { - EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); - if (ed == null) { - return ResponseEntity.notFound().build(); - } else if (ed.isServiceEnabled()) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, "Deleting an enabled Metadata Source is not allowed. Disable the source and try again.")); - } else { - entityDescriptorRepository.delete(ed); - return ResponseEntity.noContent().build(); - } - } - - //Versioning endpoints - - @GetMapping("/EntityDescriptor/{resourceId}/Versions") - @Transactional(readOnly = true) - public ResponseEntity getAllVersions(@PathVariable String resourceId) { - EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); - if (ed == null) { - return ResponseEntity.notFound().build(); - } - List versions = versionService.findVersionsForEntityDescriptor(resourceId); - if (versions.isEmpty()) { - return ResponseEntity.notFound().build(); - } - if(isAuthorizedFor(ed.getCreatedBy())) { - return ResponseEntity.ok(versions); - } - return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + public ResponseEntity getOneXml(@PathVariable String resourceId) throws MarshallingException, EntityNotFoundException, ForbiddenException { + EntityDescriptor ed = entityDescriptorService.getEntityDescriptorByResourceId(resourceId); + final String xml = this.openSamlObjects.marshalToXmlString(ed); + return ResponseEntity.ok(xml); } @GetMapping("/EntityDescriptor/{resourceId}/Versions/{versionId}") - @Transactional(readOnly = true) - public ResponseEntity getSpecificVersion(@PathVariable String resourceId, @PathVariable String versionId) { - EntityDescriptorRepresentation edRepresentation = - versionService.findSpecificVersionOfEntityDescriptor(resourceId, versionId); - - if (edRepresentation == null) { - return ResponseEntity.notFound().build(); - } - if(isAuthorizedFor(edRepresentation.getCreatedBy())) { - return ResponseEntity.ok(edRepresentation); - } - return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + @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)); } - - //Private methods - - private static URI getResourceUriFor(EntityDescriptor ed) { - return ServletUriComponentsBuilder - .fromCurrentServletMapping().path("/api/EntityDescriptor") - .pathSegment(ed.getResourceId()) - .build() - .toUri(); + + private ResponseEntity handleUploadingEntityDescriptorXml(byte[] rawXmlBytes, String spName) throws Exception { + final EntityDescriptor ed = EntityDescriptor.class.cast(openSamlObjects.unmarshalFromXml(rawXmlBytes)); + ed.setServiceProviderName(spName); + + EntityDescriptorRepresentation persistedEd = entityDescriptorService.createNew(ed); + return ResponseEntity.created(getResourceUriFor(persistedEd.getId())).body(persistedEd); } - private ResponseEntity existingEntityDescriptorCheck(String entityId) { - final EntityDescriptor ed = entityDescriptorRepository.findByEntityID(entityId); - if (ed != null) { - HttpHeaders headers = new HttpHeaders(); - headers.setLocation(getResourceUriFor(ed)); - return ResponseEntity - .status(HttpStatus.CONFLICT) - .headers(headers) - .body(new ErrorResponse(String.valueOf(HttpStatus.CONFLICT.value()), String.format("The entity descriptor with entity id [%s] already exists.", entityId))); - } - //No existing entity descriptor, which is an OK condition indicated by returning a null conflict response - return null; + @PostConstruct + public void initRestTemplate() { + this.restTemplate = restTemplateBuilder.build(); } - private ResponseEntity entityDescriptorEnablePermissionsCheck(boolean serviceEnabled) { - User user = userService.getCurrentUser(); - if (user != null) { - if (serviceEnabled && !user.getRole().equals("ROLE_ADMIN")) { - return ResponseEntity.status(HttpStatus.FORBIDDEN) - .body(new ErrorResponse(HttpStatus.FORBIDDEN, "You do not have the permissions necessary to enable this service.")); - } - } - return null; + @PutMapping("/EntityDescriptor/{resourceId}") + @Transactional + public ResponseEntity update(@RequestBody EntityDescriptorRepresentation edRepresentation, @PathVariable String resourceId) throws ForbiddenException, ConcurrentModificationException, EntityNotFoundException { + edRepresentation.setId(resourceId); // This should be the same already, but just to be safe... + EntityDescriptorRepresentation result = entityDescriptorService.update(edRepresentation); + return ResponseEntity.ok().body(result); } - private ResponseEntity handleUploadingEntityDescriptorXml(byte[] rawXmlBytes, String spName) throws Exception { - final EntityDescriptor ed = EntityDescriptor.class.cast(openSamlObjects.unmarshalFromXml(rawXmlBytes)); - - ResponseEntity existingEntityDescriptorConflictResponse = existingEntityDescriptorCheck(ed.getEntityID()); - if (existingEntityDescriptorConflictResponse != null) { - return existingEntityDescriptorConflictResponse; - } - - ed.setServiceProviderName(spName); - final EntityDescriptor persistedEd = entityDescriptorRepository.save(ed); - return ResponseEntity.created(getResourceUriFor(persistedEd)) - .body(entityDescriptorService.createRepresentationFromDescriptor(persistedEd)); + @PostMapping(value = "/EntityDescriptor", consumes = "application/xml") + @Transactional + public ResponseEntity upload(@RequestBody byte[] entityDescriptorXml, @RequestParam String spName) throws Exception { + return handleUploadingEntityDescriptorXml(entityDescriptorXml, spName); } - private boolean isAuthorizedFor(String username) { - User u = userService.getCurrentUser(); - return (u != null) && - (u.getRole().equals("ROLE_ADMIN") - || (u.getUsername().equals(username))); + @PostMapping(value = "/EntityDescriptor", consumes = "application/x-www-form-urlencoded") + @Transactional + public ResponseEntity upload(@RequestParam String metadataUrl, @RequestParam String spName) throws Exception { + try { + byte[] xmlContents = this.restTemplate.getForObject(metadataUrl, byte[].class); + return handleUploadingEntityDescriptorXml(xmlContents, spName); + } catch (Throwable e) { + log.error("Error fetching XML metadata from the provided URL: [{}]. The error is: {}", metadataUrl, e); + log.error(e.getMessage(), e); + return ResponseEntity + .badRequest() + .body(String.format("Error fetching XML metadata from the provided URL. Error: %s", e.getMessage())); + } } - } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java new file mode 100644 index 000000000..d0b18f415 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java @@ -0,0 +1,44 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import java.util.ConcurrentModificationException; + +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 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; + +@ControllerAdvice(assignableTypes = {EntityDescriptorController.class}) +public class EntityDescriptorControllerExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler({ ConcurrentModificationException.class }) + public ResponseEntity handleConcurrentModificationException(ConcurrentModificationException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponse(HttpStatus.CONFLICT, e.getMessage())); + } + + @ExceptionHandler({ EntityIdExistsException.class }) + public ResponseEntity handleEntityExistsException(EntityIdExistsException e, WebRequest request) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(EntityDescriptorController.getResourceUriFor(e.getMessage())); + return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers).body(new ErrorResponse( + String.valueOf(HttpStatus.CONFLICT.value()), + String.format("The entity descriptor with entity id [%s] already exists.", e.getMessage()))); + + } + + @ExceptionHandler({ EntityNotFoundException.class }) + public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(HttpStatus.NOT_FOUND, e.getMessage())); + } + + @ExceptionHandler({ ForbiddenException.class }) + public ResponseEntity handleForbiddenAccess(ForbiddenException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, e.getMessage())); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAuditable.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAuditable.java index 3d2ed1391..445d4cc92 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAuditable.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAuditable.java @@ -1,16 +1,9 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; -import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.EqualsAndHashCode; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; -import org.hibernate.envers.Audited; -import org.springframework.data.annotation.CreatedBy; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedBy; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import javax.persistence.Column; import javax.persistence.EntityListeners; @@ -20,12 +13,20 @@ import javax.persistence.MappedSuperclass; import javax.persistence.Transient; import javax.validation.constraints.NotNull; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; + +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.envers.Audited; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.EqualsAndHashCode; @MappedSuperclass @@ -41,11 +42,13 @@ public abstract class AbstractAuditable implements Auditable { @CreationTimestamp @CreatedDate @Column(nullable = false, updatable = false) + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS") private LocalDateTime createdDate; @UpdateTimestamp @LastModifiedDate @Column(nullable = false) + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS") private LocalDateTime modifiedDate; @Column(name = "created_by") 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 dc036fa7f..a3acff06b 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 @@ -1,10 +1,18 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.common.base.MoreObjects; import com.google.common.collect.Lists; + +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownable; +import edu.internet2.tier.shibboleth.admin.ui.security.model.OwnableType; import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + import org.hibernate.envers.Audited; import org.hibernate.envers.NotAudited; +import org.hibernate.envers.RelationTargetAuditMode; import org.opensaml.core.xml.XMLObject; import org.springframework.util.StringUtils; @@ -12,6 +20,7 @@ import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; import javax.persistence.OneToMany; import javax.persistence.OneToOne; import javax.persistence.OrderColumn; @@ -26,32 +35,9 @@ @Entity -@EqualsAndHashCode(callSuper = true, exclude={"versionModifiedTimestamp"}) +@EqualsAndHashCode(callSuper = true) @Audited -public class EntityDescriptor extends AbstractDescriptor implements org.opensaml.saml.saml2.metadata.EntityDescriptor { - private String localId; - - private String entityID; - - private String serviceProviderName; - - private boolean serviceEnabled; - - private String resourceId; - - private Long versionModifiedTimestamp; - - @OneToOne(cascade = CascadeType.ALL) - private Organization organization; - - @OneToMany(cascade = CascadeType.ALL) - @OrderColumn - private List contactPersons = new ArrayList<>(); - - @OneToMany(cascade = CascadeType.ALL) - @OrderColumn - private List roleDescriptors; - +public class EntityDescriptor extends AbstractDescriptor implements org.opensaml.saml.saml2.metadata.EntityDescriptor, Ownable { @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "entitydesc_addlmetdatlocations_id") @OrderColumn @@ -60,38 +46,80 @@ public class EntityDescriptor extends AbstractDescriptor implements org.opensaml @OneToOne(cascade = CascadeType.ALL) @NotAudited - private AuthnAuthorityDescriptor authnAuthorityDescriptor; - + private AffiliationDescriptor affiliationDescriptor; + @OneToOne(cascade = CascadeType.ALL) @NotAudited private AttributeAuthorityDescriptor attributeAuthorityDescriptor; @OneToOne(cascade = CascadeType.ALL) @NotAudited - private PDPDescriptor pdpDescriptor; + private AuthnAuthorityDescriptor authnAuthorityDescriptor; + + @OneToMany(cascade = CascadeType.ALL) + @OrderColumn + private List contactPersons = new ArrayList<>(); + + private String entityID; + + private String localId; + @OneToOne(cascade = CascadeType.ALL) + private Organization organization; + + @Getter + @Setter + private String idOfOwner; + @OneToOne(cascade = CascadeType.ALL) @NotAudited - private AffiliationDescriptor affiliationDescriptor; + private PDPDescriptor pdpDescriptor; + + private String resourceId; + + @OneToMany(cascade = CascadeType.ALL) + @OrderColumn + private List roleDescriptors; + + private boolean serviceEnabled; + + private String serviceProviderName; + + @EqualsAndHashCode.Exclude + private Long versionModifiedTimestamp; public EntityDescriptor() { super(); this.resourceId = UUID.randomUUID().toString(); } - public void setVersionModifiedTimestamp(Long versionModifiedTimestamp) { - this.versionModifiedTimestamp = versionModifiedTimestamp; + public void addContactPerson(ContactPerson contactPerson) { + this.contactPersons.add(contactPerson); } - //getters and setters @Override - public String getID() { - return this.localId; + public List getAdditionalMetadataLocations() { + return Lists.newArrayList(additionalMetadataLocations); } @Override - public void setID(String id) { - this.localId = id; + public AffiliationDescriptor getAffiliationDescriptor() { + return affiliationDescriptor; + } + + @Override + public AttributeAuthorityDescriptor getAttributeAuthorityDescriptor(String s) { + return attributeAuthorityDescriptor; + } + + @Override + public AuthnAuthorityDescriptor getAuthnAuthorityDescriptor(String s) { + return authnAuthorityDescriptor; + } + + @Override + public List getContactPersons() { + return (List) (List) this.contactPersons; } @Override @@ -100,32 +128,60 @@ public String getEntityID() { } @Override - public void setEntityID(String entityID) { - this.entityID = entityID; + public String getID() { + return this.localId; } - public String getServiceProviderName() { - return serviceProviderName; + @Override + @Transient + public IDPSSODescriptor getIDPSSODescriptor(String s) { + return (IDPSSODescriptor) this.getRoleDescriptors() + .stream() + .filter(p -> p instanceof org.opensaml.saml.saml2.metadata.IDPSSODescriptor && (StringUtils.isEmpty(s) ? true :p.isSupportedProtocol(s))) + .findFirst() + .orElse(null); } - public void setServiceProviderName(String serviceProviderName) { - this.serviceProviderName = serviceProviderName; + @Transient + public Optional getOptionalSPSSODescriptor() { + return this.getOptionalSPSSODescriptor(""); } - public boolean isServiceEnabled() { - return serviceEnabled; + @Transient + public Optional getOptionalSPSSODescriptor(String s) { + return Optional.ofNullable(this.getSPSSODescriptor(s)); } - public void setServiceEnabled(boolean serviceEnabled) { - this.serviceEnabled = serviceEnabled; + @Nullable + @Override + public List getOrderedChildren() { + final ArrayList children = new ArrayList<>(); + + if (getSignature() != null) { + children.add(getSignature()); + } + children.add(getExtensions()); + children.addAll(this.getRoleDescriptors()); + children.add(getAffiliationDescriptor()); + children.add(getOrganization()); + children.addAll(this.getContactPersons()); + children.addAll(this.getAdditionalMetadataLocations()); + + return Collections.unmodifiableList(children); } - public String getResourceId() { - return resourceId; + @Override + public org.opensaml.saml.saml2.metadata.Organization getOrganization() { + return organization; } - public void setResourceId(String resourceId) { - this.resourceId = resourceId; + @Override + public PDPDescriptor getPDPDescriptor(String s) { + return pdpDescriptor; + } + + public String getResourceId() { + return resourceId; } @Override @@ -138,10 +194,6 @@ public List getRoleDescriptors( return (List) (List) this.roleDescriptors; } - public void setRoleDescriptors(List roleDescriptors) { - this.roleDescriptors = roleDescriptors; - } - @Override public List getRoleDescriptors(QName qName) { return this.getRoleDescriptors() @@ -158,14 +210,8 @@ public List getRoleDescriptors( .collect(Collectors.toList()); } - @Override - @Transient - public IDPSSODescriptor getIDPSSODescriptor(String s) { - return (IDPSSODescriptor) this.getRoleDescriptors() - .stream() - .filter(p -> p instanceof org.opensaml.saml.saml2.metadata.IDPSSODescriptor && (StringUtils.isEmpty(s) ? true :p.isSupportedProtocol(s))) - .findFirst() - .orElse(null); + public String getServiceProviderName() { + return serviceProviderName; } @Override @@ -178,56 +224,39 @@ public SPSSODescriptor getSPSSODescriptor(String s) { .orElse(null); } - @Transient - public Optional getOptionalSPSSODescriptor(String s) { - return Optional.ofNullable(this.getSPSSODescriptor(s)); - } - - @Transient - public Optional getOptionalSPSSODescriptor() { - return this.getOptionalSPSSODescriptor(""); - } - - @Override - public AuthnAuthorityDescriptor getAuthnAuthorityDescriptor(String s) { - return authnAuthorityDescriptor; + public boolean isServiceEnabled() { + return serviceEnabled; } - public void setAuthnAuthorityDescriptor(AuthnAuthorityDescriptor authnAuthorityDescriptor) { - this.authnAuthorityDescriptor = authnAuthorityDescriptor; + public void setAdditionalMetadataLocations(List additionalMetadataLocations) { + this.additionalMetadataLocations = additionalMetadataLocations; } @Override - public AttributeAuthorityDescriptor getAttributeAuthorityDescriptor(String s) { - return attributeAuthorityDescriptor; + public void setAffiliationDescriptor(org.opensaml.saml.saml2.metadata.AffiliationDescriptor affiliationDescriptor) { + this.affiliationDescriptor = (AffiliationDescriptor) affiliationDescriptor; } public void setAttributeAuthorityDescriptor(AttributeAuthorityDescriptor attributeAuthorityDescriptor) { this.attributeAuthorityDescriptor = attributeAuthorityDescriptor; } - @Override - public PDPDescriptor getPDPDescriptor(String s) { - return pdpDescriptor; - } - - public void setPdpDescriptor(PDPDescriptor pdpDescriptor) { - this.pdpDescriptor = pdpDescriptor; + public void setAuthnAuthorityDescriptor(AuthnAuthorityDescriptor authnAuthorityDescriptor) { + this.authnAuthorityDescriptor = authnAuthorityDescriptor; } - @Override - public AffiliationDescriptor getAffiliationDescriptor() { - return affiliationDescriptor; + public void setContactPersons(List contactPersons) { + this.contactPersons = contactPersons; } @Override - public void setAffiliationDescriptor(org.opensaml.saml.saml2.metadata.AffiliationDescriptor affiliationDescriptor) { - this.affiliationDescriptor = (AffiliationDescriptor) affiliationDescriptor; + public void setEntityID(String entityID) { + this.entityID = entityID; } @Override - public org.opensaml.saml.saml2.metadata.Organization getOrganization() { - return organization; + public void setID(String id) { + this.localId = id; } @Override @@ -235,26 +264,28 @@ public void setOrganization(org.opensaml.saml.saml2.metadata.Organization organi this.organization = (Organization) organization; } - @Override - public List getContactPersons() { - return (List) (List) this.contactPersons; + public void setPdpDescriptor(PDPDescriptor pdpDescriptor) { + this.pdpDescriptor = pdpDescriptor; } - public void addContactPerson(ContactPerson contactPerson) { - this.contactPersons.add(contactPerson); + public void setResourceId(String resourceId) { + this.resourceId = resourceId; } - public void setContactPersons(List contactPersons) { - this.contactPersons = contactPersons; + public void setRoleDescriptors(List roleDescriptors) { + this.roleDescriptors = roleDescriptors; } - @Override - public List getAdditionalMetadataLocations() { - return Lists.newArrayList(additionalMetadataLocations); + public void setServiceEnabled(boolean serviceEnabled) { + this.serviceEnabled = serviceEnabled; } - public void setAdditionalMetadataLocations(List additionalMetadataLocations) { - this.additionalMetadataLocations = additionalMetadataLocations; + public void setServiceProviderName(String serviceProviderName) { + this.serviceProviderName = serviceProviderName; + } + + public void setVersionModifiedTimestamp(Long versionModifiedTimestamp) { + this.versionModifiedTimestamp = versionModifiedTimestamp; } @Override @@ -265,22 +296,12 @@ public String toString() { .add("id", id) .toString(); } - - @Nullable - @Override - public List getOrderedChildren() { - final ArrayList children = new ArrayList<>(); - - if (getSignature() != null) { - children.add(getSignature()); - } - children.add(getExtensions()); - children.addAll(this.getRoleDescriptors()); - children.add(getAffiliationDescriptor()); - children.add(getOrganization()); - children.addAll(this.getContactPersons()); - children.addAll(this.getAdditionalMetadataLocations()); - - return Collections.unmodifiableList(children); + + public String getObjectId() { + return entityID; + } + + public OwnableType getOwnableType() { + return OwnableType.ENTITY_DESCRIPTOR; } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java index 32063271e..821d9817d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java @@ -4,6 +4,9 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + import javax.validation.constraints.NotNull; import java.io.Serializable; import java.time.LocalDateTime; @@ -12,231 +15,234 @@ import java.util.Map; public class EntityDescriptorRepresentation implements Serializable { + private static final long serialVersionUID = 7753435553892353966L; - public EntityDescriptorRepresentation() { - } + private List assertionConsumerServices; - public EntityDescriptorRepresentation(String id, - String entityId, - String serviceProviderName, - boolean serviceEnabled, - LocalDateTime createdDate, - LocalDateTime modifiedDate) { - this.id = id; - this.entityId = entityId; - this.serviceProviderName = serviceProviderName; - this.serviceEnabled = serviceEnabled; - this.createdDate = createdDate; - this.modifiedDate = modifiedDate; - } + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private List attributeRelease; - private static final long serialVersionUID = 7753435553892353966L; + private List contacts; - private String id; + private String createdBy; - @NotNull - private String serviceProviderName; + private LocalDateTime createdDate; + + @JsonProperty + private boolean current; @NotNull private String entityId; - //TODO: review requirement - private OrganizationRepresentation organization = new OrganizationRepresentation(); + @Setter + @Getter + private String idOfOwner; + + private String id; - private List contacts; + private List logoutEndpoints; @JsonInclude(JsonInclude.Include.NON_EMPTY) private MduiRepresentation mdui; - private ServiceProviderSsoDescriptorRepresentation serviceProviderSsoDescriptor; + private LocalDateTime modifiedDate; - private List logoutEndpoints; + //TODO: review requirement + private OrganizationRepresentation organization = new OrganizationRepresentation(); - private SecurityInfoRepresentation securityInfo; + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Map relyingPartyOverrides; - private List assertionConsumerServices; + private SecurityInfoRepresentation securityInfo; private boolean serviceEnabled; - private LocalDateTime createdDate; - - private LocalDateTime modifiedDate; - - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private Map relyingPartyOverrides; + @NotNull + private String serviceProviderName; - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private List attributeRelease; + private ServiceProviderSsoDescriptorRepresentation serviceProviderSsoDescriptor; private int version; - private String createdBy; - - @JsonProperty - private boolean current; - - public String getId() { - return id; + public EntityDescriptorRepresentation() { } - public void setId(String id) { + public EntityDescriptorRepresentation(String id, + String entityId, + String serviceProviderName, + boolean serviceEnabled, + LocalDateTime createdDate, + LocalDateTime modifiedDate) { this.id = id; + this.entityId = entityId; + this.serviceProviderName = serviceProviderName; + this.serviceEnabled = serviceEnabled; + this.createdDate = createdDate; + this.modifiedDate = modifiedDate; } - public String getServiceProviderName() { - return serviceProviderName; + public List getAssertionConsumerServices() { + return assertionConsumerServices; } - public void setServiceProviderName(String serviceProviderName) { - this.serviceProviderName = serviceProviderName; + public List getAttributeRelease() { + return attributeRelease; } - public String getEntityId() { - return entityId; + public List getContacts() { + return contacts; } - public void setEntityId(String entityId) { - this.entityId = entityId; + public String getCreatedBy() { + return createdBy; } - public OrganizationRepresentation getOrganization() { - return organization; + public String getCreatedDate() { + return createdDate != null ? createdDate.toString() : null; } - public void setOrganization(OrganizationRepresentation organization) { - this.organization = organization; + public String getEntityId() { + return entityId; } - public List getContacts() { - return contacts; + public String getId() { + return id; + } + + public List getLogoutEndpoints() { + return this.getLogoutEndpoints(false); } - public void setContacts(List contacts) { - this.contacts = contacts; + public List getLogoutEndpoints(boolean create) { + if (create && this.logoutEndpoints == null) { + this.logoutEndpoints = new ArrayList<>(); + } + return logoutEndpoints; } public MduiRepresentation getMdui() { return mdui; } - public void setMdui(MduiRepresentation mdui) { - this.mdui = mdui; + public String getModifiedDate() { + return modifiedDate != null ? modifiedDate.toString() : null; } + @JsonIgnore + public LocalDateTime getModifiedDateAsDate() { + // we shouldn't have an ED without either modified or created date, so this is mostly for testing where data can be odd + return modifiedDate != null ? modifiedDate : createdDate != null ? createdDate : LocalDateTime.now(); + } - public ServiceProviderSsoDescriptorRepresentation getServiceProviderSsoDescriptor() { - return this.getServiceProviderSsoDescriptor(false); + + public OrganizationRepresentation getOrganization() { + return organization; } - public ServiceProviderSsoDescriptorRepresentation getServiceProviderSsoDescriptor(boolean create) { - if (create && this.serviceProviderSsoDescriptor == null) { - this.serviceProviderSsoDescriptor = new ServiceProviderSsoDescriptorRepresentation(); - } - return this.serviceProviderSsoDescriptor; + public Map getRelyingPartyOverrides() { + return relyingPartyOverrides; } - public void setServiceProviderSsoDescriptor(ServiceProviderSsoDescriptorRepresentation serviceProviderSsoDescriptor) { - this.serviceProviderSsoDescriptor = serviceProviderSsoDescriptor; + public SecurityInfoRepresentation getSecurityInfo() { + return securityInfo; } - public List getLogoutEndpoints() { - return this.getLogoutEndpoints(false); + public String getServiceProviderName() { + return serviceProviderName; } - public List getLogoutEndpoints(boolean create) { - if (create && this.logoutEndpoints == null) { - this.logoutEndpoints = new ArrayList<>(); - } - return logoutEndpoints; + public ServiceProviderSsoDescriptorRepresentation getServiceProviderSsoDescriptor() { + return this.getServiceProviderSsoDescriptor(false); } - public void setLogoutEndpoints(List logoutEndpoints) { - this.logoutEndpoints = logoutEndpoints; + public ServiceProviderSsoDescriptorRepresentation getServiceProviderSsoDescriptor(boolean create) { + if (create && this.serviceProviderSsoDescriptor == null) { + this.serviceProviderSsoDescriptor = new ServiceProviderSsoDescriptorRepresentation(); + } + return this.serviceProviderSsoDescriptor; } - public SecurityInfoRepresentation getSecurityInfo() { - return securityInfo; + public int getVersion() { + return version; } - public void setSecurityInfo(SecurityInfoRepresentation securityInfo) { - this.securityInfo = securityInfo; + public boolean isCurrent() { + return current; } - public List getAssertionConsumerServices() { - return assertionConsumerServices; + public boolean isServiceEnabled() { + return serviceEnabled; } public void setAssertionConsumerServices(List assertionConsumerServices) { this.assertionConsumerServices = assertionConsumerServices; } - public boolean isServiceEnabled() { - return serviceEnabled; + public void setAttributeRelease(List attributeRelease) { + this.attributeRelease = attributeRelease; } - public void setServiceEnabled(boolean serviceEnabled) { - this.serviceEnabled = serviceEnabled; + public void setContacts(List contacts) { + this.contacts = contacts; } - public String getCreatedDate() { - return createdDate != null ? createdDate.toString() : null; + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; } public void setCreatedDate(LocalDateTime createdDate) { this.createdDate = createdDate; } - public String getModifiedDate() { - return modifiedDate != null ? modifiedDate.toString() : null; + public void setCurrent(boolean current) { + this.current = current; } - @JsonIgnore - public LocalDateTime getModifiedDateAsDate() { - // we shouldn't have an ED without either modified or created date, so this is mostly for testing where data can be odd - return modifiedDate != null ? modifiedDate : createdDate != null ? createdDate : LocalDateTime.now(); + public void setEntityId(String entityId) { + this.entityId = entityId; } - public void setModifiedDate(LocalDateTime modifiedDate) { - this.modifiedDate = modifiedDate; + public void setId(String id) { + this.id = id; } - public Map getRelyingPartyOverrides() { - return relyingPartyOverrides; + public void setLogoutEndpoints(List logoutEndpoints) { + this.logoutEndpoints = logoutEndpoints; } - public void setRelyingPartyOverrides(Map relyingPartyOverrides) { - this.relyingPartyOverrides = relyingPartyOverrides; + public void setMdui(MduiRepresentation mdui) { + this.mdui = mdui; } - public List getAttributeRelease() { - return attributeRelease; + public void setModifiedDate(LocalDateTime modifiedDate) { + this.modifiedDate = modifiedDate; } - public void setAttributeRelease(List attributeRelease) { - this.attributeRelease = attributeRelease; + public void setOrganization(OrganizationRepresentation organization) { + this.organization = organization; } - public int getVersion() { - return version; + public void setRelyingPartyOverrides(Map relyingPartyOverrides) { + this.relyingPartyOverrides = relyingPartyOverrides; } - public void setVersion(int version) { - this.version = version; + public void setSecurityInfo(SecurityInfoRepresentation securityInfo) { + this.securityInfo = securityInfo; } - public String getCreatedBy() { - return createdBy; + public void setServiceEnabled(boolean serviceEnabled) { + this.serviceEnabled = serviceEnabled; } - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; + public void setServiceProviderName(String serviceProviderName) { + this.serviceProviderName = serviceProviderName; } - public boolean isCurrent() { - return current; + public void setServiceProviderSsoDescriptor(ServiceProviderSsoDescriptorRepresentation serviceProviderSsoDescriptor) { + this.serviceProviderSsoDescriptor = serviceProviderSsoDescriptor; } - public void setCurrent(boolean current) { - this.current = current; + public void setVersion(int version) { + this.version = version; } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityIdExistsException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityIdExistsException.java new file mode 100644 index 000000000..990eab2c3 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityIdExistsException.java @@ -0,0 +1,8 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class EntityIdExistsException extends Exception { + public EntityIdExistsException(String entityId) { + super(entityId); + } + +} 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 new file mode 100644 index 000000000..6769aaa5f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class EntityNotFoundException extends Exception { + public EntityNotFoundException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/ForbiddenException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/ForbiddenException.java new file mode 100644 index 000000000..66f2e61c5 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/ForbiddenException.java @@ -0,0 +1,11 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class ForbiddenException extends Exception { + public ForbiddenException() { + super("You are not authorized to perform the requested operation."); + } + + public ForbiddenException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java index 8a6596594..5e28e7d75 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import java.util.List; import java.util.stream.Stream; @@ -21,9 +22,16 @@ public interface EntityDescriptorRepository extends JpaRepository findAllStreamByCustomQuery(); + Stream findAllStreamByIdOfOwner(String ownerId); + @Query("select e from EntityDescriptor e, User u join u.roles r " + "where e.createdBy = u.username and e.serviceEnabled = false and r.name in ('ROLE_USER', 'ROLE_NONE')") Stream findAllDisabledAndNotOwnedByAdmin(); - - Stream findAllStreamByCreatedBy(String createdBy); + + /** + * SHIBUI-1740 This is here to aid in migration of systems using the SHIBUI prior to group functionality being added + * @deprecated - this is intended to be removed at some future date and is here only for migration purposes. + */ + @Deprecated + List findAllByIdOfOwnerIsNull(); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupController.java new file mode 100644 index 000000000..c0fc0a8ea --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupController.java @@ -0,0 +1,71 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.stereotype.Controller; +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.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.GroupDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; + +@Controller +@RequestMapping(value = "/api/admin/groups") +public class GroupController { + @Autowired + private IGroupService groupService; + + @Secured("ROLE_ADMIN") + @PostMapping + @Transactional + public ResponseEntity create(@RequestBody Group group) throws GroupExistsConflictException { + Group result = groupService.createGroup(group); + return ResponseEntity.status(HttpStatus.CREATED).body(result); + } + + @Secured("ROLE_ADMIN") + @DeleteMapping("/{resourceId}") + @Transactional + public ResponseEntity delete(@PathVariable String resourceId) throws EntityNotFoundException, GroupDeleteException { + groupService.deleteDefinition(resourceId); + return ResponseEntity.noContent().build(); + } + + @GetMapping + @Transactional(readOnly = true) + public ResponseEntity getAll() { + return ResponseEntity.ok(groupService.findAll()); + } + + @GetMapping("/{resourceId}") + @Transactional(readOnly = true) + public ResponseEntity getOne(@PathVariable String resourceId) throws EntityNotFoundException { + Group g = groupService.find(resourceId); + if (g == null) { + throw new EntityNotFoundException(String.format("Unable to find group with resource id: [%s]", resourceId)); + } + return ResponseEntity.ok(g); + } + + @Secured("ROLE_ADMIN") + @PutMapping + @Transactional + public ResponseEntity update(@RequestBody Group group) throws EntityNotFoundException { + Group result = groupService.updateGroup(group); + return ResponseEntity.ok(result); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupControllerExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupControllerExceptionHandler.java new file mode 100644 index 000000000..e8bb3b4af --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupControllerExceptionHandler.java @@ -0,0 +1,46 @@ +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.GroupDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException; + +@ControllerAdvice(assignableTypes = {GroupController.class}) +public class GroupControllerExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler({ EntityNotFoundException.class }) + public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e, WebRequest request) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/admin/groups").build().toUri()); + + return ResponseEntity.status(HttpStatus.NOT_FOUND).headers(headers) + .body(new ErrorResponse(String.valueOf(HttpStatus.NOT_FOUND.value()), e.getMessage())); + } + + @ExceptionHandler({ GroupDeleteException.class }) + public ResponseEntity handleForbiddenAccess(GroupDeleteException e, WebRequest request) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/admin/groups/{resourceId}").build().toUri()); + + return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers) + .body(new ErrorResponse(String.valueOf(HttpStatus.CONFLICT.value()), e.getMessage())); + } + + @ExceptionHandler({ GroupExistsConflictException.class }) + public ResponseEntity handleGroupExistsConflict(GroupExistsConflictException e, WebRequest request) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/admin/groups").build().toUri()); + + return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).headers(headers) + .body(new ErrorResponse(String.valueOf(HttpStatus.METHOD_NOT_ALLOWED.value()), e.getMessage())); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java index db8924242..93fec88aa 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java @@ -1,13 +1,12 @@ package edu.internet2.tier.shibboleth.admin.ui.security.controller; -import edu.internet2.tier.shibboleth.admin.ui.controller.ErrorResponse; -import edu.internet2.tier.shibboleth.admin.ui.security.model.User; -import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository; -import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; -import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +import java.security.Principal; +import java.util.List; +import java.util.Optional; + import org.apache.commons.lang.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -23,38 +22,63 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.HttpClientErrorException; -import java.security.Principal; -import java.util.List; -import java.util.Optional; - -import static org.springframework.http.HttpStatus.NOT_FOUND; +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.OwnershipConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.User; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import groovy.util.logging.Slf4j; +import jline.internal.Log; /** * Implementation of the REST resource endpoints exposing system users. - * - * @author Dmitriy Kopylenko */ @RestController @RequestMapping("/api/admin/users") +@Slf4j public class UsersController { - - private static final Logger logger = LoggerFactory.getLogger(UsersController.class); - private UserRepository userRepository; - private RoleRepository roleRepository; private UserService userService; - public UsersController(UserRepository userRepository, RoleRepository roleRepository, UserService userService) { + public UsersController(UserRepository userRepository, UserService userService) { this.userRepository = userRepository; - this.roleRepository = roleRepository; this.userService = userService; } + @PreAuthorize("hasRole('ADMIN')") + @Transactional + @DeleteMapping("/{username}") + public ResponseEntity deleteOne(@PathVariable String username) { + try { + userService.delete(username); + } + catch (EntityNotFoundException e) { + throw new HttpClientErrorException(NOT_FOUND, String.format("User with username [%s] not found", username)); + } + catch (OwnershipConflictException e) { + throw new HttpClientErrorException(HttpStatus.CONFLICT, e.getMessage()); + } + return ResponseEntity.noContent().build(); + } + + private User findUserOrThrowHttp404(String username) { + Optional result = userRepository.findByUsername(username); + return result.orElseThrow(() -> new HttpClientErrorException(NOT_FOUND, String.format("User with username [%s] not found", username))); + } + @PreAuthorize("hasRole('ADMIN')") @Transactional(readOnly = true) @GetMapping public List getAll() { - return userRepository.findAll(); + try { + List results = userRepository.findAll(); + return results; + } + catch (Exception e) { + Log.error("Unable to fetch users because: {}", e.getMessage()); + throw e; + } } @Transactional(readOnly = true) @@ -78,15 +102,6 @@ public ResponseEntity getUsersWithRole(@PathVariable String rolename) { return ResponseEntity.ok(userRepository.findByRoles_Name(rolename)); } - @PreAuthorize("hasRole('ADMIN')") - @Transactional - @DeleteMapping("/{username}") - public ResponseEntity deleteOne(@PathVariable String username) { - User user = findUserOrThrowHttp404(username); - userRepository.delete(user); - return ResponseEntity.noContent().build(); - } - @PreAuthorize("hasRole('ADMIN')") @Transactional @PostMapping @@ -101,7 +116,8 @@ ResponseEntity saveOne(@RequestBody User user) { //TODO: modify this such that additional encoders can be used user.setPassword(BCrypt.hashpw(user.getPassword(), BCrypt.gensalt())); userService.updateUserRole(user); - User savedUser = userRepository.save(user); + + User savedUser = userService.save(user); return ResponseEntity.ok(savedUser); } @@ -110,6 +126,7 @@ ResponseEntity saveOne(@RequestBody User user) { @PatchMapping("/{username}") ResponseEntity updateOne(@PathVariable(value = "username") String username, @RequestBody User user) { User persistedUser = findUserOrThrowHttp404(username); + if (StringUtils.isNotBlank(user.getFirstName())) { persistedUser.setFirstName(user.getFirstName()); } @@ -123,15 +140,13 @@ ResponseEntity updateOne(@PathVariable(value = "username") String username, @ persistedUser.setPassword(BCrypt.hashpw(user.getPassword(), BCrypt.gensalt())); } if (StringUtils.isNotBlank(user.getRole())) { - persistedUser.setRole(user.getRole()); - userService.updateUserRole(persistedUser); + if (!user.getRole().equals(persistedUser.getRole())) { + persistedUser.setRole(user.getRole()); + userService.updateUserRole(persistedUser); + } } - User savedUser = userRepository.save(persistedUser); + persistedUser.setGroupId(user.getGroupId()); + User savedUser = userService.save(persistedUser); return ResponseEntity.ok(savedUser); } - - private User findUserOrThrowHttp404(String username) { - return userRepository.findByUsername(username) - .orElseThrow(() -> new HttpClientErrorException(NOT_FOUND, String.format("User with username [%s] not found", username))); - } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/GroupDeleteException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/GroupDeleteException.java new file mode 100644 index 000000000..f95d09142 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/GroupDeleteException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.exception; + +public class GroupDeleteException extends Exception { + public GroupDeleteException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/GroupExistsConflictException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/GroupExistsConflictException.java new file mode 100644 index 000000000..5566f034c --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/GroupExistsConflictException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.exception; + +public class GroupExistsConflictException extends Exception { + public GroupExistsConflictException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/OwnershipConflictException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/OwnershipConflictException.java new file mode 100644 index 000000000..21b33e9e1 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/OwnershipConflictException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.exception; + +public class OwnershipConflictException extends Exception { + public OwnershipConflictException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java new file mode 100644 index 000000000..b07595097 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java @@ -0,0 +1,78 @@ +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.EntityListeners; +import javax.persistence.Id; +import javax.persistence.Transient; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.GroupUpdatedEntityListener; +import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.ILazyLoaderHelper; +import lombok.Data; +import lombok.EqualsAndHashCode.Exclude; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@EntityListeners(GroupUpdatedEntityListener.class) +@Entity(name = "user_groups") +public class Group implements Owner { + @Transient + @JsonIgnore + public static Group ADMIN_GROUP; + + @Column(name = "group_description", nullable = true) + String description; + + @Transient + @JsonIgnore + @Exclude + private ILazyLoaderHelper lazyLoaderHelper; + + @Column(name = "name") + private String name; + + @Transient + @JsonIgnore + private Set ownedItems = new HashSet<>(); + + @Id + @Column(name = "resource_id") + private String resourceId = UUID.randomUUID().toString(); + + /** + * Define a Group object based on the user + */ + public Group(User user) { + resourceId = user.getUsername(); + name = user.getUsername(); + description = "default user-group"; + } + + @Override + public String getOwnerId() { + return resourceId; + } + + @Override + public OwnerType getOwnerType() { + return OwnerType.GROUP; + } + + public void registerLoader(ILazyLoaderHelper lazyLoaderHelper) { + this.lazyLoaderHelper = lazyLoaderHelper; + } + + public Set getOwnedItems() { + if (lazyLoaderHelper != null) { + lazyLoaderHelper.loadOwnedItems(this); + } + return ownedItems; + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..c184b9679 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Ownable.java @@ -0,0 +1,13 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.model; + +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(); + + /** + * @return the OwnableType that describes the Ownable object + */ + public OwnableType getOwnableType(); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/OwnableType.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/OwnableType.java new file mode 100644 index 000000000..0cf82714c --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/OwnableType.java @@ -0,0 +1,5 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.model; + +public enum OwnableType { + USER, ENTITY_DESCRIPTOR, METADATA_PROVIDER +} 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 new file mode 100644 index 000000000..ee7224335 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Owner.java @@ -0,0 +1,13 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.model; + +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(); + + /** + * @return the type describing the owner + */ + public OwnerType getOwnerType(); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/OwnerType.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/OwnerType.java new file mode 100644 index 000000000..ccec1b869 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/OwnerType.java @@ -0,0 +1,5 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.model; + +public enum OwnerType { + USER, GROUP +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Ownership.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Ownership.java new file mode 100644 index 000000000..3f44e8317 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Ownership.java @@ -0,0 +1,31 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.model; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity(name = "ownership") +@Data +@NoArgsConstructor +public class Ownership { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + protected Long id; + + private String ownedId; + private String ownedType; + private String ownerId; + private String ownerType; + + public Ownership(Owner owner, Ownable ownedObject) { + ownerId = owner.getOwnerId(); + ownerType = owner.getOwnerType().name(); + + ownedId = ownedObject.getObjectId(); + ownedType = ownedObject.getOwnableType().name(); + } +} \ 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 64792774d..ea3048b32 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 @@ -42,7 +42,7 @@ public Role(String name, int rank) { private String name; @Column(name = "ROLE_RANK") - private int rank; + private int rank; // 0=ADMIN, additional ranks are higher //Ignore properties annotation here is to prevent stack overflow recursive error during JSON serialization @JsonIgnoreProperties("roles") 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 c30c4ceff..cb769c662 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 @@ -1,69 +1,148 @@ package edu.internet2.tier.shibboleth.admin.ui.security.model; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import org.apache.commons.lang.StringUtils; +import java.util.HashSet; +import java.util.Set; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.FetchType; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.Table; import javax.persistence.Transient; -import java.util.HashSet; -import java.util.Set; + +import org.apache.commons.lang.StringUtils; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; +import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.ILazyLoaderHelper; +import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.UserUpdatedEntityListener; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; /** * Models a basic administrative user in the system. * * @author Dmitriy Kopylenko */ -@Entity @NoArgsConstructor @Getter @Setter -@EqualsAndHashCode(callSuper = true, exclude = "roles") +@EqualsAndHashCode(callSuper = true) @ToString(exclude = "roles") +@EntityListeners(UserUpdatedEntityListener.class) +@Entity @Table(name = "USERS") -public class User extends AbstractAuditable { - - @Column(nullable = false, unique = true) - private String username; +public class User extends AbstractAuditable implements Owner, Ownable { + private String emailAddress; + private String firstName; + + @Transient + @EqualsAndHashCode.Exclude + private String groupId; // simplifies the ui/api + + private String lastName; + + @Transient + @JsonIgnore + @EqualsAndHashCode.Exclude + private ILazyLoaderHelper lazyLoaderHelper; + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) @Column(nullable = false) private String password; - private String firstName; - - private String lastName; - - private String emailAddress; - @Transient + @EqualsAndHashCode.Exclude private String role; @JsonIgnore @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) + @EqualsAndHashCode.Exclude private Set roles = new HashSet<>(); + @EqualsAndHashCode.Exclude + @JsonIgnore + @Transient + private Set userGroups = new HashSet<>(); + + @Column(nullable = false, unique = true) + private String username; + + /** + * @return the initial implementation, while supporting a user having multiple groups in the db side, acts as if the + * user can only belong to a single group + */ + @JsonIgnore + public Group getGroup() { + return getUserGroups().isEmpty() ? null : (Group) userGroups.toArray()[0]; + } + + public String getGroupId() { + if (groupId == null) { + groupId = getUserGroups().isEmpty() ? null : getGroup().getResourceId(); + } + return groupId; + } + + @Override + public String getObjectId() { + return username; + } + + @Override + public OwnableType getOwnableType() { + return OwnableType.USER; + } + + @Override + public String getOwnerId() { + return username; + } + + @Override + public OwnerType getOwnerType() { + return OwnerType.USER; + } + public String getRole() { if (StringUtils.isBlank(this.role)) { Set roles = this.getRoles(); if (roles.size() != 1) { - throw new RuntimeException(String.format("User with username [%s] does not have exactly one role!", this.getUsername())); + throw new RuntimeException(String.format("User with username [%s] has no role or does not have exactly one role!", this.getUsername())); } this.role = roles.iterator().next().getName(); } return this.role; } + + public Set getUserGroups() { + if (lazyLoaderHelper != null) { + lazyLoaderHelper.loadGroups(this); + } + return userGroups; + } + + public void setGroup(Group g) { + groupId = g.getResourceId(); + userGroups.clear(); + userGroups.add(g); + } + + public void setGroups(Set groups) { + this.userGroups = groups; + } + + public void registerLoader(ILazyLoaderHelper lazyLoaderHelper) { + this.lazyLoaderHelper = lazyLoaderHelper; + } } 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 new file mode 100644 index 000000000..319624fe9 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/GroupUpdatedEntityListener.java @@ -0,0 +1,42 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.model.listener; + +import java.util.Set; + +import javax.persistence.PostLoad; +import javax.persistence.PostPersist; +import javax.persistence.PostUpdate; + +import org.springframework.beans.factory.annotation.Autowired; + +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository; + +public class GroupUpdatedEntityListener implements ILazyLoaderHelper { + private static OwnershipRepository ownershipRepository; + + /** + * @see https://stackoverflow.com/questions/12155632/injecting-a-spring-dependency-into-a-jpa-entitylistener + */ + @Autowired + public void init(OwnershipRepository repo) { + GroupUpdatedEntityListener.ownershipRepository = repo; + } + + @PostPersist + @PostUpdate + @PostLoad + public synchronized void groupSavedOrFetched(Group group) { + // Because of the JPA spec, the listener can't do queries in the callback, so we force lazy loading through + // another callback to this at the time that the owned items are needed + group.registerLoader(this); + } + + @Override + public void loadOwnedItems(Group group) { + group.registerLoader(null); // once loaded, remove the helper from the group + Set ownedItems = ownershipRepository.findAllByOwner(group); + group.setOwnedItems(ownedItems); + } + +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/ILazyLoaderHelper.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/ILazyLoaderHelper.java new file mode 100644 index 000000000..f2b4df092 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/ILazyLoaderHelper.java @@ -0,0 +1,10 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.model.listener; + +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; +import edu.internet2.tier.shibboleth.admin.ui.security.model.User; + +public interface ILazyLoaderHelper { + default public void loadOwnedItems(Group g) { } + + default public void loadGroups(User u) { } +} \ 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 new file mode 100644 index 000000000..dc2291db9 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/UserUpdatedEntityListener.java @@ -0,0 +1,52 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.model.listener; + +import java.util.HashSet; +import java.util.Set; + +import javax.persistence.PostLoad; +import javax.persistence.PostPersist; +import javax.persistence.PostUpdate; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership; +import edu.internet2.tier.shibboleth.admin.ui.security.model.User; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository; + +public class UserUpdatedEntityListener implements ILazyLoaderHelper { + private static GroupsRepository groupRepository; + private static OwnershipRepository ownershipRepository; + + /** + * @see https://stackoverflow.com/questions/12155632/injecting-a-spring-dependency-into-a-jpa-entitylistener + */ + @Autowired + public void init(OwnershipRepository repo, GroupsRepository groupRepo) { + UserUpdatedEntityListener.ownershipRepository = repo; + UserUpdatedEntityListener.groupRepository = groupRepo; + } + + @PostPersist + @PostUpdate + @PostLoad + @Transactional(propagation = Propagation.REQUIRES_NEW) + public synchronized void userSavedOrFetched(User user) { + // Because of the JPA spec, the listener can't do queries in the callback, so we force lazy loading through + // another callback to this at the time that the groups are needed + user.registerLoader(this); + } + + public void loadGroups(User user) { + user.setLazyLoaderHelper(null); + Set ownerships = ownershipRepository.findAllGroupsForUser(user.getUsername()); + HashSet groups = new HashSet<>(); + ownerships.forEach(ownership -> { + groups.add(groupRepository.findByResourceId(ownership.getOwnerId())); + }); + user.setGroups(groups); + } +} \ 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 new file mode 100644 index 000000000..7c3c6e7b1 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepository.java @@ -0,0 +1,18 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; + +public interface GroupsRepository extends JpaRepository { + void deleteByResourceId(String resourceId); + + List findAll(); + + Group findByResourceId(String id); + + @SuppressWarnings("unchecked") + Group save(Group group); +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepository.java new file mode 100644 index 000000000..4aa8f4dfd --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepository.java @@ -0,0 +1,66 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.repository; + +import java.util.List; +import java.util.Set; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownable; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Owner; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership; + +public interface OwnershipRepository extends JpaRepository { + /** + * Clear out anything owned by any group + */ + @Query("DELETE FROM ownership o WHERE o.ownerType = 'GROUP'") + @Modifying(clearAutomatically = true, flushAutomatically = true) + void clearAllOwnedByGroup(); + + /** + * Delete the user from any groups they may in. + */ + @Query("DELETE FROM ownership o WHERE o.ownedId = :username AND o.ownedType = 'USER' AND o.ownerType = 'GROUP'") + @Modifying(clearAutomatically = true, flushAutomatically = true) + void clearUsersGroups(@Param("username") String username); + + /** + * Remove any ownership of the ownable object + */ + @Query("DELETE FROM ownership o WHERE o.ownedId = :#{#ownedObject.getObjectId()} AND o.ownedType = :#{#ownedObject.getOwnableType().toString()}") + @Modifying(clearAutomatically = true, flushAutomatically = true) + void deleteEntriesForOwnedObject(@Param("ownedObject") Ownable ownedObject); + + /** + * Find all items an owner owns + */ + @Query("SELECT o FROM ownership o WHERE o.ownerId = :#{#owner.getOwnerId()} AND o.ownerType = :#{#owner.getOwnerType().toString()}") + Set findAllByOwner(@Param("owner") Owner owner); + + /** + * Find all the groups that a user belongs to + */ + @Query("SELECT o FROM ownership o WHERE o.ownedId = :username AND o.ownedType = 'USER' AND o.ownerType = 'GROUP' ") + Set findAllGroupsForUser(@Param("username") String username); + + /** + * Find the owner of this object + */ + @Query("SELECT o FROM ownership o WHERE o.ownedId = :#{#ownedObject.getObjectId()} AND o.ownedType = :#{#ownedObject.getOwnableType().toString()}") + Set findOwnableObjectOwners(@Param("ownedObject") Ownable ownedObject); + + /** + * Find all things the user owns + */ + @Query("SELECT o FROM ownership o WHERE o.ownerId = :username AND o.ownerType = 'USER' ") + List findOwnedByUser(@Param("username") String username); + + /** + * Find only the users that the owner owns + */ + @Query("SELECT o FROM ownership o WHERE o.ownerId = :#{#owner.getOwnerId()} AND o.ownerType = :#{#owner.getOwnerType().toString()} AND o.ownedType='USER'") + Set findUsersByOwner(@Param("owner") Owner owner); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/UserRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/UserRepository.java index 380e8501a..0f1f7f201 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/UserRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/UserRepository.java @@ -12,7 +12,6 @@ * @author Dmitriy Kopylenko */ public interface UserRepository extends JpaRepository { - Optional findByUsername(String username); Set findByRoles_Name(String roleName); } 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 new file mode 100644 index 000000000..2501f007e --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java @@ -0,0 +1,89 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository; +import lombok.NoArgsConstructor; + +@Service +@NoArgsConstructor +public class GroupServiceImpl implements IGroupService { + @Autowired + protected GroupsRepository groupRepository; + + @Autowired + protected OwnershipRepository ownershipRepository; + + public GroupServiceImpl(GroupsRepository repo, OwnershipRepository ownershipRepository) { + this.groupRepository = repo; + this.ownershipRepository = ownershipRepository; + } + + @Override + @Transactional + public Group createGroup(Group group) throws GroupExistsConflictException { + Group foundGroup = find(group.getResourceId()); + // If already defined, we don't want to create a new one, nor do we want this call update the definition + if (foundGroup != null) { + throw new GroupExistsConflictException( + String.format("Call update (PUT) to modify the group with resource id: [%s] and name: [%s]", + foundGroup.getResourceId(), foundGroup.getName())); + } + return groupRepository.save(group); + } + + @Override + @Transactional + public void deleteDefinition(String resourceId) throws EntityNotFoundException, GroupDeleteException { + Group group = find(resourceId); + if (!ownershipRepository.findAllByOwner(group).isEmpty()) { + throw new GroupDeleteException(String.format( + "Unable to delete group with resource id: [%s] - remove all items owned by / associated with the group first", + resourceId)); + } + groupRepository.delete(group); + } + + @Override + @Transactional + public void ensureAdminGroupExists() { + Group g = groupRepository.findByResourceId("admingroup"); + if (g == null) { + g = new Group(); + g.setName("ADMIN-GROUP"); + g.setResourceId("admingroup"); + g = groupRepository.save(g); + } + Group.ADMIN_GROUP = g; + } + + @Override + @Transactional + public Group find(String resourceId) { + return groupRepository.findByResourceId(resourceId); + } + + @Override + public List findAll() { + return groupRepository.findAll(); + } + + @Override + public Group updateGroup(Group group) throws EntityNotFoundException { + Group g = find(group.getResourceId()); + if (g == null) { + throw new EntityNotFoundException(String.format("Unable to find group with resource id: [%s] and name: [%s]", + group.getResourceId(), group.getName())); + } + return groupRepository.save(group); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IGroupService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IGroupService.java new file mode 100644 index 000000000..1c4229d84 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IGroupService.java @@ -0,0 +1,25 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service; + +import java.util.List; + +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; + +public interface IGroupService { + + Group createGroup(Group group) throws GroupExistsConflictException; + + void deleteDefinition(String resourceId) throws EntityNotFoundException, GroupDeleteException; + + void ensureAdminGroupExists(); + + Group find(String resourceId); + + List findAll(); + + Group updateGroup(Group g) throws EntityNotFoundException; + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserAccess.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserAccess.java new file mode 100644 index 000000000..7fd476605 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserAccess.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service; + +public enum UserAccess { + ADMIN, // Access to everything + GROUP, // Group is the basic default + NONE // +} 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 a2d11a1ab..a04e2b574 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,52 +1,69 @@ package edu.internet2.tier.shibboleth.admin.ui.security.service; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +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.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException; +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; +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.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 org.apache.commons.lang.StringUtils; -import org.springframework.security.core.context.SecurityContextHolder; +import lombok.NoArgsConstructor; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; - -/** - * @author Bill Smith (wsmith@unicon.net) - */ +@Service +@NoArgsConstructor public class UserService { - + @Autowired + private IGroupService groupService; + + @Autowired + private OwnershipRepository ownershipRepository; + + @Autowired private RoleRepository roleRepository; + + @Autowired private UserRepository userRepository; - - public UserService(RoleRepository roleRepository, UserRepository userRepository) { + + public UserService(IGroupService groupService, OwnershipRepository ownershipRepository, RoleRepository roleRepository, UserRepository userRepository) { + this.groupService = groupService; + this.ownershipRepository = ownershipRepository; this.roleRepository = roleRepository; this.userRepository = userRepository; } - - /** - * 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())) { - Optional userRole = roleRepository.findByName(user.getRole()); - if (userRole.isPresent()) { - Set userRoles = new HashSet<>(); - userRoles.add(userRole.get()); - user.setRoles(userRoles); - } else { - 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())); - } + + 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."); + + // 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(); + userRepository.delete(user); + } + public User getCurrentUser() { //TODO: Consider returning an Optional here User user = null; @@ -61,6 +78,29 @@ public User getCurrentUser() { } return user; } + + public UserAccess getCurrentUserAccess() { + User user = getCurrentUser(); + if (user == null) { + return UserAccess.NONE; + } + if (user.getRole().equals("ROLE_ADMIN")) { + return UserAccess.ADMIN; + } + if (user.getRole().equals("ROLE_USER")) { + return UserAccess.GROUP; + } + return UserAccess.NONE; + } + + public Group getCurrentUserGroup() { + switch (getCurrentUserAccess()) { + case ADMIN: + return Group.ADMIN_GROUP; + default: + return getCurrentUser().getGroup(); + } + } public Set getUserRoles(String username) { Optional user = userRepository.findByUsername(username); @@ -70,4 +110,92 @@ public Set getUserRoles(String username) { } return result; } -} + + public boolean isAuthorizedFor(Ownable ownableObject) { + switch (getCurrentUserAccess()) { + case ADMIN: // Pure admin is authorized to do anything + return true; + case GROUP: // if the current user's group matches the object's group we are good. + Set owners = ownershipRepository.findOwnableObjectOwners(ownableObject); + String currentUsersGroupId = getCurrentUser().getGroupId(); + for (Ownership owner : owners) { + if (currentUsersGroupId.equals(owner.getOwnerId()) && OwnerType.valueOf(owner.getOwnerType()) == OwnerType.GROUP) { + return true; + } + } + return false; + default: // Currently the only cases are ADMIN or GROUP + 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. + * Finally, if the user has multiple groups, that came from an outside auth source, so we want to maintain that list + * (note that if they have the admin role, we will override any group list with the single ADMIN GROUP) + */ + @Transactional + public User save(User user) { + if (user.getRole().equalsIgnoreCase("ROLE_ADMIN") || user.getUserGroups().size() < 2) { + Group g; + if (user.getRole().equalsIgnoreCase("ROLE_ADMIN")) { + g = groupService.find(Group.ADMIN_GROUP.getResourceId()); + } else if (user.getGroupId() == null) { + // Find or create the "user's default" group + g = new Group(user); + try { + g = groupService.createGroup(g); + } + catch (GroupExistsConflictException e) { + g = groupService.find(user.getUsername()); + } + } else { + g = groupService.find(user.getGroupId()); + } + ownershipRepository.clearUsersGroups(user.getUsername()); + ownershipRepository.saveAndFlush(new Ownership(g, user)); + } else { + ownershipRepository.clearUsersGroups(user.getUsername()); + user.getUserGroups().forEach(ug -> { + Group g = groupService.find(ug.getResourceId()); + if (g == null) { + try { + Group newGroup = ug; + Ownership o = ownershipRepository.saveAndFlush(new Ownership(newGroup, user)); + g = groupService.createGroup(newGroup); + } + catch (GroupExistsConflictException e) { + // we just checked, this shouldn't happen + g = ug; + } + } + ownershipRepository.saveAndFlush(new Ownership(g, 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())) { + Optional userRole = roleRepository.findByName(user.getRole()); + if (userRole.isPresent()) { + Set userRoles = new HashSet<>(); + userRoles.add(userRole.get()); + user.setRoles(userRoles); + } else { + 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())); + } + } +} \ 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 ea57c4d06..bc63e7378 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 @@ -1,9 +1,13 @@ package edu.internet2.tier.shibboleth.admin.ui.service; import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; -import org.opensaml.saml.saml2.metadata.EntityDescriptor; +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; +import java.util.ConcurrentModificationException; import java.util.List; import java.util.Map; @@ -13,31 +17,57 @@ * @since 1.0 */ public interface EntityDescriptorService { - /** * Map from front-end data representation of entity descriptor to opensaml implementation of entity descriptor model * * @param representation of entity descriptor coming from front end layer - * @return EntityDescriptor + * @return org.opensaml.saml.saml2.metadata.EntityDescriptor opensaml model + */ + org.opensaml.saml.saml2.metadata.EntityDescriptor createDescriptorFromRepresentation(final EntityDescriptorRepresentation representation); + + /** + * @param ed - JPA EntityDescriptor to base creation on + * @return EntityDescriptorRepresentation of the created object + * @throws ForbiddenException If user is unauthorized to perform this operation + * @throws EntityIdExistsException If any EntityDescriptor already exists with the same EntityId */ - EntityDescriptor createDescriptorFromRepresentation(final EntityDescriptorRepresentation representation); + EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, EntityIdExistsException; + /** + * @param edRepresentation Incoming representation to save + * @return EntityDescriptorRepresentation + * @throws ForbiddenException If user is unauthorized to perform this operation + * @throws EntityIdExistsException If the entity already exists + */ + EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityIdExistsException; + /** * Map from opensaml implementation of entity descriptor model to front-end data representation of entity descriptor * - * @param entityDescriptor opensaml model + * @param org.opensaml.saml.saml2.metadata.EntityDescriptor opensaml model * @return EntityDescriptorRepresentation */ - EntityDescriptorRepresentation createRepresentationFromDescriptor(final EntityDescriptor entityDescriptor); + EntityDescriptorRepresentation createRepresentationFromDescriptor(final org.opensaml.saml.saml2.metadata.EntityDescriptor entityDescriptor); /** - * Update an instance of entity descriptor with information from the front-end representation - * - * @param entityDescriptor opensaml model instance to update - * @param representation front end representation to use to update + * @param resourceId - id of the JPA EntityDescriptor + * @throws ForbiddenException If user is unauthorized to perform this operation + * @throws EntityNotFoundException If the db entity is not found */ - void updateDescriptorFromRepresentation(final EntityDescriptor entityDescriptor, final EntityDescriptorRepresentation representation); + void delete(String resourceId) throws ForbiddenException, EntityNotFoundException; + /** + * @return - Iterable set of EntityDescriptorRepresentations of those items which are NOT enabled and not owned by + * "admin" + * @throws ForbiddenException - If user is not an ADMIN + */ + Iterable getAllDisabledAndNotOwnedByAdmin() throws ForbiddenException; + + /** + * @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 * @@ -46,6 +76,14 @@ public interface EntityDescriptorService { */ List getAttributeReleaseListFromAttributeList(List attributeList); + /** + * @param resourceId - id of the JPA EntityDescriptor + * @return JPA EntityDescriptor + * @throws ForbiddenException If user is unauthorized to perform this operation + * @throws EntityNotFoundException If the db entity is not found + */ + EntityDescriptor getEntityDescriptorByResourceId(String resourceId) throws EntityNotFoundException, ForbiddenException; + /** * Given a list of attributes, generate a map of relying party overrides * @@ -54,4 +92,21 @@ public interface EntityDescriptorService { */ Map getRelyingPartyOverridesRepresentationFromAttributeList(List attributeList); + /** + * @param edRepresentation Incoming representation to save + * @return EntityDescriptorRepresentation + * @throws ForbiddenException If user is unauthorized to perform this operation + * @throws EntityIdExistsException If the entity already exists + * @throws ConcurrentModificationException If the entity was already modified by another user + */ + EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityNotFoundException, ConcurrentModificationException; + + /** + * Update an instance of entity descriptor with information from the front-end representation + * + * @param entityDescriptor opensaml model instance to update + * @param representation front end representation to use to update + */ + void updateDescriptorFromRepresentation(final org.opensaml.saml.saml2.metadata.EntityDescriptor entityDescriptor, final EntityDescriptorRepresentation representation); + } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorVersionService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorVersionService.java index 5e1542ea2..c8c67fbc8 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorVersionService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorVersionService.java @@ -3,6 +3,7 @@ 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.domain.versioning.Version; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import java.util.List; @@ -11,7 +12,7 @@ */ public interface EntityDescriptorVersionService { - List findVersionsForEntityDescriptor(String resourceId); + List findVersionsForEntityDescriptor(String resourceId) throws EntityNotFoundException; - EntityDescriptorRepresentation findSpecificVersionOfEntityDescriptor(String resourceId, String versionId); + EntityDescriptorRepresentation findSpecificVersionOfEntityDescriptor(String resourceId, String versionId) throws EntityNotFoundException; } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EnversEntityDescriptorVersionService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EnversEntityDescriptorVersionService.java index 2bfced24d..99906882b 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EnversEntityDescriptorVersionService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EnversEntityDescriptorVersionService.java @@ -4,6 +4,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.versioning.Version; import edu.internet2.tier.shibboleth.admin.ui.envers.EnversVersionServiceSupport; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import java.util.List; @@ -22,13 +23,20 @@ public EnversEntityDescriptorVersionService(EnversVersionServiceSupport enversVe } @Override - public List findVersionsForEntityDescriptor(String resourceId) { - return enversVersionServiceSupport.findVersionsForPersistentEntity(resourceId, EntityDescriptor.class); + public List findVersionsForEntityDescriptor(String resourceId) throws EntityNotFoundException { + List results = enversVersionServiceSupport.findVersionsForPersistentEntity(resourceId, EntityDescriptor.class); + if (results.isEmpty()) { + throw new EntityNotFoundException(String.format("No versions found for entity descriptor with resource id [%s].", resourceId)); + } + return results; } @Override - public EntityDescriptorRepresentation findSpecificVersionOfEntityDescriptor(String resourceId, String versionId) { + public EntityDescriptorRepresentation findSpecificVersionOfEntityDescriptor(String resourceId, String versionId) throws EntityNotFoundException { Object edObject = enversVersionServiceSupport.findSpecificVersionOfPersistentEntity(resourceId, versionId, EntityDescriptor.class); - return edObject == null ? null : entityDescriptorService.createRepresentationFromDescriptor((EntityDescriptor) edObject); + if (edObject == null) { + throw new EntityNotFoundException("Unable to find specific version requested - version: " + versionId); + } + return entityDescriptorService.createRepresentationFromDescriptor((EntityDescriptor) edObject); } } 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 05477d616..9e0f7117e 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,39 +1,13 @@ package edu.internet2.tier.shibboleth.admin.ui.service; - -import com.google.common.base.Strings; -import edu.internet2.tier.shibboleth.admin.ui.domain.AssertionConsumerService; import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; -import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBuilder; -import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeValue; -import edu.internet2.tier.shibboleth.admin.ui.domain.ContactPerson; -import edu.internet2.tier.shibboleth.admin.ui.domain.ContactPersonBuilder; -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.EntityAttributes; -import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesBuilder; 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.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.PrivacyStatementURL; import edu.internet2.tier.shibboleth.admin.ui.domain.IRelyingPartyOverrideProperty; -import edu.internet2.tier.shibboleth.admin.ui.domain.SPSSODescriptor; -import edu.internet2.tier.shibboleth.admin.ui.domain.SingleLogoutService; import edu.internet2.tier.shibboleth.admin.ui.domain.UIInfo; -import edu.internet2.tier.shibboleth.admin.ui.domain.XSAny; import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean; - import edu.internet2.tier.shibboleth.admin.ui.domain.XSInteger; -import edu.internet2.tier.shibboleth.admin.ui.domain.XSString; 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; @@ -42,74 +16,58 @@ 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; 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.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 edu.internet2.tier.shibboleth.admin.util.MDDCConstants; import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; +import lombok.extern.slf4j.Slf4j; -import org.opensaml.core.xml.XMLObject; -import org.opensaml.core.xml.schema.XSBooleanValue; -import org.opensaml.xmlsec.signature.KeyInfo; -import org.opensaml.xmlsec.signature.X509Certificate; -import org.opensaml.xmlsec.signature.X509Data; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; -import java.time.LocalDateTime; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; +import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getStringListOfAttributeValues; -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getValueFromXMLObject; +import javax.transaction.Transactional; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.*; +import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.*; -/** - * Default implementation of {@link EntityDescriptorService} - * - * @since 1.0 - */ +@Slf4j +@Service public class JPAEntityDescriptorServiceImpl implements EntityDescriptorService { - - private static final Logger LOGGER = LoggerFactory.getLogger(JPAEntityDescriptorServiceImpl.class); - + @Autowired + EntityDescriptorRepository entityDescriptorRepository; + + @Autowired + IGroupService groupService; + @Autowired private OpenSamlObjects openSamlObjects; - + @Autowired - private EntityService entityService; - - private UserService userService; - - public JPAEntityDescriptorServiceImpl(OpenSamlObjects openSamlObjects, EntityService entityService, UserService userService) { - this.openSamlObjects = openSamlObjects; - this.entityService = entityService; - this.userService = userService; - } - - @Override - public void updateDescriptorFromRepresentation(org.opensaml.saml.saml2.metadata.EntityDescriptor entityDescriptor, EntityDescriptorRepresentation representation) { - if (!(entityDescriptor instanceof EntityDescriptor)) { - throw new UnsupportedOperationException("not yet implemented"); - } - buildDescriptorFromRepresentation((EntityDescriptor) entityDescriptor, representation); - } - - @Override - public EntityDescriptor createDescriptorFromRepresentation(final EntityDescriptorRepresentation representation) { - EntityDescriptor ed = openSamlObjects.buildDefaultInstanceOfType(EntityDescriptor.class); - - return buildDescriptorFromRepresentation(ed, representation); - } + private OwnershipRepository ownershipRepository; + @Autowired + UserService userService; + private EntityDescriptor buildDescriptorFromRepresentation(final EntityDescriptor ed, final EntityDescriptorRepresentation representation) { ed.setEntityID(representation.getEntityId()); + ed.setIdOfOwner(representation.getIdOfOwner()); setupSPSSODescriptor(ed, representation); ed.setServiceProviderName(representation.getServiceProviderName()); @@ -129,363 +87,32 @@ private EntityDescriptor buildDescriptorFromRepresentation(final EntityDescripto return ed; } - void setupRelyingPartyOverrides(EntityDescriptor ed, EntityDescriptorRepresentation representation) { - if (representation.getRelyingPartyOverrides() != null || (representation.getAttributeRelease() != null && representation.getAttributeRelease().size() > 0)) { - // TODO: review if we need more than a naive implementation - getOptionalEntityAttributes(ed).ifPresent(entityAttributes -> entityAttributes.getAttributes().clear()); - getEntityAttributes(ed).getAttributes().addAll(entityService.getAttributeListFromEntityRepresentation(representation)); - } else { - getOptionalEntityAttributes(ed).ifPresent(entityAttributes -> entityAttributes.getAttributes().clear()); - } - } - - void setupLogout(EntityDescriptor ed, EntityDescriptorRepresentation representation) { - // setup logout - if (representation.getLogoutEndpoints() != null && !representation.getLogoutEndpoints().isEmpty()) { - // TODO: review if we need more than a naive implementation - ed.getOptionalSPSSODescriptor().ifPresent(spssoDescriptor -> spssoDescriptor.getSingleLogoutServices().clear()); - for (LogoutEndpointRepresentation logoutEndpointRepresentation : representation.getLogoutEndpoints()) { - SingleLogoutService singleLogoutService = openSamlObjects.buildDefaultInstanceOfType(SingleLogoutService.class); - singleLogoutService.setBinding(logoutEndpointRepresentation.getBindingType()); - singleLogoutService.setLocation(logoutEndpointRepresentation.getUrl()); - - getSPSSODescriptorFromEntityDescriptor(ed).getSingleLogoutServices().add(singleLogoutService); - } - } else { - ed.getOptionalSPSSODescriptor().ifPresent(spssoDescriptor -> spssoDescriptor.getSingleLogoutServices().clear()); - } - } - - void setupACSs(EntityDescriptor ed, EntityDescriptorRepresentation representation) { - // setup ACSs - if (representation.getAssertionConsumerServices() != null && representation.getAssertionConsumerServices().size() > 0) { - // TODO: review if we need more than a naive implementation - ed.getOptionalSPSSODescriptor().ifPresent(spssoDescriptor -> spssoDescriptor.getAssertionConsumerServices().clear()); - for (AssertionConsumerServiceRepresentation acsRepresentation : representation.getAssertionConsumerServices()) { - AssertionConsumerService assertionConsumerService = openSamlObjects.buildDefaultInstanceOfType(AssertionConsumerService.class); - getSPSSODescriptorFromEntityDescriptor(ed).getAssertionConsumerServices().add(assertionConsumerService); - if (acsRepresentation.isMakeDefault()) { - assertionConsumerService.setIsDefault(true); - } - assertionConsumerService.setBinding(acsRepresentation.getBinding()); - assertionConsumerService.setLocation(acsRepresentation.getLocationUrl()); - assertionConsumerService.setIndex(acsRepresentation.getIndex()); - } - } else { - ed.getOptionalSPSSODescriptor().ifPresent(spssoDescriptor -> spssoDescriptor.getAssertionConsumerServices().clear()); - } - } - - void setupSecurity(EntityDescriptor ed, EntityDescriptorRepresentation representation) { - // setup security - if (representation.getSecurityInfo() != null) { - SecurityInfoRepresentation securityInfoRepresentation = representation.getSecurityInfo(); - if (securityInfoRepresentation.isAuthenticationRequestsSigned()) { - getSPSSODescriptorFromEntityDescriptor(ed).setAuthnRequestsSigned(true); - } - if (securityInfoRepresentation.isWantAssertionsSigned()) { - getSPSSODescriptorFromEntityDescriptor(ed).setWantAssertionsSigned(true); - } - // TODO: review if we need more than a naive implementation - ed.getOptionalSPSSODescriptor().ifPresent( i -> i.getKeyDescriptors().clear()); - if (securityInfoRepresentation.isX509CertificateAvailable()) { - for (SecurityInfoRepresentation.X509CertificateRepresentation x509CertificateRepresentation : securityInfoRepresentation.getX509Certificates()) { - KeyDescriptor keyDescriptor = createKeyDescriptor(x509CertificateRepresentation.getName(), x509CertificateRepresentation.getType(), x509CertificateRepresentation.getValue()); - getSPSSODescriptorFromEntityDescriptor(ed).addKeyDescriptor(keyDescriptor); - } - } - } else { - ed.getOptionalSPSSODescriptor().ifPresent( spssoDescriptor -> { - spssoDescriptor.setAuthnRequestsSigned((Boolean) null); - spssoDescriptor.setWantAssertionsSigned((Boolean) null); - spssoDescriptor.getKeyDescriptors().clear(); - }); - } - } - - void setupUIInfo(EntityDescriptor ed, EntityDescriptorRepresentation representation) { - // set up mdui - if (representation.getMdui() != null) { - // TODO: check if we need more than a naive implementation - removeUIInfo(ed); - MduiRepresentation mduiRepresentation = representation.getMdui(); - - if (!Strings.isNullOrEmpty(mduiRepresentation.getDisplayName())) { - DisplayName displayName = openSamlObjects.buildDefaultInstanceOfType(DisplayName.class); - getUIInfo(ed).addDisplayName(displayName); - displayName.setValue(mduiRepresentation.getDisplayName()); - displayName.setXMLLang("en"); - } else { - ed.getOptionalSPSSODescriptor() - .flatMap(SPSSODescriptor::getOptionalExtensions) - .flatMap(Extensions::getOptionalUIInfo) - .ifPresent(u -> u.getXMLObjects().removeAll(u.getDisplayNames())); - } - - if (!Strings.isNullOrEmpty(mduiRepresentation.getInformationUrl())) { - InformationURL informationURL = openSamlObjects.buildDefaultInstanceOfType(InformationURL.class); - getUIInfo(ed).addInformationURL(informationURL); - informationURL.setValue(mduiRepresentation.getInformationUrl()); - informationURL.setXMLLang("en"); - } else { - ed.getOptionalSPSSODescriptor() - .flatMap(SPSSODescriptor::getOptionalExtensions) - .flatMap(Extensions::getOptionalUIInfo) - .ifPresent(u -> u.getXMLObjects().removeAll(u.getInformationURLs())); - } - - if (!Strings.isNullOrEmpty(mduiRepresentation.getPrivacyStatementUrl())) { - PrivacyStatementURL privacyStatementURL = openSamlObjects.buildDefaultInstanceOfType(PrivacyStatementURL.class); - getUIInfo(ed).addPrivacyStatementURL(privacyStatementURL); - privacyStatementURL.setValue(mduiRepresentation.getPrivacyStatementUrl()); - privacyStatementURL.setXMLLang("en"); - } else { - ed.getOptionalSPSSODescriptor() - .flatMap(SPSSODescriptor::getOptionalExtensions) - .flatMap(Extensions::getOptionalUIInfo) - .ifPresent(u -> u.getXMLObjects().removeAll(u.getPrivacyStatementURLs())); - } - - if (!Strings.isNullOrEmpty(mduiRepresentation.getDescription())) { - Description description = openSamlObjects.buildDefaultInstanceOfType(Description.class); - getUIInfo(ed).addDescription(description); - description.setValue(mduiRepresentation.getDescription()); - description.setXMLLang("en"); - } else { - ed.getOptionalSPSSODescriptor() - .flatMap(SPSSODescriptor::getOptionalExtensions) - .flatMap(Extensions::getOptionalUIInfo) - .ifPresent(u -> u.getXMLObjects().removeAll(u.getDescriptions())); - } - - if (!Strings.isNullOrEmpty(mduiRepresentation.getLogoUrl())) { - Logo logo = openSamlObjects.buildDefaultInstanceOfType(Logo.class); - getUIInfo(ed).addLogo(logo); - logo.setURL(mduiRepresentation.getLogoUrl()); - logo.setHeight(mduiRepresentation.getLogoHeight()); - logo.setWidth(mduiRepresentation.getLogoWidth()); - logo.setXMLLang("en"); - } else { - ed.getOptionalSPSSODescriptor() - .flatMap(SPSSODescriptor::getOptionalExtensions) - .flatMap(Extensions::getOptionalUIInfo) - .ifPresent(u -> u.getXMLObjects().removeAll(u.getLogos())); - } - } else { - removeUIInfo(ed); - } - } - - void setupContacts(EntityDescriptor ed, EntityDescriptorRepresentation representation) { - // set up contacts - if (representation.getContacts() != null && representation.getContacts().size() > 0) { - ed.getContactPersons().clear(); - for (ContactRepresentation contactRepresentation : representation.getContacts()) { - ContactPerson contactPerson = ((ContactPersonBuilder) openSamlObjects.getBuilderFactory().getBuilder(ContactPerson.DEFAULT_ELEMENT_NAME)).buildObject(); - - contactPerson.setType(contactRepresentation.getType()); - - GivenName givenName = openSamlObjects.buildDefaultInstanceOfType(GivenName.class); - givenName.setName(contactRepresentation.getName()); - contactPerson.setGivenName(givenName); - - EmailAddress emailAddress = openSamlObjects.buildDefaultInstanceOfType(EmailAddress.class); - emailAddress.setAddress(contactRepresentation.getEmailAddress()); - contactPerson.addEmailAddress(emailAddress); - - ed.addContactPerson(contactPerson); - } - } else { - ed.getContactPersons().clear(); - } - } - - void setupOrganization(EntityDescriptor ed, EntityDescriptorRepresentation representation) { - // set up organization - if (representation.getOrganization() != null && representation.getOrganization().getName() != null && representation.getOrganization().getDisplayName() != null && representation.getOrganization().getUrl() != null) { - OrganizationRepresentation organizationRepresentation = representation.getOrganization(); - Organization organization = openSamlObjects.buildDefaultInstanceOfType(Organization.class); - - OrganizationName organizationName = openSamlObjects.buildDefaultInstanceOfType(OrganizationName.class); - organizationName.setXMLLang("en"); - organizationName.setValue(organizationRepresentation.getName()); - organization.getOrganizationNames().add(organizationName); - - OrganizationDisplayName organizationDisplayName = openSamlObjects.buildDefaultInstanceOfType(OrganizationDisplayName.class); - organizationDisplayName.setXMLLang("en"); - organizationDisplayName.setValue(organizationRepresentation.getDisplayName()); - organization.getDisplayNames().add(organizationDisplayName); - - OrganizationURL organizationURL = openSamlObjects.buildDefaultInstanceOfType(OrganizationURL.class); - organizationURL.setXMLLang("en"); - organizationURL.setValue(organizationRepresentation.getUrl()); - organization.getURLs().add(organizationURL); - - ed.setOrganization(organization); - } else { - ed.setOrganization(null); - } - } - - void setupSPSSODescriptor(EntityDescriptor ed, EntityDescriptorRepresentation representation) { - // setup SPSSODescriptor - if (representation.getServiceProviderSsoDescriptor() != null) { - SPSSODescriptor spssoDescriptor = getSPSSODescriptorFromEntityDescriptor(ed); - - spssoDescriptor.setSupportedProtocols(Collections.EMPTY_LIST); - if (!Strings.isNullOrEmpty(representation.getServiceProviderSsoDescriptor().getProtocolSupportEnum())) { - spssoDescriptor.setSupportedProtocols( - Arrays.stream(representation.getServiceProviderSsoDescriptor().getProtocolSupportEnum().split(",")).map(p -> MDDCConstants.PROTOCOL_BINDINGS.get(p.trim())).collect(Collectors.toList()) - ); - } - - spssoDescriptor.getNameIDFormats().clear(); - if (representation.getServiceProviderSsoDescriptor() != null && representation.getServiceProviderSsoDescriptor().getNameIdFormats() != null && representation.getServiceProviderSsoDescriptor().getNameIdFormats().size() > 0) { - for (String nameidFormat : representation.getServiceProviderSsoDescriptor().getNameIdFormats()) { - NameIDFormat nameIDFormat = openSamlObjects.buildDefaultInstanceOfType(NameIDFormat.class); - - nameIDFormat.setFormat(nameidFormat); - - spssoDescriptor.getNameIDFormats().add(nameIDFormat); - } - } - } else { - ed.setRoleDescriptors(null); - } - } - - private SPSSODescriptor getSPSSODescriptorFromEntityDescriptor(EntityDescriptor entityDescriptor) { - return getSPSSODescriptorFromEntityDescriptor(entityDescriptor, true); - } - - private SPSSODescriptor getSPSSODescriptorFromEntityDescriptor(EntityDescriptor entityDescriptor, boolean create) { - if (entityDescriptor.getSPSSODescriptor("") == null && create) { - SPSSODescriptor spssoDescriptor = openSamlObjects.buildDefaultInstanceOfType(SPSSODescriptor.class); - entityDescriptor.getRoleDescriptors().add(spssoDescriptor); - } - return entityDescriptor.getSPSSODescriptor(""); - } - - private Attribute createBaseAttribute(String name, String friendlyName) { - Attribute attribute = ((AttributeBuilder) openSamlObjects.getBuilderFactory().getBuilder(Attribute.DEFAULT_ELEMENT_NAME)).buildObject(); - attribute.setName(name); - attribute.setFriendlyName(friendlyName); - attribute.setNameFormat("urn:oasis:names:tc:SAML:2.0:attrname-format:uri"); - - return attribute; - } - - private Attribute createAttributeWithBooleanValue(String name, String friendlyName, Boolean value) { - Attribute attribute = createBaseAttribute(name, friendlyName); - - XSBoolean xsBoolean = (XSBoolean) openSamlObjects.getBuilderFactory().getBuilder(XSBoolean.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSBoolean.TYPE_NAME); - xsBoolean.setValue(XSBooleanValue.valueOf(value.toString())); - - attribute.getAttributeValues().add(xsBoolean); - return attribute; - } - - private Attribute createAttributeWithArbitraryValues(String name, String friendlyName, String... values) { - Attribute attribute = createBaseAttribute(name, friendlyName); - - for (String value : values) { - XSAny xsAny = (XSAny) openSamlObjects.getBuilderFactory().getBuilder(XSAny.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME); - xsAny.setTextContent(value); - attribute.getAttributeValues().add(xsAny); - } - - return attribute; - } - - private Attribute createAttributeWithArbitraryValues(String name, String friendlyName, List values) { - return createAttributeWithArbitraryValues(name, friendlyName, values.toArray(new String[]{})); - } - - KeyDescriptor createKeyDescriptor(String name, String type, String value) { - KeyDescriptor keyDescriptor = openSamlObjects.buildDefaultInstanceOfType(KeyDescriptor.class); - - if (!Strings.isNullOrEmpty(name)) { - keyDescriptor.setName(name); - } - - if (!"both".equals(type)) { - keyDescriptor.setUsageType(type); - } - - KeyInfo keyInfo = openSamlObjects.buildDefaultInstanceOfType(KeyInfo.class); - keyDescriptor.setKeyInfo(keyInfo); - - X509Data x509Data = openSamlObjects.buildDefaultInstanceOfType(X509Data.class); - keyInfo.getXMLObjects().add(x509Data); - - X509Certificate x509Certificate = openSamlObjects.buildDefaultInstanceOfType(X509Certificate.class); - x509Data.getXMLObjects().add(x509Certificate); - x509Certificate.setValue(value); - - return keyDescriptor; - } - - private EntityAttributes getEntityAttributes(EntityDescriptor ed) { - return getEntityAttributes(ed, true); - } - - private Optional getOptionalEntityAttributes(EntityDescriptor ed) { - return Optional.ofNullable(getEntityAttributes(ed, false)); + @Override + public EntityDescriptor createDescriptorFromRepresentation(final EntityDescriptorRepresentation representation) { + EntityDescriptor ed = openSamlObjects.buildDefaultInstanceOfType(EntityDescriptor.class); + return buildDescriptorFromRepresentation(ed, representation); } - private EntityAttributes getEntityAttributes(EntityDescriptor ed, boolean create) { - Extensions extensions = ed.getExtensions(); - if (extensions == null && !create) { - return null; - } - if (extensions == null) { - extensions = openSamlObjects.buildDefaultInstanceOfType(Extensions.class); - ed.setExtensions(extensions); - } - - EntityAttributes entityAttributes = null; - if (extensions.getUnknownXMLObjects(EntityAttributes.DEFAULT_ELEMENT_NAME).size() > 0) { - entityAttributes = (EntityAttributes) extensions.getUnknownXMLObjects(EntityAttributes.DEFAULT_ELEMENT_NAME).get(0); - } else { - if (create) { - entityAttributes = ((EntityAttributesBuilder) openSamlObjects.getBuilderFactory().getBuilder(EntityAttributes.DEFAULT_ELEMENT_NAME)).buildObject(); - extensions.getUnknownXMLObjects().add(entityAttributes); - } - } - return entityAttributes; + @Override + public EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, EntityIdExistsException { + return createNew(createRepresentationFromDescriptor(ed)); } - - private UIInfo getUIInfo(EntityDescriptor ed) { - Extensions extensions = getSPSSODescriptorFromEntityDescriptor(ed).getExtensions(); - if (extensions == null) { - extensions = openSamlObjects.buildDefaultInstanceOfType(Extensions.class); - ed.getSPSSODescriptor("").setExtensions(extensions); + + @Override + public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) throws ForbiddenException, EntityIdExistsException { + if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { + throw new ForbiddenException("You do not have the permissions necessary to enable this service."); } - - UIInfo uiInfo; - if (extensions.getUnknownXMLObjects(UIInfo.DEFAULT_ELEMENT_NAME).size() > 0) { - uiInfo = (UIInfo) extensions.getUnknownXMLObjects(UIInfo.DEFAULT_ELEMENT_NAME).get(0); - } else { - uiInfo = openSamlObjects.buildDefaultInstanceOfType(UIInfo.class); - extensions.getUnknownXMLObjects().add(uiInfo); + + if (entityDescriptorRepository.findByEntityID(edRep.getEntityId()) != null) { + throw new EntityIdExistsException(edRep.getEntityId()); } - return uiInfo; + + EntityDescriptor ed = (EntityDescriptor) createDescriptorFromRepresentation(edRep); + ed.setIdOfOwner(userService.getCurrentUserGroup().getOwnerId()); + return createRepresentationFromDescriptor(entityDescriptorRepository.save(ed)); } - private void removeUIInfo(EntityDescriptor ed) { - SPSSODescriptor spssoDescriptor = getSPSSODescriptorFromEntityDescriptor(ed, false); - if (spssoDescriptor != null) { - Extensions extensions = spssoDescriptor.getExtensions(); - if (extensions == null) { - return; - } - if (extensions.getUnknownXMLObjects(UIInfo.DEFAULT_ELEMENT_NAME).size() > 0) { - extensions.getUnknownXMLObjects().remove(extensions.getUnknownXMLObjects(UIInfo.DEFAULT_ELEMENT_NAME).get(0)); - } - } - } - - //TODO: implement @Override public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.opensaml.saml.saml2.metadata.EntityDescriptor entityDescriptor) { EntityDescriptor ed = (EntityDescriptor) entityDescriptor; @@ -500,6 +127,7 @@ public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.ope representation.setVersion(ed.hashCode()); representation.setCreatedBy(ed.getCreatedBy()); representation.setCurrent(ed.isCurrent()); + representation.setIdOfOwner(ed.getIdOfOwner()); if (ed.getSPSSODescriptor("") != null && ed.getSPSSODescriptor("").getSupportedProtocols().size() > 0) { ServiceProviderSsoDescriptorRepresentation serviceProviderSsoDescriptorRepresentation = representation.getServiceProviderSsoDescriptor(true); @@ -657,7 +285,7 @@ public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.ope if (jpaAttribute.getAttributeValues().size() != 1) { throw new RuntimeException("Multiple/No values detected where one is expected!"); } - attributeValues = getValueFromXMLObject(jpaAttribute.getAttributeValues().get(0)); + attributeValues = ModelRepresentationConversions.getValueFromXMLObject(jpaAttribute.getAttributeValues().get(0)); break; case INTEGER: if (jpaAttribute.getAttributeValues().size() != 1) { @@ -671,7 +299,7 @@ public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.ope } if (overrideProperty.getPersistType() != null && !overrideProperty.getPersistType().equals(overrideProperty.getDisplayType())) { - attributeValues = overrideProperty.getPersistValue().equals(getValueFromXMLObject(jpaAttribute.getAttributeValues().get(0))); + attributeValues = overrideProperty.getPersistValue().equals(ModelRepresentationConversions.getValueFromXMLObject(jpaAttribute.getAttributeValues().get(0))); } else { attributeValues = Boolean.valueOf(overrideProperty.getInvert()) ^ Boolean.valueOf(((XSBoolean) jpaAttribute.getAttributeValues() .get(0)).getStoredValue()); @@ -681,7 +309,7 @@ public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.ope case LIST: case SELECTION_LIST: attributeValues = jpaAttribute.getAttributeValues().stream() - .map(attributeValue -> getValueFromXMLObject(attributeValue)) + .map(attributeValue -> ModelRepresentationConversions.getValueFromXMLObject(attributeValue)) .collect(Collectors.toList()); } relyingPartyOverrides.put(((IRelyingPartyOverrideProperty) override.get()).getName(), attributeValues); @@ -695,9 +323,39 @@ public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.ope return representation; } - // TODO: remove - private String getValueFromXMLObject(XMLObject xmlObject) { - return ModelRepresentationConversions.getValueFromXMLObject(xmlObject); + @Override + public void delete(String resourceId) throws ForbiddenException, EntityNotFoundException { + EntityDescriptor ed = getEntityDescriptorByResourceId(resourceId); + if (ed.isServiceEnabled()) { + throw new ForbiddenException("Deleting an enabled Metadata Source is not allowed. Disable the source and try again."); + } + ownershipRepository.deleteEntriesForOwnedObject(ed); + entityDescriptorRepository.delete(ed); + + } + + @Override + public Iterable getAllDisabledAndNotOwnedByAdmin() throws ForbiddenException { + if (!userService.currentUserIsAdmin()) { + throw new ForbiddenException(); + } + return entityDescriptorRepository.findAllDisabledAndNotOwnedByAdmin().map(ed -> createRepresentationFromDescriptor(ed)).collect(Collectors.toList()); + } + + @Override + public List getAllRepresentationsBasedOnUserAccess() throws ForbiddenException { + switch (userService.getCurrentUserAccess()) { + case ADMIN: + return entityDescriptorRepository.findAllStreamByCustomQuery().map(ed -> createRepresentationFromDescriptor(ed)) + .collect(Collectors.toList()); + case GROUP: + User user = userService.getCurrentUser(); + Group group = user.getGroup(); + return entityDescriptorRepository.findAllStreamByIdOfOwner(group.getOwnerId()) + .map(ed -> createRepresentationFromDescriptor(ed)).collect(Collectors.toList()); + default: + throw new ForbiddenException(); + } } @Override @@ -705,8 +363,48 @@ public List getAttributeReleaseListFromAttributeList(List att return ModelRepresentationConversions.getAttributeReleaseListFromAttributeList(attributeList); } + @Override + public EntityDescriptor getEntityDescriptorByResourceId(String resourceId) throws EntityNotFoundException, ForbiddenException { + EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); + if (ed == null) { + throw new EntityNotFoundException(String.format("The entity descriptor with entity id [%s] was not found.", resourceId)); + } + if (!userService.isAuthorizedFor(ed)) { + throw new ForbiddenException(); + } + return ed; + } + @Override public Map getRelyingPartyOverridesRepresentationFromAttributeList(List attributeList) { return ModelRepresentationConversions.getRelyingPartyOverridesRepresentationFromAttributeList(attributeList); } + + @Override + public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRep) throws ForbiddenException, EntityNotFoundException { + EntityDescriptor existingEd = entityDescriptorRepository.findByResourceId(edRep.getId()); + if (existingEd == null) { + throw new EntityNotFoundException(String.format("The entity descriptor with entity id [%s] was not found for update.", edRep.getId())); + } + if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { + throw new ForbiddenException("You do not have the permissions necessary to enable this service."); + } + if (!userService.isAuthorizedFor(existingEd)) { + throw new ForbiddenException(); + } + // Verify we're the only one attempting to update the EntityDescriptor + if (edRep.getVersion() != existingEd.hashCode()) { + throw new ConcurrentModificationException(String.format("A concurrent modification has occured on entity descriptor with entity id [%s]. Please refresh and try again", edRep.getId())); + } + updateDescriptorFromRepresentation(existingEd, edRep); + return createRepresentationFromDescriptor(entityDescriptorRepository.save(existingEd)); + } + + @Override + public void updateDescriptorFromRepresentation(org.opensaml.saml.saml2.metadata.EntityDescriptor entityDescriptor, EntityDescriptorRepresentation representation) { + if (!(entityDescriptor instanceof EntityDescriptor)) { + throw new UnsupportedOperationException("not yet implemented"); + } + buildDescriptorFromRepresentation((EntityDescriptor) entityDescriptor, representation); + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/EntityDescriptorConversionUtils.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/EntityDescriptorConversionUtils.java new file mode 100644 index 000000000..29e9a9336 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/EntityDescriptorConversionUtils.java @@ -0,0 +1,378 @@ +package edu.internet2.tier.shibboleth.admin.util; + +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.getEntityAttributes; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.getOptionalEntityAttributes; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.opensaml.xmlsec.signature.KeyInfo; +import org.opensaml.xmlsec.signature.X509Certificate; +import org.opensaml.xmlsec.signature.X509Data; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.google.common.base.Strings; + +import edu.internet2.tier.shibboleth.admin.ui.domain.AssertionConsumerService; +import edu.internet2.tier.shibboleth.admin.ui.domain.ContactPerson; +import edu.internet2.tier.shibboleth.admin.ui.domain.ContactPersonBuilder; +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.EntityAttributes; +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesBuilder; +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.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.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.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.opensaml.OpenSamlObjects; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityService; +import lombok.Setter; + +@Service +public class EntityDescriptorConversionUtils { + @Autowired + @Setter + private static OpenSamlObjects openSamlObjects; + + @Autowired + @Setter + private static EntityService entityService; + + public static KeyDescriptor createKeyDescriptor(String name, String type, String value) { + KeyDescriptor keyDescriptor = openSamlObjects.buildDefaultInstanceOfType(KeyDescriptor.class); + + if (!Strings.isNullOrEmpty(name)) { + keyDescriptor.setName(name); + } + + if (!"both".equals(type)) { + keyDescriptor.setUsageType(type); + } + + KeyInfo keyInfo = openSamlObjects.buildDefaultInstanceOfType(KeyInfo.class); + keyDescriptor.setKeyInfo(keyInfo); + + X509Data x509Data = openSamlObjects.buildDefaultInstanceOfType(X509Data.class); + keyInfo.getXMLObjects().add(x509Data); + + X509Certificate x509Certificate = openSamlObjects.buildDefaultInstanceOfType(X509Certificate.class); + x509Data.getXMLObjects().add(x509Certificate); + x509Certificate.setValue(value); + + return keyDescriptor; + } + + public static EntityAttributes getEntityAttributes(EntityDescriptor ed) { + return getEntityAttributes(ed, true); + } + + public static EntityAttributes getEntityAttributes(EntityDescriptor ed, boolean create) { + Extensions extensions = ed.getExtensions(); + if (extensions == null && !create) { + return null; + } + if (extensions == null) { + extensions = openSamlObjects.buildDefaultInstanceOfType(Extensions.class); + ed.setExtensions(extensions); + } + + EntityAttributes entityAttributes = null; + if (extensions.getUnknownXMLObjects(EntityAttributes.DEFAULT_ELEMENT_NAME).size() > 0) { + entityAttributes = (EntityAttributes) extensions.getUnknownXMLObjects(EntityAttributes.DEFAULT_ELEMENT_NAME).get(0); + } else { + if (create) { + entityAttributes = ((EntityAttributesBuilder) openSamlObjects.getBuilderFactory().getBuilder(EntityAttributes.DEFAULT_ELEMENT_NAME)).buildObject(); + extensions.getUnknownXMLObjects().add(entityAttributes); + } + } + return entityAttributes; + } + + public static Optional getOptionalEntityAttributes(EntityDescriptor ed) { + return Optional.ofNullable(getEntityAttributes(ed, false)); + } + + public static SPSSODescriptor getSPSSODescriptorFromEntityDescriptor(EntityDescriptor entityDescriptor) { + return getSPSSODescriptorFromEntityDescriptor(entityDescriptor, true); + } + + public static SPSSODescriptor getSPSSODescriptorFromEntityDescriptor(EntityDescriptor entityDescriptor, boolean create) { + if (entityDescriptor.getSPSSODescriptor("") == null && create) { + SPSSODescriptor spssoDescriptor = openSamlObjects.buildDefaultInstanceOfType(SPSSODescriptor.class); + entityDescriptor.getRoleDescriptors().add(spssoDescriptor); + } + return entityDescriptor.getSPSSODescriptor(""); + } + + private static UIInfo getUIInfo(EntityDescriptor ed) { + Extensions extensions = getSPSSODescriptorFromEntityDescriptor(ed).getExtensions(); + if (extensions == null) { + extensions = openSamlObjects.buildDefaultInstanceOfType(Extensions.class); + ed.getSPSSODescriptor("").setExtensions(extensions); + } + + UIInfo uiInfo; + if (extensions.getUnknownXMLObjects(UIInfo.DEFAULT_ELEMENT_NAME).size() > 0) { + uiInfo = (UIInfo) extensions.getUnknownXMLObjects(UIInfo.DEFAULT_ELEMENT_NAME).get(0); + } else { + uiInfo = openSamlObjects.buildDefaultInstanceOfType(UIInfo.class); + extensions.getUnknownXMLObjects().add(uiInfo); + } + return uiInfo; + } + + private static void removeUIInfo(EntityDescriptor ed) { + SPSSODescriptor spssoDescriptor = getSPSSODescriptorFromEntityDescriptor(ed, false); + if (spssoDescriptor != null) { + Extensions extensions = spssoDescriptor.getExtensions(); + if (extensions == null) { + return; + } + if (extensions.getUnknownXMLObjects(UIInfo.DEFAULT_ELEMENT_NAME).size() > 0) { + extensions.getUnknownXMLObjects().remove(extensions.getUnknownXMLObjects(UIInfo.DEFAULT_ELEMENT_NAME).get(0)); + } + } + } + + public static void setupACSs(EntityDescriptor ed, EntityDescriptorRepresentation representation) { + if (representation.getAssertionConsumerServices() != null && representation.getAssertionConsumerServices().size() > 0) { + // TODO: review if we need more than a naive implementation + ed.getOptionalSPSSODescriptor().ifPresent(spssoDescriptor -> spssoDescriptor.getAssertionConsumerServices().clear()); + for (AssertionConsumerServiceRepresentation acsRepresentation : representation.getAssertionConsumerServices()) { + AssertionConsumerService assertionConsumerService = openSamlObjects.buildDefaultInstanceOfType(AssertionConsumerService.class); + getSPSSODescriptorFromEntityDescriptor(ed).getAssertionConsumerServices().add(assertionConsumerService); + if (acsRepresentation.isMakeDefault()) { + assertionConsumerService.setIsDefault(true); + } + assertionConsumerService.setBinding(acsRepresentation.getBinding()); + assertionConsumerService.setLocation(acsRepresentation.getLocationUrl()); + assertionConsumerService.setIndex(acsRepresentation.getIndex()); + } + } else { + ed.getOptionalSPSSODescriptor().ifPresent(spssoDescriptor -> spssoDescriptor.getAssertionConsumerServices().clear()); + } + } + + public static void setupContacts(EntityDescriptor ed, EntityDescriptorRepresentation representation) { + if (representation.getContacts() != null && representation.getContacts().size() > 0) { + ed.getContactPersons().clear(); + for (ContactRepresentation contactRepresentation : representation.getContacts()) { + ContactPerson contactPerson = ((ContactPersonBuilder) openSamlObjects.getBuilderFactory().getBuilder(ContactPerson.DEFAULT_ELEMENT_NAME)).buildObject(); + + contactPerson.setType(contactRepresentation.getType()); + + GivenName givenName = openSamlObjects.buildDefaultInstanceOfType(GivenName.class); + givenName.setName(contactRepresentation.getName()); + contactPerson.setGivenName(givenName); + + EmailAddress emailAddress = openSamlObjects.buildDefaultInstanceOfType(EmailAddress.class); + emailAddress.setAddress(contactRepresentation.getEmailAddress()); + contactPerson.addEmailAddress(emailAddress); + + ed.addContactPerson(contactPerson); + } + } else { + ed.getContactPersons().clear(); + } + } + + public static void setupLogout(EntityDescriptor ed, EntityDescriptorRepresentation representation) { + // setup logout + if (representation.getLogoutEndpoints() != null && !representation.getLogoutEndpoints().isEmpty()) { + // TODO: review if we need more than a naive implementation + ed.getOptionalSPSSODescriptor().ifPresent(spssoDescriptor -> spssoDescriptor.getSingleLogoutServices().clear()); + for (LogoutEndpointRepresentation logoutEndpointRepresentation : representation.getLogoutEndpoints()) { + SingleLogoutService singleLogoutService = openSamlObjects.buildDefaultInstanceOfType(SingleLogoutService.class); + singleLogoutService.setBinding(logoutEndpointRepresentation.getBindingType()); + singleLogoutService.setLocation(logoutEndpointRepresentation.getUrl()); + + getSPSSODescriptorFromEntityDescriptor(ed).getSingleLogoutServices().add(singleLogoutService); + } + } else { + ed.getOptionalSPSSODescriptor().ifPresent(spssoDescriptor -> spssoDescriptor.getSingleLogoutServices().clear()); + } + } + + public static void setupOrganization(EntityDescriptor ed, EntityDescriptorRepresentation representation) { + if (representation.getOrganization() != null && representation.getOrganization().getName() != null && representation.getOrganization().getDisplayName() != null && representation.getOrganization().getUrl() != null) { + OrganizationRepresentation organizationRepresentation = representation.getOrganization(); + Organization organization = openSamlObjects.buildDefaultInstanceOfType(Organization.class); + + OrganizationName organizationName = openSamlObjects.buildDefaultInstanceOfType(OrganizationName.class); + organizationName.setXMLLang("en"); + organizationName.setValue(organizationRepresentation.getName()); + organization.getOrganizationNames().add(organizationName); + + OrganizationDisplayName organizationDisplayName = openSamlObjects.buildDefaultInstanceOfType(OrganizationDisplayName.class); + organizationDisplayName.setXMLLang("en"); + organizationDisplayName.setValue(organizationRepresentation.getDisplayName()); + organization.getDisplayNames().add(organizationDisplayName); + + OrganizationURL organizationURL = openSamlObjects.buildDefaultInstanceOfType(OrganizationURL.class); + organizationURL.setXMLLang("en"); + organizationURL.setValue(organizationRepresentation.getUrl()); + organization.getURLs().add(organizationURL); + + ed.setOrganization(organization); + } else { + ed.setOrganization(null); + } + } + + public static void setupSecurity(EntityDescriptor ed, EntityDescriptorRepresentation representation) { + if (representation.getSecurityInfo() != null) { + SecurityInfoRepresentation securityInfoRepresentation = representation.getSecurityInfo(); + if (securityInfoRepresentation.isAuthenticationRequestsSigned()) { + getSPSSODescriptorFromEntityDescriptor(ed).setAuthnRequestsSigned(true); + } + if (securityInfoRepresentation.isWantAssertionsSigned()) { + getSPSSODescriptorFromEntityDescriptor(ed).setWantAssertionsSigned(true); + } + // TODO: review if we need more than a naive implementation + ed.getOptionalSPSSODescriptor().ifPresent( i -> i.getKeyDescriptors().clear()); + if (securityInfoRepresentation.isX509CertificateAvailable()) { + for (SecurityInfoRepresentation.X509CertificateRepresentation x509CertificateRepresentation : securityInfoRepresentation.getX509Certificates()) { + KeyDescriptor keyDescriptor = createKeyDescriptor(x509CertificateRepresentation.getName(), x509CertificateRepresentation.getType(), x509CertificateRepresentation.getValue()); + getSPSSODescriptorFromEntityDescriptor(ed).addKeyDescriptor(keyDescriptor); + } + } + } else { + ed.getOptionalSPSSODescriptor().ifPresent( spssoDescriptor -> { + spssoDescriptor.setAuthnRequestsSigned((Boolean) null); + spssoDescriptor.setWantAssertionsSigned((Boolean) null); + spssoDescriptor.getKeyDescriptors().clear(); + }); + } + } + + public static void setupSPSSODescriptor(EntityDescriptor ed, EntityDescriptorRepresentation representation) { + if (representation.getServiceProviderSsoDescriptor() != null) { + SPSSODescriptor spssoDescriptor = getSPSSODescriptorFromEntityDescriptor(ed); + + spssoDescriptor.setSupportedProtocols(Collections.EMPTY_LIST); + if (!Strings.isNullOrEmpty(representation.getServiceProviderSsoDescriptor().getProtocolSupportEnum())) { + spssoDescriptor.setSupportedProtocols( + Arrays.stream(representation.getServiceProviderSsoDescriptor().getProtocolSupportEnum().split(",")).map(p -> MDDCConstants.PROTOCOL_BINDINGS.get(p.trim())).collect(Collectors.toList()) + ); + } + + spssoDescriptor.getNameIDFormats().clear(); + if (representation.getServiceProviderSsoDescriptor() != null && representation.getServiceProviderSsoDescriptor().getNameIdFormats() != null && representation.getServiceProviderSsoDescriptor().getNameIdFormats().size() > 0) { + for (String nameidFormat : representation.getServiceProviderSsoDescriptor().getNameIdFormats()) { + NameIDFormat nameIDFormat = openSamlObjects.buildDefaultInstanceOfType(NameIDFormat.class); + + nameIDFormat.setFormat(nameidFormat); + + spssoDescriptor.getNameIDFormats().add(nameIDFormat); + } + } + } else { + ed.setRoleDescriptors(null); + } + } + + public static void setupUIInfo(EntityDescriptor ed, EntityDescriptorRepresentation representation) { + if (representation.getMdui() != null) { + // TODO: check if we need more than a naive implementation + removeUIInfo(ed); + MduiRepresentation mduiRepresentation = representation.getMdui(); + + if (!Strings.isNullOrEmpty(mduiRepresentation.getDisplayName())) { + DisplayName displayName = openSamlObjects.buildDefaultInstanceOfType(DisplayName.class); + getUIInfo(ed).addDisplayName(displayName); + displayName.setValue(mduiRepresentation.getDisplayName()); + displayName.setXMLLang("en"); + } else { + ed.getOptionalSPSSODescriptor() + .flatMap(SPSSODescriptor::getOptionalExtensions) + .flatMap(Extensions::getOptionalUIInfo) + .ifPresent(u -> u.getXMLObjects().removeAll(u.getDisplayNames())); + } + + if (!Strings.isNullOrEmpty(mduiRepresentation.getInformationUrl())) { + InformationURL informationURL = openSamlObjects.buildDefaultInstanceOfType(InformationURL.class); + getUIInfo(ed).addInformationURL(informationURL); + informationURL.setValue(mduiRepresentation.getInformationUrl()); + informationURL.setXMLLang("en"); + } else { + ed.getOptionalSPSSODescriptor() + .flatMap(SPSSODescriptor::getOptionalExtensions) + .flatMap(Extensions::getOptionalUIInfo) + .ifPresent(u -> u.getXMLObjects().removeAll(u.getInformationURLs())); + } + + if (!Strings.isNullOrEmpty(mduiRepresentation.getPrivacyStatementUrl())) { + PrivacyStatementURL privacyStatementURL = openSamlObjects.buildDefaultInstanceOfType(PrivacyStatementURL.class); + getUIInfo(ed).addPrivacyStatementURL(privacyStatementURL); + privacyStatementURL.setValue(mduiRepresentation.getPrivacyStatementUrl()); + privacyStatementURL.setXMLLang("en"); + } else { + ed.getOptionalSPSSODescriptor() + .flatMap(SPSSODescriptor::getOptionalExtensions) + .flatMap(Extensions::getOptionalUIInfo) + .ifPresent(u -> u.getXMLObjects().removeAll(u.getPrivacyStatementURLs())); + } + + if (!Strings.isNullOrEmpty(mduiRepresentation.getDescription())) { + Description description = openSamlObjects.buildDefaultInstanceOfType(Description.class); + getUIInfo(ed).addDescription(description); + description.setValue(mduiRepresentation.getDescription()); + description.setXMLLang("en"); + } else { + ed.getOptionalSPSSODescriptor() + .flatMap(SPSSODescriptor::getOptionalExtensions) + .flatMap(Extensions::getOptionalUIInfo) + .ifPresent(u -> u.getXMLObjects().removeAll(u.getDescriptions())); + } + + if (!Strings.isNullOrEmpty(mduiRepresentation.getLogoUrl())) { + Logo logo = openSamlObjects.buildDefaultInstanceOfType(Logo.class); + getUIInfo(ed).addLogo(logo); + logo.setURL(mduiRepresentation.getLogoUrl()); + logo.setHeight(mduiRepresentation.getLogoHeight()); + logo.setWidth(mduiRepresentation.getLogoWidth()); + logo.setXMLLang("en"); + } else { + ed.getOptionalSPSSODescriptor() + .flatMap(SPSSODescriptor::getOptionalExtensions) + .flatMap(Extensions::getOptionalUIInfo) + .ifPresent(u -> u.getXMLObjects().removeAll(u.getLogos())); + } + } else { + removeUIInfo(ed); + } + } + + public static void setupRelyingPartyOverrides(EntityDescriptor ed, EntityDescriptorRepresentation representation) { + if (representation.getRelyingPartyOverrides() != null || (representation.getAttributeRelease() != null && representation.getAttributeRelease().size() > 0)) { + // TODO: review if we need more than a naive implementation + getOptionalEntityAttributes(ed).ifPresent(entityAttributes -> entityAttributes.getAttributes().clear()); + getEntityAttributes(ed).getAttributes().addAll(entityService.getAttributeListFromEntityRepresentation(representation)); + } else { + getOptionalEntityAttributes(ed).ifPresent(entityAttributes -> entityAttributes.getAttributes().clear()); + } + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index de3f94759..83f2635e0 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -43,7 +43,7 @@ spring.jpa.hibernate.ddl-auto=update spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl spring.jpa.show-sql=false spring.jpa.properties.hibernate.format_sql=false - +spring.jpa.properties.hibernate.check_nullability=true spring.jpa.hibernate.use-new-id-generator-mappings=true #Envers versioning diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index ee6ab56ed..9789f7299 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -61,8 +61,11 @@ action.user-role=User Role action.toggle-view=Toggle view action.advanced=Advanced action.add-new-attribute=Add new attribute +action.add-new-group=Add new group action.add-attribute=Add Attribute action.custom-entity-attributes=Custom Entity Attributes +action.groups=Groups +action.source-group=Group value.enabled=Enabled value.disabled=Disabled @@ -166,6 +169,8 @@ label.entity-attributes=Entity Attributes label.custom-entity-attributes=Custom Entity Attributes label.help-text=Help text label.default-value=Default Value +label.groups-management=Groups Management +label.new-group=New Group label.new-attribute=New Custom Entity Attribute label.metadata-source=Metadata Source @@ -190,6 +195,8 @@ label.new-endpoint=New Endpoint label.select-binding=Select Binding Type label.mark-as-default=Mark as Default label.attribute-name=Attribute Name +label.group-name=Group Name +label.group-description=Group Description label.yes=Yes label.check-all-attributes=Check All Attributes label.clear-all-attributes=Clear All Attributes @@ -474,6 +481,7 @@ label.current=Current label.restore=Restore label.compare-selected=Compare Selected label.restore-version=Restore Version ({ date }) +label.group=Group label.saved=Saved label.by=By @@ -481,9 +489,14 @@ label.by=By label.source=Metadata Source label.provider=Metadata Provider +message.user-role-admin-group=Cannot change group for ROLE_ADMIN users. + message.delete-user-title=Delete User? message.delete-user-body=You are requesting to delete a user. If you complete this process the user will be removed. This cannot be undone. Do you wish to continue? +message.delete-group-title=Delete Group? +message.delete-group-body=You are requesting to delete a group. If you complete this process the group will be removed. This cannot be undone. Do you wish to continue? + message.delete-attribute-title=Delete Attribute? message.delete-attribute-body=You are requesting to delete a custom attribute. If you complete this process the attribute will be removed. This cannot be undone. Do you wish to continue? @@ -667,4 +680,7 @@ tooltip.match=A regular expression against which the entityID is evaluated. tooltip.remove-existing-formats=Whether to remove any existing formats from a role if any are added by the filter (unmodified roles will be untouched regardless of this setting) tooltip.nameid-formats-format=Format tooltip.nameid-formats-value=Value -tooltip.nameid-formats-type=Type \ No newline at end of file +tooltip.nameid-formats-type=Type + +tooltip.group-name=Group Name +tooltip.group-description=Group Description \ No newline at end of file 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 7f12a050d..384cb9ce6 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 @@ -5,6 +5,9 @@ import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.CustomEntityAttributeDefinitionRepository import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository import edu.internet2.tier.shibboleth.admin.ui.security.DefaultAuditorAware +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.CustomEntityAttributesDefinitionServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService import net.shibboleth.ext.spring.resource.ResourceHelper @@ -26,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile import org.springframework.core.io.ClassPathResource import org.springframework.data.domain.AuditorAware import org.springframework.mail.javamail.JavaMailSender @@ -118,4 +122,13 @@ class TestConfiguration { AuditorAware defaultAuditorAware() { return new DefaultAuditorAware() } + + @Bean + GroupServiceImpl groupServiceImpl(GroupsRepository repo, OwnershipRepository ownershipRepository) { + new GroupServiceImpl().with { + it.groupRepository = repo + it.ownershipRepository = ownershipRepository + return it + } + } } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy index 8f262b6a1..35c015681 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy @@ -37,6 +37,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") class EntitiesControllerTests extends Specification { + @Autowired + JPAEntityDescriptorServiceImpl serviceImpl + + @Autowired + UserService userService + def openSamlObjects = new OpenSamlObjects().with { init() it @@ -50,9 +56,6 @@ class EntitiesControllerTests extends Specification { initialize() it } - - @Autowired - UserService userService // This stub will spit out the results from the resolver instead of actually finding them in the DB @SpringBean @@ -62,14 +65,18 @@ class EntitiesControllerTests extends Specification { } @Subject - def controller = new EntitiesController( - openSamlObjects: openSamlObjects, - entityDescriptorService: new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects), userService), - entityDescriptorRepository: edr - ) - - def mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + def controller + def mockMvc + def setup() { + controller = new EntitiesController() + controller.openSamlObjects = openSamlObjects + controller.entityDescriptorService = serviceImpl + controller.entityDescriptorRepository = edr + + mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + } + def 'GET /api/entities/test'() { when: def result = mockMvc.perform(get("/api/entities/test")) @@ -124,9 +131,10 @@ class EntitiesControllerTests extends Specification { "createdDate":null, "modifiedDate":null, "attributeRelease":["givenName","employeeNumber"], - "version":-1891841119, + "version":1445248649, "createdBy":null, - "current":false + "current":false, + "groupId":null } ''' @@ -143,7 +151,7 @@ class EntitiesControllerTests extends Specification { // .andExpect(header().exists(HttpHeaders.ETAG)) // MUST HAVE - is done by filter, so skipped for test .andExpect(header().exists(HttpHeaders.LAST_MODIFIED)) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(expectedBody, false)) + .andExpect(jsonPath('$.entityId', is("http://test.scaldingspoon.org/test1"))) } def 'GET /entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1'() { @@ -168,9 +176,10 @@ class EntitiesControllerTests extends Specification { "createdDate":null, "modifiedDate":null, "attributeRelease":["givenName","employeeNumber"], - "version":-1891841119, + "version":1445248649, "createdBy":null, - "current":false + "current":false, + "groupId":null } ''' @@ -187,7 +196,7 @@ class EntitiesControllerTests extends Specification { // .andExpect(header().exists(HttpHeaders.ETAG)) // MUST HAVE - is done by filter, so skipped for test .andExpect(header().exists(HttpHeaders.LAST_MODIFIED)) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(expectedBody, false)) + .andExpect(jsonPath('$.entityId').value("http://test.scaldingspoon.org/test1")) } def 'GET /api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1 XML'() { 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 75ef8dd3e..a04c9ebba 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 @@ -6,52 +6,119 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.Internationalization import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +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 import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceForTesting +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService import edu.internet2.tier.shibboleth.admin.ui.service.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.service.JPAEntityServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils import groovy.json.JsonOutput import groovy.json.JsonSlurper + +import org.skyscreamer.jsonassert.Customization +import org.skyscreamer.jsonassert.JSONAssert +import org.skyscreamer.jsonassert.JSONCompareMode +import org.skyscreamer.jsonassert.ValueMatcher +import org.skyscreamer.jsonassert.comparator.CustomComparator +import org.skyscreamer.jsonassert.comparator.JSONCompareUtil +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.support.RootBeanDefinition import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Profile +import org.springframework.context.support.StaticApplicationContext import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.security.core.Authentication import org.springframework.security.core.context.SecurityContext import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.annotation.Rollback +import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.transaction.annotation.Transactional import org.springframework.web.client.RestTemplate +import org.springframework.web.servlet.config.annotation.EnableWebMvc +import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport +import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver + import spock.lang.Ignore +import spock.lang.Shared import spock.lang.Specification import spock.lang.Subject import java.time.LocalDateTime +import javax.persistence.EntityManager + import static org.hamcrest.CoreMatchers.containsString import static org.springframework.http.MediaType.* import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration, LocalConfig]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@DirtiesContext +@ActiveProfiles(["local"]) class EntityDescriptorControllerTests extends Specification { + @Autowired + EntityDescriptorRepository entityDescriptorRepository + @Autowired + EntityManager entityManager + + @Autowired + EntityService entityService + + @Autowired + GroupServiceForTesting groupService + + @Autowired + OwnershipRepository ownershipRepository + + @Autowired + RoleRepository roleRepository + + @Autowired + JPAEntityDescriptorServiceImpl service + + @Autowired + UserRepository userRepository + + @Autowired + UserService userService + RandomGenerator randomGenerator TestObjectGenerator generator def mapper - def service - - def entityDescriptorRepository = Mock(EntityDescriptorRepository) + def mockRestTemplate = Mock(RestTemplate) def openSamlObjects = new OpenSamlObjects().with { @@ -65,92 +132,87 @@ class EntityDescriptorControllerTests extends Specification { def controller Authentication authentication = Mock() - SecurityContext securityContext = Mock() - UserRepository userRepository = Mock() - RoleRepository roleRepository = Mock() - - UserService userService + SecurityContext securityContext = Mock() EntityDescriptorVersionService versionService = Mock() - + + @Transactional def setup() { + groupService.ensureAdminGroupExists() generator = new TestObjectGenerator() randomGenerator = new RandomGenerator() mapper = new ObjectMapper() - userService = new UserService(roleRepository, userRepository) - service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects), userService) + service.userService = userService + service.groupService = groupService - controller = new EntityDescriptorController(userService, versionService) - controller.entityDescriptorRepository = entityDescriptorRepository + controller = new EntityDescriptorController(versionService) controller.openSamlObjects = openSamlObjects controller.entityDescriptorService = service - controller.restTemplate = mockRestTemplate - mockMvc = MockMvcBuilders.standaloneSetup(controller).build() - + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + securityContext.getAuthentication() >> authentication SecurityContextHolder.setContext(securityContext) - + + if (roleRepository.count() == 0) { + def roles = [new Role().with { + name = 'ROLE_ADMIN' + it + }, new Role().with { + name = 'ROLE_USER' + it + }, new Role().with { + name = 'ROLE_NONE' + it + }] + roles.each { + roleRepository.save(it) + } + } + + Optional adminRole = roleRepository.findByName("ROLE_ADMIN") + User adminUser = new User(username: "admin", roles: [adminRole.get()], password: "foo") + userService.save(adminUser) + + Optional userRole = roleRepository.findByName("ROLE_USER") + User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") + userService.save(user) + + EntityDescriptorConversionUtils.setOpenSamlObjects(openSamlObjects) + EntityDescriptorConversionUtils.setEntityService(entityService) } - def 'GET /EntityDescriptors with empty repository as admin'() { + @Rollback + @WithMockUser(value = "admin", roles = ["ADMIN"]) + def 'DELETE as admin'() { given: - def username = 'admin' - def role = 'ROLE_ADMIN' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) - def emptyRecordsFromRepository = [].stream() - def expectedEmptyListResponseBody = '[]' - def expectedResponseContentType = APPLICATION_JSON - def expectedHttpResponseStatus = status().isOk() - + authentication.getName() >> 'admin' + def entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false) + entityDescriptorRepository.save(entityDescriptor) + + when: 'pre-check' + entityManager.flush() + + then: + entityDescriptorRepository.findAll().size() == 1 + when: - def result = mockMvc.perform(get('/api/EntityDescriptors')) - + def result = mockMvc.perform(delete("/api/EntityDescriptor/uuid-1")) + then: - //One call to the repo expected - 1 * entityDescriptorRepository.findAllStreamByCustomQuery() >> emptyRecordsFromRepository - result.andExpect(expectedHttpResponseStatus) - .andExpect(content().contentType(expectedResponseContentType)) - .andExpect(content().json(expectedEmptyListResponseBody)) - + result.andExpect(status().isNoContent()) + entityDescriptorRepository.findByResourceId("uuid-1") == null + entityDescriptorRepository.findAll().size() == 0 } - - //todo review - def 'GET /EntityDescriptors with 1 record in repository as admin'() { + + @Rollback + @WithMockUser(value = "admin", roles = ["ADMIN"]) + def 'GET /EntityDescriptors with empty repository as admin'() { given: - def username = 'admin' - def role = 'ROLE_ADMIN' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) - def expectedCreationDate = '2017-10-23T11:11:11' - def entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, - createdDate: LocalDateTime.parse(expectedCreationDate)) - def oneRecordFromRepository = [entityDescriptor].stream() - def version = entityDescriptor.hashCode() - def expectedOneRecordListResponseBody = """ - [ - { - "id": "uuid-1", - "serviceProviderName": "sp1", - "entityId": "eid1", - "serviceEnabled": true, - "createdDate": "$expectedCreationDate", - "modifiedDate": null, - "organization": {}, - "contacts": null, - "serviceProviderSsoDescriptor": null, - "logoutEndpoints": null, - "securityInfo": null, - "assertionConsumerServices": null, - "version": $version, - "createdBy": null, - "current": false - } - ] - """ + authentication.getName() >> 'admin' + def expectedEmptyListResponseBody = '[]' def expectedResponseContentType = APPLICATION_JSON def expectedHttpResponseStatus = status().isOk() @@ -158,122 +220,47 @@ class EntityDescriptorControllerTests extends Specification { def result = mockMvc.perform(get('/api/EntityDescriptors')) then: - //One call to the repo expected - 1 * entityDescriptorRepository.findAllStreamByCustomQuery() >> oneRecordFromRepository result.andExpect(expectedHttpResponseStatus) .andExpect(content().contentType(expectedResponseContentType)) - .andExpect(content().json(expectedOneRecordListResponseBody, true)) - + .andExpect(content().json(expectedEmptyListResponseBody)) } - //todo review - def 'GET /EntityDescriptors with 2 records in repository as admin'() { + @Rollback + @WithMockUser(value = "admin", roles = ["ADMIN"]) + def 'GET /EntityDescriptors with 1 record in repository as admin'() { given: - def username = 'admin' - def role = 'ROLE_ADMIN' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) - def expectedCreationDate = '2017-10-23T11:11:11' - def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', - serviceEnabled: true, - createdDate: LocalDateTime.parse(expectedCreationDate)) - def versionOne = entityDescriptorOne.hashCode() - def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', - serviceEnabled: false, - createdDate: LocalDateTime.parse(expectedCreationDate)) - def versionTwo = entityDescriptorTwo.hashCode() - def twoRecordsFromRepository = [entityDescriptorOne, entityDescriptorTwo].stream() - def expectedTwoRecordsListResponseBody = """ - [ - { - "id": "uuid-1", - "serviceProviderName": "sp1", - "entityId": "eid1", - "serviceEnabled": true, - "createdDate": "$expectedCreationDate", - "modifiedDate": null, - "organization": {}, - "contacts": null, - "serviceProviderSsoDescriptor": null, - "logoutEndpoints": null, - "securityInfo": null, - "assertionConsumerServices": null, - "version": $versionOne, - "createdBy": null, - "current": false - }, - { - "id": "uuid-2", - "serviceProviderName": "sp2", - "entityId": "eid2", - "serviceEnabled": false, - "createdDate": "$expectedCreationDate", - "modifiedDate": null, - "organization": {}, - "contacts": null, - "serviceProviderSsoDescriptor": null, - "logoutEndpoints": null, - "securityInfo": null, - "assertionConsumerServices": null, - "version": $versionTwo, - "createdBy": null, - "current": false - } - ] - """ - + authentication.getName() >> 'admin' + + def entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "admingroup") + entityDescriptorRepository.saveAndFlush(entityDescriptor) + def expectedResponseContentType = APPLICATION_JSON def expectedHttpResponseStatus = status().isOk() - when: + when: def result = mockMvc.perform(get('/api/EntityDescriptors')) - then: - //One call to the repo expected - 1 * entityDescriptorRepository.findAllStreamByCustomQuery() >> twoRecordsFromRepository - result.andExpect(expectedHttpResponseStatus) - .andExpect(content().contentType(expectedResponseContentType)) - .andExpect(content().json(expectedTwoRecordsListResponseBody, true)) - + then: + def mvcResult = result.andExpect(expectedHttpResponseStatus).andExpect(content().contentType(expectedResponseContentType)) + .andExpect(jsonPath("\$.[0].id").value("uuid-1")) + .andExpect(jsonPath("\$.[0].entityId").value("eid1")) + .andExpect(jsonPath("\$.[0].serviceEnabled").value(true)) + .andExpect(jsonPath("\$.[0].idOfOwner").value("admingroup")) } - - //todo review - def 'GET /EntityDescriptors with 1 record in repository as user returns only that user\'s records'() { + @Rollback + @WithMockUser(value = "admin", roles = ["ADMIN"]) + def 'GET /EntityDescriptors with 2 records in repository as admin'() { given: - def username = 'someUser' - def role = 'ROLE_USER' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) - def expectedCreationDate = '2017-10-23T11:11:11' - def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', - serviceEnabled: true, - createdDate: LocalDateTime.parse(expectedCreationDate), - createdBy: 'someUser') - def versionOne = entityDescriptorOne.hashCode() - def oneRecordFromRepository = [entityDescriptorOne].stream() - def expectedOneRecordListResponseBody = """ - [ - { - "id": "uuid-1", - "serviceProviderName": "sp1", - "entityId": "eid1", - "serviceEnabled": true, - "createdDate": "$expectedCreationDate", - "modifiedDate": null, - "organization": {}, - "contacts": null, - "serviceProviderSsoDescriptor": null, - "logoutEndpoints": null, - "securityInfo": null, - "assertionConsumerServices": null, - "version": $versionOne, - "createdBy": "someUser", - "current": false - } - ] - """ - + authentication.getName() >> 'admin' + + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "admingroup") + def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: "admingroup") + + entityDescriptorRepository.saveAndFlush(entityDescriptorOne) + entityDescriptorRepository.saveAndFlush(entityDescriptorTwo) + def expectedResponseContentType = APPLICATION_JSON def expectedHttpResponseStatus = status().isOk() @@ -281,140 +268,106 @@ class EntityDescriptorControllerTests extends Specification { def result = mockMvc.perform(get('/api/EntityDescriptors')) then: - //One call to the repo expected - 1 * entityDescriptorRepository.findAllStreamByCreatedBy('someUser') >> oneRecordFromRepository result.andExpect(expectedHttpResponseStatus) - .andExpect(content().contentType(expectedResponseContentType)) - .andExpect(content().json(expectedOneRecordListResponseBody, true)) - } - - //todo review + .andExpect(content().contentType(expectedResponseContentType)) + .andExpect(jsonPath("\$.[0].id").value("uuid-1")) + .andExpect(jsonPath("\$.[0].entityId").value("eid1")) + .andExpect(jsonPath("\$.[0].serviceEnabled").value(true)) + .andExpect(jsonPath("\$.[0].idOfOwner").value("admingroup")) + .andExpect(jsonPath("\$.[1].id").value("uuid-2")) + .andExpect(jsonPath("\$.[1].entityId").value("eid2")) + .andExpect(jsonPath("\$.[1].serviceEnabled").value(false)) + .andExpect(jsonPath("\$.[1].idOfOwner").value("admingroup")) + } + + @Rollback + @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'POST /EntityDescriptor and successfully create new record'() { given: - def username = 'admin' - def role = 'ROLE_ADMIN' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) - def expectedCreationDate = '2017-10-23T11:11:11' + authentication.getName() >> 'admin' + def expectedEntityId = 'https://shib' def expectedSpName = 'sp1' - def expectedUUID = 'uuid-1' def expectedResponseHeader = 'Location' - def expectedResponseHeaderValue = "/api/EntityDescriptor/$expectedUUID" - def entityDescriptor = new EntityDescriptor(resourceId: expectedUUID, entityID: expectedEntityId, serviceProviderName: expectedSpName, - serviceEnabled: true, - createdDate: LocalDateTime.parse(expectedCreationDate)) - def version = entityDescriptor.hashCode() + def expectedResponseHeaderValue = "/api/EntityDescriptor/" def postedJsonBody = """ - { - "serviceProviderName": "$expectedSpName", - "entityId": "$expectedEntityId", - "organization": {}, - "serviceEnabled": true, - "createdDate": null, - "modifiedDate": null, - "contacts": null, - "serviceProviderSsoDescriptor": null, - "logoutEndpoints": null, - "securityInfo": null, - "assertionConsumerServices": null, - "current": false - } - """ - - def expectedJsonBody = """ { - "id": "$expectedUUID", "serviceProviderName": "$expectedSpName", "entityId": "$expectedEntityId", "organization": {}, "serviceEnabled": true, - "createdDate": "$expectedCreationDate", + "createdDate": null, "modifiedDate": null, "contacts": null, "serviceProviderSsoDescriptor": null, "logoutEndpoints": null, "securityInfo": null, "assertionConsumerServices": null, - "version": $version, - "createdBy": null, "current": false } """ when: - def result = mockMvc.perform( - post('/api/EntityDescriptor') - .contentType(APPLICATION_JSON) - .content(postedJsonBody)) + def result = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) then: - //Stub invocation of the repository returning null for non-existent record - 1 * entityDescriptorRepository.findByEntityID(expectedEntityId) >> null - - //Expect 1 invocation of repository save() with correct EntityDescriptor - 1 * entityDescriptorRepository.save({ - it.entityID == expectedEntityId && - it.serviceProviderName == expectedSpName && - it.serviceEnabled == true - }) >> entityDescriptor - result.andExpect(status().isCreated()) - .andExpect(content().json(expectedJsonBody, true)) - .andExpect(header().string(expectedResponseHeader, containsString(expectedResponseHeaderValue))) - + .andExpect(header().string(expectedResponseHeader, containsString(expectedResponseHeaderValue))) + .andExpect(jsonPath("\$.entityId").value("https://shib")) + .andExpect(jsonPath("\$.serviceEnabled").value(true)) + .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) } + @Rollback + @WithMockUser(value = "someUser", roles = ["USER"]) def 'POST /EntityDescriptor as user disallows enabling'() { given: - def username = 'someUser' - def role = 'ROLE_USER' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) + authentication.getName() >> 'someUser' + def expectedEntityId = 'https://shib' def expectedSpName = 'sp1' - def postedJsonBody = """ - { - "serviceProviderName": "$expectedSpName", - "entityId": "$expectedEntityId", - "organization": null, - "serviceEnabled": true, - "createdDate": null, + when: + def postedJsonBody = """ + { + "serviceProviderName": "$expectedSpName", + "entityId": "$expectedEntityId", + "organization": null, + "serviceEnabled": true, + "createdDate": null, "modifiedDate": null, - "organization": null, - "contacts": null, - "mdui": null, - "serviceProviderSsoDescriptor": null, - "logoutEndpoints": null, - "securityInfo": null, - "assertionConsumerServices": null, - "relyingPartyOverrides": null, + "organization": null, + "contacts": null, + "mdui": null, + "serviceProviderSsoDescriptor": null, + "logoutEndpoints": null, + "securityInfo": null, + "assertionConsumerServices": null, + "relyingPartyOverrides": null, "attributeRelease": null } """ - - when: - def result = mockMvc.perform( - post('/api/EntityDescriptor') - .contentType(APPLICATION_JSON) - .content(postedJsonBody)) - + then: - 0 * entityDescriptorRepository.findByEntityID(_) - 0 * entityDescriptorRepository.save(_) - - result.andExpect(status().isForbidden()) + try { + def exceptionExpected = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) + } + catch (Exception e) { + e instanceof ForbiddenException == true + } } + @Rollback + @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'POST /EntityDescriptor record already exists'() { given: - def expectedEntityId = 'eid1' + authentication.getName() >> 'admin' + def postedJsonBody = """ { "serviceProviderName": "sp1", - "entityId": "$expectedEntityId", + "entityId": "eid1", "organization": null, "serviceEnabled": true, "createdDate": null, @@ -432,270 +385,187 @@ class EntityDescriptorControllerTests extends Specification { """ when: - def result = mockMvc.perform( - post('/api/EntityDescriptor') - .contentType(APPLICATION_JSON) - .content(postedJsonBody)) - + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true) + def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false) + + entityDescriptorRepository.save(entityDescriptorOne) + entityDescriptorRepository.save(entityDescriptorTwo) + entityManager.flush() + then: - //Stub invocation of the repository returning an existing record - 1 * entityDescriptorRepository.findByEntityID(expectedEntityId) >> new EntityDescriptor(entityID: expectedEntityId) - result.andExpect(status().isConflict()) + try { + def exceptionExpected = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) + } + catch (Exception e) { + e instanceof EntityIdExistsException == true + } } - + + @Rollback + @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'GET /EntityDescriptor/{resourceId} non-existent'() { - given: - def username = 'admin' - def role = 'ROLE_ADMIN' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) - def providedResourceId = 'uuid-1' - when: - def result = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId")) + authentication.getName() >> 'admin' then: - //No EntityDescriptor found - 1 * entityDescriptorRepository.findByResourceId(providedResourceId) >> null - result.andExpect(status().isNotFound()) + try { + def exceptionExpected = mockMvc.perform(get("/api/EntityDescriptor/uuid-1")) + } + catch (Exception e) { + e instanceof EntityNotFoundException == true + } } - - //todo review + @Rollback + @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'GET /EntityDescriptor/{resourceId} existing'() { given: - def username = 'admin' - def role = 'ROLE_ADMIN' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) - def expectedCreationDate = '2017-10-23T11:11:11' - def providedResourceId = 'uuid-1' - def expectedSpName = 'sp1' - def expectedEntityId = 'eid1' - - def entityDescriptor = new EntityDescriptor(resourceId: providedResourceId, entityID: expectedEntityId, serviceProviderName: expectedSpName, - serviceEnabled: true, - createdDate: LocalDateTime.parse(expectedCreationDate)) - def version = entityDescriptor.hashCode() - - def expectedJsonBody = """ - { - "id": "${providedResourceId}", - "serviceProviderName": "$expectedSpName", - "entityId": "$expectedEntityId", - "organization": {}, - "serviceEnabled": true, - "createdDate": "$expectedCreationDate", - "modifiedDate": null, - "contacts": null, - "serviceProviderSsoDescriptor": null, - "logoutEndpoints": null, - "securityInfo": null, - "assertionConsumerServices": null, - "version": $version, - "createdBy": null, - "current": false - } - """ - + authentication.getName() >> 'admin' + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "admingroup") + entityDescriptorRepository.save(entityDescriptorOne) + entityManager.flush() + when: - def result = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId")) - + def result = mockMvc.perform(get("/api/EntityDescriptor/uuid-1")) + then: - //EntityDescriptor found - 1 * entityDescriptorRepository.findByResourceId(providedResourceId) >> entityDescriptor - - result.andExpect(status().isOk()) - .andExpect(content().json(expectedJsonBody, true)) + .andExpect(jsonPath("\$.entityId").value("eid1")) + .andExpect(jsonPath("\$.serviceProviderName").value("sp1")) + .andExpect(jsonPath("\$.serviceEnabled").value(true)) + .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) } - - //todo review - def 'GET /EntityDescriptor/{resourceId} existing, owned by non-admin'() { + @Rollback + @WithMockUser(value = "someUser", roles = ["USER"]) + def 'GET /EntityDescriptor/{resourceId} existing, validate group access'() { given: - def username = 'someUser' - def role = 'ROLE_USER' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) - def expectedCreationDate = '2017-10-23T11:11:11' - def providedResourceId = 'uuid-1' - def expectedSpName = 'sp1' - def expectedEntityId = 'eid1' - - def entityDescriptor = new EntityDescriptor(resourceId: providedResourceId, entityID: expectedEntityId, serviceProviderName: expectedSpName, - serviceEnabled: true, - createdDate: LocalDateTime.parse(expectedCreationDate), - createdBy: 'someUser') - def version = entityDescriptor.hashCode() - - def expectedJsonBody = """ - { - "id": "${providedResourceId}", - "serviceProviderName": "$expectedSpName", - "entityId": "$expectedEntityId", - "organization": {}, - "serviceEnabled": true, - "createdDate": "$expectedCreationDate", - "modifiedDate": null, - "contacts": null, - "serviceProviderSsoDescriptor": null, - "logoutEndpoints": null, - "securityInfo": null, - "assertionConsumerServices": null, - "version": $version, - "createdBy": "someUser", - "current": false - } - """ - + authentication.getName() >> 'someUser' + Group g = userService.getCurrentUserGroup() + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "someUser") + def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) + + entityDescriptorRepository.saveAndFlush(entityDescriptorOne) + entityDescriptorRepository.saveAndFlush(entityDescriptorTwo) + + ownershipRepository.saveAndFlush(new Ownership(g, entityDescriptorOne)) + ownershipRepository.saveAndFlush(new Ownership(Group.ADMIN_GROUP, entityDescriptorTwo)) + + when: - def result = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId")) + def result = mockMvc.perform(get("/api/EntityDescriptor/uuid-1")) then: - //EntityDescriptor found - 1 * entityDescriptorRepository.findByResourceId(providedResourceId) >> entityDescriptor - - result.andExpect(status().isOk()) - .andExpect(content().json(expectedJsonBody, true)) + .andExpect(jsonPath("\$.entityId").value("eid1")) + .andExpect(jsonPath("\$.serviceProviderName").value("sp1")) + .andExpect(jsonPath("\$.serviceEnabled").value(true)) + .andExpect(jsonPath("\$.idOfOwner").value("someUser")) } + @Rollback + @WithMockUser(value = "someUser", roles = ["USER"]) def 'GET /EntityDescriptor/{resourceId} existing, owned by some other user'() { - given: - def username = 'someUser' - def role = 'ROLE_USER' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) - def expectedCreationDate = '2017-10-23T11:11:11' - def providedResourceId = 'uuid-1' - def expectedSpName = 'sp1' - def expectedEntityId = 'eid1' - - def entityDescriptor = new EntityDescriptor(resourceId: providedResourceId, entityID: expectedEntityId, serviceProviderName: expectedSpName, - serviceEnabled: true, - createdDate: LocalDateTime.parse(expectedCreationDate), - createdBy: 'someOtherUser') - when: - def result = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId")) + authentication.getName() >> 'someUser' + Group g = userService.getCurrentUserGroup() + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) + def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) + + entityDescriptorRepository.saveAndFlush(entityDescriptorOne) + entityDescriptorRepository.saveAndFlush(entityDescriptorTwo) + + ownershipRepository.saveAndFlush(new Ownership(g, entityDescriptorOne)) + ownershipRepository.saveAndFlush(new Ownership(Group.ADMIN_GROUP, entityDescriptorTwo)) then: - //EntityDescriptor found - 1 * entityDescriptorRepository.findByResourceId(providedResourceId) >> entityDescriptor - - result.andExpect(status().is(403)) + try { + def exceptionExpected = mockMvc.perform(get("/api/EntityDescriptor/uuid-2")) + } + catch (Exception e) { + e instanceof ForbiddenException == true + } } + @Rollback + @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'GET /EntityDescriptor/{resourceId} existing (xml)'() { given: - def username = 'admin' - def role = 'ROLE_ADMIN' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) - def expectedCreationDate = '2017-10-23T11:11:11' - def providedResourceId = 'uuid-1' - def expectedSpName = 'sp1' - def expectedEntityId = 'eid1' - - def entityDescriptor = new EntityDescriptor(resourceId: providedResourceId, entityID: expectedEntityId, serviceProviderName: expectedSpName, - serviceEnabled: true, - createdDate: LocalDateTime.parse(expectedCreationDate)) - entityDescriptor.setElementLocalName("EntityDescriptor") - entityDescriptor.setNamespacePrefix("md") - entityDescriptor.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") + authentication.getName() >> 'admin' + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true) + entityDescriptorOne.setElementLocalName("EntityDescriptor") + entityDescriptorOne.setNamespacePrefix("md") + entityDescriptorOne.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") + entityDescriptorRepository.save(entityDescriptorOne) + entityManager.flush() def expectedXML = """ """ + xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="eid1"/>""" when: - def result = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId") - .accept(APPLICATION_XML)) + def result = mockMvc.perform(get("/api/EntityDescriptor/uuid-1").accept(APPLICATION_XML)) then: - //EntityDescriptor found - 1 * entityDescriptorRepository.findByResourceId(providedResourceId) >> entityDescriptor - - - result.andExpect(status().isOk()) - .andExpect(content().xml(expectedXML)) + result.andExpect(status().isOk()).andExpect(content().xml(expectedXML)) } + @Rollback + @WithMockUser(value = "someUser", roles = ["USER"]) def 'GET /EntityDescriptor/{resourceId} existing (xml), user-owned'() { given: - def username = 'someUser' - def role = 'ROLE_USER' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) - def expectedCreationDate = '2017-10-23T11:11:11' - def providedResourceId = 'uuid-1' - def expectedSpName = 'sp1' - def expectedEntityId = 'eid1' - - def entityDescriptor = new EntityDescriptor(resourceId: providedResourceId, entityID: expectedEntityId, serviceProviderName: expectedSpName, - serviceEnabled: true, - createdDate: LocalDateTime.parse(expectedCreationDate), - createdBy: 'someUser') - entityDescriptor.setElementLocalName("EntityDescriptor") - entityDescriptor.setNamespacePrefix("md") - entityDescriptor.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") + authentication.getName() >> 'someUser' + Group g = userService.getCurrentUserGroup() + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) + entityDescriptorOne.setElementLocalName("EntityDescriptor") + entityDescriptorOne.setNamespacePrefix("md") + entityDescriptorOne.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") + entityDescriptorOne = entityDescriptorRepository.saveAndFlush(entityDescriptorOne) + ownershipRepository.saveAndFlush(new Ownership(g,entityDescriptorOne)) def expectedXML = """ """ + xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="eid1"/>""" when: - def result = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId") - .accept(APPLICATION_XML)) + def result = mockMvc.perform(get("/api/EntityDescriptor/uuid-1").accept(APPLICATION_XML)) then: - //EntityDescriptor found - 1 * entityDescriptorRepository.findByResourceId(providedResourceId) >> entityDescriptor - - - result.andExpect(status().isOk()) - .andExpect(content().xml(expectedXML)) - } + result.andExpect(status().isOk()).andExpect(content().xml(expectedXML)) + } + @Rollback + @WithMockUser(value = "someUser", roles = ["USER"]) def 'GET /EntityDescriptor/{resourceId} existing (xml), other user-owned'() { - given: - def username = 'someUser' - def role = 'ROLE_USER' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) - def expectedCreationDate = '2017-10-23T11:11:11' - def providedResourceId = 'uuid-1' - def expectedSpName = 'sp1' - def expectedEntityId = 'eid1' - - def entityDescriptor = new EntityDescriptor(resourceId: providedResourceId, entityID: expectedEntityId, serviceProviderName: expectedSpName, - serviceEnabled: true, - createdDate: LocalDateTime.parse(expectedCreationDate), - createdBy: 'someOtherUser') - entityDescriptor.setElementLocalName("EntityDescriptor") - entityDescriptor.setNamespacePrefix("md") - entityDescriptor.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") - when: - def result = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId") - .accept(APPLICATION_XML)) - + authentication.getName() >> 'someUser' + Group g = Group.ADMIN_GROUP + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) + entityDescriptorOne.setElementLocalName("EntityDescriptor") + entityDescriptorOne.setNamespacePrefix("md") + entityDescriptorOne.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") + entityDescriptorRepository.save(entityDescriptorOne) + entityManager.flush() + then: - //EntityDescriptor found - 1 * entityDescriptorRepository.findByResourceId(providedResourceId) >> entityDescriptor - - result.andExpect(status().is(403)) + try { + def exceptionExpected = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId").accept(APPLICATION_XML)) + } + catch (Exception e) { + e instanceof ForbiddenException == true + } } - - //todo review + @Rollback + @WithMockUser(value = "admin", roles = ["ADMIN"]) def "POST /EntityDescriptor handles XML happily"() { given: - def username = 'admin' - def role = 'ROLE_ADMIN' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) + authentication.getName() >> 'admin' + def postedBody = ''' @@ -715,109 +585,33 @@ class EntityDescriptorControllerTests extends Specification { ''' - def spName = randomGenerator.randomString() - - def expectedEntityDescriptor = EntityDescriptor.class.cast(openSamlObjects.unmarshalFromXml(postedBody.bytes)) - - 1 * entityDescriptorRepository.findByEntityID(_) >> null - 1 * entityDescriptorRepository.save(_) >> expectedEntityDescriptor - - def expectedJson = """ -{ - "version": ${expectedEntityDescriptor.hashCode()}, - "id": "${expectedEntityDescriptor.resourceId}", - "serviceProviderName": null, - "entityId": "http://test.scaldingspoon.org/test1", - "organization": {}, - "contacts": null, - "serviceProviderSsoDescriptor": { - "protocolSupportEnum": "SAML 2", - "nameIdFormats": [ - "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" - ] - }, - "logoutEndpoints": null, - "securityInfo": null, - "assertionConsumerServices": [ - { - "locationUrl": "https://test.scaldingspoon.org/test1/acs", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", - "makeDefault": false - } - ], - "serviceEnabled": false, - "createdDate": null, - "modifiedDate": null, - "attributeRelease": [ - "givenName", - "employeeNumber" - ], - "createdBy": null, - "current": false -} -""" + def expectedResponseHeader = 'Location' + def expectedResponseHeaderValue = "/api/EntityDescriptor/" when: - def result = mockMvc.perform(post("/api/EntityDescriptor") - .contentType(APPLICATION_XML) - .content(postedBody) - .param("spName", spName)) - + def spName = randomGenerator.randomString() + def result = mockMvc.perform(post("/api/EntityDescriptor").contentType(APPLICATION_XML).content(postedBody).param("spName", spName)) then: result.andExpect(status().isCreated()) - .andExpect(content().json(expectedJson, false)) + .andExpect(header().string(expectedResponseHeader, containsString(expectedResponseHeaderValue))) + .andExpect(jsonPath("\$.entityId").value("http://test.scaldingspoon.org/test1")) + .andExpect(jsonPath("\$.serviceEnabled").value(false)) + .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) + .andExpect(jsonPath("\$.serviceProviderSsoDescriptor.protocolSupportEnum").value("SAML 2")) + .andExpect(jsonPath("\$.serviceProviderSsoDescriptor.nameIdFormats[0]").value("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified")) + .andExpect(jsonPath("\$.assertionConsumerServices[0].binding").value("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST")) + .andExpect(jsonPath("\$.assertionConsumerServices[0].makeDefault").value(false)) + .andExpect(jsonPath("\$.assertionConsumerServices[0].locationUrl").value("https://test.scaldingspoon.org/test1/acs")) + } + @Rollback + @WithMockUser(value = "admin", roles = ["ADMIN"]) def "POST /EntityDescriptor returns error for duplicate entity id"() { - given: - def postedBody = ''' - - - - - internal - - - givenName - employeeNumber - - - - - urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified - - - -''' - def spName = randomGenerator.randomString() - - def expectedEntityDescriptor = EntityDescriptor.class.cast(openSamlObjects.unmarshalFromXml(postedBody.bytes)) - - 1 * entityDescriptorRepository.findByEntityID(expectedEntityDescriptor.entityID) >> expectedEntityDescriptor - 0 * entityDescriptorRepository.save(_) - when: - def result = mockMvc.perform(post("/api/EntityDescriptor") - .contentType(APPLICATION_XML) - .content(postedBody) - .param("spName", spName)) - - - then: - result.andExpect(status().isConflict()) - .andExpect(content().string("{\"errorCode\":\"409\",\"errorMessage\":\"The entity descriptor with entity id [http://test.scaldingspoon.org/test1] already exists.\",\"cause\":null}")) - } - - @Ignore("until we handle the workaround for SHIBUI-1237") - def "POST /EntityDescriptor handles x-www-form-urlencoded happily"() { - given: - def username = 'admin' - def role = 'ROLE_ADMIN' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) - def postedMetadataUrl = "http://test.scaldingspoon.org/test1" - def restXml = ''' + authentication.getName() >> 'admin' + def postedBody = ''' @@ -836,173 +630,151 @@ class EntityDescriptorControllerTests extends Specification { ''' - def spName = randomGenerator.randomString() - - def expectedEntityDescriptor = EntityDescriptor.class.cast(openSamlObjects.unmarshalFromXml(restXml.bytes)) - - 1 * mockRestTemplate.getForObject(_, _) >> restXml.bytes - 1 * entityDescriptorRepository.findByEntityID(_) >> null - 1 * entityDescriptorRepository.save(_) >> expectedEntityDescriptor - - def expectedJson = """ -{ - "version": ${expectedEntityDescriptor.hashCode()}, - "id": "${expectedEntityDescriptor.resourceId}", - "serviceProviderName": null, - "entityId": "http://test.scaldingspoon.org/test1", - "organization": null, - "contacts": null, - "mdui": null, - "serviceProviderSsoDescriptor": { - "protocolSupportEnum": "SAML 2", - "nameIdFormats": [ - "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" - ] - }, - "logoutEndpoints": null, - "securityInfo": null, - "assertionConsumerServices": [ - { - "locationUrl": "https://test.scaldingspoon.org/test1/acs", - "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", - "makeDefault": false - } - ], - "serviceEnabled": false, - "createdDate": null, - "modifiedDate": null, - "relyingPartyOverrides": {}, - "attributeRelease": [ - "givenName", - "employeeNumber" - ], - "createdBy": null -} -""" - - when: - def result = mockMvc.perform(post("/api/EntityDescriptor") - .contentType(APPLICATION_FORM_URLENCODED) - .param("metadataUrl", postedMetadataUrl) - .param("spName", spName)) - - + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true) + def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'http://test.scaldingspoon.org/test1', serviceProviderName: 'sp2', serviceEnabled: false) + + entityDescriptorRepository.save(entityDescriptorOne) + entityDescriptorRepository.save(entityDescriptorTwo) + entityManager.flush() + then: - result.andExpect(status().isCreated()) - .andExpect(content().json(expectedJson, true)) + try { + def exceptionExpected = mockMvc.perform(post("/api/EntityDescriptor").contentType(APPLICATION_XML).content(postedBody).param("spName", spName)) + } + catch (Exception e) { + e instanceof EntityIdExistsException == true + } } + @Rollback + @WithMockUser(value = "admin", roles = ["ADMIN"]) def "PUT /EntityDescriptor updates entity descriptors properly as admin"() { given: - def username = 'admin' - def role = 'ROLE_ADMIN' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) - def entityDescriptor = generator.buildEntityDescriptor() - def updatedEntityDescriptor = generator.buildEntityDescriptor() - updatedEntityDescriptor.resourceId = entityDescriptor.resourceId - def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(updatedEntityDescriptor) - updatedEntityDescriptorRepresentation.version = entityDescriptor.hashCode() + authentication.getName() >> 'admin' + + def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) + + entityDescriptorTwo = entityDescriptorRepository.save(entityDescriptorTwo) + entityManager.flush() + entityManager.clear() + + def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(entityDescriptorTwo) + updatedEntityDescriptorRepresentation.setServiceProviderName("newName") def postedJsonBody = mapper.writeValueAsString(updatedEntityDescriptorRepresentation) - - def resourceId = entityDescriptor.resourceId - - 1 * entityDescriptorRepository.findByResourceId(resourceId) >> entityDescriptor - 1 * entityDescriptorRepository.save(_) >> updatedEntityDescriptor - + when: - def result = mockMvc.perform( - put("/api/EntityDescriptor/$resourceId") - .contentType(APPLICATION_JSON) - .content(postedJsonBody)) + def result = mockMvc.perform(put("/api/EntityDescriptor/uuid-2").contentType(APPLICATION_JSON).content(postedJsonBody)) then: - def expectedJson = new JsonSlurper().parseText(postedJsonBody) - expectedJson << [version: updatedEntityDescriptor.hashCode()] result.andExpect(status().isOk()) - .andExpect(content().json(JsonOutput.toJson(expectedJson), true)) + .andExpect(jsonPath("\$.entityId").value("eid2")) + .andExpect(jsonPath("\$.serviceEnabled").value(false)) + .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) + .andExpect(jsonPath("\$.serviceProviderName").value("newName")) } - def "PUT /EntityDescriptor disallows user from enabling"() { + @Rollback + @WithMockUser(value = "someUser", roles = ["USER"]) + def "PUT /EntityDescriptor disallows non-admin user from enabling"() { given: - def username = 'someUser' - def role = 'ROLE_USER' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) - def entityDescriptor = generator.buildEntityDescriptor() - entityDescriptor.serviceEnabled = false - def updatedEntityDescriptor = generator.buildEntityDescriptor() - updatedEntityDescriptor.serviceEnabled = true - updatedEntityDescriptor.resourceId = entityDescriptor.resourceId - def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(updatedEntityDescriptor) - updatedEntityDescriptorRepresentation.version = entityDescriptor.hashCode() - def postedJsonBody = mapper.writeValueAsString(updatedEntityDescriptorRepresentation) - - def resourceId = entityDescriptor.resourceId - - 1 * entityDescriptorRepository.findByResourceId(resourceId) >> entityDescriptor - 0 * entityDescriptorRepository.save(_) >> updatedEntityDescriptor + authentication.getName() >> 'someUser' + Group g = userService.getCurrentUserGroup() + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false, idOfOwner: g.getOwnerId()) + entityDescriptorOne = entityDescriptorRepository.save(entityDescriptorOne) + entityManager.flush() when: - def result = mockMvc.perform( - put("/api/EntityDescriptor/$resourceId") - .contentType(APPLICATION_JSON) - .content(postedJsonBody)) + entityDescriptorOne.serviceEnabled = true + entityDescriptorOne.resourceId = 'uuid-1' + def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(entityDescriptorOne) + updatedEntityDescriptorRepresentation.version = entityDescriptorOne.hashCode() + def postedJsonBody = mapper.writeValueAsString(updatedEntityDescriptorRepresentation) then: - result.andExpect(status().isForbidden()) + try { + def exceptionExpected = mockMvc.perform(put("/api/EntityDescriptor/uuid-1").contentType(APPLICATION_JSON).content(postedJsonBody)) + } + catch (Exception e) { + e instanceof ForbiddenException == true + } } - def "PUT /EntityDescriptor denies the request if the PUTing user is not an ADMIN and not the createdBy user"() { + @Rollback + @WithMockUser(value = "someUser", roles = ["USER"]) + def "PUT /EntityDescriptor denies the request if the PUTing user is not an ADMIN and not the createdBy user"() { given: - def username = 'someUser' - def role = 'ROLE_USERN' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) - def entityDescriptor = generator.buildEntityDescriptor() - entityDescriptor.createdBy = 'someoneElse' - def updatedEntityDescriptor = generator.buildEntityDescriptor() - updatedEntityDescriptor.createdBy = 'someoneElse' - updatedEntityDescriptor.resourceId = entityDescriptor.resourceId - def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(updatedEntityDescriptor) - def postedJsonBody = mapper.writeValueAsString(updatedEntityDescriptorRepresentation) - def resourceId = entityDescriptor.resourceId - 1 * entityDescriptorRepository.findByResourceId(resourceId) >> entityDescriptor + authentication.getName() >> 'someUser' + Group g = userService.getCurrentUserGroup() + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) + entityDescriptorOne = entityDescriptorRepository.save(entityDescriptorOne) + entityManager.flush() when: - def result = mockMvc.perform( - put("/api/EntityDescriptor/$resourceId") - .contentType(APPLICATION_JSON) - .content(postedJsonBody)) + entityDescriptorOne.serviceProviderName = 'foo' + entityDescriptorOne.resourceId = 'uuid-1' + def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(entityDescriptorOne) + updatedEntityDescriptorRepresentation.version = entityDescriptorOne.hashCode() + def postedJsonBody = mapper.writeValueAsString(updatedEntityDescriptorRepresentation) then: - result.andExpect(status().is(403)) + try { + def exceptionExpected = mockMvc.perform(put("/api/EntityDescriptor/uuid-1").contentType(APPLICATION_JSON).content(postedJsonBody)) + } + catch (Exception e) { + e instanceof ForbiddenException == true + } } - def "PUT /EntityDescriptor 409's if the version numbers don't match"() { - given: - def username = 'admin' - def role = 'ROLE_ADMIN' - authentication.getName() >> username - userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) - def entityDescriptor = generator.buildEntityDescriptor() - def updatedEntityDescriptor = generator.buildEntityDescriptor() - updatedEntityDescriptor.resourceId = entityDescriptor.resourceId - def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(updatedEntityDescriptor) - def postedJsonBody = mapper.writeValueAsString(updatedEntityDescriptorRepresentation) - - def resourceId = entityDescriptor.resourceId - - 1 * entityDescriptorRepository.findByResourceId(resourceId) >> entityDescriptor + @Rollback + @WithMockUser(value = "admin", roles = ["ADMIN"]) + def "PUT /EntityDescriptor throws a concurrent mod exception if the version numbers don't match"() { + given: + authentication.getName() >> 'admin' + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) + entityDescriptorOne = entityDescriptorRepository.save(entityDescriptorOne) + entityManager.flush() when: - def result = mockMvc.perform( - put("/api/EntityDescriptor/$resourceId") - .contentType(APPLICATION_JSON) - .content(postedJsonBody)) + entityDescriptorOne.serviceProviderName = 'foo' + entityDescriptorOne.resourceId = 'uuid-1' + def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(entityDescriptorOne) + + def postedJsonBody = mapper.writeValueAsString(updatedEntityDescriptorRepresentation) then: - result.andExpect(status().is(409)) + try { + def exception = mockMvc.perform(put("/api/EntityDescriptor/$resourceId").contentType(APPLICATION_JSON).content(postedJsonBody)) + } + catch (Exception e) { + e instanceof ConcurrentModificationException == true + } + } + + @org.springframework.boot.test.context.TestConfiguration + @Profile(value = "local") + static class LocalConfig { + @Bean + GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { + GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { + it.groupRepository = repo + it.ownershipRepository = ownershipRepository + return it + }) + result.ensureAdminGroupExists() + return result + } } } + +//when: +//def Set ownerships = ownershipRepository.findOwnableObjectOwners(ed) +// +//then: +//ownerships.size() == 1 +//ownerships.each { +// it.ownerId == groupFromDb.resourceId +//} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptorTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptorTest.groovy index 94cf7762a..0747b9d4c 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptorTest.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptorTest.groovy @@ -10,6 +10,9 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ReloadableMetadat import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlFileBackedHTTPMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator @@ -22,8 +25,10 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Profile import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration import spock.lang.Specification @@ -37,6 +42,7 @@ import java.nio.file.Files @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@ActiveProfiles(value="local") class EntityDescriptorTest extends Specification { RandomGenerator randomGenerator @@ -84,6 +90,7 @@ class EntityDescriptorTest extends Specification { } @TestConfiguration + @Profile("local") static class MyConfig { @Bean MetadataResolver metadataResolver() { @@ -93,5 +100,14 @@ class EntityDescriptorTest extends Specification { metadataResolver.initialize() return metadataResolver } + + @Bean + GroupServiceImpl groupService(GroupsRepository repo, OwnershipRepository ownershipRepository) { + new GroupServiceImpl().with { + it.groupRepository = repo + it.ownershipRepository = ownershipRepository + return it + } + } } } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepositoryTests.groovy index 5c7f8cf8e..64dc3cdd3 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepositoryTests.groovy @@ -85,7 +85,7 @@ class CustomEntityAttributeDefinitionRepositoryTests extends Specification { then: // Missing non-nullable field should thrown error - final def exception = thrown(javax.persistence.PersistenceException) + final def exception = thrown(org.springframework.dao.DataIntegrityViolationException) } def "basic CRUD operations validated"() { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy index 70331ffe6..8f69e2424 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy @@ -5,10 +5,15 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.Internationalization import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import edu.internet2.tier.shibboleth.admin.ui.service.CustomEntityAttributesDefinitionServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import org.apache.lucene.analysis.Analyzer @@ -19,11 +24,16 @@ import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Profile import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration import spock.lang.Specification +import java.util.stream.Stream + import javax.persistence.EntityManager /** @@ -34,6 +44,7 @@ import javax.persistence.EntityManager @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) +@ActiveProfiles(value = "local") class EntityDescriptorRepositoryTest extends Specification { @Autowired EntityDescriptorRepository entityDescriptorRepository @@ -49,18 +60,21 @@ class EntityDescriptorRepositoryTest extends Specification { @Autowired UserRepository userRepository + + @Autowired + GroupsRepository groupRepository OpenSamlObjects openSamlObjects = new OpenSamlObjects().with { it.init() it } - def service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects), new UserService(roleRepository, userRepository)) + @Autowired + EntityDescriptorService service def "SHIBUI-553.2"() { when: def input = openSamlObjects.unmarshalFromXml(this.class.getResource('/metadata/SHIBUI-553.2.xml').bytes) as EntityDescriptor - entityDescriptorRepository.save(input) def item1 = entityDescriptorRepository.findByResourceId(input.resourceId) @@ -88,8 +102,46 @@ class EntityDescriptorRepositoryTest extends Specification { then: noExceptionThrown() } + + def "SHIBUI-1849 - extend data model for ownership"() { + given: + def group = new Group().with { + it.name = "group-name" + it.description = "some description" + it + } + group = groupRepository.saveAndFlush(group) + + def gList = groupRepository.findAll() + def groupFromDb = gList.get(0).asType(Group) + + def ed = openSamlObjects.unmarshalFromXml(this.class.getResource('/metadata/SHIBUI-553.2.xml').bytes) as EntityDescriptor + ed.with { + it.idOfOwner = groupFromDb.resourceId + } + entityDescriptorRepository.saveAndFlush(ed) + + when: + def edStreamFromDb = entityDescriptorRepository.findAllStreamByIdOfOwner(null); + + then: + ((Stream)edStreamFromDb).count() == 0 + + when: + def edStreamFromDb2 = entityDescriptorRepository.findAllStreamByIdOfOwner("random value"); + + then: + ((Stream)edStreamFromDb2).count() == 0 + + when: + def edStreamFromDb3 = entityDescriptorRepository.findAllStreamByIdOfOwner(groupFromDb.resourceId); + + then: + ((Stream)edStreamFromDb3).count() == 1 + } @TestConfiguration + @Profile("local") static class LocalConfig { @Bean MetadataResolver metadataResolver() { @@ -105,6 +157,15 @@ class EntityDescriptorRepositoryTest extends Specification { return new EnglishAnalyzer() } + @Bean + GroupServiceImpl groupService(GroupsRepository repo, OwnershipRepository ownershipRepository) { + new GroupServiceImpl().with { + it.groupRepository = repo + it.ownershipRepository = ownershipRepository + return it + } + } + @Bean CustomEntityAttributesDefinitionServiceImpl customEntityAttributesDefinitionServiceImpl() { new CustomEntityAttributesDefinitionServiceImpl().with { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy index a0ba13aca..99ff22481 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy @@ -10,10 +10,8 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetada import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver -import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects -import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import org.springframework.beans.factory.annotation.Autowired @@ -44,15 +42,7 @@ class MetadataResolverRepositoryTests extends Specification { EntityManager entityManager @Autowired - OpenSamlObjects openSamlObjects - - @Autowired - RoleRepository roleRepository - - @Autowired - UserRepository userRepository - - def service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects), new UserService(roleRepository, userRepository)) + EntityDescriptorService service def "test persisting a metadata resolver"() { when: diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy index f47928082..7c1acc5da 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy @@ -10,11 +10,14 @@ import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import edu.internet2.tier.shibboleth.admin.ui.service.FileCheckingFileWritingService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils + import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest @@ -33,34 +36,36 @@ import spock.lang.Specification @EntityScan("edu.internet2.tier.shibboleth.admin.ui") class EntityDescriptorFilesScheduledTasksTests extends Specification { + @Autowired + OpenSamlObjects openSamlObjects + + @Autowired + IGroupService groupService + def tempPath = "/tmp/shibui" def directory - @Autowired - OpenSamlObjects openSamlObjects - def entityDescriptorRepository = Mock(EntityDescriptorRepository) def entityDescriptorFilesScheduledTasks - def service - def randomGenerator - - @Autowired - RoleRepository roleRepository - - @Autowired - UserRepository userRepository - + + + def service + def setup() { randomGenerator = new RandomGenerator() tempPath = tempPath + randomGenerator.randomRangeInt(10000, 20000) - service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects), new UserService(roleRepository, userRepository)) + EntityDescriptorConversionUtils.setOpenSamlObjects(openSamlObjects) entityDescriptorFilesScheduledTasks = new EntityDescriptorFilesScheduledTasks(tempPath, entityDescriptorRepository, openSamlObjects, new FileCheckingFileWritingService()) directory = new File(tempPath) directory.mkdir() + + service = new JPAEntityDescriptorServiceImpl() + service.openSamlObjects = openSamlObjects + service.groupService = groupService } def "generateEntityDescriptorFiles properly generates a file from an Entity Descriptor"() { @@ -122,7 +127,7 @@ class EntityDescriptorFilesScheduledTasksTests extends Specification { http://test.example.org - ''' + ''' def entityDescriptor = service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { it.entityId = 'http://test.example.org/test1' diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy new file mode 100644 index 000000000..0f1305c51 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy @@ -0,0 +1,257 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.controller + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* + +import javax.persistence.EntityManager + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.annotation.Rollback +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.setup.MockMvcBuilders + +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException +import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupDeleteException +import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import groovy.json.JsonOutput +import spock.lang.Specification + +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, TestConfiguration, InternationalizationConfiguration, SearchConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@DirtiesContext +class GroupsControllerIntegrationTests extends Specification { + @Autowired + EntityManager entityManager + + @Autowired + GroupsRepository groupsRepository + + @Autowired + GroupServiceImpl groupService + + @Autowired + RoleRepository roleRepository + + @Autowired + UserRepository userRepository + + @Autowired + UserService userService + + static RESOURCE_URI = '/api/admin/groups' + + def MockMvc mockMvc + + def setup() { + groupService.ensureAdminGroupExists() + + def GroupController groupController = new GroupController().with ({ + it.groupService = this.groupService + it + }) + mockMvc = MockMvcBuilders.standaloneSetup(groupController).build(); + + if (roleRepository.count() == 0) { + def roles = [new Role().with { + name = 'ROLE_ADMIN' + it + }, new Role().with { + name = 'ROLE_USER' + it + }, new Role().with { + name = 'ROLE_NONE' + it + }] + roles.each { + roleRepository.save(it) + } + } + + Optional adminRole = roleRepository.findByName("ROLE_ADMIN") + User adminUser = new User(username: "admin", roles: [adminRole.get()], password: "foo") + userService.save(adminUser) + + Optional userRole = roleRepository.findByName("ROLE_USER") + User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") + user = userService.save(user) + entityManager.flush() + } + + + @Rollback + @WithMockUser(value = "admin", roles = ["ADMIN"]) + def 'POST new group persists properly'() { + given: + def newGroup = [name: 'Foo', + description: 'Bar', + resourceId: 'FooBar'] + + def expectedJson = """ + { + "name":"Foo", + "description":"Bar", + "resourceId":"FooBar" + } +""" + when: + def result = mockMvc.perform(post(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON) + .content(JsonOutput.toJson(newGroup)).accept(MediaType.APPLICATION_JSON)) + + then: + result.andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(expectedJson, false)) + + + //'Try to create with an existing resource id' + try { + mockMvc.perform(post(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON) + .content(JsonOutput.toJson(newGroup)) + .accept(MediaType.APPLICATION_JSON)) + 1 == 2 + } catch (Throwable expected) { + expected instanceof GroupExistsConflictException + } + } + + @Rollback + @WithMockUser(value = "admin", roles = ["ADMIN"]) + def 'PUT (update) existing group persists properly'() { + given: + groupsRepository.deleteByResourceId("AAA") + def Group groupAAA = new Group().with({ + it.name = "AAA" + it.description = "AAA" + it.resourceId = "AAA" + it + }) + groupAAA = groupsRepository.save(groupAAA) + groupAAA.setDescription("Updated AAA") + groupAAA.setName("NOT AAA") + + when: + def result = mockMvc.perform(put(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON) + .content(JsonOutput.toJson(groupAAA)).accept(MediaType.APPLICATION_JSON)) + + then: + result.andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("\$.name").value("NOT AAA")) + .andExpect(jsonPath("\$.resourceId").value("AAA")) + .andExpect(jsonPath("\$.description").value("Updated AAA")) + + when: 'Try to update with a non-existing resource id' + def newGroup = [name: 'XXXXX', + description: 'should not work', + resourceId: 'XXXX'] + + then: + try { + mockMvc.perform(put(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON) + .content(JsonOutput.toJson(newGroup)) + .accept(MediaType.APPLICATION_JSON)) + 1 == 2 + } catch (Throwable expected) { + expected instanceof EntityNotFoundException + } + } + + @Rollback + @WithMockUser(value = "admin", roles = ["ADMIN"]) + def 'GET checks for groups (when there are existing groups)'() { + given: + groupsRepository.deleteByResourceId("AAA") + groupsRepository.deleteByResourceId("BBB") + def Group groupAAA = new Group().with({ + it.name = "AAA" + it.description = "AAA" + it.resourceId = "AAA" + it + }) + groupsRepository.save(groupAAA) + def Group groupBBB = new Group().with({ + it.name = "BBB" + it.description = "BBB" + it.resourceId = "BBB" + it + }) + groupsRepository.save(groupBBB) + + when: 'GET request is made for ALL groups in the system, and system has groups in it' + def result = mockMvc.perform(get(RESOURCE_URI)) + + then: 'Request completed with HTTP 200 and returned a list of groups' + result.andExpect(status().isOk()) + + when: 'GET request for a single specific group in a system that has groups' + def singleGroupRequest = mockMvc.perform(get("$RESOURCE_URI/BBB")) + + then: 'GET request for a single specific group completed with HTTP 200' + singleGroupRequest.andExpect(status().isOk()) + + // 'GET request for a single non-existent group in a system that has groups' + try { + mockMvc.perform(get("$RESOURCE_URI/CCC")) + 1 == 2 + } catch (Throwable expected) { + expected instanceof EntityNotFoundException + } + } + + @Rollback + @WithMockUser(value = "admin", roles = ["ADMIN"]) + def 'DELETE performs correctly when group attached to a user'() { + // When the user is created in the setup method above, a new group "someUser" is created to be associated with that user + // User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") + // userService.save(user) + + when: 'try to delete group that is attached to a user' + def nothingtodo + + then: + try { + mockMvc.perform(delete("$RESOURCE_URI/someUser")) + 1 == 2 + } catch(Throwable expected) { + expected instanceof GroupDeleteException + } + + when: + def Group groupAAA = new Group().with({ + it.name = "AAA" + it.description = "AAA" + it.resourceId = "AAA" + it + }) + groupAAA = groupsRepository.save(groupAAA) + + def User user = userRepository.findByUsername("someUser").get() + user.setGroup(groupAAA) + userService.save(user) + + then: + mockMvc.perform(delete("$RESOURCE_URI/someUser")) + } +} 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 5ecb34dc0..f1bd5a591 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersControllerIntegrationTests.groovy @@ -1,113 +1,221 @@ package edu.internet2.tier.shibboleth.admin.ui.security.controller +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter -import groovy.json.JsonOutput import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc -import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Profile +import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.http.MediaType +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder import org.springframework.security.test.context.support.WithMockUser import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.annotation.Rollback import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.result.MockMvcResultHandlers -import spock.lang.Ignore +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.transaction.annotation.Transactional + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer + +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration +import edu.internet2.tier.shibboleth.admin.ui.controller.support.RestControllersSupport +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceForTesting +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import groovy.json.JsonOutput import spock.lang.Specification -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status - -/** - * @author Dmitriy Kopylenko - */ -@SpringBootTest -@AutoConfigureMockMvc -@ActiveProfiles(["no-auth", "dev"]) +import static org.springframework.http.MediaType.* +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* + +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, TestConfiguration, InternationalizationConfiguration, SearchConfiguration, LocalConfig]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@DirtiesContext +@ActiveProfiles(["no-auth", "local"]) +@ComponentScan(basePackages="{ edu.internet2.tier.shibboleth.admin.ui.configuration }") class UsersControllerIntegrationTests extends Specification { - @Autowired - private MockMvc mockMvc - + GroupsRepository groupsRepository + + @Autowired + GroupServiceForTesting groupService + + @Autowired + def ObjectMapper mapper + + @Autowired + OwnershipRepository ownershipRepository + + @Autowired + RoleRepository roleRepository + + @Autowired + UserRepository userRepository + + @Autowired + UserService userService + + def MockMvc mockMvc + def users + static RESOURCE_URI = '/api/admin/users' + + def setup() { + def controller = new UsersController(userRepository, userService) + mockMvc = MockMvcBuilders.standaloneSetup(controller).setControllerAdvice(new RestControllersSupport()).build() + createDevUsersAndGroups() + } + @Transactional + void createDevUsersAndGroups() { + userRepository.findAll().forEach { + userService.delete(it.getUsername()) + } + userRepository.flush() + + roleRepository.deleteAll() + roleRepository.flush() + groupService.clearAllForTesting() //leaves us just the admingroup + + def groups = [ + new Group().with { + it.name = "A1" + it.description = "AAA Group" + it.resourceId = "AAA" + it + }, + new Group().with { + it.name = "B1" + it.description = "BBB Group" + it.resourceId = "BBB" + it + }] + groups.each { + try { + groupsRepository.save(it) + } catch (Throwable e) { + // Must already exist (from a unit test) + } + } + groupsRepository.flush() + + if (roleRepository.count() == 0) { + def roles = [new Role().with { + name = 'ROLE_ADMIN' + it + }, new Role().with { + name = 'ROLE_USER' + it + }, new Role().with { + name = 'ROLE_NONE' + it + }] + roles.each { + roleRepository.save(it) + } + } + roleRepository.flush() + if (userRepository.count() == 0) { + users = [new User().with { + username = 'admin' + password = '{noop}adminpass' + firstName = 'Joe' + lastName = 'Doe' + emailAddress = 'joe@institution.edu' + roles.add(roleRepository.findByName('ROLE_ADMIN').get()) + it + }, new User().with { + username = 'nonadmin' + password = '{noop}nonadminpass' + firstName = 'Peter' + lastName = 'Vandelay' + emailAddress = 'peter@institution.edu' + roles.add(roleRepository.findByName('ROLE_USER').get()) + it + }, new User().with { + username = 'none' + password = '{noop}nonepass' + firstName = 'Bad' + lastName = 'robot' + emailAddress = 'badboy@institution.edu' + roles.add(roleRepository.findByName('ROLE_NONE').get()) + it + }, new User().with { // allow us to auto-login as an admin + username = 'anonymousUser' + password = '{noop}anonymous' + firstName = 'Anon' + lastName = 'Ymous' + emailAddress = 'anon@institution.edu' + roles.add(roleRepository.findByName('ROLE_ADMIN').get()) + it + }] + users.each { + it = userService.save(it) + } + } + } + @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'GET ALL users (when there are existing users)'() { - given: - def expectedJson = """ -[ - { - "modifiedBy" : "anonymousUser", - "firstName" : "Joe", - "emailAddress" : "joe@institution.edu", - "role" : "ROLE_ADMIN", - "username" : "admin", - "createdBy" : anonymousUser, - "lastName" : "Doe" - }, - { - "modifiedBy" : "anonymousUser", - "firstName" : "Peter", - "emailAddress" : "peter@institution.edu", - "role" : "ROLE_USER", - "username" : "nonadmin", - "createdBy" : anonymousUser, - "lastName" : "Vandelay" - }, - { - "modifiedBy" : "anonymousUser", - "firstName" : "Bad", - "emailAddress" : "badboy@institution.edu", - "role" : "ROLE_NONE", - "username" : "none", - "createdBy" : "anonymousUser", - "lastName" : "robot" - }, - { - "modifiedBy" : "anonymousUser", - "firstName" : "Anon", - "emailAddress" : "anon@institution.edu", - "role" : "ROLE_ADMIN", - "username" : "anonymousUser", - "createdBy" : "anonymousUser", - "lastName" : "Ymous" - } -]""" + // given: users created in setup + when: 'GET request is made for ALL users in the system, and system has users in it' def result = mockMvc.perform(get(RESOURCE_URI)) - then: 'Request completed with HTTP 200 and returned a list of users' - result - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(expectedJson, false)) + result.andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("\$.[0].username").value("admin")) + .andExpect(jsonPath("\$.[0].emailAddress").value("joe@institution.edu")) + .andExpect(jsonPath("\$.[0].role").value("ROLE_ADMIN")) + .andExpect(jsonPath("\$.[0].groupId").value("admingroup")) + .andExpect(jsonPath("\$.[1].username").value("nonadmin")) + .andExpect(jsonPath("\$.[1].emailAddress").value("peter@institution.edu")) + .andExpect(jsonPath("\$.[1].role").value("ROLE_USER")) + .andExpect(jsonPath("\$.[1].groupId").value("nonadmin")) + .andExpect(jsonPath("\$.[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("\$.[3].username").value("anonymousUser")) + .andExpect(jsonPath("\$.[3].emailAddress").value("anon@institution.edu")) + .andExpect(jsonPath("\$.[3].role").value("ROLE_ADMIN")) + .andExpect(jsonPath("\$.[3].groupId").value("admingroup")) } @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'GET ONE existing user'() { - given: - def expectedJson = """ -{ - "modifiedBy" : anonymousUser, - "firstName" : "Joe", - "emailAddress" : "joe@institution.edu", - "role" : "ROLE_ADMIN", - "username" : "admin", - "createdBy" : anonymousUser, - "lastName" : "Doe" -}""" when: 'GET request is made for one existing user' def result = mockMvc.perform(get("$RESOURCE_URI/admin")) then: 'Request completed with HTTP 200 and returned one user' result - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(expectedJson, false)) + .andExpect(status().isOk()).andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("\$.username").value("admin")) + .andExpect(jsonPath("\$.emailAddress").value("joe@institution.edu")) + .andExpect(jsonPath("\$.role").value("ROLE_ADMIN")) + .andExpect(jsonPath("\$.groupId").value("admingroup")) } @WithMockUser(value = "admin", roles = ["ADMIN"]) @@ -119,9 +227,7 @@ class UsersControllerIntegrationTests extends Specification { result.andExpect(status().isNotFound()) } - //TODO: These are broken due to a bug in Spring Boot. Unignore these after we update to spring boot 2.0.8+. - @Ignore - @DirtiesContext + @Rollback @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'DELETE ONE existing user'() { when: 'GET request is made for one existing user' @@ -136,14 +242,17 @@ class UsersControllerIntegrationTests extends Specification { then: 'DELETE was successful' result.andExpect(status().isNoContent()) - when: 'GET request is made for the deleted user' - result = mockMvc.perform(get("$RESOURCE_URI/nonadmin")) - - then: 'The deleted user is gone' - result.andExpect(status().isNotFound()) + // 'GET request is made for the deleted user' + try { + result = mockMvc.perform(get("$RESOURCE_URI/nonadmin")) + false + } + catch (org.springframework.web.util.NestedServletException expectedResult) { + expectedResult.getCause() instanceof org.springframework.web.client.HttpClientErrorException + } } - @Ignore + @Rollback @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'POST new user persists properly'() { given: @@ -164,7 +273,7 @@ class UsersControllerIntegrationTests extends Specification { result.andExpect(status().isOk()) } - @Ignore + @Rollback @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'POST new duplicate username returns 409'() { given: @@ -189,38 +298,53 @@ class UsersControllerIntegrationTests extends Specification { result.andExpect(status().isConflict()) } - @Ignore + @Rollback @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'PATCH updates user properly'() { given: - def newUser = [firstName: 'Foo', - lastName: 'Bar', - username: 'FooBar', - password: 'somepass', - emailAddress: 'foo@institution.edu', - role: 'ROLE_USER'] - + def String userString = mockMvc.perform(get("$RESOURCE_URI/none")).andReturn().getResponse().getContentAsString() + def User user = mapper.readValue(userString, User.class); + user.setFirstName("somethingnew") + when: - def result = mockMvc.perform(post(RESOURCE_URI) + def result = mockMvc.perform(patch("$RESOURCE_URI/$user.username") .contentType(MediaType.APPLICATION_JSON) - .content(JsonOutput.toJson(newUser)) + .content(mapper.writeValueAsString(user)) .accept(MediaType.APPLICATION_JSON)) then: result.andExpect(status().isOk()) - + when: - newUser['firstName'] = 'Bob' - result = mockMvc.perform(patch("$RESOURCE_URI/$newUser.username") - .contentType(MediaType.APPLICATION_JSON) - .content(JsonOutput.toJson(newUser)) - .accept(MediaType.APPLICATION_JSON)) - + user.setGroupId("AAA") + def resultNewGroup = mockMvc.perform(patch("$RESOURCE_URI/$user.username").contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(user)).accept(MediaType.APPLICATION_JSON)) + then: - result.andExpect(status().isOk()) + resultNewGroup.andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("\$.groupId").value("AAA")) + + def groups = ownershipRepository.findAllGroupsForUser(user.username) + groups.size() == 1 + + when: 'Updating user role to admin puts the user in the admin group' + user.setRole("ROLE_ADMIN") + user.setGroupId("AAA") // Dont care that this is different, ROLE_ADMIN should take precedence + def resultUserNewRole = mockMvc.perform(patch("$RESOURCE_URI/$user.username").contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(user)).accept(MediaType.APPLICATION_JSON)) + + then: + resultUserNewRole.andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("\$.groupId").value("admingroup")) + + def groupsCheck = ownershipRepository.findAllGroupsForUser(user.username) + groupsCheck.size() == 1 + } - @Ignore + @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'PATCH detects unknown username'() { given: def newUser = [firstName: 'Foo', @@ -238,4 +362,28 @@ class UsersControllerIntegrationTests extends Specification { then: result.andExpect(status().isNotFound()) } -} + + @org.springframework.boot.test.context.TestConfiguration + @Profile(value = "local") + static class LocalConfig { + @Bean + GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { + GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { + it.groupRepository = repo + it.ownershipRepository = ownershipRepository + return it + }) + result.ensureAdminGroupExists() + return result + } + + @Bean + ObjectMapper objectMapper() { + JavaTimeModule module = new JavaTimeModule() + LocalDateTimeDeserializer localDateTimeDeserializer = new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS")) + module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer) + + return Jackson2ObjectMapperBuilder.json().modules(module).featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build() + } + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepositoryTests.groovy new file mode 100644 index 000000000..1a23778da --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepositoryTests.groovy @@ -0,0 +1,230 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.repository + +import javax.persistence.EntityManager +import javax.transaction.Transactional + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.context.annotation.Bean +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.annotation.Rollback +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration + +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership +import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.GroupUpdatedEntityListener +import spock.lang.Specification + +/** + * Tests to validate the repo and model for groups + * @author chasegawa + */ +@DataJpaTest +@ContextConfiguration(classes=[InternationalizationConfiguration, LocalConfig]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@ActiveProfiles("test") +class GroupsRepositoryTests extends Specification { + @Autowired + GroupsRepository groupsRepo + + @Autowired + OwnershipRepository ownershipRepository + + @Transactional + def setup() { + groupsRepo.deleteAll() + ownershipRepository.deleteAll() + def ownerships = [ + new Ownership().with { + it.ownedId = "aaa" + it.ownedType = "USER" + it.ownerId = "g1" + it.ownerType = "GROUP" + it + }, + new Ownership().with { + it.ownedId = "bbb" + it.ownedType = "USER" + it.ownerId = "g1" + it.ownerType = "GROUP" + it + }, + new Ownership().with { + it.ownedId = "ccc" + it.ownedType = "USER" + it.ownerId = "g1" + it.ownerType = "GROUP" + it + }, + new Ownership().with { + it.ownedId = "ccc" + it.ownedType = "USER" + it.ownerId = "g2" + it.ownerType = "GROUP" + it + }, + new Ownership().with { + it.ownedId = "bbb" + it.ownedType = "ENTITY_DESCRIPTOR" + it.ownerId = "aaa" + it.ownerType = "GROUP" + it + }, + new Ownership().with { + it.ownedId = "aaa" + it.ownedType = "ENTITY_DESCRIPTOR" + it.ownerId = "aaa" + it.ownerType = "USER" + it + } + ] + ownerships.each { + ownershipRepository.save(it) + } + } + + def "group ownership tests"() { + when: "Simple create test" + def group = new Group().with { + it.name = "group 1" + it.description = "some description" + it.resourceId = "g1" + it + } + def Group savedGroup = groupsRepo.saveAndFlush(group) + def Collection all = ownershipRepository.findAllByOwner(savedGroup) + + then: + all.size() == 3 + savedGroup.ownedItems.size() == 3 + all.each { + savedGroup.ownedItems.contains(it) + } + } + + @Rollback + def "simple create test"() { + given: + def group = new Group().with { + it.name = "group-name" + it.description = "some description" + it + } + + // Confirm empty state + when: + def groups = groupsRepo.findAll() + + then: + groups.size() == 0 + + // save check + when: + group = groupsRepo.save(group) + + then: + // save check + def gList = groupsRepo.findAll() + gList.size() == 1 + def groupFromDb = gList.get(0).asType(Group) + groupFromDb.equals(group) == true + + // fetch checks + groupsRepo.findByResourceId("not an id") == null + groupsRepo.findByResourceId(groupFromDb.resourceId).equals(group) + } + + def "expected error"() { + given: + def group = new Group().with { + it.description = "some description" + it + } + + // Confirm empty state + when: + def gList = groupsRepo.findAll() + + then: + gList.size() == 0 + + // save check + when: + def savedGroup = groupsRepo.save(group) + + then: + // Missing non-nullable field (name) should thrown error + final def exception = thrown(org.springframework.dao.DataIntegrityViolationException) + } + + @Rollback + def "basic CRUD operations validated"() { + given: + def group = new Group().with { + it.name = "group-name" + it.description = "some description" + it + } + + // Confirm empty state + when: + def groups = groupsRepo.findAll() + + then: + groups.size() == 0 + + // save check + when: + groupsRepo.save(group) + + then: + // save check + def gList = groupsRepo.findAll() + gList.size() == 1 + def groupFromDb = gList.get(0).asType(Group) + groupFromDb.equals(group) == true + + // update check + groupFromDb.with { + it.description = "some new text that wasn't there before" + } + groupFromDb.equals(group) == false + + when: + groupsRepo.save(groupFromDb) + + then: + def gList2 = groupsRepo.findAll() + gList2.size() == 1 + def groupFromDb2 = gList2.get(0).asType(Group) + groupFromDb2.equals(group) == false + groupFromDb2.equals(groupFromDb) == true + + // delete tests + when: + groupsRepo.delete(groupFromDb2) + + then: + groupsRepo.findAll().size() == 0 + + when: + def nothingThere = groupsRepo.findByResourceId(null); + + then: + nothingThere == null + } + + @org.springframework.boot.test.context.TestConfiguration + static class LocalConfig { + @Bean + GroupUpdatedEntityListener groupUpdatedEntityListener(OwnershipRepository repo) { + GroupUpdatedEntityListener result = new GroupUpdatedEntityListener() + result.init(repo) + return result + } + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepositoryTests.groovy new file mode 100644 index 000000000..229763c58 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepositoryTests.groovy @@ -0,0 +1,273 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.repository + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.annotation.Rollback +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.annotation.Transactional + +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownable +import edu.internet2.tier.shibboleth.admin.ui.security.model.OwnableType +import edu.internet2.tier.shibboleth.admin.ui.security.model.Owner +import edu.internet2.tier.shibboleth.admin.ui.security.model.OwnerType +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership +import spock.lang.Specification + +/** + * Tests to validate the repo and model for groups + * @author chasegawa + */ +@DataJpaTest +@ContextConfiguration(classes=[InternationalizationConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class OwnershipRepositoryTests extends Specification { + @Autowired + OwnershipRepository repo + + @Transactional + def setup() { + repo.deleteAll() + def ownerships = [ + new Ownership().with { + it.ownedId = "aaa" + it.ownedType = "USER" + it.ownerId = "g1" + it.ownerType = "GROUP" + it + }, + new Ownership().with { + it.ownedId = "bbb" + it.ownedType = "USER" + it.ownerId = "g1" + it.ownerType = "GROUP" + it + }, + new Ownership().with { + it.ownedId = "ccc" + it.ownedType = "USER" + it.ownerId = "g1" + it.ownerType = "GROUP" + it + }, + new Ownership().with { + it.ownedId = "ccc" + it.ownedType = "USER" + it.ownerId = "g2" + it.ownerType = "GROUP" + it + }, + new Ownership().with { + it.ownedId = "bbb" + it.ownedType = "ENTITY_DESCRIPTOR" + it.ownerId = "aaa" + it.ownerType = "GROUP" + it + }, + new Ownership().with { + it.ownedId = "aaa" + it.ownedType = "ENTITY_DESCRIPTOR" + it.ownerId = "aaa" + it.ownerType = "USER" + it + } + ] + ownerships.each { + repo.save(it) + } + } + + @Rollback + def "test clearUsersGroups"() { + when: "remove entries where the user is the owned object of a group" + repo.clearUsersGroups("aaa"); + def result = repo.findAllGroupsForUser("aaa") + + then: + result.size() == 0 + + when: "find objects owned by user aaa has not changed" + result = repo.findOwnedByUser("aaa") + + then: + result.size() == 1 + result.each { + it.ownerId == "aaa" + it.ownerType == "USER" + it.ownedId == "aaa" + it.ownedType == "ENTITY_DESCRIPTOR" + } + + when: "remove entries where the user is the owned object of groups" + repo.clearUsersGroups("ccc"); + result = repo.findAllGroupsForUser("ccc") + + then: + result.size() == 0 + } + + @Rollback + def "test deleteEntriesForOwnedObject"() { + when: "remove entries where the user is the owned object of a group" + repo.deleteEntriesForOwnedObject(new Ownable() { + public String getObjectId() { return "aaa" } + + public OwnableType getOwnableType() { OwnableType.USER } + }) + def result = repo.findAllGroupsForUser("aaa") + + then: + result.size() == 0 + + when: "find objects owned by user aaa has not changed" + result = repo.findOwnedByUser("aaa") + + then: + result.size() == 1 + result.each { + it.ownerId == "aaa" + it.ownerType == "USER" + it.ownedId == "aaa" + it.ownedType == "ENTITY_DESCRIPTOR" + } + + when: "remove entries where the user is the owned object of groups" + repo.deleteEntriesForOwnedObject(new Ownable() { + public String getObjectId() { return "ccc" } + + public OwnableType getOwnableType() { OwnableType.USER } + }); + result = repo.findAllGroupsForUser("ccc") + + then: + result.size() == 0 + } + + def "test findUsersByOwner"() { + when: "find all the users owned by group g1" + ArrayList userIds = new ArrayList<>() + userIds.add("aaa") + userIds.add("bbb") + userIds.add("ccc") + def result = repo.findUsersByOwner(new Owner() { + public String getOwnerId() { return "g1" } + + public OwnerType getOwnerType() { OwnerType.GROUP } + }) + + then: + result.size() == 3 + result.each { + userIds.contains(it.getOwnedId()) + it.ownedType == "USER" + } + + when: + result = repo.findUsersByOwner(new Owner() { + public String getOwnerId() { return "aaa" } + + public OwnerType getOwnerType() { return OwnerType.USER } + }) + + then: + result.size() == 0 + } + + def "test findOwnedByUser"() { + when: "find objects owned by user" + def result = repo.findOwnedByUser("aaa") + + then: + result.size() == 1 + result.each { + it.ownerId == "aaa" + it.ownerType == "USER" + it.ownedId == "aaa" + it.ownedType == "ENTITY_DESCRIPTOR" + } + } + + def "test findOwnableObjectOwners"() { + when: "find owners for OWNABLE" + ArrayList groupIds = new ArrayList<>() + groupIds.add("g1") + groupIds.add("g2") + def result = repo.findOwnableObjectOwners(new Ownable() { + public String getObjectId() { return "ccc" } + public OwnableType getOwnableType() { return OwnableType.USER } + }) + + then: + result.size() == 2 + result.each { + it.ownerType == "GROUP" + it.ownedId == "ccc" + groupIds.contains(it.getOwnedId()) + } + } + + def "test findAllGroupsForUser"() { + when: "find all groups for user aaa" + def result = repo.findAllGroupsForUser("aaa") + + then: + result.size() == 1 + result.each { + it.ownedId == "g1" + it.ownedType == "GROUP" + } + + when: "find all groups for user ccc" + ArrayList groupIds = new ArrayList<>() + groupIds.add("g1") + groupIds.add("g2") + result = repo.findAllGroupsForUser("ccc") + + then: + result.size() == 2 + result.each { + it.ownerType == "GROUP" + it.ownedId == "ccc" + groupIds.contains(it.getOwnedId()) + } + } + + def "test findAllByOwner" () { + when: "Find all for group g1" + ArrayList userIds = new ArrayList() + userIds.add("aaa") + userIds.add("bbb") + userIds.add("ccc") + def result = repo.findAllByOwner(new Owner() { + public String getOwnerId() { return "g1" } + + public OwnerType getOwnerType() { return OwnerType.GROUP } + }) + + then: + result.size() == 3 + result.each { + userIds.contains(it.getOwnedId()) + it.ownedType == "USER" + } + + when: "Find all items owned by user aaa" + result = repo.findAllByOwner(new Owner() { + public String getOwnerId() { return "aaa" } + + public OwnerType getOwnerType() { return OwnerType.USER } + }) + + then: + result.size() == 1 + result.each { + it.ownerId == "aaa" + it.ownerType == "USER" + it.ownedId == "aaa" + it.ownedType == "ENTITY_DESCRIPTOR" + } + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..9916ae084 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy @@ -0,0 +1,21 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service + +import org.springframework.context.annotation.Profile +import org.springframework.transaction.annotation.Transactional + +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl + +@Profile('test') +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() + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy new file mode 100644 index 000000000..b6431f79f --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy @@ -0,0 +1,283 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +import javax.persistence.EntityManager + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Profile +import org.springframework.context.annotation.PropertySource +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.annotation.Rollback +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.annotation.Transactional + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer + +import edu.internet2.tier.shibboleth.admin.ui.ShibbolethUiApplication +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +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.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.GroupUpdatedEntityListener +import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.UserUpdatedEntityListener +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import spock.lang.Specification + +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, CustomPropertiesConfiguration, LocalConfig]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@DirtiesContext +@ActiveProfiles(["test", "local"]) +@ComponentScan(basePackages="{ edu.internet2.tier.shibboleth.admin.ui.configuration }") +class UserServiceTests extends Specification { + + @Autowired + EntityManager entityManager + + @Autowired + GroupServiceForTesting groupService + + @Autowired + OwnershipRepository ownershipRepository + + @Autowired + RoleRepository roleRepository + + @Autowired + UserRepository userRepository + + @Autowired + UserService userService + + @Transactional + def setup() { + 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) + } + } + + @Rollback + def "When creating user, user is set to the correct group"() { + given: + Group gb = new Group(); + gb.setResourceId("testingGroupBBB") + gb.setName("Group BBB") + gb = groupService.createGroup(gb) + + Optional userRole = roleRepository.findByName("ROLE_USER") + def User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") + user.setGroup(gb) + + when: + def User result = userService.save(user) + + then: + result.groupId == "testingGroupBBB" + result.username == "someUser" + result.userGroups.size() == 1 + + // Raw check that the DB is correct for ownership + def Set users = ownershipRepository.findUsersByOwner(gb) + users.size() == 1 + users.getAt(0).ownedId == "someUser" + + // Validate that loading the group has the correct list as well + Group g = groupService.find("testingGroupBBB"); + g.ownedItems.size() == 1 + } + + @Rollback + def "When updating user, user is set to the correct group"() { + given: + Group ga = new Group() + ga.setResourceId("testingGroup") + ga.setName("Group A") + ga = groupService.createGroup(ga) + + Group gb = new Group(); + gb.setResourceId("testingGroupBBB") + gb.setName("Group BBB") + gb = groupService.createGroup(gb) + + Optional userRole = roleRepository.findByName("ROLE_USER") + def User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") + user.setGroup(gb) + def User userInB = userService.save(user) + + when: + userInB.setGroupId("testingGroup") // changing groups will happen by updating the user's groupid (from the ui) + def User result = userService.save(userInB) + + then: + result.groupId == "testingGroup" + result.username == "someUser" + result.userGroups.size() == 1 + + // Raw check that the DB is correct for ownership + def Set users = ownershipRepository.findUsersByOwner(ga) + users.size() == 1 + users.getAt(0).ownedId == "someUser" + + // check db is correct for the previous group as well + def Set users2 = ownershipRepository.findUsersByOwner(gb) + users2.size() == 0 + + // Validate that loading the group has the correct list as well + Group g = groupService.find("testingGroup"); + g.ownedItems.size() == 1 + + Group g2 = groupService.find("testingGroupBBB"); + g2.ownedItems.size() == 0 + } + + @Rollback + def "logically try to match user controller test causing headaches"() { + given: + Group ga = new Group() + ga.setResourceId("testingGroup") + ga.setName("Group A") + ga = groupService.createGroup(ga) + + Optional userRole = roleRepository.findByName("ROLE_USER") + def User user = new User(username: "someUser", firstName: "Fred", lastName: "Flintstone", roles:[userRole.get()], password: "foo") + user.setGroup(ga) + userService.save(user) + + when: + def User flintstoneUser = userRepository.findByUsername("someUser").get() + flintstoneUser.setFirstName("Wilma") + flintstoneUser.setGroupId("testingGroup") + + def User result = userService.save(flintstoneUser) + + then: + result.groupId == "testingGroup" + result.username == "someUser" + result.userGroups.size() == 1 + result.firstName == "Wilma" + } + + @Rollback + def "When creating user, user with multiple groups is saved correctly"() { + given: + Group ga = new Group() + ga.setResourceId("testingGroup") + ga.setName("Group A") + ga = groupService.createGroup(ga) + + Group gb = new Group(); + gb.setResourceId("testingGroupBBB") + gb.setName("Group BBB") + gb = groupService.createGroup(gb) + + Optional userRole = roleRepository.findByName("ROLE_USER") + User user = new User().with( { + it.username = "someUser" + it.roles = [userRole.get()] + it.password = "foo" + it + }) + + HashSet groups = new HashSet<>() + groups.add(ga) + groups.add(gb) + user.setGroups(groups) + + when: + def result = userService.save(user) + + then: + result.userGroups.size() == 2 + + // Raw check that the DB is correct for ownership + def Set users = ownershipRepository.findUsersByOwner(ga) + users.size() == 1 + users.getAt(0).ownedId == "someUser" + + def Set users2 = ownershipRepository.findUsersByOwner(gb) + users2.size() == 1 + users2.getAt(0).ownedId == "someUser" + + when: + def userFromDb = userRepository.findById(result.id).get(); + + then: + userFromDb.getUserGroups().size() == 2 + + when: + Group gbUpdated = groupService.find("testingGroupBBB") + + then: + gbUpdated.ownedItems.size() == 1 + } + + @org.springframework.boot.test.context.TestConfiguration + @Profile("local") + static class LocalConfig { + @Bean + GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { + GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { + it.groupRepository = repo + it.ownershipRepository = ownershipRepository + return it + }) + result.ensureAdminGroupExists() + return result + } + + @Bean + ObjectMapper objectMapper() { + JavaTimeModule module = new JavaTimeModule() + LocalDateTimeDeserializer localDateTimeDeserializer = new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS")) + module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer) + return Jackson2ObjectMapperBuilder.json().modules(module).featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build() + } + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy index e20b5d8f8..6ceae1eef 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy @@ -9,7 +9,7 @@ import org.springframework.test.context.ActiveProfiles import spock.lang.Specification /** - * Tests for AdminUserService + * Tests for AdminUserService (well, really it tests that the DevConfig worked as much as anything) * * @author Dmitriy Kopylenko */ @@ -20,13 +20,6 @@ class AdminUserServiceTests extends Specification { @Autowired AdminUserService adminUserService - @Autowired - RoleRepository adminRoleRepository - - @Autowired - UserRepository adminUserRepository - - def "Loading existing admin user with admin role"() { given: 'Valid user with admin role is available (loaded by Spring Boot Listener in dev profile)' def user = adminUserService.loadUserByUsername('admin') 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 02fde7269..26c7eb5f1 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 @@ -5,6 +5,8 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.JsonSchemaComponents import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor 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 + import org.springframework.core.io.DefaultResourceLoader import org.springframework.core.io.ResourceLoader import org.springframework.mock.http.MockHttpInputMessage @@ -14,31 +16,33 @@ import spock.lang.Specification import java.time.LocalDateTime class AuxiliaryIntegrationTests extends Specification { - @Shared OpenSamlObjects openSamlObjects = new OpenSamlObjects().with { it.init() it } - @Shared - EntityDescriptorService entityDescriptorService - - @Shared + JPAEntityDescriptorServiceImpl entityDescriptorService ObjectMapper objectMapper - - @Shared ResourceLoader resourceLoader void setup() { - this.entityDescriptorService = new JPAEntityDescriptorServiceImpl(openSamlObjects, null, null) - this.objectMapper = new ObjectMapper() - this.resourceLoader = new DefaultResourceLoader() + entityDescriptorService = new JPAEntityDescriptorServiceImpl() + entityDescriptorService.openSamlObjects = openSamlObjects + objectMapper = new ObjectMapper() + resourceLoader = new DefaultResourceLoader() } 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 - def entityDescriptorRepresentation = this.entityDescriptorService.createRepresentationFromDescriptor(entityDescriptor).with { + entityDescriptor.idOfOwner = "foo" + + def entityDescriptorRepresentation = entityDescriptorService.createRepresentationFromDescriptor(entityDescriptor).with { it.serviceProviderName = 'testme' it.contacts = [] it.securityInfo.x509Certificates[0].name = 'testcert' @@ -47,7 +51,7 @@ class AuxiliaryIntegrationTests extends Specification { it.setModifiedDate(LocalDateTime.now()) it } - def json = this.objectMapper.writeValueAsString(entityDescriptorRepresentation) + def json = objectMapper.writeValueAsString(entityDescriptorRepresentation) def schemaUri = edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.metadataSourcesSchema(new JsonSchemaComponentsConfiguration().jsonSchemaResourceLocationRegistry(this.resourceLoader, this.objectMapper)).uri when: 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 31cec75b2..0cde7c38b 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 @@ -11,6 +11,9 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilF import edu.internet2.tier.shibboleth.admin.ui.domain.filters.SignatureValidationFilter import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility import groovy.xml.XmlUtil @@ -22,7 +25,9 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Profile import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration import spock.lang.Specification @@ -33,6 +38,7 @@ import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.* @ContextConfiguration(classes = [CoreShibUiConfiguration, SearchConfiguration, InternationalizationConfiguration, edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration ,LocalConfig]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@ActiveProfiles(value = "local") class IncommonJPAMetadataResolverServiceImplTests extends Specification { @Autowired MetadataResolverService metadataResolverService @@ -104,8 +110,8 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { } } - //TODO: check that this configuration is sufficient @TestConfiguration + @Profile("local") static class LocalConfig { @Autowired OpenSamlObjects openSamlObjects @@ -147,5 +153,14 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { return resolver } + + @Bean + GroupServiceImpl groupService(GroupsRepository repo, OwnershipRepository ownershipRepository) { + new GroupServiceImpl().with { + it.groupRepository = repo + it.ownershipRepository = ownershipRepository + return it + } + } } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy index 3d05ae598..63a5a133b 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 @@ -22,16 +22,20 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSso import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils + import org.opensaml.saml.ext.saml2mdattr.EntityAttributes import org.skyscreamer.jsonassert.JSONAssert import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.json.JacksonTester import org.springframework.context.annotation.PropertySource +import org.springframework.test.annotation.DirtiesContext import org.springframework.test.context.ContextConfiguration import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input @@ -42,43 +46,34 @@ import spock.lang.Specification @ContextConfiguration(classes=[CoreShibUiConfiguration, CustomPropertiesConfiguration]) @SpringBootTest(classes = ShibbolethUiApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) @PropertySource("classpath:application.yml") -class JPAEntityDescriptorServiceImplTests extends Specification { - +@DirtiesContext +class JPAEntityDescriptorServiceImplTests extends Specification { + @Autowired + EntityService entityService; + @Autowired - CustomPropertiesConfiguration customPropertiesConfiguration + JPAEntityDescriptorServiceImpl service def testObjectGenerator - + OpenSamlObjects openSamlObjects = new OpenSamlObjects().with { init() it } - - JPAEntityDescriptorServiceImpl service - + JacksonTester jacksonTester - ObjectMapper mapper - RandomGenerator generator - - @Autowired - RoleRepository roleRepository - - @Autowired - UserRepository userRepository - - def setup() { - service = new JPAEntityDescriptorServiceImpl(openSamlObjects, - new JPAEntityServiceImpl(openSamlObjects, new AttributeUtility(openSamlObjects), customPropertiesConfiguration), new UserService(roleRepository, userRepository)) + + def setup() { mapper = new ObjectMapper() JacksonTester.initFields(this, mapper) generator = new RandomGenerator() testObjectGenerator = new TestObjectGenerator() + EntityDescriptorConversionUtils.openSamlObjects = openSamlObjects + EntityDescriptorConversionUtils.entityService = entityService } - - def "simple Entity Descriptor"() { when: def expected = ''' attributesList = new ArrayList() - for (int index = 0; index < testRunIndex; index++) { - attributesList.add("additionalAttributes" + index) - } - - when: - def attribute = service.createAttributeWithArbitraryValues(expectedName, - expectedFriendlyName, - attributesList) - - then: - expectedName == attribute.getName() - expectedFriendlyName == attribute.getFriendlyName() - attribute.getAttributeValues().size() == testRunIndex - for (int index = 0; index < testRunIndex; index++) { - attribute.getAttributeValues().get(index) instanceof XSAny - ((XSAny)attribute.getAttributeValues().get(index)).getTextContent() == "additionalAttributes" + index - } - - where: - testRunIndex << (1..5) - } - def "updateDescriptorFromRepresentation updates descriptor properly"() { given: def randomEntityDescriptor = generateRandomEntityDescriptor() @@ -806,48 +712,6 @@ class JPAEntityDescriptorServiceImplTests extends Specification { expectedVersion == actualVersion } - def "SHIBUI-1220 getValueFromXMLObject handles XSAny"() { - given: - def builder = new XSAnyBuilder() - def xsAny = builder.buildObject('namespace', 'localname', 'prefix') - def expectedTextContent = 'expectedTextContent' - xsAny.setTextContent(expectedTextContent) - - when: - def result = service.getValueFromXMLObject(xsAny) - - then: - result == expectedTextContent - } - - def "SHIBUI-1220 getValueFromXMLObject handles XSString"() { - given: - def builder = new XSStringBuilder() - def xsString = builder.buildObject('namespace', 'localname', 'prefix') - def expectedValue = 'expectedValue' - xsString.setValue(expectedValue) - - when: - def result = service.getValueFromXMLObject(xsString) - - then: - result == expectedValue - } - - def "SHIBUI-1220 getValueFromXMLObject handles XSBoolean"() { - given: - def builder = new XSBooleanBuilder() - def xsBoolean = builder.buildObject('namespace', 'localname', 'prefix') - def expectedValue = 'true' - xsBoolean.setStoredValue(expectedValue) - - when: - def result = service.getValueFromXMLObject(xsBoolean) - - then: - result == expectedValue - } - def "SHIBUI-1220 getValueFromXMLObject throws RuntimeException for unhandled object type"() { given: def unhandledObject = new Object() diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy new file mode 100644 index 000000000..13847472c --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy @@ -0,0 +1,131 @@ +package edu.internet2.tier.shibboleth.admin.ui.service + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Profile +import org.springframework.context.annotation.PropertySource +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.annotation.Rollback +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.annotation.Transactional + +import edu.internet2.tier.shibboleth.admin.ui.ShibbolethUiApplication +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation +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.IGroupService +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import spock.lang.Specification + +@ContextConfiguration(classes=[CoreShibUiConfiguration, CustomPropertiesConfiguration, LocalConfig]) +@SpringBootTest(classes = ShibbolethUiApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@PropertySource("classpath:application.yml") +@DirtiesContext +@ActiveProfiles(value="local") +class JPAEntityDescriptorServiceImplTests2 extends Specification { + + @Autowired + GroupServiceForTesting groupService + + @Autowired + RoleRepository roleRepository + + @Autowired + JPAEntityDescriptorServiceImpl entityDescriptorService + + @Autowired + UserRepository userRepository + + @Autowired + UserService userService + + @Transactional + def setup() { + // ensure we start fresh with only expected users and roles and groups + userRepository.deleteAll() + roleRepository.deleteAll() + groupService.clearAllForTesting() + + Group ga = new Group() + ga.setResourceId("testingGroup") + ga.setName("Group A") + ga = groupService.createGroup(ga) + + Group gb = new Group(); + gb.setResourceId("testingGroupBBB") + gb.setName("Group BBB") + gb = groupService.createGroup(gb) + + def roles = [new Role().with { + name = 'ROLE_ADMIN' + it + }, new Role().with { + name = 'ROLE_USER' + it + }, new Role().with { + name = 'ROLE_NONE' + it + }] + roles.each { + roleRepository.save(it) + } + + Optional adminRole = roleRepository.findByName("ROLE_ADMIN") + User adminUser = new User(username: "admin", roles: [adminRole.get()], password: "foo") + userService.save(adminUser) + + Optional userRole = roleRepository.findByName("ROLE_USER") + User user = new User(username: "someUser", roles:[userRole.get()], password: "foo", group: gb) + userService.save(user) + } + + @WithMockUser(value = "someUser", roles = ["USER"]) + @Rollback + def "When creating Entity Descriptor, ED is assigned to the user's group"() { + given: + User current = userService.getCurrentUser() + current.setGroupId("testingGroupBBB") + + def expectedCreationDate = '2017-10-23T11:11:11' + def expectedEntityId = 'https://shib' + def expectedSpName = 'sp1' + def expectedUUID = 'uuid-1' + def expectedResponseHeader = 'Location' + def expectedResponseHeaderValue = "/api/EntityDescriptor/$expectedUUID" + def entityDescriptor = new EntityDescriptor(resourceId: expectedUUID, entityID: expectedEntityId, serviceProviderName: expectedSpName, serviceEnabled: false) + + when: + def result = entityDescriptorService.createNew(entityDescriptor) + + then: + ((EntityDescriptorRepresentation)result).getIdOfOwner() == "testingGroupBBB" + } + + @TestConfiguration + @Profile("local") + static class LocalConfig { + @Bean + GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { + GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { + it.groupRepository = repo + it.ownershipRepository = ownershipRepository + return it + }) + return result + } + } +} 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 0214b6ff8..e91f3fbab 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 @@ -21,6 +21,9 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.TemplateScheme import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility import edu.internet2.tier.shibboleth.admin.util.TokenPlaceholderResolvers @@ -38,9 +41,11 @@ import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Profile import org.springframework.core.io.ClassPathResource import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input @@ -55,6 +60,7 @@ import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.generatedX @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +@ActiveProfiles(value = "local") class JPAMetadataResolverServiceImplTests extends Specification { @Autowired MetadataResolverRepository metadataResolverRepository @@ -479,6 +485,7 @@ class JPAMetadataResolverServiceImplTests extends Specification { } @TestConfiguration + @Profile("local") static class Config { @Autowired OpenSamlObjects openSamlObjects @@ -509,5 +516,14 @@ class JPAMetadataResolverServiceImplTests extends Specification { it } } + + @Bean + GroupServiceImpl groupService(GroupsRepository repo, OwnershipRepository ownershipRepository) { + new GroupServiceImpl().with { + it.groupRepository = repo + it.ownershipRepository = ownershipRepository + return it + } + } } } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy index 0a5db2bcf..95eb8d09f 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy @@ -7,6 +7,8 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest @@ -33,12 +35,23 @@ class UserBootstrapTests extends Specification { @Autowired RoleRepository roleRepository + + @Autowired + UserService userService + + @Autowired + IGroupService groupService + def setup() { + groupService.ensureAdminGroupExists() + roleRepository.deleteAll() + } + def "simple test"() { setup: shibUIConfiguration.roles = [] shibUIConfiguration.userBootstrapResource = new ClassPathResource('/conf/1044.csv') - def userBootstrap = new UserBootstrap(shibUIConfiguration, userRepository, roleRepository) + def userBootstrap = new UserBootstrap(shibUIConfiguration, userRepository, roleRepository, userService, groupService) when: userBootstrap.bootstrapUsersAndRoles(null) @@ -52,7 +65,7 @@ class UserBootstrapTests extends Specification { def "bootstrap roles"() { setup: shibUIConfiguration.roles = ['ROLE_ADMIN', 'ROLE_USER'] - def userbootstrap = new UserBootstrap(shibUIConfiguration, userRepository, roleRepository) + def userbootstrap = new UserBootstrap(shibUIConfiguration, userRepository, roleRepository, userService, groupService) when: userbootstrap.bootstrapUsersAndRoles(null) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryJPAEntityDescriptorServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy similarity index 94% rename from backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryJPAEntityDescriptorServiceImplTests.groovy rename to backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy index f78f8d354..d7f7a427b 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryJPAEntityDescriptorServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy @@ -1,4 +1,7 @@ -package edu.internet2.tier.shibboleth.admin.ui.service +package edu.internet2.tier.shibboleth.admin.ui.util + +import org.opensaml.saml.common.xml.SAMLConstants +import org.opensaml.saml.saml2.metadata.ContactPersonTypeEnumeration import edu.internet2.tier.shibboleth.admin.ui.domain.ContactPerson import edu.internet2.tier.shibboleth.admin.ui.domain.Description @@ -22,63 +25,29 @@ 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 org.opensaml.saml.common.xml.SAMLConstants -import org.opensaml.saml.saml2.metadata.ContactPersonTypeEnumeration +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils import spock.lang.Shared import spock.lang.Specification import spock.lang.Unroll -class AuxiliaryJPAEntityDescriptorServiceImplTests extends Specification { - @Shared - OpenSamlObjects openSAMLObjects = new OpenSamlObjects().with { - it.init() - it - } +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.* +class EntityDescriptorConversionUtilsTests extends Specification { @Shared - JPAEntityDescriptorServiceImpl entityDescriptorService - - void setup() { - entityDescriptorService = new JPAEntityDescriptorServiceImpl(openSAMLObjects, null, null) - } - - def "simple test"() { - assert true - } - - // this is a stub to build out the DataFields - def "pretest"() { - given: - def dataField = new Data.DataField( - method: 'setupLogout', - description: 'no change', - representation: new EntityDescriptorRepresentation(), - starter: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class), - expected: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class) - ) - - when: - def (expected, starter) = [dataField.expected, dataField.starter] - expected.setResourceId(starter.getResourceId()) - entityDescriptorService."${dataField.method}"(starter, dataField.representation) - - then: - assert expected == starter - } - - @Unroll - def "test #method(#description)"() { - setup: - expected.setResourceId(starter.getResourceId()) - entityDescriptorService."$method"(starter, representation) - - expect: - assert starter == expected + def OpenSamlObjects openSAMLObjects + + def setup() { + openSAMLObjects = new OpenSamlObjects().with { + it.init() + it + } - where: - [method, description, representation, starter, expected] << Data.getData(openSAMLObjects) + def EntityDescriptorConversionUtils utilsUnderTest = new EntityDescriptorConversionUtils().with { + it.openSamlObjects = openSAMLObjects + it + } } - + def "test createKeyDescriptor, single type"() { given: def expectedXml = ''' @@ -92,12 +61,12 @@ class AuxiliaryJPAEntityDescriptorServiceImplTests extends Specification { expected.name = 'testName' when: - def keyDescriptor = entityDescriptorService.createKeyDescriptor('testName', 'signing', 'testValue') + def keyDescriptor = EntityDescriptorConversionUtils.createKeyDescriptor('testName', 'signing', 'testValue') then: assert keyDescriptor == expected } - + def "test createKeyDescriptor, both type"() { given: def expectedXml = ''' @@ -111,7 +80,7 @@ class AuxiliaryJPAEntityDescriptorServiceImplTests extends Specification { expected.name = 'testName' when: - def keyDescriptor = entityDescriptorService.createKeyDescriptor('testName', 'both', 'testValue') + def keyDescriptor = EntityDescriptorConversionUtils.createKeyDescriptor('testName', 'both', 'testValue') def x = openSAMLObjects.marshalToXmlString(keyDescriptor) then: assert keyDescriptor == expected @@ -119,16 +88,33 @@ class AuxiliaryJPAEntityDescriptorServiceImplTests extends Specification { def 'test createKeyDescriptor equality'() { when: - def key1 = entityDescriptorService.createKeyDescriptor('test', 'signing', 'test') - def key2 = entityDescriptorService.createKeyDescriptor('test', 'signing', 'test') + def key1 = EntityDescriptorConversionUtils.createKeyDescriptor('test', 'signing', 'test') + def key2 = EntityDescriptorConversionUtils.createKeyDescriptor('test', 'signing', 'test') then: assert key1.equals(key2) } + + @Unroll + def "test #method(#description)"() { + setup: + expected.setResourceId(starter.getResourceId()) + EntityDescriptorConversionUtils."$method"(starter, representation) + + expect: + assert starter == expected + + where: + [method, description, representation, starter, expected] << Data.getData(openSAMLObjects) + } static class Data { static def getData(OpenSamlObjects openSAMLObjects) { - JPAEntityDescriptorServiceImpl entityDescriptorService = new JPAEntityDescriptorServiceImpl(openSAMLObjects, null, null) + EntityDescriptorConversionUtils utilsUnderTest = new EntityDescriptorConversionUtils().with { + it.openSamlObjects = openSAMLObjects + it + } + def data = [] data << new DataField( @@ -621,7 +607,8 @@ class AuxiliaryJPAEntityDescriptorServiceImplTests extends Specification { expected: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(entityDescriptorService.createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor( + utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) it } ) @@ -645,7 +632,7 @@ class AuxiliaryJPAEntityDescriptorServiceImplTests extends Specification { starter: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(entityDescriptorService.createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) it } ) @@ -654,8 +641,8 @@ class AuxiliaryJPAEntityDescriptorServiceImplTests extends Specification { expected: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(entityDescriptorService.createKeyDescriptor('test', 'signing', 'test')) - it.addKeyDescriptor(entityDescriptorService.createKeyDescriptor('test2', 'encryption', 'test2')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test2', 'encryption', 'test2')) it } ) @@ -678,8 +665,8 @@ class AuxiliaryJPAEntityDescriptorServiceImplTests extends Specification { starter: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(entityDescriptorService.createKeyDescriptor('test', 'signing', 'test')) - it.addKeyDescriptor(entityDescriptorService.createKeyDescriptor('test2', 'encryption', 'test2')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test2', 'encryption', 'test2')) it } ) @@ -688,7 +675,7 @@ class AuxiliaryJPAEntityDescriptorServiceImplTests extends Specification { expected: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(entityDescriptorService.createKeyDescriptor('test2', 'encryption', 'test2')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test2', 'encryption', 'test2')) it } ) @@ -708,8 +695,8 @@ class AuxiliaryJPAEntityDescriptorServiceImplTests extends Specification { starter: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(entityDescriptorService.createKeyDescriptor('test', 'signing', 'test')) - it.addKeyDescriptor(entityDescriptorService.createKeyDescriptor('test', 'encryption', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'encryption', 'test')) it } ) @@ -729,8 +716,8 @@ class AuxiliaryJPAEntityDescriptorServiceImplTests extends Specification { starter: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(entityDescriptorService.createKeyDescriptor('test', 'signing', 'test')) - it.addKeyDescriptor(entityDescriptorService.createKeyDescriptor('test', 'encryption', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'encryption', 'test')) it } ) @@ -1108,11 +1095,9 @@ class AuxiliaryJPAEntityDescriptorServiceImplTests extends Specification { it } ) - return data } - static class DataField implements Iterable { String method String description @@ -1126,4 +1111,4 @@ class AuxiliaryJPAEntityDescriptorServiceImplTests extends Specification { } } } -} +} \ No newline at end of file diff --git a/ui/public/assets/schema/groups/group.json b/ui/public/assets/schema/groups/group.json new file mode 100644 index 000000000..02bcb2dea --- /dev/null +++ b/ui/public/assets/schema/groups/group.json @@ -0,0 +1,22 @@ +{ + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "title": "label.group-name", + "description": "tooltip.group-name", + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "description": { + "title": "label.group-description", + "description": "tooltip.group-description", + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } +} \ No newline at end of file diff --git a/ui/public/group.json b/ui/public/group.json new file mode 100644 index 000000000..07a21daa2 --- /dev/null +++ b/ui/public/group.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} \ No newline at end of file diff --git a/ui/src/app/App.js b/ui/src/app/App.js index 5d0c4e1e4..2fb4797b6 100644 --- a/ui/src/app/App.js +++ b/ui/src/app/App.js @@ -27,6 +27,7 @@ import { Filter } from './metadata/Filter'; import { Contention } from './metadata/contention/ContentionContext'; import { SessionModal } from './core/user/SessionModal'; import Button from 'react-bootstrap/Button'; +import { Groups } from './admin/Groups'; function App() { @@ -77,8 +78,9 @@ function App() { - + + diff --git a/ui/src/app/admin/Groups.js b/ui/src/app/admin/Groups.js new file mode 100644 index 000000000..ec69c4f61 --- /dev/null +++ b/ui/src/app/admin/Groups.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { Switch, Route, useRouteMatch, Redirect } from 'react-router-dom'; +import { GroupsProvider } from './hoc/GroupsProvider'; +import { NewGroup } from './container/NewGroup'; +import { EditGroup } from './container/EditGroup'; +import { GroupsList } from './container/GroupsList'; + +export function Groups() { + + let { path } = useRouteMatch(); + + return ( + <> + + + + {(groups, onDelete) => + + } + + } /> + + + } /> + + + } /> + + + + ); +} \ No newline at end of file diff --git a/ui/src/app/admin/component/GroupForm.js b/ui/src/app/admin/component/GroupForm.js new file mode 100644 index 000000000..da8c5af8b --- /dev/null +++ b/ui/src/app/admin/component/GroupForm.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 { useGroupUiSchema } 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 GroupForm ({group = {}, errors = [], loading = false, schema, onSave, onCancel}) { + + const { dispatch } = React.useContext(FormContext); + const onChange = ({formData, errors}) => { + dispatch(setFormDataAction(formData)); + dispatch(setFormErrorAction(errors)); + }; + + const uiSchema = useGroupUiSchema(); + + 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 72847104d..c4542438f 100644 --- a/ui/src/app/admin/component/UserMaintenance.js +++ b/ui/src/app/admin/component/UserMaintenance.js @@ -3,13 +3,19 @@ import React from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faTrash } from '@fortawesome/free-solid-svg-icons'; import Button from 'react-bootstrap/Button'; +import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; +import Tooltip from 'react-bootstrap/Tooltip'; import Translate from '../../i18n/components/translate'; import { useCurrentUser } from '../../core/user/UserContext'; -export default function UserMaintenance({ users, roles, onDeleteUser, onChangeUserRole }) { +import { GroupsProvider } from '../hoc/GroupsProvider'; +import { useTranslator } from '../../i18n/hooks'; + +export default function UserMaintenance({ users, roles, loading, onDeleteUser, onChangeUserRole, onChangeUserGroup }) { const currentUser = useCurrentUser(); + const translator = useTranslator(); return (
@@ -20,43 +26,74 @@ export default function UserMaintenance({ users, roles, onDeleteUser, onChangeUs Name Email Role + Group Delete? - {users.map((user, idx) => - - {user.username} - {user.firstName} {user.lastName} - {user.emailAddress} - - - - - - {currentUser.username !== user.username && - - } - - - )} + + {(groups, onRemove, loadingGroups) => + + {users.map((user, idx) => + + {user.username} + {user.firstName} {user.lastName} + {user.emailAddress} + + + + + + {translator(`message.user-role-admin-group`)} + }> + + + + + + + + + {currentUser.username !== user.username && + + } + + + )} + + } +
diff --git a/ui/src/app/admin/container/EditGroup.js b/ui/src/app/admin/container/EditGroup.js new file mode 100644 index 000000000..066350356 --- /dev/null +++ b/ui/src/app/admin/container/EditGroup.js @@ -0,0 +1,91 @@ +import React from 'react'; + +import { Prompt, useHistory } from 'react-router'; +import { useParams } from 'react-router-dom'; +import Translate from '../../i18n/components/translate'; +import { useGroups } from '../hooks'; +import { Schema } from '../../form/Schema'; +import { FormManager } from '../../form/FormManager'; + +import { GroupForm } from '../component/GroupForm'; +import { GroupProvider } from '../hoc/GroupProvider'; +import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; +import { useTranslator } from '../../i18n/hooks'; + +export function EditGroup() { + + const { id } = useParams(); + + const notifier = useNotificationDispatcher(); + const translator = useTranslator(); + + const history = useHistory(); + + const { put, response, loading } = useGroups(); + + const [blocking, setBlocking] = React.useState(false); + + async function save(metadata) { + let toast; + const resp = await put(``, metadata); + if (response.ok) { + gotoDetail({ refresh: true }); + toast = createNotificationAction(`Updated group successfully.`, NotificationTypes.SUCCESS); + } else { + toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); + } + if (toast) { + notifier(toast); + } + }; + + const cancel = () => { + gotoDetail(); + }; + + const gotoDetail = (state = null) => { + setBlocking(false); + history.push(`/groups`, state); + }; + + return ( +
+ + `message.unsaved-editor` + } + /> +
+
+
+
+ Edit group +
+
+
+
+ + {(group) => + + {(schema) => + <>{group && + + {(data, errors) => + save(data)} + onCancel={() => cancel()} />} + + }} + + } + +
+
+
+ ); +} \ No newline at end of file diff --git a/ui/src/app/admin/container/GroupsList.js b/ui/src/app/admin/container/GroupsList.js new file mode 100644 index 000000000..d86ff11b0 --- /dev/null +++ b/ui/src/app/admin/container/GroupsList.js @@ -0,0 +1,82 @@ +import React from 'react'; +import { faEdit, faPlusCircle, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import Button from 'react-bootstrap/Button'; +import { Link } from 'react-router-dom'; + +import { Translate } from '../../i18n/components/translate'; + +import { DeleteConfirmation } from '../../core/components/DeleteConfirmation'; + +export function GroupsList({ groups, onDelete }) { + + const remove = (id) => { + onDelete(id); + } + + return ( + + {(block) => +
+
+
+
+ + Groups Management + +
+
+
+ +   + Add new group + +
+
+ + + + + + + + + + {( groups?.length > 0 ) ? groups.map((group, i) => + + + + + + ) : + + } + +
+ Group Name + + Group Description + Actions
{group.name}{group.description || ''} + + + + Edit + + + +
No groups defined.
+
+
+
+
+
+ } +
+ ); +} \ No newline at end of file diff --git a/ui/src/app/admin/container/NewGroup.js b/ui/src/app/admin/container/NewGroup.js new file mode 100644 index 000000000..28994abd0 --- /dev/null +++ b/ui/src/app/admin/container/NewGroup.js @@ -0,0 +1,79 @@ +import React from 'react'; + +import { Prompt, useHistory } from 'react-router'; +import Translate from '../../i18n/components/translate'; +import { useGroups } from '../hooks'; +import { Schema } from '../../form/Schema'; +import { FormManager } from '../../form/FormManager'; +import { GroupForm } from '../component/GroupForm'; + +import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; +import { useTranslator } from '../../i18n/hooks'; + +export function NewGroup() { + const history = useHistory(); + const notifier = useNotificationDispatcher(); + const translator = useTranslator(); + + const { post, response, loading } = useGroups({}); + + const [blocking, setBlocking] = React.useState(false); + + async function save(group) { + let toast; + const resp = await post(``, group); + if (response.ok) { + gotoDetail({ refresh: true }); + toast = createNotificationAction(`Added group successfully.`, NotificationTypes.SUCCESS); + } else { + toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); + } + if (toast) { + notifier(toast); + } + }; + + const cancel = () => { + gotoDetail(); + }; + + const gotoDetail = (state = null) => { + setBlocking(false); + history.push(`/groups`, state); + }; + + return ( +
+ + `message.unsaved-editor` + } + /> +
+
+
+
+ Add a new group +
+
+
+
+ + {(schema) => + + {(data, errors) => + save(data)} + onCancel={() => cancel()} />} + } + +
+
+
+ ); +} \ No newline at end of file diff --git a/ui/src/app/admin/container/UserManagement.js b/ui/src/app/admin/container/UserManagement.js index e4e1140ef..29f4a8108 100644 --- a/ui/src/app/admin/container/UserManagement.js +++ b/ui/src/app/admin/container/UserManagement.js @@ -10,13 +10,13 @@ import Translate from '../../i18n/components/translate'; import API_BASE_PATH from '../../App.constant'; import { NotificationContext, createNotificationAction} from '../../notifications/hoc/Notifications'; -export default function UserManagement({ users, children, reload }) { +export default function UserManagement({ users, children, reload}) { const [roles, setRoles] = React.useState([]); const { dispatch } = React.useContext(NotificationContext); - const { get, patch, del, response } = useFetch(`${API_BASE_PATH}`, {}); + const { get, patch, del, response, loading } = useFetch(`${API_BASE_PATH}`, {}); async function loadRoles() { const roles = await get('/supportedRoles') @@ -26,6 +26,7 @@ export default function UserManagement({ users, children, reload }) { } async function setUserRoleRequest(user, role) { + user.role = role; await patch(`/admin/users/${user.username}`, { ...user, role @@ -38,6 +39,20 @@ export default function UserManagement({ users, children, reload }) { } } + async function setUserGroupRequest(user, groupId) { + user.groupId = groupId; + await patch(`/admin/users/${user.username}`, { + ...user, + groupId + }); + if (response.ok && reload) { + dispatch(createNotificationAction( + `User update successful for ${user.username}.` + )); + reload(); + } + } + async function deleteUserRequest(id) { await del(`/admin/users/${id}`); if (response.ok && reload) { @@ -66,7 +81,7 @@ export default function UserManagement({ users, children, reload }) { return (
- {children(users, roles, setUserRoleRequest, (id) => setDeleting(id))} + {children(users, roles, setUserRoleRequest, setUserGroupRequest, (id) => setDeleting(id), loading)} setDeleting(null)}> Delete User? diff --git a/ui/src/app/admin/hoc/GroupProvider.js b/ui/src/app/admin/hoc/GroupProvider.js new file mode 100644 index 000000000..5cc2adffb --- /dev/null +++ b/ui/src/app/admin/hoc/GroupProvider.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { useGroup } from '../hooks'; + +export function GroupProvider({ id, children }) { + + const [group, setGroup] = React.useState(); + const { get, response } = useGroup(id); + + async function loadGroup() { + const group = await get(``); + if (response.ok) { + setGroup(group); + } + } + + /*eslint-disable react-hooks/exhaustive-deps*/ + React.useEffect(() => { loadGroup() }, []); + + return (<>{children(group)}); +} \ No newline at end of file diff --git a/ui/src/app/admin/hoc/GroupsProvider.js b/ui/src/app/admin/hoc/GroupsProvider.js new file mode 100644 index 000000000..016bcee13 --- /dev/null +++ b/ui/src/app/admin/hoc/GroupsProvider.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { useGroups } from '../hooks'; +import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; +import { useTranslator } from '../../i18n/hooks'; + +export function GroupsProvider({ children, cache = 'no-cache' }) { + + const [groups, setGroups] = React.useState([]); + + const notifier = useNotificationDispatcher(); + const translator = useTranslator(); + + const { get, del, response, loading } = useGroups({ + cachePolicy: cache + }); + + async function loadGroups() { + const list = await get(``); + if (response.ok) { + setGroups(list); + } + } + + async function removeGroup(id) { + let toast; + const resp = await del(`/${id}`); + if (response.ok) { + loadGroups(); + toast = createNotificationAction(`Deleted group successfully.`, NotificationTypes.SUCCESS); + } else { + toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); + } + if (toast) { + notifier(toast); + } + } + + /*eslint-disable react-hooks/exhaustive-deps*/ + React.useEffect(() => { loadGroups() }, []); + + return (<>{children(groups, removeGroup, loading)}); +} \ No newline at end of file diff --git a/ui/src/app/admin/hooks.js b/ui/src/app/admin/hooks.js new file mode 100644 index 000000000..28aef201a --- /dev/null +++ b/ui/src/app/admin/hooks.js @@ -0,0 +1,20 @@ +import useFetch from 'use-http'; +import API_BASE_PATH from '../App.constant'; + +export function useGroups (opts = { cachePolicy: 'no-cache' }) { + return useFetch(`${API_BASE_PATH}/admin/groups`, opts); +} + +export function useGroup(id) { + return useFetch(`${API_BASE_PATH}/admin/groups/${id}`, { + cachePolicy: 'no-cache' + }); +} + +export function useGroupUiSchema () { + return { + description: { + 'ui:widget': 'textarea' + } + }; +} \ No newline at end of file diff --git a/ui/src/app/metadata/component/DeleteConfirmation.js b/ui/src/app/core/components/DeleteConfirmation.js similarity index 100% rename from ui/src/app/metadata/component/DeleteConfirmation.js rename to ui/src/app/core/components/DeleteConfirmation.js diff --git a/ui/src/app/metadata/component/DeleteConfirmation.test.js b/ui/src/app/core/components/DeleteConfirmation.test.js similarity index 100% rename from ui/src/app/metadata/component/DeleteConfirmation.test.js rename to ui/src/app/core/components/DeleteConfirmation.test.js diff --git a/ui/src/app/core/components/Header.js b/ui/src/app/core/components/Header.js index 54d0c402c..ca22fe12f 100644 --- a/ui/src/app/core/components/Header.js +++ b/ui/src/app/core/components/Header.js @@ -6,7 +6,7 @@ import Navbar from 'react-bootstrap/Navbar'; import Dropdown from 'react-bootstrap/Dropdown'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTh, faSignOutAlt, faPlusCircle, faCube, faCubes } from '@fortawesome/free-solid-svg-icons'; +import { faTh, faSignOutAlt, faPlusCircle, faCube, faCubes, faUsersCog } from '@fortawesome/free-solid-svg-icons'; import Translate from '../../i18n/components/translate'; import { useTranslator } from '../../i18n/hooks'; @@ -41,6 +41,10 @@ export function Header () { + + + + } diff --git a/ui/src/app/core/user/UserContext.js b/ui/src/app/core/user/UserContext.js index b1235877c..d656ac4a4 100644 --- a/ui/src/app/core/user/UserContext.js +++ b/ui/src/app/core/user/UserContext.js @@ -39,5 +39,16 @@ function useIsAdmin() { return user.role === 'ROLE_ADMIN'; } +function useIsInGroup(id) { + const user = useCurrentUser(); + return user.group === id; +} + +function useIsAdminOrInGroup() { + const isAdmin = useIsAdmin(); + const isInGroup = useIsInGroup(); + return isAdmin || isInGroup; +} + -export { UserContext, UserProvider, Consumer as UserConsumer, useCurrentUser, useIsAdmin }; \ No newline at end of file +export { UserContext, UserProvider, Consumer as UserConsumer, useCurrentUser, useIsAdmin, useIsAdminOrInGroup }; \ No newline at end of file diff --git a/ui/src/app/dashboard/view/AdminTab.js b/ui/src/app/dashboard/view/AdminTab.js index a458c810a..5c116c242 100644 --- a/ui/src/app/dashboard/view/AdminTab.js +++ b/ui/src/app/dashboard/view/AdminTab.js @@ -39,8 +39,13 @@ export function AdminTab () {
- {(u, roles, onChangeUserRole, onDeleteUser) => - } + {(u, roles, onChangeUserRole, onChangeUserGroup, onDeleteUser, loading) => + }
diff --git a/ui/src/app/dashboard/view/SourcesTab.js b/ui/src/app/dashboard/view/SourcesTab.js index 852ec221a..d60218f08 100644 --- a/ui/src/app/dashboard/view/SourcesTab.js +++ b/ui/src/app/dashboard/view/SourcesTab.js @@ -2,7 +2,7 @@ import React from 'react'; import Translate from '../../i18n/components/translate'; import SourceList from '../../metadata/domain/source/component/SourceList'; -import { useMetadataEntities } from '../../metadata/hooks/api'; +import { useMetadataEntities, useMetadataEntity } from '../../metadata/hooks/api'; import { Search } from '../component/Search'; const searchProps = ['serviceProviderName', 'entityId', 'createdBy']; @@ -15,6 +15,10 @@ export function SourcesTab () { cachePolicy: 'no-cache' }); + const updater = useMetadataEntity('source', { + cachePolicy: 'no-cache' + }) + async function loadSources() { const sources = await get(''); if (response.ok) { @@ -27,6 +31,16 @@ export function SourcesTab () { /*eslint-disable react-hooks/exhaustive-deps*/ React.useEffect(() => { loadSources() }, []); + async function changeSourceGroup(source, group) { + await updater.put(`/${source.id}`, { + ...source, + idOfOwner: group + }); + if (updater.response.ok) { + loadSources(); + } + } + return (
@@ -37,7 +51,7 @@ export function SourcesTab () {
- {(searched) => } + {(searched) => }
diff --git a/ui/src/app/form/FormManager.js b/ui/src/app/form/FormManager.js new file mode 100644 index 000000000..029c49c3c --- /dev/null +++ b/ui/src/app/form/FormManager.js @@ -0,0 +1,105 @@ +import React from 'react'; + +const initialState = { + data: {}, + errors: [] +}; + +const FormContext = React.createContext(); + +const { Provider, Consumer } = FormContext; + +export const FormActions = { + SET_FORM_ERROR: 'set form error', + SET_FORM_DATA: 'set form data' +}; + +export const setFormDataAction = (payload) => { + return { + type: FormActions.SET_FORM_DATA, + payload + } +} + +export const setFormErrorAction = (errors) => { + return { + type: FormActions.SET_FORM_ERROR, + payload: errors + } +} + +function reducer(state, action) { + switch (action.type) { + + case FormActions.SET_FORM_ERROR: + return { + ...state, + errors: action.payload + }; + case FormActions.SET_FORM_DATA: + return { + ...state, + data: action.payload + }; + default: + return state; + } +} + +/*eslint-disable react-hooks/exhaustive-deps*/ +function FormManager({ children, initial = {} }) { + + const data = { + ...initial + }; + + const [state, dispatch] = React.useReducer(reducer, { + ...initialState, + data + }); + const contextValue = React.useMemo(() => ({ state, dispatch }), [state, dispatch]); + + return ( + + {children(state.data, state.errors)} + + ); +} + +function useFormErrors() { + const { state } = React.useContext(FormContext); + const { errors } = state; + + return errors; +} + +function useFormContext() { + return React.useContext(FormContext); +} + +function useFormDispatcher() { + const { dispatch } = useFormContext(); + return dispatch; +} + +function useFormState() { + const { state } = useFormContext(); + return state; +} + +function useFormData() { + const { data } = useFormContext(); + return data; +} + +export { + useFormErrors, + useFormContext, + useFormDispatcher, + useFormState, + useFormData, + FormManager, + FormContext, + Provider as FormProvider, + Consumer as FormConsumer +}; \ No newline at end of file diff --git a/ui/src/app/form/Schema.js b/ui/src/app/form/Schema.js new file mode 100644 index 000000000..92aefe325 --- /dev/null +++ b/ui/src/app/form/Schema.js @@ -0,0 +1,24 @@ +import React from 'react'; +import useFetch from 'use-http'; + +export function Schema({ path, children }) { + + const [schema, setSchema] = React.useState({}); + + + const { get, response } = useFetch(path, { + cachePolicy: 'no-cache' + }); + + async function loadSchema() { + const list = await get(``); + if (response.ok) { + setSchema(list); + } + } + + /*eslint-disable react-hooks/exhaustive-deps*/ + React.useEffect(() => { loadSchema() }, []); + + return (<>{children(schema)}); +} \ No newline at end of file diff --git a/ui/src/app/metadata/component/MetadataHeader.js b/ui/src/app/metadata/component/MetadataHeader.js index d8a2baa85..4c9a04326 100644 --- a/ui/src/app/metadata/component/MetadataHeader.js +++ b/ui/src/app/metadata/component/MetadataHeader.js @@ -1,21 +1,82 @@ import React from 'react'; import FormattedDate from '../../core/components/FormattedDate'; +import { useIsAdmin } from '../../core/user/UserContext'; import Translate from '../../i18n/components/translate'; +import { GroupsProvider } from '../../admin/hoc/GroupsProvider'; +import { useMetadataEntity } from '../hooks/api'; +import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; +import { useTranslator } from '../../i18n/hooks'; +import { useMetadataLoader } from '../hoc/MetadataSelector'; + +export function MetadataHeader ({ showGroup, model, current = true, enabled = true, children, ...props }) { + + const isAdmin = useIsAdmin(); + const translator = useTranslator(); + + const { put, response } = useMetadataEntity('source', { + cachePolicy: 'no-cache' + }); + + const notifier = useNotificationDispatcher(); + + async function changeSourceGroup(s, group) { + let toast; + const resp = await put(`/${s.id}`, { + ...s, + idOfOwner: group + }); + if (response.ok) { + toast = createNotificationAction(`Updated group successfully.`, NotificationTypes.SUCCESS); + reload(); + } else { + toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); + } + if (toast) { + notifier(toast); + } + } + + const reload = useMetadataLoader(); -export function MetadataHeader ({ model, current = true, enabled = true, children, ...props }) { return (
- Saved:  - - - -
- By:  - {model.createdBy } +

+ Saved:  + + + +

+

+ By:  + {model.createdBy } +

+ {isAdmin && showGroup && + + {(groups, removeGroup, loadingGroups) => +
+ + + +
+ } +
+ }
{children}
@@ -29,6 +90,7 @@ export function MetadataHeader ({ model, current = true, enabled = true, childre Current

+
); diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilters.js b/ui/src/app/metadata/domain/filter/component/MetadataFilters.js index 13a22b53c..4e5ff8a98 100644 --- a/ui/src/app/metadata/domain/filter/component/MetadataFilters.js +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilters.js @@ -1,6 +1,6 @@ import React from 'react'; import { useMetadataFilters } from '../../../hooks/api'; -import { DeleteConfirmation } from '../../../component/DeleteConfirmation'; +import { DeleteConfirmation } from '../../../../core/components/DeleteConfirmation'; export const MetadataFiltersContext = React.createContext(); diff --git a/ui/src/app/metadata/domain/source/component/SourceList.js b/ui/src/app/metadata/domain/source/component/SourceList.js index 41e2c7adb..bead9da9b 100644 --- a/ui/src/app/metadata/domain/source/component/SourceList.js +++ b/ui/src/app/metadata/domain/source/component/SourceList.js @@ -12,8 +12,13 @@ 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 { GroupsProvider } from '../../../../admin/hoc/GroupsProvider'; + +export default function SourceList({ entities, onDelete, onEnable, onChangeGroup }) { + + const isAdmin = useIsAdmin(); -export default function SourceList({ entities, onDelete, onEnable }) { return ( {(onDeleteSource) => @@ -24,11 +29,12 @@ export default function SourceList({ entities, onDelete, onEnable }) { - - - - - + + + + + + {isAdmin && onChangeGroup && } {onDeleteSource && } @@ -45,7 +51,7 @@ export default function SourceList({ entities, onDelete, onEnable }) { {source.createdBy} - + } + {onDeleteSource && +
TitleEntity IDAuthorCreated DateEnabledTitleEntity IDAuthorCreated DateEnabledGroup
+ {onEnable ? + {isAdmin && onChangeGroup && + + + {(groups, removeGroup, loadingGroups) => + + + + + } + + diff --git a/ui/src/app/metadata/editor/MetadataEditorNav.js b/ui/src/app/metadata/editor/MetadataEditorNav.js index bfae1bb73..61a67a510 100644 --- a/ui/src/app/metadata/editor/MetadataEditorNav.js +++ b/ui/src/app/metadata/editor/MetadataEditorNav.js @@ -5,15 +5,12 @@ import Dropdown from 'react-bootstrap/Dropdown'; import Button from 'react-bootstrap/Button'; import Translate from '../../i18n/components/translate'; -// import { usePagesWithErrors } from '../hoc/MetadataFormContext'; export function MetadataEditorNav ({ definition, current, children, format = 'tabs', onNavigate }) { const [routes, setRoutes] = React.useState([]); const [active, setActive] = React.useState(null); - // const errors = usePagesWithErrors(definition); - React.useEffect(() => { setRoutes(definition ? definition.steps.map(step => ({ path: step.id, label: step.label })) : []) }, [definition]); diff --git a/ui/src/app/metadata/hoc/MetadataSchema.js b/ui/src/app/metadata/hoc/MetadataSchema.js index ec6dd49f0..c8a8365ee 100644 --- a/ui/src/app/metadata/hoc/MetadataSchema.js +++ b/ui/src/app/metadata/hoc/MetadataSchema.js @@ -9,7 +9,9 @@ export function MetadataSchema({ type, children, wizard = false }) { const definition = React.useMemo(() => wizard ? getWizard(type) : getDefinition(type), [type, wizard]); - const { get, response } = useFetch(``, {}, []); + const { get, response } = useFetch(``, { + cachePolicy: 'no-cache' + }); const [schema, setSchema] = React.useState(); diff --git a/ui/src/app/metadata/hoc/MetadataSelector.js b/ui/src/app/metadata/hoc/MetadataSelector.js index 7d7155066..acdf115be 100644 --- a/ui/src/app/metadata/hoc/MetadataSelector.js +++ b/ui/src/app/metadata/hoc/MetadataSelector.js @@ -4,6 +4,7 @@ import { useMetadataEntity } from '../hooks/api'; export const MetadataTypeContext = React.createContext(); export const MetadataObjectContext = React.createContext(); +export const MetadataLoaderContext = React.createContext(); /*eslint-disable react-hooks/exhaustive-deps*/ export function MetadataSelector({ children, ...props }) { @@ -31,10 +32,15 @@ export function MetadataSelector({ children, ...props }) { setMetadata(source); } } - React.useEffect(() => { loadMetadata(id) }, [id]); + + function reload() { + loadMetadata(id); + } + + React.useEffect(() => reload(), [id]); return ( - <> + {type && {metadata && metadata.version && @@ -42,7 +48,7 @@ export function MetadataSelector({ children, ...props }) { } } - + ); } @@ -50,4 +56,8 @@ export function useMetadataObject () { return React.useContext(MetadataObjectContext); } +export function useMetadataLoader() { + return React.useContext(MetadataLoaderContext); +} + export default MetadataSelector; \ No newline at end of file diff --git a/ui/src/app/metadata/hooks/api.js b/ui/src/app/metadata/hooks/api.js index d0f30ef38..cc2afa9c7 100644 --- a/ui/src/app/metadata/hooks/api.js +++ b/ui/src/app/metadata/hooks/api.js @@ -61,7 +61,8 @@ export const xmlRequestInterceptor = ({ options }) => { export function useMetadataEntityXml(type = 'source', opts = { interceptors: { request: xmlRequestInterceptor - } + }, + cachePolicy: 'no-cache' }) { return useFetch(`${API_BASE_PATH}${getMetadataPath(type)}`, opts); } diff --git a/ui/src/app/metadata/view/MetadataAttributeList.js b/ui/src/app/metadata/view/MetadataAttributeList.js index f585f805f..69e6754f6 100644 --- a/ui/src/app/metadata/view/MetadataAttributeList.js +++ b/ui/src/app/metadata/view/MetadataAttributeList.js @@ -8,7 +8,7 @@ import { Link } from 'react-router-dom'; import { Translate } from '../../i18n/components/translate'; import { useTranslator } from '../../i18n/hooks'; -import { DeleteConfirmation } from '../component/DeleteConfirmation'; +import { DeleteConfirmation } from '../../core/components/DeleteConfirmation'; export function MetadataAttributeList ({entities, onDelete}) { diff --git a/ui/src/app/metadata/view/MetadataOptions.js b/ui/src/app/metadata/view/MetadataOptions.js index 93011229a..38bd4a951 100644 --- a/ui/src/app/metadata/view/MetadataOptions.js +++ b/ui/src/app/metadata/view/MetadataOptions.js @@ -21,6 +21,7 @@ import { MetadataFilterTypes } from '../domain/filter'; import { useMetadataSchema } from '../hooks/schema'; import { FilterableProviders } from '../domain/provider'; + export function MetadataOptions () { const metadata = React.useContext(MetadataObjectContext); @@ -60,7 +61,8 @@ export function MetadataOptions () { + model={metadata} + showGroup={type === 'source'}> {type === 'source' && onDeleteSource &&