diff --git a/.gitignore b/.gitignore index cb8ad4931..f75d64309 100644 --- a/.gitignore +++ b/.gitignore @@ -406,4 +406,5 @@ beacon/spring/out *.classpath *.settings *.project -*bin \ No newline at end of file +*bin +/a.json diff --git a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/service/envers/EnversEntityDescriptorVersionServiceTests.groovy b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/service/envers/EnversEntityDescriptorVersionServiceTests.groovy index c5b1f4138..f996c534d 100644 --- a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/service/envers/EnversEntityDescriptorVersionServiceTests.groovy +++ b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/service/envers/EnversEntityDescriptorVersionServiceTests.groovy @@ -58,7 +58,7 @@ class EnversEntityDescriptorVersionServiceTests extends Specification { when: 'Second version' ed.serviceProviderName = 'SP2' - ed = edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.doInExplicitTransaction(txMgr) { + ed = EnversTestsSupport.doInExplicitTransaction(txMgr) { entityDescriptorRepository.save(ed) } versions = entityDescriptorVersionService.findVersionsForEntityDescriptor(ed.resourceId) @@ -71,7 +71,7 @@ class EnversEntityDescriptorVersionServiceTests extends Specification { when: 'Third version' ed.serviceProviderName = 'SP3' - ed = edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.doInExplicitTransaction(txMgr) { + ed = EnversTestsSupport.doInExplicitTransaction(txMgr) { entityDescriptorRepository.save(ed) } versions = entityDescriptorVersionService.findVersionsForEntityDescriptor(ed.resourceId) @@ -86,7 +86,7 @@ class EnversEntityDescriptorVersionServiceTests extends Specification { def "versioning service returns correct entity descriptor for version number"() { when: 'Initial version' EntityDescriptor ed = new EntityDescriptor(entityID: 'ed', serviceProviderName: 'SP1', createdBy: 'anonymousUser') - ed = edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.doInExplicitTransaction(txMgr) { + ed = EnversTestsSupport.doInExplicitTransaction(txMgr) { entityDescriptorRepository.save(ed) } def versions = entityDescriptorVersionService.findVersionsForEntityDescriptor(ed.resourceId) @@ -98,7 +98,7 @@ class EnversEntityDescriptorVersionServiceTests extends Specification { when: 'Update the original' ed.serviceProviderName = 'SP2' - ed = edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.doInExplicitTransaction(txMgr) { + ed = EnversTestsSupport.doInExplicitTransaction(txMgr) { entityDescriptorRepository.save(ed) } versions = entityDescriptorVersionService.findVersionsForEntityDescriptor(ed.resourceId) @@ -112,17 +112,17 @@ class EnversEntityDescriptorVersionServiceTests extends Specification { def "versioning service throws EntityNotFoundException for non existent version number"() { when: 'Initial version' EntityDescriptor ed = new EntityDescriptor(entityID: 'ed', serviceProviderName: 'SP1', createdBy: 'anonymousUser') - ed = edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.doInExplicitTransaction(txMgr) { + ed = EnversTestsSupport.doInExplicitTransaction(txMgr) { entityDescriptorRepository.save(ed) } then: try { def edRepresentation = entityDescriptorVersionService.findSpecificVersionOfEntityDescriptor(ed.resourceId, '1000') - 1 == 2 + false } catch (EntityNotFoundException expected) { - 1 == 1 + true } } -} +} \ No newline at end of file diff --git a/backend/src/integration/groovy/edu/internet2/tier/shibboleth/admin/ui/SeleniumSIDETest.groovy b/backend/src/integration/groovy/edu/internet2/tier/shibboleth/admin/ui/SeleniumSIDETest.groovy index f63de0276..e156451ee 100644 --- a/backend/src/integration/groovy/edu/internet2/tier/shibboleth/admin/ui/SeleniumSIDETest.groovy +++ b/backend/src/integration/groovy/edu/internet2/tier/shibboleth/admin/ui/SeleniumSIDETest.groovy @@ -131,6 +131,8 @@ class SeleniumSIDETest extends Specification { 'SHIBUI-1740: Verify dev profile group membership' | '/SHIBUI-1740-2.side' 'SHIBUI-1740: Verify admin-owned resource not visible to nonadmins' | '/SHIBUI-1740-3.side' 'SHIBUI-1740: Verify nonadmin-owned resource visibility' | '/SHIBUI-1740-4.side' + 'SHIBUI-1742: Verify enabler role allows enabling' | '/SHIBUI-1742-1.side' + 'SHIBUI-1742: Verify role CRUD operations' | '/SHIBUI-1742-2.side' } } diff --git a/backend/src/integration/resources/SHIBUI-1742-1.side b/backend/src/integration/resources/SHIBUI-1742-1.side new file mode 100644 index 000000000..cc8b94078 --- /dev/null +++ b/backend/src/integration/resources/SHIBUI-1742-1.side @@ -0,0 +1,551 @@ +{ + "id": "13a69ee7-b636-4bfe-8f84-ef4670ded81e", + "version": "2.0", + "name": "SHIBUI-1742-1", + "url": "http://localhost:10101", + "tests": [{ + "id": "a26f2eda-ddf2-4c49-a29d-d86a282bb75a", + "name": "SHIBUI-1742-1", + "commands": [{ + "id": "db1854f2-6ab7-49d9-b2cc-ba4dfcb7cafd", + "comment": "", + "command": "open", + "target": "/login", + "targets": [], + "value": "" + }, { + "id": "46f32c9e-84a3-4aad-b61c-9fb6a9de3b1a", + "comment": "", + "command": "type", + "target": "id=username", + "targets": [ + ["id=username", "id"], + ["name=username", "name"], + ["css=#username", "css:finder"], + ["xpath=//input[@id='username']", "xpath:attributes"], + ["xpath=//input", "xpath:position"] + ], + "value": "admin" + }, { + "id": "9a5a9a52-3c62-47fd-b6f6-4525bcde990d", + "comment": "", + "command": "type", + "target": "id=password", + "targets": [ + ["id=password", "id"], + ["name=password", "name"], + ["css=#password", "css:finder"], + ["xpath=//input[@id='password']", "xpath:attributes"], + ["xpath=//p[2]/input", "xpath:position"] + ], + "value": "adminpass" + }, { + "id": "06d4ce18-c03f-424d-9eac-0c44b52dd9ab", + "comment": "", + "command": "sendKeys", + "target": "id=password", + "targets": [ + ["id=password", "id"], + ["name=password", "name"], + ["css=#password", "css:finder"], + ["xpath=//input[@id='password']", "xpath:attributes"], + ["xpath=//p[2]/input", "xpath:position"] + ], + "value": "${KEY_ENTER}" + }, { + "id": "9572c7f3-5365-4864-a335-a0ed4c87ec7f", + "comment": "", + "command": "open", + "target": "/api/heheheheheheheWipeout", + "targets": [], + "value": "" + }, { + "id": "f7a3bd08-36e1-4b1d-97ad-88b3fcca1569", + "comment": "", + "command": "assertText", + "target": "css=body", + "targets": [], + "value": "yes, you did it" + }, { + "id": "661c8c9a-0a29-46e2-b29a-0649f16d7ed3", + "comment": "", + "command": "open", + "target": "/", + "targets": [], + "value": "" + }, { + "id": "5665bd34-1f12-40ed-b33a-7c800d24988b", + "comment": "", + "command": "click", + "target": "linkText=Admin", + "targets": [ + ["linkText=Admin", "linkText"], + ["css=.nav-item:nth-child(3) > .nav-link", "css:finder"], + ["xpath=//a[contains(text(),'Admin')]", "xpath:link"], + ["xpath=//div[@id='root']/div/main/div/div/div[3]/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/dashboard/admin/management')]", "xpath:href"], + ["xpath=//div[3]/a", "xpath:position"], + ["xpath=//a[contains(.,'Admin')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "5d37e95f-2c20-4ff4-9d78-95471f0999e2", + "comment": "", + "command": "click", + "target": "id=role-anonymousUser", + "targets": [ + ["id=role-anonymousUser", "id"], + ["name=role-anonymousUser", "name"], + ["css=#role-anonymousUser", "css:finder"], + ["xpath=//select[@id='role-anonymousUser']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[4]/td[4]/select", "xpath:idRelative"], + ["xpath=//tr[4]/td[4]/select", "xpath:position"] + ], + "value": "" + }, { + "id": "2f0250cc-19c8-4ef2-aeb9-1199292ab2f9", + "comment": "", + "command": "select", + "target": "id=role-anonymousUser", + "targets": [], + "value": "label=ROLE_ENABLE" + }, { + "id": "58c0e31f-f73c-4b5c-82b2-7825dc5efa67", + "comment": "", + "command": "click", + "target": "id=group-anonymousUser", + "targets": [ + ["id=group-anonymousUser", "id"], + ["name=group-anonymousUser", "name"], + ["css=#group-anonymousUser", "css:finder"], + ["xpath=//select[@id='group-anonymousUser']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[4]/td[5]/span/select", "xpath:idRelative"], + ["xpath=//tr[4]/td[5]/span/select", "xpath:position"] + ], + "value": "" + }, { + "id": "e6960825-45d2-4399-871d-5db5295ee797", + "comment": "", + "command": "select", + "target": "id=group-anonymousUser", + "targets": [], + "value": "label=A1" + }, { + "id": "b2e8d04b-d72f-4095-be44-da0608dcbb90", + "comment": "", + "command": "click", + "target": "id=group-nonadmin", + "targets": [ + ["id=group-nonadmin", "id"], + ["name=group-nonadmin", "name"], + ["css=#group-nonadmin", "css:finder"], + ["xpath=//select[@id='group-nonadmin']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[2]/td[5]/span/select", "xpath:idRelative"], + ["xpath=//tr[2]/td[5]/span/select", "xpath:position"] + ], + "value": "" + }, { + "id": "6ac3fde8-10fb-4ebd-b29a-13c194714fd3", + "comment": "", + "command": "select", + "target": "id=group-nonadmin", + "targets": [], + "value": "label=A1" + }, { + "id": "7109b2ff-b84f-403f-afd0-9e260bc1fc81", + "comment": "", + "command": "click", + "target": "linkText=Logout", + "targets": [ + ["linkText=Logout", "linkText"], + ["css=.nav-link:nth-child(4)", "css:finder"], + ["xpath=//a[contains(text(),'Logout')]", "xpath:link"], + ["xpath=//div[@id='basic-navbar-nav']/div/a[2]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/logout')]", "xpath:href"], + ["xpath=//a[2]", "xpath:position"], + ["xpath=//a[contains(.,'Logout')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "cdb975a8-eb89-4d03-a544-fde32d069448", + "comment": "", + "command": "type", + "target": "id=username", + "targets": [ + ["id=username", "id"], + ["name=username", "name"], + ["css=#username", "css:finder"], + ["xpath=//input[@id='username']", "xpath:attributes"], + ["xpath=//input", "xpath:position"] + ], + "value": "nonadmin" + }, { + "id": "fb80084d-56db-4dec-9ca8-4eaefa6e4178", + "comment": "", + "command": "type", + "target": "id=password", + "targets": [ + ["id=password", "id"], + ["name=password", "name"], + ["css=#password", "css:finder"], + ["xpath=//input[@id='password']", "xpath:attributes"], + ["xpath=//p[2]/input", "xpath:position"] + ], + "value": "nonadminpass" + }, { + "id": "1c46ef5e-e997-40f5-87a6-d8ef4c7f4f2a", + "comment": "", + "command": "sendKeys", + "target": "id=password", + "targets": [ + ["id=password", "id"], + ["name=password", "name"], + ["css=#password", "css:finder"], + ["xpath=//input[@id='password']", "xpath:attributes"], + ["xpath=//p[2]/input", "xpath:position"] + ], + "value": "${KEY_ENTER}" + }, { + "id": "bbbdc0b9-5e2d-4283-8cbf-7ccfa089d23d", + "comment": "", + "command": "click", + "target": "id=dropdown-basic", + "targets": [ + ["id=dropdown-basic", "id"], + ["css=#dropdown-basic", "css:finder"], + ["xpath=//button[@id='dropdown-basic']", "xpath:attributes"], + ["xpath=//div[@id='basic-nav-dropdown']/button", "xpath:idRelative"], + ["xpath=//div/button", "xpath:position"], + ["xpath=//button[contains(.,'Add New')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "76fc5d62-a49c-4788-91cb-c285cb9456d5", + "comment": "", + "command": "click", + "target": "linkText=Add a new metadata source", + "targets": [ + ["linkText=Add a new metadata source", "linkText"], + ["css=.text-primary", "css:finder"], + ["xpath=//a[contains(text(),'Add a new metadata source')]", "xpath:link"], + ["xpath=//div[@id='basic-nav-dropdown']/div/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/metadata/source/new')]", "xpath:href"], + ["xpath=//div/a", "xpath:position"], + ["xpath=//a[contains(.,'Add a new metadata source')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "40684f5b-1c6b-4d6d-839d-f87e74f793a6", + "comment": "", + "command": "click", + "target": "id=root_serviceProviderName", + "targets": [ + ["id=root_serviceProviderName", "id"], + ["css=#root_serviceProviderName", "css:finder"], + ["xpath=//input[@id='root_serviceProviderName']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[3]/div/div/form/div/div/div/div/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "" + }, { + "id": "e06e32d9-4979-4ba0-95bb-57db5ab2a591", + "comment": "", + "command": "type", + "target": "id=root_serviceProviderName", + "targets": [ + ["id=root_serviceProviderName", "id"], + ["css=#root_serviceProviderName", "css:finder"], + ["xpath=//input[@id='root_serviceProviderName']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[3]/div/div/form/div/div/div/div/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "test" + }, { + "id": "e3c65f02-18d9-45a0-82bf-e3c3d79a5f66", + "comment": "", + "command": "click", + "target": "id=root_entityId", + "targets": [ + ["id=root_entityId", "id"], + ["css=#root_entityId", "css:finder"], + ["xpath=//input[@id='root_entityId']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[3]/div/div/form/div/div/div/div/div/div[2]/div/div/input", "xpath:idRelative"], + ["xpath=//div[2]/div/div/input", "xpath:position"] + ], + "value": "" + }, { + "id": "a0664314-8c95-43c9-9b56-8686e686c68a", + "comment": "", + "command": "type", + "target": "id=root_entityId", + "targets": [ + ["id=root_entityId", "id"], + ["css=#root_entityId", "css:finder"], + ["xpath=//input[@id='root_entityId']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[3]/div/div/form/div/div/div/div/div/div[2]/div/div/input", "xpath:idRelative"], + ["xpath=//div[2]/div/div/input", "xpath:position"] + ], + "value": "test" + }, { + "id": "c8db7219-1b35-4e73-a8ae-1f166aad6562", + "comment": "", + "command": "click", + "target": "css=.label", + "targets": [ + ["css=.label", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[2]/div/nav/ul/li[2]/button/span", "xpath:idRelative"], + ["xpath=//li[2]/button/span", "xpath:position"], + ["xpath=//span[contains(.,'2. Organization Information')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "088c3f4f-7a89-494f-95e0-0ad9b3385109", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[4]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button", "xpath:idRelative"], + ["xpath=//li[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "10ab17b6-b92c-480e-867f-3a579cea41a3", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[4]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button", "xpath:idRelative"], + ["xpath=//li[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "396d28c6-0c02-4cba-929c-1cae2c373b56", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[4]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button", "xpath:idRelative"], + ["xpath=//li[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "0ca516e1-78bf-4a19-b18a-edefe5b13144", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[4]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button", "xpath:idRelative"], + ["xpath=//li[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "c504aa92-cf30-4384-ad03-c1f0b2fe4c88", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[4]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button", "xpath:idRelative"], + ["xpath=//li[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "13d6a627-02ea-4dab-b600-9f3dfab8ccb7", + "comment": "", + "command": "click", + "target": "css=.label:nth-child(1)", + "targets": [ + ["css=.label:nth-child(1)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button/span", "xpath:idRelative"], + ["xpath=//li[3]/button/span", "xpath:position"], + ["xpath=//span[contains(.,'7. Assertion Consumer Service')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "53f22ab7-97c2-46ae-96f8-1f783941d800", + "comment": "", + "command": "click", + "target": "css=.label:nth-child(1)", + "targets": [ + ["css=.label:nth-child(1)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button/span", "xpath:idRelative"], + ["xpath=//li[3]/button/span", "xpath:position"], + ["xpath=//span[contains(.,'8. Relying Party Overrides')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "f9bcd184-7f40-481a-ab13-0e59f020528c", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[4]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button", "xpath:idRelative"], + ["xpath=//li[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "77caf09c-ad60-4f46-81df-c90b23b4c7bc", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[4]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button", "xpath:idRelative"], + ["xpath=//li[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "7adae5ea-9424-4709-8472-54514cd189a5", + "comment": "", + "command": "click", + "target": "linkText=Logout", + "targets": [ + ["linkText=Logout", "linkText"], + ["css=.nav-link:nth-child(3)", "css:finder"], + ["xpath=//a[contains(text(),'Logout')]", "xpath:link"], + ["xpath=//div[@id='basic-navbar-nav']/div/a[2]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/logout')]", "xpath:href"], + ["xpath=//a[2]", "xpath:position"], + ["xpath=//a[contains(.,'Logout')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "06af2891-5091-40ba-9b0a-5a98f3e96b00", + "comment": "", + "command": "type", + "target": "id=username", + "targets": [ + ["id=username", "id"], + ["name=username", "name"], + ["css=#username", "css:finder"], + ["xpath=//input[@id='username']", "xpath:attributes"], + ["xpath=//input", "xpath:position"] + ], + "value": "anonymousUser" + }, { + "id": "857dc0c2-943b-4a2c-8903-415ff473db3b", + "comment": "", + "command": "type", + "target": "id=password", + "targets": [ + ["id=password", "id"], + ["name=password", "name"], + ["css=#password", "css:finder"], + ["xpath=//input[@id='password']", "xpath:attributes"], + ["xpath=//p[2]/input", "xpath:position"] + ], + "value": "anonymous" + }, { + "id": "53c20686-7317-4dd8-913a-e6db052853c1", + "comment": "", + "command": "sendKeys", + "target": "id=password", + "targets": [ + ["id=password", "id"], + ["name=password", "name"], + ["css=#password", "css:finder"], + ["xpath=//input[@id='password']", "xpath:attributes"], + ["xpath=//p[2]/input", "xpath:position"] + ], + "value": "${KEY_ENTER}" + }, { + "id": "9585df73-48e3-4ca3-b13c-74ecdee11461", + "comment": "", + "command": "click", + "target": "xpath=//table/tbody/tr/td[5]/span/div/input", + "targets": [ + ["css=.justify-content-center", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/div/table/tbody/tr/td[5]/span", "xpath:idRelative"], + ["xpath=//td[5]/span", "xpath:position"] + ], + "value": "" + }, { + "id": "0d322ab9-f8d7-4cc3-8b91-48c684955b1a", + "comment": "", + "command": "click", + "target": "linkText=Logout", + "targets": [ + ["linkText=Logout", "linkText"], + ["css=.nav-link:nth-child(3)", "css:finder"], + ["xpath=//a[contains(text(),'Logout')]", "xpath:link"], + ["xpath=//div[@id='basic-navbar-nav']/div/a[2]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/logout')]", "xpath:href"], + ["xpath=//a[2]", "xpath:position"], + ["xpath=//a[contains(.,'Logout')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "fbf1ba38-145e-42fb-899b-a7d8bd86d8a2", + "comment": "", + "command": "type", + "target": "id=username", + "targets": [ + ["id=username", "id"], + ["name=username", "name"], + ["css=#username", "css:finder"], + ["xpath=//input[@id='username']", "xpath:attributes"], + ["xpath=//input", "xpath:position"] + ], + "value": "admin" + }, { + "id": "d1af6437-5307-4102-aadb-7d87bbaa08cb", + "comment": "", + "command": "type", + "target": "id=password", + "targets": [ + ["id=password", "id"], + ["name=password", "name"], + ["css=#password", "css:finder"], + ["xpath=//input[@id='password']", "xpath:attributes"], + ["xpath=//p[2]/input", "xpath:position"] + ], + "value": "adminpass" + }, { + "id": "7dbd5a0c-31ea-4cf6-83fd-de40f4e239fc", + "comment": "", + "command": "sendKeys", + "target": "id=password", + "targets": [ + ["id=password", "id"], + ["name=password", "name"], + ["css=#password", "css:finder"], + ["xpath=//input[@id='password']", "xpath:attributes"], + ["xpath=//p[2]/input", "xpath:position"] + ], + "value": "${KEY_ENTER}" + }, { + "id": "79423a30-b82b-443f-b0ea-80370a6d397b", + "comment": "", + "command": "assertChecked", + "target": "xpath=//table/tbody/tr/td[5]/span/div/input", + "targets": [ + ["css=.custom-control-label", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/div/table/tbody/tr/td[5]/span/div/label", "xpath:idRelative"], + ["xpath=//span/div/label", "xpath:position"] + ], + "value": "" + }] + }], + "suites": [{ + "id": "8a97286b-5660-452c-9f23-4c5f5bf8de3b", + "name": "Default Suite", + "persistSession": false, + "parallel": false, + "timeout": 300, + "tests": ["a26f2eda-ddf2-4c49-a29d-d86a282bb75a"] + }], + "urls": ["http://localhost:10101/"], + "plugins": [] +} \ No newline at end of file diff --git a/backend/src/integration/resources/SHIBUI-1742-2.side b/backend/src/integration/resources/SHIBUI-1742-2.side new file mode 100644 index 000000000..a0684ca69 --- /dev/null +++ b/backend/src/integration/resources/SHIBUI-1742-2.side @@ -0,0 +1,462 @@ +{ + "id": "913c2183-c59c-4c54-8434-5866b49a982b", + "version": "2.0", + "name": "SHIBUI-1742-2", + "url": "http://localhost:10101", + "tests": [{ + "id": "629ba6a4-486d-4dbe-9ea1-4727a1e35567", + "name": "SHIBUI-1742-2", + "commands": [{ + "id": "252698e5-62bc-4087-b465-6cce517e4f42", + "comment": "", + "command": "open", + "target": "/login", + "targets": [], + "value": "" + }, { + "id": "68e6f41c-273d-47d5-a3bc-3886f3bb0361", + "comment": "", + "command": "type", + "target": "id=username", + "targets": [ + ["id=username", "id"], + ["name=username", "name"], + ["css=#username", "css:finder"], + ["xpath=//input[@id='username']", "xpath:attributes"], + ["xpath=//input", "xpath:position"] + ], + "value": "admin" + }, { + "id": "890b33ad-e28f-430d-a8f1-99d028c16c1e", + "comment": "", + "command": "type", + "target": "id=password", + "targets": [ + ["id=password", "id"], + ["name=password", "name"], + ["css=#password", "css:finder"], + ["xpath=//input[@id='password']", "xpath:attributes"], + ["xpath=//p[2]/input", "xpath:position"] + ], + "value": "adminpass" + }, { + "id": "52216d6c-70de-47ee-a385-8b015e244b42", + "comment": "", + "command": "sendKeys", + "target": "id=password", + "targets": [ + ["id=password", "id"], + ["name=password", "name"], + ["css=#password", "css:finder"], + ["xpath=//input[@id='password']", "xpath:attributes"], + ["xpath=//p[2]/input", "xpath:position"] + ], + "value": "${KEY_ENTER}" + }, { + "id": "53ffb74f-8635-453b-8b3e-22c0a11e0902", + "comment": "", + "command": "pause", + "target": "5000", + "targets": [], + "value": "" + }, { + "id": "c820e378-569c-44ba-834a-c7d6203d3e12", + "comment": "", + "command": "click", + "target": "xpath=//button[text()='Advanced']", + "targets": [], + "value": "30000" + }, { + "id": "8f1af018-7f7f-441c-bb59-9591469cc0da", + "comment": "", + "command": "click", + "target": "linkText=Roles", + "targets": [ + ["linkText=Roles", "linkText"], + ["css=.text-primary:nth-child(3)", "css:finder"], + ["xpath=//a[contains(text(),'Roles')]", "xpath:link"], + ["xpath=//div[@id='basic-nav-dropdown']/div/a[3]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/roles')]", "xpath:href"], + ["xpath=//a[3]", "xpath:position"], + ["xpath=//a[contains(.,'Roles')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "be331a79-e161-460b-87e5-0458bd6d87bc", + "comment": "", + "command": "click", + "target": "linkText=Add new role", + "targets": [ + ["linkText=Add new role", "linkText"], + ["css=.btn-success", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/roles/new')]", "xpath:href"], + ["xpath=//div[2]/div/a", "xpath:position"], + ["xpath=//a[contains(.,'  Add new role')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "b9f67743-c715-4db6-9380-77c39f23bc89", + "comment": "", + "command": "type", + "target": "id=root_name", + "targets": [ + ["id=root_name", "id"], + ["css=#root_name", "css:finder"], + ["xpath=//input[@id='root_name']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div/form/div/div/div/div/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "test" + }, { + "id": "548d864c-87ba-4889-823e-3e9729d596db", + "comment": "", + "command": "click", + "target": "css=.fa-save", + "targets": [ + ["css=.fa-save", "css:finder"] + ], + "value": "" + }, { + "id": "5a6af58f-5509-4d8c-ab15-11bb8ea124fc", + "comment": "", + "command": "waitForElementVisible", + "target": "xpath=//div[@role=\"alert\" and contains(., \"Added role successfully.\")]", + "targets": [], + "value": "30000" + }, { + "id": "7721b7e5-53d3-4000-b9b3-7553f517a3e2", + "comment": "", + "command": "click", + "target": "css=tr:nth-child(5) .text-primary > .svg-inline--fa", + "targets": [ + ["css=tr:nth-child(5) .text-primary > .svg-inline--fa", "css:finder"] + ], + "value": "" + }, { + "id": "49fd8a6e-9483-483a-9a51-cc41a58b8f8e", + "comment": "", + "command": "type", + "target": "id=root_name", + "targets": [ + ["id=root_name", "id"], + ["css=#root_name", "css:finder"], + ["xpath=//input[@id='root_name']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div/form/div/div/div/div/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "" + }, { + "id": "905bf81b-a0c1-4ec4-a874-621b2deea715", + "comment": "", + "command": "type", + "target": "id=root_name", + "targets": [ + ["id=root_name", "id"], + ["css=#root_name", "css:finder"], + ["xpath=//input[@id='root_name']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div/form/div/div/div/div/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "test role" + }, { + "id": "a2365448-e076-49ed-8fe6-daf35dc6e800", + "comment": "", + "command": "click", + "target": "css=.btn-info", + "targets": [ + ["css=.btn-info", "css:finder"], + ["xpath=(//button[@type='button'])[4]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/button", "xpath:idRelative"], + ["xpath=//div[2]/div/div/button", "xpath:position"], + ["xpath=//button[contains(.,' Save')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "159a7bf1-39e9-460f-890f-47c42d3e22ce", + "comment": "", + "command": "assertElementPresent", + "target": "xpath=//div[@role=\"alert\" and contains(., \"Updated role successfully.\")]", + "targets": [], + "value": "" + }, { + "id": "2317a47a-e308-4c1f-bd08-50360950c082", + "comment": "", + "command": "click", + "target": "xpath=(//button[@id='dropdown-basic'])[2]", + "targets": [ + ["xpath=(//button[@id='dropdown-basic'])[2]", "xpath:attributes"], + ["xpath=(//div[@id='basic-nav-dropdown']/button)[2]", "xpath:idRelative"], + ["xpath=//div[2]/button", "xpath:position"], + ["xpath=//button[contains(.,'Add New')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "cfe9cc6f-d96a-48ad-a273-d03eba7cefce", + "comment": "", + "command": "click", + "target": "id=dropdown-basic", + "targets": [ + ["id=dropdown-basic", "id"], + ["xpath=//button[@id='dropdown-basic']", "xpath:attributes"], + ["xpath=//div[@id='basic-nav-dropdown']/button", "xpath:idRelative"], + ["xpath=//div/button", "xpath:position"], + ["xpath=//button[contains(.,'Advanced')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "231fa40a-bb22-4d83-99e2-9a5a9d3ff823", + "comment": "", + "command": "click", + "target": "css=.p-3 > .d-flex", + "targets": [ + ["css=.p-3 > .d-flex", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div", "xpath:idRelative"], + ["xpath=//section/div/div[2]/div", "xpath:position"] + ], + "value": "" + }, { + "id": "e20c0727-c04f-465d-a7dc-7c7a9210c47b", + "comment": "", + "command": "click", + "target": "linkText=Dashboard", + "targets": [ + ["linkText=Dashboard", "linkText"], + ["css=.nav-link:nth-child(3)", "css:finder"], + ["xpath=//a[contains(text(),'Dashboard')]", "xpath:link"], + ["xpath=//div[@id='basic-navbar-nav']/div/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/dashboard')]", "xpath:href"], + ["xpath=//nav/div/div/a", "xpath:position"], + ["xpath=//a[contains(.,'Dashboard')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "733d176c-c0a3-4427-a7dd-1bc5ed42d96e", + "comment": "", + "command": "click", + "target": "linkText=Admin", + "targets": [ + ["linkText=Admin", "linkText"], + ["css=.nav-item:nth-child(3) > .nav-link", "css:finder"], + ["xpath=//a[contains(text(),'Admin')]", "xpath:link"], + ["xpath=//div[@id='root']/div/main/div/div/div[3]/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/dashboard/admin/management')]", "xpath:href"], + ["xpath=//div[3]/a", "xpath:position"], + ["xpath=//a[contains(.,'Admin')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "a5c105ee-9b6c-4226-bda6-fc17a77f2b35", + "comment": "", + "command": "click", + "target": "id=role-nonadmin", + "targets": [ + ["id=role-nonadmin", "id"], + ["name=role-nonadmin", "name"], + ["css=#role-nonadmin", "css:finder"], + ["xpath=//select[@id='role-nonadmin']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[2]/td[4]/select", "xpath:idRelative"], + ["xpath=//tr[2]/td[4]/select", "xpath:position"] + ], + "value": "" + }, { + "id": "eb737ab9-42bf-45c4-82c0-04c14fa26e63", + "comment": "", + "command": "select", + "target": "id=role-nonadmin", + "targets": [], + "value": "label=test role" + }, { + "id": "aada4cb0-0f3f-436c-b342-f83ea1e35ba4", + "comment": "", + "command": "waitForElementVisible", + "target": "//div[@role=\"alert\" and contains(., \"User update successful for nonadmin\")]", + "targets": [], + "value": "30000" + }, { + "id": "e8bedd18-aafa-4bff-85a4-ba36d7685225", + "comment": "", + "command": "click", + "target": "id=dropdown-basic", + "targets": [ + ["id=dropdown-basic", "id"], + ["xpath=//button[@id='dropdown-basic']", "xpath:attributes"], + ["xpath=//div[@id='basic-nav-dropdown']/button", "xpath:idRelative"], + ["xpath=//div/button", "xpath:position"], + ["xpath=//button[contains(.,'Advanced')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "e091ca7a-286d-4982-80e3-b180cf03a233", + "comment": "", + "command": "click", + "target": "linkText=Roles", + "targets": [ + ["linkText=Roles", "linkText"], + ["css=.text-primary:nth-child(3)", "css:finder"], + ["xpath=//a[contains(text(),'Roles')]", "xpath:link"], + ["xpath=//div[@id='basic-nav-dropdown']/div/a[3]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/roles')]", "xpath:href"], + ["xpath=//a[3]", "xpath:position"], + ["xpath=//a[contains(.,'Roles')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "a378e628-4227-451b-9fb6-ef9edb7fc4fa", + "comment": "", + "command": "click", + "target": "css=tr:nth-child(5) .text-danger path", + "targets": [ + ["css=tr:nth-child(5) .text-danger path", "css:finder"] + ], + "value": "" + }, { + "id": "0c411e9d-b9de-40e4-a39d-99a3b734f6a1", + "comment": "", + "command": "click", + "target": "css=.btn-danger", + "targets": [ + ["css=.btn-danger", "css:finder"], + ["xpath=(//button[@type='button'])[9]", "xpath:attributes"], + ["xpath=//div[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "22bbdc62-995b-43e4-abe1-24d7035b1d49", + "comment": "", + "command": "assertElementPresent", + "target": "xpath=//div[@role=\"alert\" and contains(., \"remove role from all users first\")]", + "targets": [], + "value": "" + }, { + "id": "e704f343-5942-4053-aed7-1fa94e13320c", + "comment": "", + "command": "click", + "target": "id=dropdown-basic", + "targets": [ + ["id=dropdown-basic", "id"], + ["xpath=//button[@id='dropdown-basic']", "xpath:attributes"], + ["xpath=//div[@id='basic-nav-dropdown']/button", "xpath:idRelative"], + ["xpath=//div/button", "xpath:position"], + ["xpath=//button[contains(.,'Advanced')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "78b2edf1-a85d-4a0e-9880-33a2bdef7b68", + "comment": "", + "command": "click", + "target": "linkText=Dashboard", + "targets": [ + ["linkText=Dashboard", "linkText"], + ["css=.nav-link:nth-child(3)", "css:finder"], + ["xpath=//a[contains(text(),'Dashboard')]", "xpath:link"], + ["xpath=//div[@id='basic-navbar-nav']/div/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/dashboard')]", "xpath:href"], + ["xpath=//nav/div/div/a", "xpath:position"], + ["xpath=//a[contains(.,'Dashboard')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "28b88c8a-a4f1-49ad-9f7c-0d81a75c25e9", + "comment": "", + "command": "click", + "target": "linkText=Admin", + "targets": [ + ["linkText=Admin", "linkText"], + ["css=.nav-item:nth-child(3) > .nav-link", "css:finder"], + ["xpath=//a[contains(text(),'Admin')]", "xpath:link"], + ["xpath=//div[@id='root']/div/main/div/div/div[3]/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/dashboard/admin/management')]", "xpath:href"], + ["xpath=//div[3]/a", "xpath:position"], + ["xpath=//a[contains(.,'Admin')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "246af097-6ab4-4173-a7cb-c4054e572fc5", + "comment": "", + "command": "click", + "target": "id=role-nonadmin", + "targets": [ + ["id=role-nonadmin", "id"], + ["name=role-nonadmin", "name"], + ["css=#role-nonadmin", "css:finder"], + ["xpath=//select[@id='role-nonadmin']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[2]/td[4]/select", "xpath:idRelative"], + ["xpath=//tr[2]/td[4]/select", "xpath:position"] + ], + "value": "" + }, { + "id": "dd177cf2-605d-4e49-934d-6d88b3efa7b6", + "comment": "", + "command": "select", + "target": "id=role-nonadmin", + "targets": [], + "value": "label=ROLE_USER" + }, { + "id": "2f499bda-2230-45d2-85c5-efee606c5121", + "comment": "", + "command": "click", + "target": "id=dropdown-basic", + "targets": [ + ["id=dropdown-basic", "id"], + ["xpath=//button[@id='dropdown-basic']", "xpath:attributes"], + ["xpath=//div[@id='basic-nav-dropdown']/button", "xpath:idRelative"], + ["xpath=//div/button", "xpath:position"], + ["xpath=//button[contains(.,'Advanced')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "e59ac7eb-4100-4c7f-8666-5aeb7980362b", + "comment": "", + "command": "click", + "target": "linkText=Roles", + "targets": [ + ["linkText=Roles", "linkText"], + ["css=.text-primary:nth-child(3)", "css:finder"], + ["xpath=//a[contains(text(),'Roles')]", "xpath:link"], + ["xpath=//div[@id='basic-nav-dropdown']/div/a[3]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/roles')]", "xpath:href"], + ["xpath=//a[3]", "xpath:position"], + ["xpath=//a[contains(.,'Roles')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "f4201d01-e582-4071-921b-ede543ce181a", + "comment": "", + "command": "click", + "target": "css=tr:nth-child(5) .text-danger path", + "targets": [ + ["css=tr:nth-child(5) .text-danger path", "css:finder"] + ], + "value": "" + }, { + "id": "7ff656ef-a5c1-4d32-abc6-bac132c45349", + "comment": "", + "command": "click", + "target": "css=.btn-danger", + "targets": [ + ["css=.btn-danger", "css:finder"], + ["xpath=(//button[@type='button'])[9]", "xpath:attributes"], + ["xpath=//div[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "7092ea4b-e3ea-4ef4-9172-dbf84910ec3c", + "comment": "", + "command": "assertElementPresent", + "target": "xpath=//div[@role=\"alert\" and contains(., \"Deleted role successfully.\")]", + "targets": [], + "value": "" + }] + }], + "suites": [{ + "id": "2feb47ff-b645-47f5-9128-6b3a51eca540", + "name": "Default Suite", + "persistSession": false, + "parallel": false, + "timeout": 300, + "tests": ["629ba6a4-486d-4dbe-9ea1-4727a1e35567"] + }], + "urls": ["http://localhost:10101/"], + "plugins": [] +} \ No newline at end of file diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy index 46ff633cd..62fda2ed9 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy @@ -96,6 +96,9 @@ class DevConfig { }, new Role().with { name = 'ROLE_NONE' it + }, new Role().with { + name = 'ROLE_ENABLE' + it }] roles.each { roleRepository.save(it) @@ -207,4 +210,4 @@ class DevConfig { return it }) } -} +} \ No newline at end of file diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy index 7ffb5b5f7..03e041322 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy @@ -2,26 +2,19 @@ package edu.internet2.tier.shibboleth.admin.ui.service import com.google.common.base.Predicate import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityRoleWhiteListFilter -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilter -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilFilter -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.SignatureValidationFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.* import edu.internet2.tier.shibboleth.admin.ui.domain.filters.opensaml.OpenSamlNameIdFormatFilter -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FilesystemMetadataResolver -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataQueryProtocolScheme -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataRequestURLConstructionScheme -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.RegexScheme -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.TemplateScheme +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.* import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.Refilterable +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import edu.internet2.tier.shibboleth.admin.util.OpenSamlChainingMetadataResolverUtil import groovy.util.logging.Slf4j import groovy.xml.DOMBuilder import groovy.xml.MarkupBuilder @@ -34,22 +27,25 @@ import org.opensaml.saml.metadata.resolver.filter.impl.NameIDFormatFilter import org.opensaml.saml.saml2.core.Attribute import org.opensaml.saml.saml2.metadata.EntityDescriptor import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service import org.w3c.dom.Document import javax.annotation.Nonnull -import static edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilterTarget.NameIdFormatFilterTargetType.ENTITY -import static edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilterTarget.NameIdFormatFilterTargetType.CONDITION_SCRIPT -import static edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilterTarget.NameIdFormatFilterTargetType.REGEX +import static edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilterTarget.NameIdFormatFilterTargetType.* import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver.ResourceType.CLASSPATH import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver.ResourceType.SVN @Slf4j +@Service class JPAMetadataResolverServiceImpl implements MetadataResolverService { @Autowired private MetadataResolver metadataResolver + @Autowired + MetadataResolverConverterService metadataResolverConverterService + @Autowired private MetadataResolverRepository metadataResolverRepository @@ -62,154 +58,8 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { @Autowired private ShibUIConfiguration shibUIConfiguration - // TODO: enhance - @Override - void reloadFilters(String metadataResolverResourceId) { - OpenSamlChainingMetadataResolver chainingMetadataResolver = (OpenSamlChainingMetadataResolver) metadataResolver - MetadataResolver targetMetadataResolver = chainingMetadataResolver.getResolvers().find { - it.id == metadataResolverResourceId - } - edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver jpaMetadataResolver = metadataResolverRepository.findByResourceId(metadataResolverResourceId) - - if (targetMetadataResolver && targetMetadataResolver.getMetadataFilter() instanceof MetadataFilterChain) { - MetadataFilterChain metadataFilterChain = (MetadataFilterChain) targetMetadataResolver.getMetadataFilter() - - List metadataFilters = new ArrayList<>() - - // set up namespace protection - if (shibUIConfiguration.protectedAttributeNamespaces && shibUIConfiguration.protectedAttributeNamespaces.size() > 0 && targetMetadataResolver && jpaMetadataResolver.type in ['FileBackedHttpMetadataResolver', 'DynamicHttpMetadataResolver']) { - def target = new org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter() - target.attributeFilter = new ScriptedPredicate(new EvaluableScript(protectedNamespaceScript())) - metadataFilters.add(target) - } - - for (edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter metadataFilter : jpaMetadataResolver.getMetadataFilters()) { - if (metadataFilter instanceof EntityAttributesFilter) { - EntityAttributesFilter entityAttributesFilter = (EntityAttributesFilter) metadataFilter - - org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter target = new org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter() - Map, Collection> rules = new HashMap<>() - switch (entityAttributesFilter.getEntityAttributesFilterTarget().getEntityAttributesFilterTargetType()) { - case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY: - rules.put( - new EntityIdPredicate(entityAttributesFilter.getEntityAttributesFilterTarget().getValue()), - (List) (List) entityAttributesFilter.getAttributes() - ) - break - case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.CONDITION_SCRIPT: - rules.put(new ScriptedPredicate(new EvaluableScript(entityAttributesFilter.entityAttributesFilterTarget.value[0])), - (List) (List) entityAttributesFilter.getAttributes()) - break - case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.REGEX: - rules.put(new ScriptedPredicate(new EvaluableScript(generateJavaScriptRegexScript(entityAttributesFilter.entityAttributesFilterTarget.value[0]))), - (List) (List) entityAttributesFilter.getAttributes()) - break - default: - // do nothing, we'd have exploded elsewhere previously. - break - } - target.setRules(rules) - metadataFilters.add(target) - } - if (metadataFilter instanceof NameIdFormatFilter) { - NameIdFormatFilter nameIdFormatFilter = NameIdFormatFilter.cast(metadataFilter) - NameIDFormatFilter openSamlTargetFilter = new OpenSamlNameIdFormatFilter() - openSamlTargetFilter.removeExistingFormats = nameIdFormatFilter.removeExistingFormats - Map, Collection> predicateRules = [:] - def type = nameIdFormatFilter.nameIdFormatFilterTarget.nameIdFormatFilterTargetType - def values = nameIdFormatFilter.nameIdFormatFilterTarget.value - switch (type) { - case ENTITY: - predicateRules[new EntityIdPredicate(values)] = nameIdFormatFilter.formats - break - case CONDITION_SCRIPT: - predicateRules[new ScriptedPredicate(new EvaluableScript(values[0]))] = nameIdFormatFilter.formats - break - case REGEX: - predicateRules[new ScriptedPredicate(new EvaluableScript(generateJavaScriptRegexScript(values[0])))] = nameIdFormatFilter.formats - break - default: - // do nothing, we'd have exploded elsewhere previously. - break - } - openSamlTargetFilter.rules = predicateRules - metadataFilters << openSamlTargetFilter - } - } - metadataFilterChain.setFilters(metadataFilters) - } - - if (targetMetadataResolver != null && targetMetadataResolver instanceof Refilterable) { - (Refilterable) targetMetadataResolver.refilter() - } else { - //TODO: Do something here if we need to refilter other non-Batch resolvers - log.warn("Target resolver is not a Refilterable resolver. Skipping refilter()") - } - } - - private String protectedNamespaceScript() { - return """(function (attribute) { - "use strict"; - var namespaces = [${shibUIConfiguration.protectedAttributeNamespaces.collect({"\"${it}\""}).join(', ')}]; - // check the parameter - if (attribute === null) { return true; } - for (var i in namespaces) { - if (attribute.getName().startsWith(namespaces[i])) { - return false; - } - } - return true; - }(input));""" - } - - private class ScriptedPredicate extends net.shibboleth.utilities.java.support.logic.ScriptedPredicate { - protected ScriptedPredicate(@Nonnull EvaluableScript theScript) { - super(theScript) - } - } - - // TODO: enhance - @Override - Document generateConfiguration() { - // TODO: this can probably be a better writer - new StringWriter().withCloseable { writer -> - def xml = new MarkupBuilder(writer) - xml.omitEmptyAttributes = true - xml.omitNullAttributes = true - - xml.MetadataProvider(id: 'ShibbolethMetadata', - xmlns: 'urn:mace:shibboleth:2.0:metadata', - 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', - 'xsi:type': 'ChainingMetadataProvider', - 'xsi:schemaLocation': 'urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd urn:mace:shibboleth:2.0:resource http://shibboleth.net/schema/idp/shibboleth-resource.xsd urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd' - ) { - - - resolversPositionOrderContainerService.allMetadataResolversInDefinedOrderOrUnordered.each { - edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr -> - //TODO: We do not currently marshall the internal incommon chaining resolver (with BaseMetadataResolver type) - if ((mr.type != 'BaseMetadataResolver') && (mr.enabled)) { - constructXmlNodeForResolver(mr, delegate) { - //TODO: enhance - def didNamespaceProtectionFilter = !(shibUIConfiguration.protectedAttributeNamespaces && shibUIConfiguration.protectedAttributeNamespaces.size() > 0) - def doNamespaceProtectionFilter = { def filter -> - if (mr.type in ['FileBackedMetadataResolver', 'DynamicHttpMetadataResolver'] && (filter == null || filter instanceof EntityAttributesFilter) && !didNamespaceProtectionFilter) { - constructXmlNodeForEntityAttributeNamespaceProtection(delegate) - didNamespaceProtectionFilter = true - } - } - mr.metadataFilters.each { edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter filter -> - doNamespaceProtectionFilter() - constructXmlNodeForFilter(filter, delegate) - } - doNamespaceProtectionFilter() - } - } - } - } - return DOMBuilder.newInstance().parseText(writer.toString()) - } - } + @Autowired + private UserService userService void constructXmlNodeForEntityAttributeNamespaceProtection(def markupBuilderDelegate) { markupBuilderDelegate.MetadataFilter('xsi:type': 'EntityAttributes') { @@ -221,21 +71,6 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } - void constructXmlNodeForFilter(SignatureValidationFilter filter, def markupBuilderDelegate) { - if (filter.xmlShouldBeGenerated()) { - markupBuilderDelegate.MetadataFilter(id: filter.name, - 'xsi:type': 'SignatureValidation', - 'xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata', - 'requireSignedRoot': !filter.requireSignedRoot ?: null, - 'certificateFile': filter.certificateFile, - 'defaultCriteriaRef': filter.defaultCriteriaRef, - 'signaturePrevalidatorRef': filter.signaturePrevalidatorRef, - 'dynamicTrustedNamesStrategyRef': filter.dynamicTrustedNamesStrategyRef, - 'trustEngineRef': filter.trustEngineRef, - 'publicKey': filter.publicKey) - } - } - void constructXmlNodeForFilter(EntityAttributesFilter filter, def markupBuilderDelegate) { markupBuilderDelegate.MetadataFilter('xsi:type': 'EntityAttributes') { // TODO: enhance. currently this does weird things with namespaces @@ -274,12 +109,6 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } - private String generateJavaScriptRegexScript(String regex) { - return """ - "use strict"; - ${regex.startsWith('/') ? '' : '/'}${regex}${regex.endsWith('/') ? '' : '/'}.test(input.getEntityID());\n""" - } - void constructXmlNodeForFilter(EntityRoleWhiteListFilter filter, def markupBuilderDelegate) { if (!filter.retainedRoles?.isEmpty()) { markupBuilderDelegate.MetadataFilter( @@ -294,14 +123,6 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } - void constructXmlNodeForFilter(RequiredValidUntilFilter filter, def markupBuilderDelegate) { - if (filter.xmlShouldBeGenerated()) { - markupBuilderDelegate.MetadataFilter( - 'xsi:type': 'RequiredValidUntil', - maxValidityInterval: filter.maxValidityInterval - ) - } - } void constructXmlNodeForFilter(NameIdFormatFilter filter, def markupBuilderDelegate) { def type = filter.nameIdFormatFilterTarget.nameIdFormatFilterTargetType @@ -341,27 +162,27 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } - void constructXmlNodeForResolver(FilesystemMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { - markupBuilderDelegate.MetadataProvider(id: resolver.xmlId, - 'xsi:type': 'FilesystemMetadataProvider', - metadataFile: resolver.metadataFile, - - requireValidMetadata: resolver.requireValidMetadata, - failFastInitialization: resolver.failFastInitialization, - sortKey: resolver.sortKey, - criterionPredicateRegistryRef: resolver.criterionPredicateRegistryRef, - useDefaultPredicateRegistry: resolver.useDefaultPredicateRegistry, - satisfyAnyPredicates: resolver.satisfyAnyPredicates, - - parserPoolRef: resolver.reloadableMetadataResolverAttributes?.parserPoolRef, - minRefreshDelay: resolver.reloadableMetadataResolverAttributes?.minRefreshDelay, - maxRefreshDelay: resolver.reloadableMetadataResolverAttributes?.maxRefreshDelay, - refreshDelayFactor: resolver.reloadableMetadataResolverAttributes?.refreshDelayFactor, - indexesRef: resolver.reloadableMetadataResolverAttributes?.indexesRef, - resolveViaPredicatesOnly: resolver.reloadableMetadataResolverAttributes?.resolveViaPredicatesOnly ?: null, - expirationWarningThreshold: resolver.reloadableMetadataResolverAttributes?.expirationWarningThreshold) { + void constructXmlNodeForFilter(RequiredValidUntilFilter filter, def markupBuilderDelegate) { + if (filter.xmlShouldBeGenerated()) { + markupBuilderDelegate.MetadataFilter( + 'xsi:type': 'RequiredValidUntil', + maxValidityInterval: filter.maxValidityInterval + ) + } + } - childNodes() + void constructXmlNodeForFilter(SignatureValidationFilter filter, def markupBuilderDelegate) { + if (filter.xmlShouldBeGenerated()) { + markupBuilderDelegate.MetadataFilter(id: filter.name, + 'xsi:type': 'SignatureValidation', + 'xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata', + 'requireSignedRoot': !filter.requireSignedRoot ?: null, + 'certificateFile': filter.certificateFile, + 'defaultCriteriaRef': filter.defaultCriteriaRef, + 'signaturePrevalidatorRef': filter.signaturePrevalidatorRef, + 'dynamicTrustedNamesStrategyRef': filter.dynamicTrustedNamesStrategyRef, + 'trustEngineRef': filter.trustEngineRef, + 'publicKey': filter.publicKey) } } @@ -483,6 +304,30 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } + void constructXmlNodeForResolver(FilesystemMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { + markupBuilderDelegate.MetadataProvider(id: resolver.xmlId, + 'xsi:type': 'FilesystemMetadataProvider', + metadataFile: resolver.metadataFile, + + requireValidMetadata: resolver.requireValidMetadata, + failFastInitialization: resolver.failFastInitialization, + sortKey: resolver.sortKey, + criterionPredicateRegistryRef: resolver.criterionPredicateRegistryRef, + useDefaultPredicateRegistry: resolver.useDefaultPredicateRegistry, + satisfyAnyPredicates: resolver.satisfyAnyPredicates, + + parserPoolRef: resolver.reloadableMetadataResolverAttributes?.parserPoolRef, + minRefreshDelay: resolver.reloadableMetadataResolverAttributes?.minRefreshDelay, + maxRefreshDelay: resolver.reloadableMetadataResolverAttributes?.maxRefreshDelay, + refreshDelayFactor: resolver.reloadableMetadataResolverAttributes?.refreshDelayFactor, + indexesRef: resolver.reloadableMetadataResolverAttributes?.indexesRef, + resolveViaPredicatesOnly: resolver.reloadableMetadataResolverAttributes?.resolveViaPredicatesOnly ?: null, + expirationWarningThreshold: resolver.reloadableMetadataResolverAttributes?.expirationWarningThreshold) { + + childNodes() + } + } + void constructXmlNodeForResolver(LocalDynamicMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { markupBuilderDelegate.MetadataProvider(sourceDirectory: resolver.sourceDirectory, sourceManagerRef: resolver.sourceManagerRef, @@ -556,7 +401,191 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { childNodes() } + } + + public edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver findByResourceId(String resourceId) throws EntityNotFoundException { + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver result = metadataResolverRepository.findByResourceId(resourceId) + if (result == null ) { + throw new EntityNotFoundException("No Provider with resourceId[" + resourceId + "] was found") + } + return result + } + + @Override + Document generateConfiguration() { + // TODO: this can probably be a better writer + new StringWriter().withCloseable { writer -> + def xml = new MarkupBuilder(writer) + xml.omitEmptyAttributes = true + xml.omitNullAttributes = true + + xml.MetadataProvider(id: 'ShibbolethMetadata', + xmlns: 'urn:mace:shibboleth:2.0:metadata', + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:type': 'ChainingMetadataProvider', + 'xsi:schemaLocation': 'urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd urn:mace:shibboleth:2.0:resource http://shibboleth.net/schema/idp/shibboleth-resource.xsd urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd' + ) { + + + resolversPositionOrderContainerService.allMetadataResolversInDefinedOrderOrUnordered.each { + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr -> + //TODO: We do not currently marshall the internal incommon chaining resolver (with BaseMetadataResolver type) + if ((mr.type != 'BaseMetadataResolver') && (mr.enabled)) { + constructXmlNodeForResolver(mr, delegate) { + //TODO: enhance + def didNamespaceProtectionFilter = !(shibUIConfiguration.protectedAttributeNamespaces && shibUIConfiguration.protectedAttributeNamespaces.size() > 0) + def doNamespaceProtectionFilter = { def filter -> + if (mr.type in ['FileBackedMetadataResolver', 'DynamicHttpMetadataResolver'] && (filter == null || filter instanceof EntityAttributesFilter) && !didNamespaceProtectionFilter) { + constructXmlNodeForEntityAttributeNamespaceProtection(delegate) + didNamespaceProtectionFilter = true + } + } + mr.metadataFilters.each { edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter filter -> + doNamespaceProtectionFilter() + constructXmlNodeForFilter(filter, delegate) + } + doNamespaceProtectionFilter() + } + } + } + } + return DOMBuilder.newInstance().parseText(writer.toString()) + } + } + + private String generateJavaScriptRegexScript(String regex) { + return """ + "use strict"; + ${regex.startsWith('/') ? '' : '/'}${regex}${regex.endsWith('/') ? '' : '/'}.test(input.getEntityID());\n""" + } + + private String protectedNamespaceScript() { + return """(function (attribute) { + "use strict"; + var namespaces = [${shibUIConfiguration.protectedAttributeNamespaces.collect({ "\"${it}\"" }).join(', ')}]; + // check the parameter + if (attribute === null) { return true; } + for (var i in namespaces) { + if (attribute.getName().startsWith(namespaces[i])) { + return false; + } + } + return true; + }(input));""" + } + + @Override + void reloadFilters(String metadataResolverResourceId) { + OpenSamlChainingMetadataResolver chainingMetadataResolver = (OpenSamlChainingMetadataResolver) metadataResolver + MetadataResolver targetMetadataResolver = chainingMetadataResolver.getResolvers().find { + it.id == metadataResolverResourceId + } + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver jpaMetadataResolver = metadataResolverRepository.findByResourceId(metadataResolverResourceId) + + if (targetMetadataResolver && targetMetadataResolver.getMetadataFilter() instanceof MetadataFilterChain) { + MetadataFilterChain metadataFilterChain = (MetadataFilterChain) targetMetadataResolver.getMetadataFilter() + + List metadataFilters = new ArrayList<>() + // set up namespace protection + if (shibUIConfiguration.protectedAttributeNamespaces && shibUIConfiguration.protectedAttributeNamespaces.size() > 0 && targetMetadataResolver && jpaMetadataResolver.type in ['FileBackedHttpMetadataResolver', 'DynamicHttpMetadataResolver']) { + def target = new org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter() + target.attributeFilter = new ScriptedPredicate(new EvaluableScript(protectedNamespaceScript())) + metadataFilters.add(target) + } + + for (edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter metadataFilter : jpaMetadataResolver.getMetadataFilters()) { + if (metadataFilter instanceof EntityAttributesFilter) { + EntityAttributesFilter entityAttributesFilter = (EntityAttributesFilter) metadataFilter + + org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter target = new org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter() + Map, Collection> rules = new HashMap<>() + switch (entityAttributesFilter.getEntityAttributesFilterTarget().getEntityAttributesFilterTargetType()) { + case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY: + rules.put( + new EntityIdPredicate(entityAttributesFilter.getEntityAttributesFilterTarget().getValue()), + (List) (List) entityAttributesFilter.getAttributes() + ) + break + case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.CONDITION_SCRIPT: + rules.put(new ScriptedPredicate(new EvaluableScript(entityAttributesFilter.entityAttributesFilterTarget.value[0])), + (List) (List) entityAttributesFilter.getAttributes()) + break + case EntityAttributesFilterTarget.EntityAttributesFilterTargetType.REGEX: + rules.put(new ScriptedPredicate(new EvaluableScript(generateJavaScriptRegexScript(entityAttributesFilter.entityAttributesFilterTarget.value[0]))), + (List) (List) entityAttributesFilter.getAttributes()) + break + default: + // do nothing, we'd have exploded elsewhere previously. + break + } + target.setRules(rules) + metadataFilters.add(target) + } + if (metadataFilter instanceof NameIdFormatFilter) { + NameIdFormatFilter nameIdFormatFilter = NameIdFormatFilter.cast(metadataFilter) + NameIDFormatFilter openSamlTargetFilter = new OpenSamlNameIdFormatFilter() + openSamlTargetFilter.removeExistingFormats = nameIdFormatFilter.removeExistingFormats + Map, Collection> predicateRules = [:] + def type = nameIdFormatFilter.nameIdFormatFilterTarget.nameIdFormatFilterTargetType + def values = nameIdFormatFilter.nameIdFormatFilterTarget.value + switch (type) { + case ENTITY: + predicateRules[new EntityIdPredicate(values)] = nameIdFormatFilter.formats + break + case CONDITION_SCRIPT: + predicateRules[new ScriptedPredicate(new EvaluableScript(values[0]))] = nameIdFormatFilter.formats + break + case REGEX: + predicateRules[new ScriptedPredicate(new EvaluableScript(generateJavaScriptRegexScript(values[0])))] = nameIdFormatFilter.formats + break + default: + // do nothing, we'd have exploded elsewhere previously. + break + } + openSamlTargetFilter.rules = predicateRules + metadataFilters << openSamlTargetFilter + } + } + metadataFilterChain.setFilters(metadataFilters) + } + + if (targetMetadataResolver != null && targetMetadataResolver instanceof Refilterable) { + (Refilterable) targetMetadataResolver.refilter() + } else { + //TODO: Do something here if we need to refilter other non-Batch resolvers + log.warn("Target resolver is not a Refilterable resolver. Skipping refilter()") + } + } + + public edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver updateMetadataResolverEnabledStatus(edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver updatedResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException { + if (!userService.currentUserCanEnable(updatedResolver)) { + throw new ForbiddenException("You do not have the permissions necessary to change the enable status of this filter.") + } + + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver persistedResolver = metadataResolverRepository.save(updatedResolver) + + if (persistedResolver.getDoInitialization()) { + MetadataResolver openSamlRepresentation = null + try { + openSamlRepresentation = metadataResolverConverterService.convertToOpenSamlRepresentation(persistedResolver) + } catch (FileNotFoundException e) { + throw new MetadataFileNotFoundException("message.file-doesnt-exist") + } + try { + OpenSamlChainingMetadataResolverUtil.updateChainingMetadataResolver((OpenSamlChainingMetadataResolver) chainingMetadataResolver, openSamlRepresentation); + } + catch (Throwable e) { + throw new InitializationException(e); + } + } + + } + + private class ScriptedPredicate extends net.shibboleth.utilities.java.support.logic.ScriptedPredicate { + protected ScriptedPredicate(@Nonnull EvaluableScript theScript) { + super(theScript) + } } -} +} \ No newline at end of file diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy index e007c62c3..16a4ed443 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrap.groovy @@ -59,4 +59,4 @@ class UserBootstrap { } } } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java index c117b415f..c897a31b5 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java @@ -1,27 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration; -import javax.servlet.http.HttpServletRequest; - -import org.apache.lucene.analysis.Analyzer; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.support.ResourceBundleMessageSource; -import org.springframework.core.io.Resource; -import org.springframework.web.servlet.LocaleResolver; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; -import org.springframework.web.util.UrlPathHelper; - import com.fasterxml.jackson.databind.Module; - import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; @@ -36,30 +15,35 @@ import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; -import edu.internet2.tier.shibboleth.admin.ui.service.DefaultMetadataResolversPositionOrderContainerService; -import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryService; -import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryServiceImpl; -import edu.internet2.tier.shibboleth.admin.ui.service.EntityIdsSearchService; -import edu.internet2.tier.shibboleth.admin.ui.service.EntityIdsSearchServiceImpl; -import edu.internet2.tier.shibboleth.admin.ui.service.EntityService; -import edu.internet2.tier.shibboleth.admin.ui.service.FileCheckingFileWritingService; -import edu.internet2.tier.shibboleth.admin.ui.service.FileWritingService; -import edu.internet2.tier.shibboleth.admin.ui.service.FilterService; -import edu.internet2.tier.shibboleth.admin.ui.service.FilterTargetService; -import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl; -import edu.internet2.tier.shibboleth.admin.ui.service.JPAFilterServiceImpl; -import edu.internet2.tier.shibboleth.admin.ui.service.JPAFilterTargetServiceImpl; -import edu.internet2.tier.shibboleth.admin.ui.service.JPAMetadataResolverServiceImpl; -import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; -import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolversPositionOrderContainerService; +import edu.internet2.tier.shibboleth.admin.ui.service.*; import edu.internet2.tier.shibboleth.admin.util.AttributeUtility; import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils; import edu.internet2.tier.shibboleth.admin.util.LuceneUtility; import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; +import org.apache.lucene.analysis.Analyzer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.core.io.Resource; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; +import org.springframework.web.util.UrlPathHelper; + +import javax.servlet.http.HttpServletRequest; @Configuration -@ComponentScan(basePackages="{ edu.internet2.tier.shibboleth.admin.ui.service }") -@EnableConfigurationProperties({ CustomPropertiesConfiguration.class, ShibUIConfiguration.class }) +@Import(SearchConfiguration.class) +@ComponentScan(basePackages = "{ edu.internet2.tier.shibboleth.admin.ui.service }") +@EnableConfigurationProperties({CustomPropertiesConfiguration.class, ShibUIConfiguration.class}) public class CoreShibUiConfiguration { @Bean public OpenSamlObjects openSamlObjects() { @@ -70,22 +54,12 @@ public OpenSamlObjects openSamlObjects() { public EntityService jpaEntityService() { return new JPAEntityServiceImpl(openSamlObjects()); } - - @Bean - public FilterService jpaFilterService() { - return new JPAFilterServiceImpl(); - } @Bean public FilterTargetService jpaFilterTargetService() { return new JPAFilterTargetServiceImpl(); } - @Bean - public MetadataResolverService metadataResolverService() { - return new JPAMetadataResolverServiceImpl(); - } - @Bean public AttributeUtility attributeUtility() { return new AttributeUtility(openSamlObjects()); @@ -221,18 +195,18 @@ public EntityDescriptorConversionUtils EntityDescriptorConverstionUtilsInit(Enti EntityDescriptorConversionUtils.setOpenSamlObjects(oso); return new EntityDescriptorConversionUtils(); } - + @Bean public GroupUpdatedEntityListener groupUpdatedEntityListener(OwnershipRepository repo) { GroupUpdatedEntityListener listener = new GroupUpdatedEntityListener(); listener.init(repo); return listener; } - + @Bean public UserUpdatedEntityListener userUpdatedEntityListener(OwnershipRepository repo, GroupsRepository groupRepo) { UserUpdatedEntityListener listener = new UserUpdatedEntityListener(); listener.init(repo, groupRepo); return listener; } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConverterConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConverterConfiguration.java deleted file mode 100644 index 6380e0018..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConverterConfiguration.java +++ /dev/null @@ -1,17 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.configuration; - -import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverConverterService; -import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverConverterServiceImpl; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * @author Bill Smith (wsmith@unicon.net) - */ -@Configuration -public class MetadataResolverConverterConfiguration { - @Bean - public MetadataResolverConverterService metadataResolverConverterService() { - return new MetadataResolverConverterServiceImpl(); - } -} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/MigrationTasksContextLoadedListener.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/MigrationTasksContextLoadedListener.java index f14d35763..11ea80113 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/MigrationTasksContextLoadedListener.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/MigrationTasksContextLoadedListener.java @@ -40,7 +40,7 @@ public void onApplicationEvent(ContextRefreshedEvent event) { } @Transactional - private void doshibui_1740_migration() { + void doshibui_1740_migration() { groupService.ensureAdminGroupExists(); // do first // SHIBUI-1740: Adding admin group to all existing entity descriptors that do not have a group already. @@ -64,4 +64,4 @@ private void doshibui_1740_migration() { }); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java index 3d66de957..7e19425e7 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java @@ -9,7 +9,6 @@ import edu.internet2.tier.shibboleth.admin.ui.security.springsecurity.AdminUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -38,6 +37,9 @@ @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) public class WebSecurityConfig { + @Value("${shibui.roles.authenticated}") + private String[] acceptedAuthenticationRoles; + @Value("${shibui.logout-url:/dashboard}") private String logoutUrl; @@ -76,7 +78,7 @@ protected void configure(HttpSecurity http) throws Exception { .and() .authorizeRequests() .antMatchers("/unsecured/**/*").permitAll() - .anyRequest().hasAnyRole("USER", "ADMIN") + .anyRequest().hasAnyRole(acceptedAuthenticationRoles) .and() .exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> response.sendRedirect("/unsecured/error.html")) .and() @@ -157,5 +159,4 @@ public void configure(WebSecurity web) throws Exception { } }; } -} - +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java new file mode 100644 index 000000000..60705cd15 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java @@ -0,0 +1,65 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import javax.script.ScriptException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; +import edu.internet2.tier.shibboleth.admin.ui.service.FilterService; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; + +@RestController +@RequestMapping("/api/activate") +public class ActivateController { + + @Autowired + private EntityDescriptorService entityDescriptorService; + + @Autowired + private FilterService filterService; + + @Autowired + private MetadataResolverService metadataResolverService; + + @PatchMapping(path = "/entityDescriptor/{resourceId}/{mode}") + @Transactional + public ResponseEntity enableEntityDescriptor(@PathVariable String resourceId, @PathVariable String mode) throws EntityNotFoundException, ForbiddenException { + boolean status = "enable".equalsIgnoreCase(mode); + EntityDescriptorRepresentation edr = entityDescriptorService.updateEntityDescriptorEnabledStatus(resourceId, status); + return ResponseEntity.ok(edr); + } + + @PatchMapping(path = "/MetadataResolvers/{metadataResolverId}/Filter/{resourceId}/{mode}") + @Transactional + public ResponseEntity enableFilter(@PathVariable String metadataResolverId, @PathVariable String resourceId, @PathVariable String mode) throws EntityNotFoundException, ForbiddenException, ScriptException { + boolean status = "enable".equalsIgnoreCase(mode); + MetadataFilter persistedFilter = filterService.updateFilterEnabledStatus(metadataResolverId, resourceId, status); + return ResponseEntity.ok(persistedFilter); + } + + @PatchMapping("/MetadataResolvers/{resourceId}/{mode}") + @Transactional + public ResponseEntity enableProvider(@PathVariable String resourceId, @PathVariable String mode) throws EntityNotFoundException, ForbiddenException, MetadataFileNotFoundException, InitializationException { + boolean status = "enable".equalsIgnoreCase(mode); + MetadataResolver existingResolver = metadataResolverService.findByResourceId(resourceId); + existingResolver.setEnabled(status); + existingResolver = metadataResolverService.updateMetadataResolverEnabledStatus(existingResolver); + + return ResponseEntity.ok(existingResolver); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java new file mode 100644 index 000000000..0c766c53c --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java @@ -0,0 +1,48 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + +import javax.script.ScriptException; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException; + +@ControllerAdvice(assignableTypes = {ActivateController.class}) +public class ActivateExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler({ EntityNotFoundException.class }) + public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(HttpStatus.NOT_FOUND, e.getMessage())); + } + + @ExceptionHandler({ ForbiddenException.class }) + public ResponseEntity handleForbiddenAccess(ForbiddenException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(String.valueOf(HttpStatus.FORBIDDEN.value()), e.getMessage())); + } + + @ExceptionHandler({ InitializationException.class }) + public ResponseEntity handleInitializationException(InitializationException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), e.getMessage())); + } + + @ExceptionHandler({ MetadataFileNotFoundException.class }) + public ResponseEntity handleMetadataFileNotFoundException(MetadataFileNotFoundException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse(INTERNAL_SERVER_ERROR.toString(), e.getLocalizedMessage())); + } + + @ExceptionHandler({ ScriptException.class }) + public ResponseEntity handleScriptException(ScriptException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), e.getMessage())); + } + + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ActivatableType.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ActivatableType.java new file mode 100644 index 000000000..8042d56d4 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ActivatableType.java @@ -0,0 +1,5 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +public enum ActivatableType { + ENTITY_DESCRIPTOR, METADATA_RESOLVER, FILTER +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java index a3acff06b..185b43918 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java @@ -33,11 +33,12 @@ import java.util.UUID; import java.util.stream.Collectors; +import static edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType.ENTITY_DESCRIPTOR; @Entity @EqualsAndHashCode(callSuper = true) @Audited -public class EntityDescriptor extends AbstractDescriptor implements org.opensaml.saml.saml2.metadata.EntityDescriptor, Ownable { +public class EntityDescriptor extends AbstractDescriptor implements org.opensaml.saml.saml2.metadata.EntityDescriptor, Ownable, IActivatable { @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "entitydesc_addlmetdatlocations_id") @OrderColumn @@ -47,7 +48,7 @@ public class EntityDescriptor extends AbstractDescriptor implements org.opensaml @OneToOne(cascade = CascadeType.ALL) @NotAudited private AffiliationDescriptor affiliationDescriptor; - + @OneToOne(cascade = CascadeType.ALL) @NotAudited private AttributeAuthorityDescriptor attributeAuthorityDescriptor; @@ -61,7 +62,7 @@ public class EntityDescriptor extends AbstractDescriptor implements org.opensaml private List contactPersons = new ArrayList<>(); private String entityID; - + private String localId; @OneToOne(cascade = CascadeType.ALL) @@ -70,7 +71,7 @@ public class EntityDescriptor extends AbstractDescriptor implements org.opensaml @Getter @Setter private String idOfOwner; - + @OneToOne(cascade = CascadeType.ALL) @NotAudited private PDPDescriptor pdpDescriptor; @@ -254,6 +255,10 @@ public void setEntityID(String entityID) { this.entityID = entityID; } + public void setEnabled(Boolean serviceEnabled) { + this.serviceEnabled = (serviceEnabled == null) ? false : serviceEnabled; + } + @Override public void setID(String id) { this.localId = id; @@ -296,12 +301,16 @@ public String toString() { .add("id", id) .toString(); } - + public String getObjectId() { return entityID; } - + public OwnableType getOwnableType() { return OwnableType.ENTITY_DESCRIPTOR; } -} + + @Override public ActivatableType getActivatableType() { + return ENTITY_DESCRIPTOR; + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IActivatable.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IActivatable.java new file mode 100644 index 000000000..84a31078a --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IActivatable.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +public interface IActivatable { + ActivatableType getActivatableType(); + + void setEnabled(Boolean enabled); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java index 9363f5711..548c97356 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java @@ -1,10 +1,9 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.filters; -import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.*; import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; +import edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType; +import edu.internet2.tier.shibboleth.admin.ui.domain.IActivatable; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -20,6 +19,8 @@ import javax.persistence.Transient; import java.util.UUID; +import static edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType.*; + /** * Domain class to store information about {@link org.opensaml.saml.metadata.resolver.filter.MetadataFilter} */ @@ -38,22 +39,27 @@ @JsonSubTypes.Type(value=NameIdFormatFilter.class, name="NameIDFormat")}) @Audited @AuditOverride(forClass = AbstractAuditable.class) -public abstract class MetadataFilter extends AbstractAuditable implements IConcreteMetadataFilterType { +public abstract class MetadataFilter extends AbstractAuditable implements IConcreteMetadataFilterType, IActivatable { - @JsonProperty("@type") - @Transient - String type; + private boolean filterEnabled; private String name; @Column(unique=true) private String resourceId = UUID.randomUUID().toString(); - private boolean filterEnabled; + @JsonProperty("@type") + @Transient + String type; @Transient private transient Integer version; + @JsonIgnore + public ActivatableType getActivatableType() { + return FILTER; + } + @JsonGetter("version") public int getVersion() { if (version != null && version != 0) { @@ -61,4 +67,8 @@ public int getVersion() { } return this.hashCode(); } -} + + public void setEnabled(Boolean serviceEnabled) { + this.filterEnabled = (serviceEnabled == null) ? false : serviceEnabled; + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java index 13d573b56..995fab868 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java @@ -6,6 +6,8 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; +import edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType; +import edu.internet2.tier.shibboleth.admin.ui.domain.IActivatable; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; import lombok.EqualsAndHashCode; @@ -28,6 +30,8 @@ import java.util.List; import java.util.UUID; +import static edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType.*; + @Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) @EqualsAndHashCode(callSuper = true, exclude = {"version", "versionModifiedTimestamp"}) @@ -43,7 +47,7 @@ @JsonSubTypes.Type(value = ResourceBackedMetadataResolver.class, name = "ResourceBackedMetadataResolver")}) @Audited @AuditOverride(forClass = AbstractAuditable.class) -public class MetadataResolver extends AbstractAuditable { +public class MetadataResolver extends AbstractAuditable implements IActivatable { @JsonProperty("@type") @Transient @@ -84,31 +88,37 @@ public class MetadataResolver extends AbstractAuditable { @Transient private Integer version; - @JsonGetter("version") - public int getVersion() { - if (this.version != null && this.version != 0 ) { - return this.version; - } - return this.hashCode(); - } - public void addFilter(MetadataFilter metadataFilter) { //To make sure that Spring Data auditing infrastructure recognizes update and "touched" modifiedDate markAsModified(); this.metadataFilters.add(metadataFilter); } - public void markAsModified() { - this.versionModifiedTimestamp = System.currentTimeMillis(); - } - public void entityAttributesFilterIntoTransientRepresentation() { //expose explicit API to call to convert into transient representation //used in unit/integration tests where JPA's @PostLoad callback execution engine is not available this.metadataFilters - .stream() - .filter(EntityAttributesFilter.class::isInstance) - .map(EntityAttributesFilter.class::cast) - .forEach(EntityAttributesFilter::intoTransientRepresentation); + .stream() + .filter(EntityAttributesFilter.class::isInstance) + .map(EntityAttributesFilter.class::cast) + .forEach(EntityAttributesFilter::intoTransientRepresentation); + } + + @Override + @JsonIgnore + public ActivatableType getActivatableType() { + return METADATA_RESOLVER; + } + + @JsonGetter("version") + public int getVersion() { + if (this.version != null && this.version != 0 ) { + return this.version; + } + return this.hashCode(); + } + + public void markAsModified() { + this.versionModifiedTimestamp = System.currentTimeMillis(); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java index 6769aaa5f..4d0009523 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java @@ -4,4 +4,4 @@ public class EntityNotFoundException extends Exception { public EntityNotFoundException(String message) { super(message); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InitializationException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InitializationException.java new file mode 100644 index 000000000..f18483176 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InitializationException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class InitializationException extends Exception { + public InitializationException(Exception e) { + super(e); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java new file mode 100644 index 000000000..d576e0630 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java @@ -0,0 +1,74 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.controller; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IRolesService; + +@RestController +@RequestMapping("/api/admin/roles") +public class RolesController { + @Autowired + private IRolesService rolesService; + + @Secured("ROLE_ADMIN") + @PostMapping + @Transactional + public ResponseEntity create(@RequestBody Role role) throws RoleExistsConflictException { + Role result = rolesService.createRole(role); + return ResponseEntity.status(HttpStatus.CREATED).body(result); + } + + @Secured("ROLE_ADMIN") + @DeleteMapping("/{resourceId}") + @Transactional + public ResponseEntity delete(@PathVariable String resourceId) throws EntityNotFoundException, RoleDeleteException { + rolesService.deleteDefinition(resourceId); + return ResponseEntity.noContent().build(); + } + + @GetMapping + @Transactional(readOnly = true) + public ResponseEntity getAll() { + return ResponseEntity.ok(rolesService.findAll()); + } + + @GetMapping("/{resourceId}") + @Transactional(readOnly = true) + public ResponseEntity getOne(@PathVariable String resourceId) throws EntityNotFoundException { + Role role = rolesService.findByResourceId(resourceId); + return ResponseEntity.ok(role); + } + + @Secured("ROLE_ADMIN") + @PutMapping(path = {"/", "/{resourceId}" }) + @Transactional + public ResponseEntity update(@RequestBody Role incomingRoleDetail, @PathVariable Optional resourceId) throws EntityNotFoundException { + Role updateRole; + if (resourceId.isPresent()) { + updateRole = rolesService.findByResourceId(resourceId.get()); + } else { + updateRole = rolesService.findByResourceId(incomingRoleDetail.getResourceId()); + } + updateRole.setName(incomingRoleDetail.getName()); + Role result = rolesService.updateRole(updateRole); + return ResponseEntity.ok(result); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesExceptionHandler.java new file mode 100644 index 000000000..e4b840f1a --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesExceptionHandler.java @@ -0,0 +1,38 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.controller; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import edu.internet2.tier.shibboleth.admin.ui.controller.ErrorResponse; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; + +@ControllerAdvice(assignableTypes = {RolesController.class}) +public class RolesExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler({ EntityNotFoundException.class }) + public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(HttpStatus.NOT_FOUND, e.getMessage())); + } + + @ExceptionHandler({ RoleDeleteException.class }) + public ResponseEntity handleForbiddenAccess(RoleDeleteException e, WebRequest request) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/admin/role/{resourceId}").build().toUri()); + return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers) + .body(new ErrorResponse(String.valueOf(HttpStatus.CONFLICT.value()), e.getMessage())); + + } + + @ExceptionHandler({RoleExistsConflictException.class}) + public ResponseEntity handleRoleExistsConflictException(RoleExistsConflictException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponse(HttpStatus.CONFLICT, e.getMessage())); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleDeleteException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleDeleteException.java new file mode 100644 index 000000000..a7a35ab4f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleDeleteException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.exception; + +public class RoleDeleteException extends Exception { + public RoleDeleteException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleExistsConflictException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleExistsConflictException.java new file mode 100644 index 000000000..f5364c6ff --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleExistsConflictException.java @@ -0,0 +1,9 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.exception; + +public class RoleExistsConflictException extends Exception { + + public RoleExistsConflictException(String message) { + super(message); + } + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java index 8ccd02b1b..aeded7229 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java @@ -27,7 +27,7 @@ public class Group implements Owner { @JsonIgnore public static Group ADMIN_GROUP; - @Column(name = "group_description", nullable = true) + @Column(name = "group_description") String description; @Transient diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Ownable.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Ownable.java index c184b9679..557c92f58 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Ownable.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Ownable.java @@ -4,10 +4,10 @@ public interface Ownable { /** * @return representation of the id of the object. This is likely (but not limited to) the resource id of the object */ - public String getObjectId(); + String getObjectId(); /** * @return the OwnableType that describes the Ownable object */ - public OwnableType getOwnableType(); + OwnableType getOwnableType(); } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Owner.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Owner.java index ee7224335..4b9b28925 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Owner.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Owner.java @@ -4,10 +4,10 @@ public interface Owner { /** * @return representation of the id of the owner. This is likely (but not limited to) the resource id of the owner */ - public String getOwnerId(); + String getOwnerId(); /** * @return the type describing the owner */ - public OwnerType getOwnerType(); + OwnerType getOwnerType(); } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java index ea3048b32..ad9dd4844 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java @@ -1,6 +1,16 @@ package edu.internet2.tier.shibboleth.admin.ui.security.model; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ManyToMany; + import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -8,14 +18,6 @@ import lombok.Setter; import lombok.ToString; -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.ManyToMany; -import java.util.HashSet; -import java.util.Set; - /** * Models a basic administrative role concept in the system. * @@ -29,24 +31,27 @@ @ToString(exclude = "users") public class Role extends AbstractAuditable { - public Role(String name) { - this.name = name; - } - - public Role(String name, int rank) { - this.name = name; - this.rank = rank; - } - @Column(unique = true) private String name; @Column(name = "ROLE_RANK") private int rank; // 0=ADMIN, additional ranks are higher + @Column(name = "resource_id") + String resourceId = UUID.randomUUID().toString(); + //Ignore properties annotation here is to prevent stack overflow recursive error during JSON serialization @JsonIgnoreProperties("roles") @ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY) private Set users = new HashSet<>(); + public Role(String name) { + this.name = name; + } + + public Role(String name, int rank) { + this.name = name; + this.rank = rank; + } + } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/GroupUpdatedEntityListener.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/GroupUpdatedEntityListener.java index 319624fe9..515adface 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/GroupUpdatedEntityListener.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/GroupUpdatedEntityListener.java @@ -16,7 +16,7 @@ public class GroupUpdatedEntityListener implements ILazyLoaderHelper { private static OwnershipRepository ownershipRepository; /** - * @see https://stackoverflow.com/questions/12155632/injecting-a-spring-dependency-into-a-jpa-entitylistener + * see https://stackoverflow.com/questions/12155632/injecting-a-spring-dependency-into-a-jpa-entitylistener */ @Autowired public void init(OwnershipRepository repo) { 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 index f2b4df092..689306932 100644 --- 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 @@ -4,7 +4,7 @@ import edu.internet2.tier.shibboleth.admin.ui.security.model.User; public interface ILazyLoaderHelper { - default public void loadOwnedItems(Group g) { } + default void loadOwnedItems(Group g) { } - default public void loadGroups(User u) { } + default 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 index dc2291db9..2adbde700 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/UserUpdatedEntityListener.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/UserUpdatedEntityListener.java @@ -22,7 +22,7 @@ public class UserUpdatedEntityListener implements ILazyLoaderHelper { private static OwnershipRepository ownershipRepository; /** - * @see https://stackoverflow.com/questions/12155632/injecting-a-spring-dependency-into-a-jpa-entitylistener + * see https://stackoverflow.com/questions/12155632/injecting-a-spring-dependency-into-a-jpa-entitylistener */ @Autowired public void init(OwnershipRepository repo, GroupsRepository groupRepo) { @@ -44,9 +44,7 @@ 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())); - }); + 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 index 7c3c6e7b1..daf3ce265 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepository.java @@ -8,11 +8,6 @@ public interface GroupsRepository extends JpaRepository { void deleteByResourceId(String resourceId); - - List findAll(); - + Group findByResourceId(String id); - - @SuppressWarnings("unchecked") - Group save(Group group); -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java index 120f2938e..fb77d0e9d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java @@ -12,5 +12,9 @@ */ public interface RoleRepository extends JpaRepository { + void deleteByResourceId(String resourceId); + Optional findByName(final String name); + + Optional findByResourceId(String resourceId); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IRolesService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IRolesService.java new file mode 100644 index 000000000..26653d241 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IRolesService.java @@ -0,0 +1,22 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service; + +import java.util.List; + +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; + +public interface IRolesService { + + Role createRole(Role role) throws RoleExistsConflictException; + + Role updateRole(Role role) throws EntityNotFoundException; + + List findAll(); + + Role findByResourceId(String resourceId) throws EntityNotFoundException; + + void deleteDefinition(String resourceId) throws EntityNotFoundException, RoleDeleteException; + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/RolesServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/RolesServiceImpl.java new file mode 100644 index 000000000..cfbe57fb0 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/RolesServiceImpl.java @@ -0,0 +1,62 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service; + +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository; + +@Service +public class RolesServiceImpl implements IRolesService { + @Autowired + private RoleRepository roleRepository; + + @Override + public Role createRole(Role role) throws RoleExistsConflictException { + Optional found = roleRepository.findByName(role.getName()); + // If already defined, we don't want to create a new one, nor do we want this call update the definition + if (found.isPresent()) { + throw new RoleExistsConflictException( + String.format("Call update (PUT) to modify the role with name: [%s]", role.getName())); + } + return roleRepository.save(role); + } + + @Override + public void deleteDefinition(String resourceId) throws EntityNotFoundException, RoleDeleteException { + Optional found = roleRepository.findByResourceId(resourceId); + if (found.isPresent() && !found.get().getUsers().isEmpty()) { + throw new RoleDeleteException(String.format("Unable to delete role with resource id: [%s] - remove role from all users first", resourceId)); + } + roleRepository.deleteByResourceId(resourceId); + } + + @Override + public List findAll() { + return roleRepository.findAll(); + } + + @Override + public Role findByResourceId(String resourceId) throws EntityNotFoundException { + Optional found = roleRepository.findByResourceId(resourceId); + if (found.isEmpty()) { + throw new EntityNotFoundException(String.format("Unable to find role with resource id: [%s]", resourceId)); + } + return found.get(); + } + + @Override + public Role updateRole(Role role) throws EntityNotFoundException { + Optional found = roleRepository.findByName(role.getName()); + if (found.isEmpty()) { + throw new EntityNotFoundException(String.format("Unable to find role with name: [%s]", role.getName())); + } + return roleRepository.save(role); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java index cc579cb70..304b93028 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,15 +1,7 @@ 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.domain.EntityDescriptor; +import edu.internet2.tier.shibboleth.admin.ui.domain.IActivatable; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException; import edu.internet2.tier.shibboleth.admin.ui.security.exception.OwnershipConflictException; @@ -22,30 +14,67 @@ import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository; import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository; import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; +import static edu.internet2.tier.shibboleth.admin.ui.security.service.UserAccess.ADMIN; +import static edu.internet2.tier.shibboleth.admin.ui.security.service.UserAccess.GROUP; +import static edu.internet2.tier.shibboleth.admin.ui.security.service.UserAccess.NONE; import lombok.NoArgsConstructor; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; @Service @NoArgsConstructor public class UserService { @Autowired private IGroupService groupService; - + @Autowired private OwnershipRepository ownershipRepository; - + @Autowired private RoleRepository roleRepository; - + @Autowired private UserRepository userRepository; - + public UserService(IGroupService groupService, OwnershipRepository ownershipRepository, RoleRepository roleRepository, UserRepository userRepository) { this.groupService = groupService; this.ownershipRepository = ownershipRepository; this.roleRepository = roleRepository; this.userRepository = userRepository; } - + + public boolean currentUserCanEnable(IActivatable activatableObject) { + if (currentUserIsAdmin()) { return true; } + switch (activatableObject.getActivatableType()) { + case ENTITY_DESCRIPTOR: { + return currentUserHasExpectedRole(Arrays.asList("ROLE_ENABLE" )) && getCurrentUserGroup().getOwnerId().equals(((EntityDescriptor) activatableObject).getIdOfOwner()); + } + // Currently filters and providers dont have ownership, so we just look for the right role + case FILTER: + case METADATA_RESOLVER: + return currentUserHasExpectedRole(Arrays.asList("ROLE_ENABLE" )); + default: + return false; + } + } + + /** + * This basic logic assumes users only have a single role (despite users having a list of roles, we assume only 1 currently) + */ + private boolean currentUserHasExpectedRole(List acceptedRoles) { + User user = getCurrentUser(); + return acceptedRoles.contains(user.getRole()); + } + public boolean currentUserIsAdmin() { User user = getCurrentUser(); return user != null && user.getRole().equals("ROLE_ADMIN"); @@ -55,7 +84,7 @@ public boolean currentUserIsAdmin() { public void delete(String username) throws EntityNotFoundException, OwnershipConflictException { Optional userToRemove = userRepository.findByUsername(username); if (userToRemove.isEmpty()) throw new EntityNotFoundException("User does not exist"); - if (!ownershipRepository.findOwnedByUser(username).isEmpty()) throw new OwnershipConflictException("User ["+username+"] has ownership of entities in the system. Please remove all items before attemtping to delete the user."); + if (!ownershipRepository.findOwnedByUser(username).isEmpty()) throw new OwnershipConflictException("User ["+username+"] has ownership of entities in the system. Please remove all items before attempting to delete the user."); // ok, user exists and doesn't own anything in the system, so delete them // If the user is owned by anything, clear that first @@ -86,15 +115,15 @@ public User getCurrentUser() { public UserAccess getCurrentUserAccess() { User user = getCurrentUser(); if (user == null) { - return UserAccess.NONE; + return NONE; } if (user.getRole().equals("ROLE_ADMIN")) { - return UserAccess.ADMIN; + return ADMIN; } - if (user.getRole().equals("ROLE_USER")) { - return UserAccess.GROUP; + if (user.getRole().equals("ROLE_USER") || user.getRole().equals("ROLE_ENABLE")) { + return GROUP; } - return UserAccess.NONE; + return NONE; } public Group getCurrentUserGroup() { @@ -113,6 +142,7 @@ public Set getUserRoles(String username) { return result; } + // @TODO - probably delegate this out to something plugable at some point public boolean isAuthorizedFor(Ownable ownableObject) { switch (getCurrentUserAccess()) { case ADMIN: // Pure admin is authorized to do anything @@ -164,7 +194,7 @@ public User save(User user) { if (g == null) { try { Group newGroup = ug; - Ownership o = ownershipRepository.saveAndFlush(new Ownership(newGroup, user)); + ownershipRepository.saveAndFlush(new Ownership(newGroup, user)); g = groupService.createGroup(newGroup); } catch (GroupExistsConflictException e) { @@ -196,7 +226,7 @@ public void updateUserRole(User user) { throw new RuntimeException(String.format("User with username [%s] is defined with role [%s] which does not exist in the system!", user.getUsername(), user.getRole())); } } else { - throw new RuntimeException(String.format("User with username [%s] has no role defined and therefor cannot be updated!", user.getUsername())); + throw new RuntimeException(String.format("User with username [%s] has no role defined and therefore cannot be updated!", user.getUsername())); } } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java index bc63e7378..c8c39bbb3 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 @@ -40,7 +40,7 @@ public interface EntityDescriptorService { * @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 * @@ -67,7 +67,7 @@ public interface EntityDescriptorService { * @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 * @@ -96,7 +96,6 @@ public interface EntityDescriptorService { * @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; @@ -109,4 +108,5 @@ public interface EntityDescriptorService { */ void updateDescriptorFromRepresentation(final org.opensaml.saml.saml2.metadata.EntityDescriptor entityDescriptor, final EntityDescriptorRepresentation representation); + EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String resourceId, boolean status) throws EntityNotFoundException, ForbiddenException; } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java index 2ef9ab08e..6d752928b 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java @@ -1,7 +1,12 @@ package edu.internet2.tier.shibboleth.admin.ui.service; +import javax.script.ScriptException; + import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; /** * Main backend facade API that defines operations pertaining to manipulating {@link EntityAttributesFilter} objects. @@ -25,4 +30,6 @@ public interface FilterService { * @return FilterRepresentation front end representation */ FilterRepresentation createRepresentationFromFilter(final EntityAttributesFilter entityAttributesFilter); + + MetadataFilter updateFilterEnabledStatus(String metadataResolverId, String resourceId, boolean status) throws EntityNotFoundException, ForbiddenException, ScriptException; } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java index 9e0f7117e..d23b16365 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,70 +1,42 @@ 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.EntityAttributes; -import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; -import edu.internet2.tier.shibboleth.admin.ui.domain.KeyDescriptor; -import edu.internet2.tier.shibboleth.admin.ui.domain.IRelyingPartyOverrideProperty; -import edu.internet2.tier.shibboleth.admin.ui.domain.UIInfo; -import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean; -import edu.internet2.tier.shibboleth.admin.ui.domain.XSInteger; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ContactRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.LogoutEndpointRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.MduiRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSsoDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.*; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.*; 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.model.*; 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.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; -import javax.transaction.Transactional; - import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.*; -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.*; +import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getStringListOfAttributeValues; @Slf4j @Service public class JPAEntityDescriptorServiceImpl implements EntityDescriptorService { @Autowired - EntityDescriptorRepository entityDescriptorRepository; - - @Autowired - IGroupService groupService; - + private EntityDescriptorRepository entityDescriptorRepository; + @Autowired private OpenSamlObjects openSamlObjects; - + @Autowired private OwnershipRepository ownershipRepository; @Autowired - UserService userService; - + private UserService userService; + private EntityDescriptor buildDescriptorFromRepresentation(final EntityDescriptor ed, final EntityDescriptorRepresentation representation) { ed.setEntityID(representation.getEntityId()); ed.setIdOfOwner(representation.getIdOfOwner()); @@ -97,20 +69,25 @@ public EntityDescriptor createDescriptorFromRepresentation(final EntityDescripto public EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, EntityIdExistsException { return createNew(createRepresentationFromDescriptor(ed)); } - + @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."); } - + if (entityDescriptorRepository.findByEntityID(edRep.getEntityId()) != null) { throw new EntityIdExistsException(edRep.getEntityId()); } - + EntityDescriptor ed = (EntityDescriptor) createDescriptorFromRepresentation(edRep); ed.setIdOfOwner(userService.getCurrentUserGroup().getOwnerId()); - return createRepresentationFromDescriptor(entityDescriptorRepository.save(ed)); + ed = entityDescriptorRepository.save(ed); + + ownershipRepository.deleteEntriesForOwnedObject(ed); + ownershipRepository.save(new Ownership(userService.getCurrentUserGroup(), ed)); + + return createRepresentationFromDescriptor(ed); } @Override @@ -331,14 +308,14 @@ public void delete(String resourceId) throws ForbiddenException, EntityNotFoundE } 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()); } @@ -371,40 +348,61 @@ public EntityDescriptor getEntityDescriptorByResourceId(String resourceId) throw } 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())); + throw new EntityNotFoundException(String.format("The entity descriptor with entity id [%s] was not found for update.", edRep.getId())); } - if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { + if (edRep.isServiceEnabled() && !userService.currentUserCanEnable(existingEd)) { throw new ForbiddenException("You do not have the permissions necessary to enable this service."); } if (!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())); + 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)); + existingEd = entityDescriptorRepository.save(existingEd); + ownershipRepository.deleteEntriesForOwnedObject(existingEd); + ownershipRepository.save(new Ownership(new Owner() { + public String getOwnerId() { return edRep.getIdOfOwner(); } + public OwnerType getOwnerType() { return OwnerType.GROUP; } + }, existingEd)); + return createRepresentationFromDescriptor(existingEd); } @Override + // This should be private, but we use it in a couple different test classes not sure we should keep... public void updateDescriptorFromRepresentation(org.opensaml.saml.saml2.metadata.EntityDescriptor entityDescriptor, EntityDescriptorRepresentation representation) { if (!(entityDescriptor instanceof EntityDescriptor)) { throw new UnsupportedOperationException("not yet implemented"); } buildDescriptorFromRepresentation((EntityDescriptor) entityDescriptor, representation); } -} + + @Override + public EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String resourceId, boolean status) throws EntityNotFoundException, ForbiddenException { + EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); + if (ed == null) { + throw new EntityNotFoundException("Entity with resourceid[" + resourceId + "] was not found for update"); + } + if (!userService.currentUserCanEnable(ed)) { + throw new ForbiddenException("You do not have the permissions necessary to change the enable status of this entity descriptor."); + } + ed.setServiceEnabled(status); + ed = entityDescriptorRepository.save(ed); + return createRepresentationFromDescriptor(ed); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java index 0693b538a..c42bd7cad 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java @@ -1,13 +1,26 @@ package edu.internet2.tier.shibboleth.admin.ui.service; +import edu.internet2.tier.shibboleth.admin.ui.domain.IActivatable; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; + import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.interceptor.TransactionAspectSupport; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Optional; + +import javax.script.ScriptException; /** * Default implementation of {@link FilterService} @@ -15,19 +28,29 @@ * @since 1.0 * @author Bill Smith (wsmith@unicon.net) */ +@Service public class JPAFilterServiceImpl implements FilterService { - - private static final Logger LOGGER = LoggerFactory.getLogger(JPAFilterServiceImpl.class); - @Autowired EntityDescriptorService entityDescriptorService; - + @Autowired EntityService entityService; + @Autowired + FilterRepository filterRepository; + @Autowired FilterTargetService filterTargetService; + + @Autowired + private MetadataResolverRepository metadataResolverRepository; + + @Autowired + private MetadataResolverService metadataResolverService; + @Autowired + private UserService userService; + @Override public EntityAttributesFilter createFilterFromRepresentation(FilterRepresentation representation) { //TODO? use OpenSamlObjects.buildDefaultInstanceOfType(EntityAttributesFilter.class)? @@ -66,4 +89,52 @@ public FilterRepresentation createRepresentationFromFilter(EntityAttributesFilte representation.setVersion(entityAttributesFilter.hashCode()); return representation; } -} + + private void reloadFiltersAndHandleScriptException(String resolverResourceId) throws ScriptException { + try { + metadataResolverService.reloadFilters(resolverResourceId); + } catch (Throwable ex) { + //explicitly mark transaction for rollback when we get ScriptException as we call reloadFilters + //after persistence call. Then re-throw the exception with pertinent message + if (ex instanceof ScriptException) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + throw new ScriptException("Caught invalid script parsing error when reloading filters. Please fix the script data"); + } + } + } + + /** + * Logic taken directly from the MetadataFiltersController and then modified slightly. + */ + @Override + public MetadataFilter updateFilterEnabledStatus(String metadataResolverId, String resourceId, boolean status) + throws EntityNotFoundException, ForbiddenException, ScriptException { + + MetadataResolver metadataResolver = metadataResolverRepository.findByResourceId(metadataResolverId); + // Now we operate directly on the filter attached to MetadataResolver, + // Instead of fetching filter separately, to accommodate correct envers versioning with uni-directional one-to-many + Optional filterTobeUpdatedOptional = metadataResolver.getMetadataFilters().stream() + .filter(it -> it.getResourceId().equals(resourceId)).findFirst(); + if (filterTobeUpdatedOptional.isEmpty()) { + throw new EntityNotFoundException("Filter with resource id[" + resourceId + "] not found"); + } + + MetadataFilter filterTobeUpdated = filterTobeUpdatedOptional.get(); + + if (!userService.currentUserCanEnable(filterTobeUpdated)) { + throw new ForbiddenException("You do not have the permissions necessary to change the enable status of this filter."); + } + + filterTobeUpdated.setFilterEnabled(status); + MetadataFilter persistedFilter = filterRepository.save(filterTobeUpdated); + + // To support envers versioning from MetadataResolver side + metadataResolver.markAsModified(); + metadataResolverRepository.save(metadataResolver); + + // TODO: do we need to reload filters here? + reloadFiltersAndHandleScriptException(metadataResolver.getResourceId()); + + return persistedFilter; + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java index ee5f8cec3..2343206a7 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java @@ -22,6 +22,7 @@ import org.opensaml.saml.metadata.resolver.MetadataResolver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; import java.io.File; import java.io.FileNotFoundException; @@ -32,6 +33,7 @@ /** * @author Bill Smith (wsmith@unicon.net) */ +@Service public class MetadataResolverConverterServiceImpl implements MetadataResolverConverterService { @Autowired diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java index 376c34aa5..5fd205c20 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java @@ -2,8 +2,18 @@ import org.w3c.dom.Document; +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException; + public interface MetadataResolverService { - public void reloadFilters(String metadataResolverName); + public MetadataResolver findByResourceId(String resourceId) throws EntityNotFoundException; public Document generateConfiguration(); + + public void reloadFilters(String metadataResolverName); + + public MetadataResolver updateMetadataResolverEnabledStatus(MetadataResolver existingResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException; } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 83f2635e0..400212d09 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -20,6 +20,7 @@ spring.datasource.platform=h2 spring.datasource.driverClassName=org.h2.Driver spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.h2.console.enabled=true +spring.h2.console.settings.web-allow-others=true # spring.jackson.default-property-inclusion=non_absent spring.jackson.default-property-inclusion=NON_NULL @@ -87,7 +88,11 @@ shibui.mail.text-email-template-path-prefix=/mail/text/ shibui.mail.html.email-template-path-prefix=/mail/html/ shibui.mail.system-email-address=doNotReply@shibui.org -shibui.roles=ROLE_ADMIN,ROLE_USER,ROLE_NONE + +#ShibUIConfiguration slurps in these values and they are bootstrapped in on startup +shibui.roles=ROLE_ADMIN,ROLE_ENABLE,ROLE_USER,ROLE_NONE +#Authenticated access roles - used by Spring Security to allow access when authenticated +shibui.roles.authenticated=ADMIN,ENABLE,USER #In order to enable authentication via configured pac4j library (with external SAMl Idp, for example) #This property must be set to true and pac4j properties configured. For sample pac4j properties, see application.yml @@ -97,4 +102,4 @@ shibui.roles=ROLE_ADMIN,ROLE_USER,ROLE_NONE #This property must be set to true in order to enable posting stats to beacon endpoint. Furthermore, appropriate #environment variables must be set for beacon publisher to be used (the ones that are set when running shib-ui in #docker container -shibui.beacon-enabled=true +shibui.beacon-enabled=true \ No newline at end of file diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index b01f704ba..2b3ca2ee8 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -66,6 +66,12 @@ action.add-attribute=Add Attribute action.custom-entity-attributes=Custom Entity Attributes action.groups=Groups action.source-group=Group +action.enable=Enable +action.disable=Disable + +action.add-new-role=Add new role +action.roles=Roles +action.source-role=Role value.enabled=Enabled value.disabled=Disabled @@ -100,7 +106,6 @@ value.dynamic-http-metadata-provider=DynamicHttpMetadataProvider value.entity-attributes-filter=EntityAttributes Filter value.spdescriptor=SPSSODescriptor value.attr-auth-descriptor=AttributeAuthorityDescriptor -value.local-dynamic-metadata-provider=LocalDynamicMetadataProvider value.md-query-protocol=MetadataQueryProtocol value.template=Template @@ -427,7 +432,6 @@ label.attribute-eduPersonUniqueId=eduPersonUniqueId label.attribute-employeeNumber=employeeNumber label.force-authn=Force AuthN -label.dynamic-attributes=Dynamic Attributes label.min-cache-duration=Min Cache Duration label.max-cache-duration=Max Cache Duration label.max-idle-entity-data=Max Idle Entity Data @@ -458,7 +462,6 @@ label.admin=Admin label.user-maintenance=User Maintenance label.user-id=UserId label.email=Email -label.role=Role label.delete=Delete? label.title=Title @@ -489,8 +492,20 @@ label.by=By label.source=Metadata Source label.provider=Metadata Provider + + message.user-role-admin-group=Cannot change group for ROLE_ADMIN users. +label.roles-management=Role Management +label.new-role=New Role +label.edit-role=Edit Role +label.role-name=Role Name +label.role-description=Role Description +label.role=Role + +message.delete-role-title=Delete Role? + +message.delete-role-body=You are requesting to delete a role. If you complete this process the role will be removed. This cannot be undone. Do you wish to continue? message.duration=Requires a valid ISO 8601 duration (ex. PT2D) message.delete-user-title=Delete User? @@ -685,4 +700,7 @@ tooltip.nameid-formats-value=Value tooltip.nameid-formats-type=Type tooltip.group-name=Group Name -tooltip.group-description=Group Description \ No newline at end of file +tooltip.group-description=Group Description + +tooltip.role-name=Role Name +tooltip.role-description=Role Description \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy index 384cb9ce6..f46eb33d8 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy @@ -45,7 +45,7 @@ class TestConfiguration { final OpenSamlObjects openSamlObjects final MetadataResolverRepository metadataResolverRepository - final Logger logger = LoggerFactory.getLogger(TestConfiguration.class); + final Logger logger = LoggerFactory.getLogger(TestConfiguration.class) @Autowired private CustomEntityAttributeDefinitionRepository repository; @@ -91,12 +91,12 @@ class TestConfiguration { for (String entityId: this.getBackingStore().getIndexedDescriptors().keySet()) { Document document = new Document(); - document.add(new StringField("id", entityId, Field.Store.YES)); - document.add(new TextField("content", entityId, Field.Store.YES)); // TODO: change entityId to be content of entity descriptor block + document.add(new StringField("id", entityId, Field.Store.YES)) + document.add(new TextField("content", entityId, Field.Store.YES)) // TODO: change entityId to be content of entity descriptor block try { - indexWriter.addDocument(document); + indexWriter.addDocument(document) } catch (IOException e) { - logger.error(e.getMessage(), e); + logger.error(e.getMessage(), e) } } try { @@ -131,4 +131,4 @@ class TestConfiguration { return it } } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy index 35c015681..5affa3e26 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 @@ -27,7 +27,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders import spock.lang.Specification import spock.lang.Subject -import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.is import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* @@ -110,34 +110,6 @@ class EntitiesControllerTests extends Specification { } def 'GET /api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1'() { - given: - def expectedBody = ''' - { - "id":null, - "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"], - "version":1445248649, - "createdBy":null, - "current":false, - "groupId":null - } - ''' - when: def result = mockMvc.perform(get('/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1')) @@ -155,34 +127,6 @@ class EntitiesControllerTests extends Specification { } def 'GET /entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1'() { - given: - def expectedBody = ''' - { - "id":null, - "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"], - "version":1445248649, - "createdBy":null, - "current":false, - "groupId":null - } - ''' - when: def result = mockMvc.perform(get('/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1')) @@ -258,4 +202,4 @@ class EntitiesControllerTests extends Specification { .andExpect(content().contentType('application/xml;charset=ISO-8859-1')) .andExpect(content().xml(expectedBody)) } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy index a04c9ebba..f7b44786a 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 @@ -21,35 +21,18 @@ 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 @@ -62,21 +45,14 @@ 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.http.MediaType.APPLICATION_JSON +import static org.springframework.http.MediaType.APPLICATION_XML import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* @@ -143,14 +119,13 @@ class EntityDescriptorControllerTests extends Specification { mapper = new ObjectMapper() service.userService = userService - service.groupService = groupService 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) @@ -241,11 +216,11 @@ class EntityDescriptorControllerTests extends Specification { def result = mockMvc.perform(get('/api/EntityDescriptors')) 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")) + 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")) } @Rollback @@ -351,10 +326,10 @@ class EntityDescriptorControllerTests extends Specification { then: try { - def exceptionExpected = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) + mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) } catch (Exception e) { - e instanceof ForbiddenException == true + e instanceof ForbiddenException } } @@ -394,10 +369,10 @@ class EntityDescriptorControllerTests extends Specification { then: try { - def exceptionExpected = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) + mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) } catch (Exception e) { - e instanceof EntityIdExistsException == true + e instanceof EntityIdExistsException } } @@ -409,10 +384,10 @@ class EntityDescriptorControllerTests extends Specification { then: try { - def exceptionExpected = mockMvc.perform(get("/api/EntityDescriptor/uuid-1")) + mockMvc.perform(get("/api/EntityDescriptor/uuid-1")) } catch (Exception e) { - e instanceof EntityNotFoundException == true + e instanceof EntityNotFoundException } } @@ -482,10 +457,10 @@ class EntityDescriptorControllerTests extends Specification { then: try { - def exceptionExpected = mockMvc.perform(get("/api/EntityDescriptor/uuid-2")) + mockMvc.perform(get("/api/EntityDescriptor/uuid-2")) } catch (Exception e) { - e instanceof ForbiddenException == true + e instanceof ForbiddenException } } @@ -553,10 +528,10 @@ class EntityDescriptorControllerTests extends Specification { then: try { - def exceptionExpected = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId").accept(APPLICATION_XML)) + mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId").accept(APPLICATION_XML)) } catch (Exception e) { - e instanceof ForbiddenException == true + e instanceof ForbiddenException } } @@ -640,10 +615,10 @@ class EntityDescriptorControllerTests extends Specification { then: try { - def exceptionExpected = mockMvc.perform(post("/api/EntityDescriptor").contentType(APPLICATION_XML).content(postedBody).param("spName", spName)) + mockMvc.perform(post("/api/EntityDescriptor").contentType(APPLICATION_XML).content(postedBody).param("spName", spName)) } catch (Exception e) { - e instanceof EntityIdExistsException == true + e instanceof EntityIdExistsException } } @@ -694,10 +669,10 @@ class EntityDescriptorControllerTests extends Specification { then: try { - def exceptionExpected = mockMvc.perform(put("/api/EntityDescriptor/uuid-1").contentType(APPLICATION_JSON).content(postedJsonBody)) + mockMvc.perform(put("/api/EntityDescriptor/uuid-1").contentType(APPLICATION_JSON).content(postedJsonBody)) } catch (Exception e) { - e instanceof ForbiddenException == true + e instanceof ForbiddenException } } @@ -721,10 +696,10 @@ class EntityDescriptorControllerTests extends Specification { then: try { - def exceptionExpected = mockMvc.perform(put("/api/EntityDescriptor/uuid-1").contentType(APPLICATION_JSON).content(postedJsonBody)) + mockMvc.perform(put("/api/EntityDescriptor/uuid-1").contentType(APPLICATION_JSON).content(postedJsonBody)) } catch (Exception e) { - e instanceof ForbiddenException == true + e instanceof ForbiddenException } } @@ -747,10 +722,10 @@ class EntityDescriptorControllerTests extends Specification { then: try { - def exception = mockMvc.perform(put("/api/EntityDescriptor/$resourceId").contentType(APPLICATION_JSON).content(postedJsonBody)) + mockMvc.perform(put("/api/EntityDescriptor/$resourceId").contentType(APPLICATION_JSON).content(postedJsonBody)) } catch (Exception e) { - e instanceof ConcurrentModificationException == true + e instanceof ConcurrentModificationException } } @@ -768,13 +743,4 @@ class EntityDescriptorControllerTests extends Specification { 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 +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorOwnershipIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorOwnershipIntegrationTests.groovy new file mode 100644 index 000000000..59510ac27 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorOwnershipIntegrationTests.groovy @@ -0,0 +1,216 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceForTesting +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionService +import edu.internet2.tier.shibboleth.admin.ui.service.EntityService +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.client.RestTemplate +import spock.lang.Specification +import spock.lang.Stepwise +import spock.lang.Subject + +import javax.persistence.EntityManager + +import static org.springframework.http.MediaType.APPLICATION_JSON +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +/** + * Test to recreate an issue discovered while trying to validate fixes for other bugs - SHIBUI-2033 + */ +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration, LocalConfig]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@ActiveProfiles(["edoi-test"]) +@Stepwise +class EntityDescriptorOwnershipIntegrationTests extends Specification { + @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 + + def mockRestTemplate = Mock(RestTemplate) + + def openSamlObjects = new OpenSamlObjects().with { + init() + it + } + + Group cuGroup = new Group().with { + it.name = "College Users" + it.resourceId = "cu-group" + it + } + + def mockMvc + + @Subject + def controller + + @Transactional + def setup() { + groupService.clearAllForTesting() + + EntityDescriptorVersionService versionService = Mock() + controller = new EntityDescriptorController(versionService) + controller.openSamlObjects = openSamlObjects + controller.entityDescriptorService = service + controller.restTemplate = mockRestTemplate + + mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + + if (roleRepository.count() == 0) { + def roles = [new Role().with { + name = 'ROLE_ADMIN' + it + }, new Role().with { + name = 'ROLE_USER' + it + }, new Role().with { + name = 'ROLE_ENABLE' + it + }] + roles.each { + roleRepository.save(it) + } + } + + Optional adminRole = roleRepository.findByName("ROLE_ADMIN") + User adminUser = new User(username: "admin", roles: [adminRole.get()], password: "foo") + userService.save(adminUser) + + Optional userRole = roleRepository.findByName("ROLE_USER") + User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") + userService.save(user) + + EntityDescriptorConversionUtils.setOpenSamlObjects(openSamlObjects) + EntityDescriptorConversionUtils.setEntityService(entityService) + } + + @WithMockUser(value = "admin", roles = ["ADMIN"]) + def "The test scenario"() { + when:"step 1 - create new group" + cuGroup = groupService.createGroup(cuGroup) + + then: + groupService.findAll().size() == 3 + + when: "step 2 - assign the user to new group" + User user = userRepository.findByUsername("someUser").get() + user.setGroup(cuGroup) + def updatedUser = userService.save(user) + + then: + updatedUser.getGroupId() == "cu-group" + ownershipRepository.findAllByOwner(cuGroup).size() == 1 + + when: "step 3 - create a new ED and then change its ownership to the cu group" + def expectedEntityId = 'https://shib' + def expectedSpName = 'sp1' + + def postedJsonBody = """ + { + "serviceProviderName": "$expectedSpName", + "entityId": "$expectedEntityId", + "organization": {}, + "serviceEnabled": false, + "current": false + } + """ + def result = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) + + then: + result.andExpect(status().isCreated()) + .andExpect(jsonPath("\$.entityId").value("https://shib")) + .andExpect(jsonPath("\$.serviceEnabled").value(false)) + .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) + + ownershipRepository.findAllByOwner(cuGroup).size() == 1 // someUser + ownershipRepository.findAllByOwner(Group.ADMIN_GROUP).size() == 2 // admin user + entity descriptor + + when: "step 4 - change ownership of the ED" + String contentAsString = result.andReturn().getResponse().getContentAsString() + def mapper = new ObjectMapper() + mapper.enable(SerializationFeature.INDENT_OUTPUT) + mapper.registerModule(new JavaTimeModule()) + EntityDescriptorRepresentation edRep = mapper.readValue(contentAsString, EntityDescriptorRepresentation.class) + edRep.setIdOfOwner(cuGroup.getOwnerId()) + service.update(edRep) + + then: + ownershipRepository.findAllByOwner(cuGroup).size() == 2 // someUser + entity descriptor + ownershipRepository.findAllByOwner(Group.ADMIN_GROUP).size() == 1 // admin user + } + + @org.springframework.boot.test.context.TestConfiguration + @Profile(value = "edoi-test") + static class LocalConfig { + @Bean + @Primary + GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { + GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { + it.groupRepository = repo + it.ownershipRepository = ownershipRepository + return it + }) + result.ensureAdminGroupExists() + return result + } + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy index 280294aa1..b915d6fac 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy @@ -7,10 +7,14 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.Internationalization import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository import edu.internet2.tier.shibboleth.admin.ui.service.FilterService @@ -86,6 +90,18 @@ class MetadataFiltersControllerTests extends Specification { Document generateConfiguration() { return null } + + @Override + public MetadataResolver updateMetadataResolverEnabledStatus(MetadataResolver existingResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException { + // This won't get called + return null + } + + @Override + public MetadataResolver findByResourceId(String resourceId) throws EntityNotFoundException { + // This won't get called + return null + } }, chainingMetadataResolver: new OpenSamlChainingMetadataResolver().with { it.id = 'chain' diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy index b8487c9c8..05f6b62c8 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.StringTrimModule import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver @@ -21,6 +22,7 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Profile import org.springframework.http.HttpEntity import org.springframework.http.HttpHeaders import org.springframework.test.annotation.DirtiesContext @@ -63,6 +65,7 @@ class MetadataResolversControllerIntegrationTests extends Specification { mapper.enable(SerializationFeature.INDENT_OUTPUT) mapper.setSerializationInclusion(NON_NULL) mapper.registerModule(new JavaTimeModule()) + mapper.registerModule(new StringTrimModule()) metadataResolverRepository.deleteAll() } @@ -206,7 +209,7 @@ class MetadataResolversControllerIntegrationTests extends Specification { 'ResourceBacked' | _ 'Filesystem' | _ } - + @DirtiesContext def "SHIBUI-1992 - error creating FileBackedHTTPMetadata"() { def resolver = new FileBackedHttpMetadataResolver().with { @@ -360,10 +363,10 @@ class MetadataResolversControllerIntegrationTests extends Specification { } @TestConfiguration - static class Config { + static class LocalConfig { @Bean MetadataResolver metadataResolver() { new OpenSamlChainingMetadataResolver() } } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepositoryTests.groovy index 64dc3cdd3..bfdc8f57a 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomEntityAttributeDefinitionRepositoryTests.groovy @@ -1,5 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.repository +import org.springframework.dao.DataIntegrityViolationException + import javax.persistence.EntityManager import org.springframework.beans.factory.annotation.Autowired @@ -55,7 +57,7 @@ class CustomEntityAttributeDefinitionRepositoryTests extends Specification { def cas = repo.findAll() cas.size() == 1 def caFromDb1 = cas.get(0).asType(CustomEntityAttributeDefinition) - caFromDb1.equals(ca) == true + caFromDb1.equals(ca) // fetch checks repo.findByName("not a name") == null @@ -85,7 +87,7 @@ class CustomEntityAttributeDefinitionRepositoryTests extends Specification { then: // Missing non-nullable field should thrown error - final def exception = thrown(org.springframework.dao.DataIntegrityViolationException) + final def exception = thrown(DataIntegrityViolationException) } def "basic CRUD operations validated"() { @@ -116,7 +118,7 @@ class CustomEntityAttributeDefinitionRepositoryTests extends Specification { def cas = repo.findAll() cas.size() == 1 def caFromDb1 = cas.get(0).asType(CustomEntityAttributeDefinition) - caFromDb1.equals(ca) == true + caFromDb1.equals(ca) // fetch checks repo.findByName("not a name") == null @@ -138,7 +140,7 @@ class CustomEntityAttributeDefinitionRepositoryTests extends Specification { cas2.size() == 1 def caFromDb2 = cas2.get(0).asType(CustomEntityAttributeDefinition) caFromDb2.equals(ca) == false - caFromDb2.equals(caFromDb1) == true + caFromDb2.equals(caFromDb1) // delete tests when: @@ -171,7 +173,7 @@ class CustomEntityAttributeDefinitionRepositoryTests extends Specification { def cas = repo.findAll() cas.size() == 1 def ca3FromDb = cas.get(0).asType(CustomEntityAttributeDefinition) - ca3FromDb.equals(ca3) == true + ca3FromDb.equals(ca3) // now update the attribute list items ca3FromDb.with { @@ -190,7 +192,7 @@ class CustomEntityAttributeDefinitionRepositoryTests extends Specification { it.resourceId = ca3FromDb.resourceId it } - caFromDb4.equals(ca4) == true + caFromDb4.equals(ca4) // now remove items ca3FromDb.with { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepositoryTest.groovy index 8f69e2424..de4c0ac9e 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 @@ -50,7 +50,7 @@ class EntityDescriptorRepositoryTest extends Specification { EntityDescriptorRepository entityDescriptorRepository @Autowired - private CustomEntityAttributeDefinitionRepository repository; + private CustomEntityAttributeDefinitionRepository repository @Autowired EntityManager entityManager @@ -110,7 +110,7 @@ class EntityDescriptorRepositoryTest extends Specification { it.description = "some description" it } - group = groupRepository.saveAndFlush(group) + groupRepository.saveAndFlush(group) def gList = groupRepository.findAll() def groupFromDb = gList.get(0).asType(Group) @@ -122,19 +122,19 @@ class EntityDescriptorRepositoryTest extends Specification { entityDescriptorRepository.saveAndFlush(ed) when: - def edStreamFromDb = entityDescriptorRepository.findAllStreamByIdOfOwner(null); + def edStreamFromDb = entityDescriptorRepository.findAllStreamByIdOfOwner(null) then: ((Stream)edStreamFromDb).count() == 0 when: - def edStreamFromDb2 = entityDescriptorRepository.findAllStreamByIdOfOwner("random value"); + def edStreamFromDb2 = entityDescriptorRepository.findAllStreamByIdOfOwner("random value") then: ((Stream)edStreamFromDb2).count() == 0 when: - def edStreamFromDb3 = entityDescriptorRepository.findAllStreamByIdOfOwner(groupFromDb.resourceId); + def edStreamFromDb3 = entityDescriptorRepository.findAllStreamByIdOfOwner(groupFromDb.resourceId) then: ((Stream)edStreamFromDb3).count() == 1 @@ -175,4 +175,4 @@ class EntityDescriptorRepositoryTest extends Specification { } } } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy index 7c1acc5da..41aa6a9cb 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy @@ -1,23 +1,18 @@ package edu.internet2.tier.shibboleth.admin.ui.scheduled -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepresentation import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository -import edu.internet2.tier.shibboleth.admin.ui.security.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 @@ -65,7 +60,6 @@ class EntityDescriptorFilesScheduledTasksTests extends Specification { service = new JPAEntityDescriptorServiceImpl() service.openSamlObjects = openSamlObjects - service.groupService = groupService } def "generateEntityDescriptorFiles properly generates a file from an Entity Descriptor"() { @@ -114,21 +108,6 @@ class EntityDescriptorFilesScheduledTasksTests extends Specification { def "removeDanglingEntityDescriptorFiles properly deletes files"() { given: - def expectedXml = ''' - - - name - display name - http://test.example.org - - - ''' - def entityDescriptor = service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { it.entityId = 'http://test.example.org/test1' it.organization = new OrganizationRepresentation().with { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy index 0f1305c51..a19e4f807 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy @@ -62,16 +62,16 @@ class GroupsControllerIntegrationTests extends Specification { static RESOURCE_URI = '/api/admin/groups' - def MockMvc mockMvc + MockMvc mockMvc def setup() { groupService.ensureAdminGroupExists() - def GroupController groupController = new GroupController().with ({ + GroupController groupController = new GroupController().with ({ it.groupService = this.groupService it }) - mockMvc = MockMvcBuilders.standaloneSetup(groupController).build(); + mockMvc = MockMvcBuilders.standaloneSetup(groupController).build() if (roleRepository.count() == 0) { def roles = [new Role().with { @@ -95,7 +95,7 @@ class GroupsControllerIntegrationTests extends Specification { Optional userRole = roleRepository.findByName("ROLE_USER") User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") - user = userService.save(user) + userService.save(user) entityManager.flush() } @@ -141,7 +141,7 @@ class GroupsControllerIntegrationTests extends Specification { def 'PUT (update) existing group persists properly'() { given: groupsRepository.deleteByResourceId("AAA") - def Group groupAAA = new Group().with({ + Group groupAAA = new Group().with({ it.name = "AAA" it.description = "AAA" it.resourceId = "AAA" @@ -184,14 +184,14 @@ class GroupsControllerIntegrationTests extends Specification { given: groupsRepository.deleteByResourceId("AAA") groupsRepository.deleteByResourceId("BBB") - def Group groupAAA = new Group().with({ + Group groupAAA = new Group().with({ it.name = "AAA" it.description = "AAA" it.resourceId = "AAA" it }) groupsRepository.save(groupAAA) - def Group groupBBB = new Group().with({ + Group groupBBB = new Group().with({ it.name = "BBB" it.description = "BBB" it.resourceId = "BBB" @@ -239,7 +239,7 @@ class GroupsControllerIntegrationTests extends Specification { } when: - def Group groupAAA = new Group().with({ + Group groupAAA = new Group().with({ it.name = "AAA" it.description = "AAA" it.resourceId = "AAA" @@ -247,11 +247,11 @@ class GroupsControllerIntegrationTests extends Specification { }) groupAAA = groupsRepository.save(groupAAA) - def User user = userRepository.findByUsername("someUser").get() + User user = userRepository.findByUsername("someUser").get() user.setGroup(groupAAA) userService.save(user) then: mockMvc.perform(delete("$RESOURCE_URI/someUser")) } -} +} \ 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 index 1a23778da..b2d079dfc 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepositoryTests.groovy @@ -1,5 +1,9 @@ package edu.internet2.tier.shibboleth.admin.ui.security.repository +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Profile +import org.springframework.dao.DataIntegrityViolationException + import javax.persistence.EntityManager import javax.transaction.Transactional @@ -26,7 +30,7 @@ import spock.lang.Specification @ContextConfiguration(classes=[InternationalizationConfiguration, LocalConfig]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@ActiveProfiles("test") +@ActiveProfiles(["test","local"]) class GroupsRepositoryTests extends Specification { @Autowired GroupsRepository groupsRepo @@ -95,8 +99,8 @@ class GroupsRepositoryTests extends Specification { it.resourceId = "g1" it } - def Group savedGroup = groupsRepo.saveAndFlush(group) - def Collection all = ownershipRepository.findAllByOwner(savedGroup) + Group savedGroup = groupsRepo.saveAndFlush(group) + Collection all = ownershipRepository.findAllByOwner(savedGroup) then: all.size() == 3 @@ -131,7 +135,7 @@ class GroupsRepositoryTests extends Specification { def gList = groupsRepo.findAll() gList.size() == 1 def groupFromDb = gList.get(0).asType(Group) - groupFromDb.equals(group) == true + groupFromDb.equals(group) // fetch checks groupsRepo.findByResourceId("not an id") == null @@ -154,11 +158,11 @@ class GroupsRepositoryTests extends Specification { // save check when: - def savedGroup = groupsRepo.save(group) + groupsRepo.save(group) then: // Missing non-nullable field (name) should thrown error - final def exception = thrown(org.springframework.dao.DataIntegrityViolationException) + thrown(DataIntegrityViolationException) } @Rollback @@ -186,7 +190,7 @@ class GroupsRepositoryTests extends Specification { def gList = groupsRepo.findAll() gList.size() == 1 def groupFromDb = gList.get(0).asType(Group) - groupFromDb.equals(group) == true + groupFromDb.equals(group) // update check groupFromDb.with { @@ -202,7 +206,7 @@ class GroupsRepositoryTests extends Specification { gList2.size() == 1 def groupFromDb2 = gList2.get(0).asType(Group) groupFromDb2.equals(group) == false - groupFromDb2.equals(groupFromDb) == true + groupFromDb2.equals(groupFromDb) // delete tests when: @@ -212,13 +216,14 @@ class GroupsRepositoryTests extends Specification { groupsRepo.findAll().size() == 0 when: - def nothingThere = groupsRepo.findByResourceId(null); + def nothingThere = groupsRepo.findByResourceId(null) then: nothingThere == null } - @org.springframework.boot.test.context.TestConfiguration + @TestConfiguration + @Profile("local") static class LocalConfig { @Bean GroupUpdatedEntityListener groupUpdatedEntityListener(OwnershipRepository repo) { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepositoryTests.groovy index 229763c58..5e36f19ce 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepositoryTests.groovy @@ -83,7 +83,7 @@ class OwnershipRepositoryTests extends Specification { @Rollback def "test clearUsersGroups"() { when: "remove entries where the user is the owned object of a group" - repo.clearUsersGroups("aaa"); + repo.clearUsersGroups("aaa") def result = repo.findAllGroupsForUser("aaa") then: @@ -102,7 +102,7 @@ class OwnershipRepositoryTests extends Specification { } when: "remove entries where the user is the owned object of groups" - repo.clearUsersGroups("ccc"); + repo.clearUsersGroups("ccc") result = repo.findAllGroupsForUser("ccc") then: @@ -113,9 +113,9 @@ class OwnershipRepositoryTests extends Specification { 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" } + String getObjectId() { return "aaa" } - public OwnableType getOwnableType() { OwnableType.USER } + OwnableType getOwnableType() { OwnableType.USER } }) def result = repo.findAllGroupsForUser("aaa") @@ -136,10 +136,10 @@ class OwnershipRepositoryTests extends Specification { when: "remove entries where the user is the owned object of groups" repo.deleteEntriesForOwnedObject(new Ownable() { - public String getObjectId() { return "ccc" } + String getObjectId() { return "ccc" } - public OwnableType getOwnableType() { OwnableType.USER } - }); + OwnableType getOwnableType() { OwnableType.USER } + }) result = repo.findAllGroupsForUser("ccc") then: @@ -153,9 +153,9 @@ class OwnershipRepositoryTests extends Specification { userIds.add("bbb") userIds.add("ccc") def result = repo.findUsersByOwner(new Owner() { - public String getOwnerId() { return "g1" } + String getOwnerId() { return "g1" } - public OwnerType getOwnerType() { OwnerType.GROUP } + OwnerType getOwnerType() { OwnerType.GROUP } }) then: @@ -167,9 +167,9 @@ class OwnershipRepositoryTests extends Specification { when: result = repo.findUsersByOwner(new Owner() { - public String getOwnerId() { return "aaa" } + String getOwnerId() { return "aaa" } - public OwnerType getOwnerType() { return OwnerType.USER } + OwnerType getOwnerType() { return OwnerType.USER } }) then: @@ -196,8 +196,8 @@ class OwnershipRepositoryTests extends Specification { groupIds.add("g1") groupIds.add("g2") def result = repo.findOwnableObjectOwners(new Ownable() { - public String getObjectId() { return "ccc" } - public OwnableType getOwnableType() { return OwnableType.USER } + String getObjectId() { return "ccc" } + OwnableType getOwnableType() { return OwnableType.USER } }) then: @@ -242,9 +242,9 @@ class OwnershipRepositoryTests extends Specification { userIds.add("bbb") userIds.add("ccc") def result = repo.findAllByOwner(new Owner() { - public String getOwnerId() { return "g1" } + String getOwnerId() { return "g1" } - public OwnerType getOwnerType() { return OwnerType.GROUP } + OwnerType getOwnerType() { return OwnerType.GROUP } }) then: @@ -256,9 +256,9 @@ class OwnershipRepositoryTests extends Specification { when: "Find all items owned by user aaa" result = repo.findAllByOwner(new Owner() { - public String getOwnerId() { return "aaa" } + String getOwnerId() { return "aaa" } - public OwnerType getOwnerType() { return OwnerType.USER } + OwnerType getOwnerType() { return OwnerType.USER } }) then: diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy index 9916ae084..a3f223efb 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy @@ -7,15 +7,15 @@ import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl @Profile('test') class GroupServiceForTesting extends GroupServiceImpl { - public GroupServiceForTesting(GroupServiceImpl impl) { + GroupServiceForTesting(GroupServiceImpl impl) { this.groupRepository = impl.groupRepository this.ownershipRepository = impl.ownershipRepository } @Transactional - public void clearAllForTesting() { - groupRepository.deleteAll(); + void clearAllForTesting() { + groupRepository.deleteAll() ownershipRepository.clearAllOwnedByGroup() ensureAdminGroupExists() } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy index b6431f79f..80ce68ab9 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy @@ -1,19 +1,28 @@ package edu.internet2.tier.shibboleth.admin.ui.security.service -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - -import javax.persistence.EntityManager - +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.IActivatable +import edu.internet2.tier.shibboleth.admin.ui.security.model.* +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.boot.test.context.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 @@ -22,34 +31,12 @@ 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 +import javax.persistence.EntityManager +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + @DataJpaTest @ContextConfiguration(classes=[CoreShibUiConfiguration, CustomPropertiesConfiguration, LocalConfig]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @@ -70,13 +57,18 @@ class UserServiceTests extends Specification { @Autowired RoleRepository roleRepository - + + @Autowired + ShibUIConfiguration shibUIConfiguration + @Autowired UserRepository userRepository @Autowired UserService userService - + + def users + @Transactional def setup() { userRepository.findAll().forEach { @@ -87,22 +79,217 @@ class UserServiceTests extends Specification { 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' + + shibUIConfiguration.roles.each { it -> + def role = new Role(name: it) + roleRepository.saveAndFlush(role) + } + + if (userRepository.count() == 0) { + users = [new User().with { + username = 'admin' + password = '{noop}adminpass' + firstName = 'Joe' + lastName = 'Doe' + emailAddress = 'joe@institution.edu' + roles.add(roleRepository.findByName('ROLE_ADMIN').get()) + it + }, new User().with { + username = 'nonadmin' + password = '{noop}nonadminpass' + firstName = 'Peter' + lastName = 'Vandelay' + emailAddress = 'peter@institution.edu' + roles.add(roleRepository.findByName('ROLE_USER').get()) + it + }, new User().with { + username = 'robot' + password = '{noop}nonepass' + firstName = 'Bad' + lastName = 'robot' + emailAddress = 'badboy@institution.edu' + roles.add(roleRepository.findByName('ROLE_ENABLE').get()) + it + }, new User().with { + username = 'robot2' + password = '{noop}nonepass' + firstName = 'Bad2' + lastName = 'robot2' + emailAddress = 'badboy2@institution.edu' + roles.add(roleRepository.findByName('ROLE_ENABLE').get()) + it + }] + users.each { + it = userService.save(it) + } + } + } + + @WithMockUser(value = "admin", roles = ["ADMIN"]) + def "Validate isAuthorizedFor with admin user (always auth)"() { + when: "the object doesn't matter if user is admin" + def isAuth = userService.isAuthorizedFor(new Ownable(){ + String getObjectId() { return null } + OwnableType getOwnableType() { return null } + }); + + then: + isAuth + } + + @WithMockUser(value = "admin", roles = ["ADMIN"]) + def "Admin can activate"() { + when: "the object doesn't matter if user is admin" + def canEnable = userService.currentUserCanEnable(new IActivatable() { + ActivatableType getActivatableType() { return ActivatableType.ENTITY_DESCRIPTOR } + void setEnabled(Boolean enabled) { } + }); + + then: + canEnable + + when: "the object doesn't matter if user is admin" + canEnable = userService.currentUserCanEnable(new IActivatable() { + ActivatableType getActivatableType() { return ActivatableType.FILTER } + void setEnabled(Boolean enabled) { } + }); + + then: + canEnable + + when: "the object doesn't matter if user is admin" + canEnable = userService.currentUserCanEnable(new IActivatable() { + ActivatableType getActivatableType() { return ActivatableType.METADATA_RESOLVER } + void setEnabled(Boolean enabled) { } + }); + + then: + canEnable + } + + @WithMockUser(value = "nonadmin", roles = ["USER"]) + def "nonadmin cannot activate without enable role"() { + when: + def canEnable = userService.currentUserCanEnable(new IActivatable() { + ActivatableType getActivatableType() { return ActivatableType.ENTITY_DESCRIPTOR } + void setEnabled(Boolean enabled) { } + }); + + then: + !canEnable + + when: + canEnable = userService.currentUserCanEnable(new IActivatable() { + ActivatableType getActivatableType() { return ActivatableType.FILTER } + void setEnabled(Boolean enabled) { } + }); + + then: + !canEnable + + when: + canEnable = userService.currentUserCanEnable(new IActivatable() { + ActivatableType getActivatableType() { return ActivatableType.METADATA_RESOLVER } + void setEnabled(Boolean enabled) { } + }); + + then: + !canEnable + } + + @WithMockUser(value = "robot", roles = ["ENABLE"]) + def "nonadmin can activate with enable role"() { + given: + def ed = new EntityDescriptor().with { + it.idOfOwner = 'robot' it - }, new Role().with { - name = 'ROLE_NONE' + } + when: + def canEnable = userService.currentUserCanEnable(ed); + + then: + canEnable + + when: + canEnable = userService.currentUserCanEnable(new IActivatable() { + ActivatableType getActivatableType() { return ActivatableType.FILTER } + void setEnabled(Boolean enabled) { } + }); + + then: + canEnable + + when: + canEnable = userService.currentUserCanEnable(new IActivatable() { + ActivatableType getActivatableType() { return ActivatableType.METADATA_RESOLVER } + void setEnabled(Boolean enabled) { } + }); + + then: + canEnable + } + + @WithMockUser(value = "robot2", roles = ["ENABLE"]) + def "nonadmin cannot activate entity descriptor with enable role"() { + given: + def ed = new EntityDescriptor().with { + it.idOfOwner = 'robot' it - }] - roles.each { - roleRepository.save(it) - } + } + when: + def canEnable = userService.currentUserCanEnable(ed); + + then: + !canEnable + + when: + canEnable = userService.currentUserCanEnable(new IActivatable() { + ActivatableType getActivatableType() { return ActivatableType.FILTER } + void setEnabled(Boolean enabled) { } + }); + + then: + canEnable + + when: + canEnable = userService.currentUserCanEnable(new IActivatable() { + ActivatableType getActivatableType() { return ActivatableType.METADATA_RESOLVER } + void setEnabled(Boolean enabled) { } + }); + + then: + canEnable } - + + @WithMockUser(value = "nonadmin", roles = ["USER"]) + @Rollback + def "Validate isAuthorizedFor with non-admin user"() { + given: + ownershipRepository.saveAndFlush(new Ownership(new Owner() { + String getOwnerId() { return "nonadmin" } + OwnerType getOwnerType() { return OwnerType.GROUP } + }, new Ownable() { + String getObjectId() { return "foothing" } + OwnableType getOwnableType() { return OwnableType.ENTITY_DESCRIPTOR } + })) + when: + def isAuth = userService.isAuthorizedFor(new Ownable(){ + String getObjectId() { return "foothing" } + OwnableType getOwnableType() { return OwnableType.ENTITY_DESCRIPTOR } + }); + + then: + isAuth + } + + def "Double Check that shibUIConfiguration includes the enable role"() { + when: + def Optional role = roleRepository.findByName("ROLE_ENABLE") + + then: + role.isPresent() + } + @Rollback def "When creating user, user is set to the correct group"() { given: @@ -110,24 +297,24 @@ class UserServiceTests extends Specification { 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: + + 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 @@ -140,31 +327,31 @@ class UserServiceTests extends Specification { 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 @@ -172,11 +359,11 @@ class UserServiceTests extends Specification { // 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: @@ -184,26 +371,26 @@ class UserServiceTests extends Specification { 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: @@ -211,12 +398,12 @@ class UserServiceTests extends Specification { 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" @@ -224,41 +411,41 @@ class UserServiceTests extends Specification { 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 + @TestConfiguration @Profile("local") static class LocalConfig { @Bean diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy index 26c7eb5f1..4c572e2ad 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy @@ -3,6 +3,7 @@ package edu.internet2.tier.shibboleth.admin.ui.service import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.configuration.JsonSchemaComponentsConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup import edu.internet2.tier.shibboleth.admin.ui.jsonschema.LowLevelJsonSchemaValidator import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.security.model.Group @@ -34,11 +35,6 @@ class AuxiliaryIntegrationTests extends Specification { def "SHIBUI-1723: after enabling saved entity descriptor, it should still have valid xml"() { given: - def group = new Group().with { - it.name = "foo" - it.resourceId = "foo" - it - } def entityDescriptor = openSamlObjects.unmarshalFromXml(this.class.getResource('/metadata/SHIBUI-1723-1.xml').bytes) as EntityDescriptor entityDescriptor.idOfOwner = "foo" @@ -52,7 +48,7 @@ class AuxiliaryIntegrationTests extends Specification { it } def json = objectMapper.writeValueAsString(entityDescriptorRepresentation) - def schemaUri = edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.metadataSourcesSchema(new JsonSchemaComponentsConfiguration().jsonSchemaResourceLocationRegistry(this.resourceLoader, this.objectMapper)).uri + def schemaUri = JsonSchemaLocationLookup.metadataSourcesSchema(new JsonSchemaComponentsConfiguration().jsonSchemaResourceLocationRegistry(this.resourceLoader, this.objectMapper)).uri when: LowLevelJsonSchemaValidator.validatePayloadAgainstSchema(new MockHttpInputMessage(json.bytes), schemaUri) @@ -60,4 +56,4 @@ class AuxiliaryIntegrationTests extends Specification { then: noExceptionThrown() } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy index 0cde7c38b..3733a5fe1 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 @@ -153,7 +153,7 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { return resolver } - + @Bean GroupServiceImpl groupService(GroupsRepository repo, OwnershipRepository ownershipRepository) { new GroupServiceImpl().with { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy index 13847472c..11d870149 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy @@ -1,18 +1,5 @@ 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 @@ -27,15 +14,27 @@ import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceForTesting import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl -import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile +import org.springframework.context.annotation.PropertySource +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.annotation.Rollback +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.annotation.Transactional import spock.lang.Specification @ContextConfiguration(classes=[CoreShibUiConfiguration, CustomPropertiesConfiguration, LocalConfig]) @SpringBootTest(classes = ShibbolethUiApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) @PropertySource("classpath:application.yml") @DirtiesContext -@ActiveProfiles(value="local") +@ActiveProfiles(value="jpaeds2-test") class JPAEntityDescriptorServiceImplTests2 extends Specification { @Autowired @@ -63,12 +62,12 @@ class JPAEntityDescriptorServiceImplTests2 extends Specification { Group ga = new Group() ga.setResourceId("testingGroup") ga.setName("Group A") - ga = groupService.createGroup(ga) + groupService.createGroup(ga) - Group gb = new Group(); + Group gb = new Group() gb.setResourceId("testingGroupBBB") gb.setName("Group BBB") - gb = groupService.createGroup(gb) + groupService.createGroup(gb) def roles = [new Role().with { name = 'ROLE_ADMIN' @@ -95,17 +94,15 @@ class JPAEntityDescriptorServiceImplTests2 extends Specification { @WithMockUser(value = "someUser", roles = ["USER"]) @Rollback + @Transactional def "When creating Entity Descriptor, ED is assigned to the user's group"() { given: User current = userService.getCurrentUser() current.setGroupId("testingGroupBBB") - def 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: @@ -116,9 +113,10 @@ class JPAEntityDescriptorServiceImplTests2 extends Specification { } @TestConfiguration - @Profile("local") + @Profile("jpaeds2-test") static class LocalConfig { @Bean + @Primary GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { it.groupRepository = repo @@ -128,4 +126,4 @@ class JPAEntityDescriptorServiceImplTests2 extends Specification { return result } } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy index e91f3fbab..20b4abcde 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 @@ -2,7 +2,6 @@ package edu.internet2.tier.shibboleth.admin.ui.service import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConverterConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.PlaceholderResolverComponentsConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration @@ -56,7 +55,7 @@ import spock.lang.Unroll import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.generatedXmlIsTheSameAsExpectedXml @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, MetadataResolverConverterConfiguration, SearchConfiguration, InternationalizationConfiguration, PlaceholderResolverComponentsConfiguration, edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration ,Config]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, InternationalizationConfiguration, PlaceholderResolverComponentsConfiguration, edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration , LocalConfig]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) @@ -486,7 +485,7 @@ class JPAMetadataResolverServiceImplTests extends Specification { @TestConfiguration @Profile("local") - static class Config { + static class LocalConfig { @Autowired OpenSamlObjects openSamlObjects @@ -516,7 +515,7 @@ class JPAMetadataResolverServiceImplTests extends Specification { it } } - + @Bean GroupServiceImpl groupService(GroupsRepository repo, OwnershipRepository ownershipRepository) { new GroupServiceImpl().with { @@ -526,4 +525,4 @@ class JPAMetadataResolverServiceImplTests extends Specification { } } } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImplTests.groovy index 8a261565e..f5d0f72e3 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImplTests.groovy @@ -1,9 +1,13 @@ package edu.internet2.tier.shibboleth.admin.ui.service +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicMetadataResolverAttributes import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ContextConfiguration + import spock.lang.Specification @SpringBootTest 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 95eb8d09f..0deb53e69 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 @@ -35,10 +35,10 @@ class UserBootstrapTests extends Specification { @Autowired RoleRepository roleRepository - + @Autowired UserService userService - + @Autowired IGroupService groupService @@ -46,7 +46,7 @@ class UserBootstrapTests extends Specification { groupService.ensureAdminGroupExists() roleRepository.deleteAll() } - + def "simple test"() { setup: shibUIConfiguration.roles = [] @@ -76,4 +76,4 @@ class UserBootstrapTests extends Specification { assert roleRepository.findByName('ROLE_ADMIN').get() assert roleRepository.findByName('ROLE_USER').get() } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy index d7f7a427b..0b9a4e81c 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy @@ -34,7 +34,7 @@ import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversio class EntityDescriptorConversionUtilsTests extends Specification { @Shared - def OpenSamlObjects openSAMLObjects + OpenSamlObjects openSAMLObjects def setup() { openSAMLObjects = new OpenSamlObjects().with { @@ -42,7 +42,7 @@ class EntityDescriptorConversionUtilsTests extends Specification { it } - def EntityDescriptorConversionUtils utilsUnderTest = new EntityDescriptorConversionUtils().with { + new EntityDescriptorConversionUtils().with { it.openSamlObjects = openSAMLObjects it } @@ -61,7 +61,7 @@ class EntityDescriptorConversionUtilsTests extends Specification { expected.name = 'testName' when: - def keyDescriptor = EntityDescriptorConversionUtils.createKeyDescriptor('testName', 'signing', 'testValue') + def keyDescriptor = createKeyDescriptor('testName', 'signing', 'testValue') then: assert keyDescriptor == expected @@ -80,16 +80,16 @@ class EntityDescriptorConversionUtilsTests extends Specification { expected.name = 'testName' when: - def keyDescriptor = EntityDescriptorConversionUtils.createKeyDescriptor('testName', 'both', 'testValue') - def x = openSAMLObjects.marshalToXmlString(keyDescriptor) + def keyDescriptor = createKeyDescriptor('testName', 'both', 'testValue') + openSAMLObjects.marshalToXmlString(keyDescriptor) // not sure why we do this?? then: assert keyDescriptor == expected } def 'test createKeyDescriptor equality'() { when: - def key1 = EntityDescriptorConversionUtils.createKeyDescriptor('test', 'signing', 'test') - def key2 = EntityDescriptorConversionUtils.createKeyDescriptor('test', 'signing', 'test') + def key1 = createKeyDescriptor('test', 'signing', 'test') + def key2 = createKeyDescriptor('test', 'signing', 'test') then: assert key1.equals(key2) @@ -608,7 +608,7 @@ class EntityDescriptorConversionUtilsTests extends Specification { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { it.addKeyDescriptor( - utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) + createKeyDescriptor('test', 'signing', 'test')) it } ) @@ -632,7 +632,7 @@ class EntityDescriptorConversionUtilsTests extends Specification { starter: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(createKeyDescriptor('test', 'signing', 'test')) it } ) @@ -641,8 +641,8 @@ class EntityDescriptorConversionUtilsTests extends Specification { expected: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) - it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test2', 'encryption', 'test2')) + it.addKeyDescriptor(createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(createKeyDescriptor('test2', 'encryption', 'test2')) it } ) @@ -665,8 +665,8 @@ class EntityDescriptorConversionUtilsTests extends Specification { starter: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) - it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test2', 'encryption', 'test2')) + it.addKeyDescriptor(createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(createKeyDescriptor('test2', 'encryption', 'test2')) it } ) @@ -675,7 +675,7 @@ class EntityDescriptorConversionUtilsTests extends Specification { expected: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test2', 'encryption', 'test2')) + it.addKeyDescriptor(createKeyDescriptor('test2', 'encryption', 'test2')) it } ) @@ -695,8 +695,8 @@ class EntityDescriptorConversionUtilsTests extends Specification { starter: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) - it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'encryption', 'test')) + it.addKeyDescriptor(createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(createKeyDescriptor('test', 'encryption', 'test')) it } ) @@ -716,8 +716,8 @@ class EntityDescriptorConversionUtilsTests extends Specification { starter: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) - it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'encryption', 'test')) + it.addKeyDescriptor(createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(createKeyDescriptor('test', 'encryption', 'test')) it } ) diff --git a/backend/src/test/resources/conf/520.xml b/backend/src/test/resources/conf/520.xml index 50efab7e9..57f86a334 100644 --- a/backend/src/test/resources/conf/520.xml +++ b/backend/src/test/resources/conf/520.xml @@ -9,3 +9,4 @@ metadataFile="metadata/metadata.xml" xsi:type="FilesystemMetadataProvider"/> + \ No newline at end of file diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java index b0f92d3ae..ee440c13f 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/AddNewUserFilter.java @@ -67,7 +67,7 @@ User buildAndPersistNewUserFromProfile(CommonProfile profile) { user.setEmailAddress(profile.getEmail()); // get profile attribute for groups - Object obj = profile.getAttribute(simpleProfileMapping.getGroupsName()); + Object obj = profile.getAttribute(simpleProfileMapping.getGroups()); if (obj != null) { final ArrayList groupNames = new ArrayList<>(); if (obj instanceof String) { diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/BetterSAML2Profile.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/BetterSAML2Profile.java index 7bef931f8..38ec220f3 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/BetterSAML2Profile.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/BetterSAML2Profile.java @@ -29,7 +29,7 @@ public String getFirstName() { } public List getGroups() { - return (List) getAttribute(profileMapping.getGroupsName()); + return (List) getAttribute(profileMapping.getGroups()); } @Override diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java index 68b1925fe..41f65d43b 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java @@ -1,16 +1,12 @@ package net.unicon.shibui.pac4j; -import javax.servlet.Filter; - -import org.pac4j.springframework.security.web.CallbackFilter; +import lombok.Getter; +import lombok.Setter; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.stereotype.Component; -import lombok.Getter; -import lombok.Setter; - @Component @ConfigurationProperties(prefix = "shibui.pac4j") @EnableConfigurationProperties @@ -40,7 +36,7 @@ public class Pac4jConfigurationProperties { public static class SimpleProfileMapping { private String email; private String firstName; - private String groupsName; + private String groups; private String lastName; private String username; } diff --git a/pac4j-module/src/main/resources/application.yml b/pac4j-module/src/main/resources/application.yml index 82c3e831d..4dd28dc1f 100644 --- a/pac4j-module/src/main/resources/application.yml +++ b/pac4j-module/src/main/resources/application.yml @@ -6,4 +6,4 @@ shibui: firstName: givenName lastName: sn email: mail - groupsName: urn:oid:1.3.6.1.4.1.5923.1.5.1.1 # attributeId - isMemberOf \ No newline at end of file + groups: urn:oid:1.3.6.1.4.1.5923.1.5.1.1 # attributeId - isMemberOf \ No newline at end of file diff --git a/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy index 5d70c9386..9b8f6131d 100644 --- a/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy +++ b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy @@ -123,7 +123,7 @@ class AddNewUserFilterTests extends Specification { 'FirstName': 'New', 'LastName': 'User', 'Email': 'newuser@institution.edu', - 'GroupsName':'AAAGroup' + 'Groups':'AAAGroup' ].each { key, value -> saml2Profile.getAttribute(profileMapping."get${key}"()) >> [value] } diff --git a/ui/public/assets/schema/roles/role.json b/ui/public/assets/schema/roles/role.json new file mode 100644 index 000000000..8145fae88 --- /dev/null +++ b/ui/public/assets/schema/roles/role.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "title": "label.role-name", + "description": "tooltip.role-name", + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } +} \ No newline at end of file diff --git a/ui/src/app/App.js b/ui/src/app/App.js index 2fb4797b6..e52a45f4a 100644 --- a/ui/src/app/App.js +++ b/ui/src/app/App.js @@ -26,6 +26,7 @@ import { NewProvider } from './metadata/new/NewProvider'; import { Filter } from './metadata/Filter'; import { Contention } from './metadata/contention/ContentionContext'; import { SessionModal } from './core/user/SessionModal'; +import { Roles } from './admin/Roles'; import Button from 'react-bootstrap/Button'; import { Groups } from './admin/Groups'; @@ -66,7 +67,7 @@ function App() { {(message, confirm, confirmCallback, setConfirm, getConfirmation) => - +
@@ -80,6 +81,7 @@ function App() { + diff --git a/ui/src/app/admin/Roles.js b/ui/src/app/admin/Roles.js new file mode 100644 index 000000000..08daed90d --- /dev/null +++ b/ui/src/app/admin/Roles.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { Switch, Route, useRouteMatch, Redirect } from 'react-router-dom'; +import { RolesProvider } from './hoc/RolesProvider'; +import { NewRole } from './container/NewRole'; +import { EditRole } from './container/EditRole'; +import { RoleList } from './container/RoleList'; + +export function Roles() { + + let { path } = useRouteMatch(); + + return ( + <> + + + + {(roles, onDelete) => + + } + + } /> + + + } /> + + + } /> + + + + ); +} \ No newline at end of file diff --git a/ui/src/app/admin/component/RoleForm.js b/ui/src/app/admin/component/RoleForm.js new file mode 100644 index 000000000..074cb5b09 --- /dev/null +++ b/ui/src/app/admin/component/RoleForm.js @@ -0,0 +1,68 @@ +import React from 'react'; +import Button from 'react-bootstrap/Button'; +import Form from '@rjsf/bootstrap-4'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSpinner, faSave } from '@fortawesome/free-solid-svg-icons'; +import Translate from '../../i18n/components/translate'; + +import { useRoleUiSchema } from '../hooks'; +import { fields, widgets } from '../../form/component'; +import { templates } from '../../form/component'; +import { FormContext, setFormDataAction, setFormErrorAction } from '../../form/FormManager'; + +function ErrorListTemplate() { + return (<>); +} + +export function RoleForm({ role = {}, errors = [], loading = false, schema, onSave, onCancel }) { + + const { dispatch } = React.useContext(FormContext); + const onChange = ({ formData, errors }) => { + dispatch(setFormDataAction(formData)); + dispatch(setFormErrorAction(errors)); + }; + + const uiSchema = useRoleUiSchema(); + + return (<> +
+
+ + + + +
+
+
+
+
onChange(form)} + schema={schema} + uiSchema={uiSchema} + FieldTemplate={templates.FieldTemplate} + ObjectFieldTemplate={templates.ObjectFieldTemplate} + ArrayFieldTemplate={templates.ArrayFieldTemplate} + fields={fields} + widgets={widgets} + liveValidate={true} + ErrorList={ErrorListTemplate}> + <> +
+
+
+
+ ) +} +/**/ \ No newline at end of file diff --git a/ui/src/app/admin/component/UserMaintenance.js b/ui/src/app/admin/component/UserMaintenance.js index c4542438f..e9a7b8c33 100644 --- a/ui/src/app/admin/component/UserMaintenance.js +++ b/ui/src/app/admin/component/UserMaintenance.js @@ -23,10 +23,10 @@ export default function UserMaintenance({ users, roles, loading, onDeleteUser, o UserId - Name + Name Email - Role - Group + Role + Group Delete? @@ -36,10 +36,10 @@ export default function UserMaintenance({ users, roles, loading, onDeleteUser, o {users.map((user, idx) => - {user.username} - {user.firstName} {user.lastName} - {user.emailAddress} - + {user.username} + {user.firstName} {user.lastName} + {user.emailAddress} + changeSourceGroup(model, event.target.value)} - value={model.idOfOwner} - disabled={loadingGroups} - disablevalidation="true"> - - {groups.map((g, ridx) => ( - - ))} - - - - } - + + + + } + } {children} - +

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

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

No Filters

-

No filters have been added to this Metadata Provider

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

No Filters

+

No filters have been added to this Metadata Provider

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