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 815360908..4b97efa3e 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy @@ -2,6 +2,7 @@ package edu.internet2.tier.shibboleth.admin.ui.service import com.google.common.base.Predicate import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityRoleWhiteListFilter @@ -20,8 +21,13 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMet import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.TemplateScheme import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.Refilterable +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import edu.internet2.tier.shibboleth.admin.util.OpenSamlChainingMetadataResolverUtil import groovy.util.logging.Slf4j import groovy.xml.DOMBuilder import groovy.xml.MarkupBuilder @@ -34,6 +40,7 @@ import org.opensaml.saml.metadata.resolver.filter.impl.NameIDFormatFilter import org.opensaml.saml.saml2.core.Attribute import org.opensaml.saml.saml2.metadata.EntityDescriptor import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service import org.w3c.dom.Document import javax.annotation.Nonnull @@ -45,11 +52,15 @@ import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBa import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver.ResourceType.SVN @Slf4j +@Service class JPAMetadataResolverServiceImpl implements MetadataResolverService { @Autowired private MetadataResolver metadataResolver + @Autowired + MetadataResolverConverterService metadataResolverConverterService + @Autowired private MetadataResolverRepository metadataResolverRepository @@ -62,6 +73,9 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { @Autowired private ShibUIConfiguration shibUIConfiguration + @Autowired + private UserService userService + // TODO: enhance @Override void reloadFilters(String metadataResolverResourceId) { @@ -162,12 +176,6 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { }(input));""" } - private class ScriptedPredicate extends net.shibboleth.utilities.java.support.logic.ScriptedPredicate { - protected ScriptedPredicate(@Nonnull EvaluableScript theScript) { - super(theScript) - } - } - // TODO: enhance @Override Document generateConfiguration() { @@ -210,6 +218,8 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { return DOMBuilder.newInstance().parseText(writer.toString()) } } + @Autowired + private UserService userService void constructXmlNodeForEntityAttributeNamespaceProtection(def markupBuilderDelegate) { markupBuilderDelegate.MetadataFilter('xsi:type': 'EntityAttributes') { @@ -221,21 +231,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 +269,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 +283,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 +322,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 +464,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 +561,192 @@ 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/auto/MigrationTasksContextLoadedListener.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/MigrationTasksContextLoadedListener.java index 5e6e64b6c..9fc942dc8 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/MigrationTasksContextLoadedListener.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/MigrationTasksContextLoadedListener.java @@ -44,7 +44,7 @@ public void onApplicationEvent(ContextRefreshedEvent event) { } @Transactional - private void doshibui_1740_migration() { + void doshibui_1740_migration() { groupService.ensureAdminGroupExists(); // do first // SHIBUI-1740: Adding admin group to all existing entity descriptors that do not have a group already. diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java index 3d66de957..8617bfa3b 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java @@ -9,7 +9,6 @@ import edu.internet2.tier.shibboleth.admin.ui.security.springsecurity.AdminUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -27,9 +26,8 @@ import org.springframework.security.web.firewall.StrictHttpFirewall; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import java.util.Collections; - import javax.transaction.Transactional; +import java.util.Collections; /** * Web security configuration. @@ -38,20 +36,32 @@ @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) public class WebSecurityConfig { - @Value("${shibui.logout-url:/dashboard}") - private String logoutUrl; + @Value("${shibui.roles.authenticated}") + private String[] acceptedAuthenticationRoles; @Value("${shibui.default-password:}") private String defaultPassword; + @Value("${shibui.logout-url:/dashboard}") + private String logoutUrl; + + @Autowired + private RoleRepository roleRepository; + + @Value("${shibui.default-rootuser:root}") + private String rootUser; + @Autowired private UserRepository userRepository; @Autowired private UserService userService; - - @Autowired - private RoleRepository roleRepository; + + @Bean + @Profile("!no-auth") + public AdminUserService adminUserService(UserRepository userRepository) { + return new AdminUserService(userRepository); + } private HttpFirewall allowUrlEncodedSlashHttpFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); @@ -60,8 +70,10 @@ private HttpFirewall allowUrlEncodedSlashHttpFirewall() { return firewall; } - private HttpFirewall defaultFirewall() { - return new DefaultHttpFirewall(); + @Bean + @Profile("!no-auth") + public AuditorAware defaultAuditorAware() { + return new DefaultAuditorAware(); } @Bean @@ -76,7 +88,7 @@ protected void configure(HttpSecurity http) throws Exception { .and() .authorizeRequests() .antMatchers("/unsecured/**/*").permitAll() - .anyRequest().hasAnyRole("USER", "ADMIN") + .anyRequest().hasAnyRole(acceptedAuthenticationRoles) .and() .exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> response.sendRedirect("/unsecured/error.html")) .and() @@ -92,9 +104,9 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); if (defaultPassword != null && !"".equals(defaultPassword)) { // TODO: yeah, this isn't good, but we gotta initialize this user for now - User adminUser = userRepository.findByUsername("root").orElseGet(() ->{ + User adminUser = userRepository.findByUsername(rootUser).orElseGet(() ->{ User u = new User(); - u.setUsername("root"); + u.setUsername(rootUser); u.setPassword(defaultPassword); u.setFirstName("admin"); u.setLastName("user"); @@ -127,16 +139,8 @@ public void configure(WebSecurity web) throws Exception { }; } - @Bean - @Profile("!no-auth") - public AuditorAware defaultAuditorAware() { - return new DefaultAuditorAware(); - } - - @Bean - @Profile("!no-auth") - public AdminUserService adminUserService(UserRepository userRepository) { - return new AdminUserService(userRepository); + private HttpFirewall defaultFirewall() { + return new DefaultHttpFirewall(); } @Bean @@ -157,5 +161,4 @@ public void configure(WebSecurity web) throws Exception { } }; } -} - +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java new file mode 100644 index 000000000..60705cd15 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java @@ -0,0 +1,65 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import javax.script.ScriptException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; +import edu.internet2.tier.shibboleth.admin.ui.service.FilterService; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; + +@RestController +@RequestMapping("/api/activate") +public class ActivateController { + + @Autowired + private EntityDescriptorService entityDescriptorService; + + @Autowired + private FilterService filterService; + + @Autowired + private MetadataResolverService metadataResolverService; + + @PatchMapping(path = "/entityDescriptor/{resourceId}/{mode}") + @Transactional + public ResponseEntity enableEntityDescriptor(@PathVariable String resourceId, @PathVariable String mode) throws EntityNotFoundException, ForbiddenException { + boolean status = "enable".equalsIgnoreCase(mode); + EntityDescriptorRepresentation edr = entityDescriptorService.updateEntityDescriptorEnabledStatus(resourceId, status); + return ResponseEntity.ok(edr); + } + + @PatchMapping(path = "/MetadataResolvers/{metadataResolverId}/Filter/{resourceId}/{mode}") + @Transactional + public ResponseEntity enableFilter(@PathVariable String metadataResolverId, @PathVariable String resourceId, @PathVariable String mode) throws EntityNotFoundException, ForbiddenException, ScriptException { + boolean status = "enable".equalsIgnoreCase(mode); + MetadataFilter persistedFilter = filterService.updateFilterEnabledStatus(metadataResolverId, resourceId, status); + return ResponseEntity.ok(persistedFilter); + } + + @PatchMapping("/MetadataResolvers/{resourceId}/{mode}") + @Transactional + public ResponseEntity enableProvider(@PathVariable String resourceId, @PathVariable String mode) throws EntityNotFoundException, ForbiddenException, MetadataFileNotFoundException, InitializationException { + boolean status = "enable".equalsIgnoreCase(mode); + MetadataResolver existingResolver = metadataResolverService.findByResourceId(resourceId); + existingResolver.setEnabled(status); + existingResolver = metadataResolverService.updateMetadataResolverEnabledStatus(existingResolver); + + return ResponseEntity.ok(existingResolver); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java new file mode 100644 index 000000000..0c766c53c --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java @@ -0,0 +1,48 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + +import javax.script.ScriptException; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException; + +@ControllerAdvice(assignableTypes = {ActivateController.class}) +public class ActivateExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler({ EntityNotFoundException.class }) + public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(HttpStatus.NOT_FOUND, e.getMessage())); + } + + @ExceptionHandler({ ForbiddenException.class }) + public ResponseEntity handleForbiddenAccess(ForbiddenException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(String.valueOf(HttpStatus.FORBIDDEN.value()), e.getMessage())); + } + + @ExceptionHandler({ InitializationException.class }) + public ResponseEntity handleInitializationException(InitializationException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), e.getMessage())); + } + + @ExceptionHandler({ MetadataFileNotFoundException.class }) + public ResponseEntity handleMetadataFileNotFoundException(MetadataFileNotFoundException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse(INTERNAL_SERVER_ERROR.toString(), e.getLocalizedMessage())); + } + + @ExceptionHandler({ ScriptException.class }) + public ResponseEntity handleScriptException(ScriptException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), e.getMessage())); + } + + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ActivatableType.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ActivatableType.java new file mode 100644 index 000000000..8042d56d4 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ActivatableType.java @@ -0,0 +1,5 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +public enum ActivatableType { + ENTITY_DESCRIPTOR, METADATA_RESOLVER, FILTER +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java index a3acff06b..185b43918 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java @@ -33,11 +33,12 @@ import java.util.UUID; import java.util.stream.Collectors; +import static edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType.ENTITY_DESCRIPTOR; @Entity @EqualsAndHashCode(callSuper = true) @Audited -public class EntityDescriptor extends AbstractDescriptor implements org.opensaml.saml.saml2.metadata.EntityDescriptor, Ownable { +public class EntityDescriptor extends AbstractDescriptor implements org.opensaml.saml.saml2.metadata.EntityDescriptor, Ownable, IActivatable { @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "entitydesc_addlmetdatlocations_id") @OrderColumn @@ -47,7 +48,7 @@ public class EntityDescriptor extends AbstractDescriptor implements org.opensaml @OneToOne(cascade = CascadeType.ALL) @NotAudited private AffiliationDescriptor affiliationDescriptor; - + @OneToOne(cascade = CascadeType.ALL) @NotAudited private AttributeAuthorityDescriptor attributeAuthorityDescriptor; @@ -61,7 +62,7 @@ public class EntityDescriptor extends AbstractDescriptor implements org.opensaml private List contactPersons = new ArrayList<>(); private String entityID; - + private String localId; @OneToOne(cascade = CascadeType.ALL) @@ -70,7 +71,7 @@ public class EntityDescriptor extends AbstractDescriptor implements org.opensaml @Getter @Setter private String idOfOwner; - + @OneToOne(cascade = CascadeType.ALL) @NotAudited private PDPDescriptor pdpDescriptor; @@ -254,6 +255,10 @@ public void setEntityID(String entityID) { this.entityID = entityID; } + public void setEnabled(Boolean serviceEnabled) { + this.serviceEnabled = (serviceEnabled == null) ? false : serviceEnabled; + } + @Override public void setID(String id) { this.localId = id; @@ -296,12 +301,16 @@ public String toString() { .add("id", id) .toString(); } - + public String getObjectId() { return entityID; } - + public OwnableType getOwnableType() { return OwnableType.ENTITY_DESCRIPTOR; } -} + + @Override public ActivatableType getActivatableType() { + return ENTITY_DESCRIPTOR; + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IActivatable.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IActivatable.java new file mode 100644 index 000000000..84a31078a --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IActivatable.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +public interface IActivatable { + ActivatableType getActivatableType(); + + void setEnabled(Boolean enabled); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java index 9363f5711..548c97356 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java @@ -1,10 +1,9 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.filters; -import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.*; import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; +import edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType; +import edu.internet2.tier.shibboleth.admin.ui.domain.IActivatable; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -20,6 +19,8 @@ import javax.persistence.Transient; import java.util.UUID; +import static edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType.*; + /** * Domain class to store information about {@link org.opensaml.saml.metadata.resolver.filter.MetadataFilter} */ @@ -38,22 +39,27 @@ @JsonSubTypes.Type(value=NameIdFormatFilter.class, name="NameIDFormat")}) @Audited @AuditOverride(forClass = AbstractAuditable.class) -public abstract class MetadataFilter extends AbstractAuditable implements IConcreteMetadataFilterType { +public abstract class MetadataFilter extends AbstractAuditable implements IConcreteMetadataFilterType, IActivatable { - @JsonProperty("@type") - @Transient - String type; + private boolean filterEnabled; private String name; @Column(unique=true) private String resourceId = UUID.randomUUID().toString(); - private boolean filterEnabled; + @JsonProperty("@type") + @Transient + String type; @Transient private transient Integer version; + @JsonIgnore + public ActivatableType getActivatableType() { + return FILTER; + } + @JsonGetter("version") public int getVersion() { if (version != null && version != 0) { @@ -61,4 +67,8 @@ public int getVersion() { } return this.hashCode(); } -} + + public void setEnabled(Boolean serviceEnabled) { + this.filterEnabled = (serviceEnabled == null) ? false : serviceEnabled; + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java index f9347fab3..63a04d764 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java @@ -6,6 +6,9 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; +import edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType; +import static edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType.METADATA_RESOLVER; +import edu.internet2.tier.shibboleth.admin.ui.domain.IActivatable; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; import lombok.EqualsAndHashCode; @@ -43,7 +46,7 @@ @JsonSubTypes.Type(value = ResourceBackedMetadataResolver.class, name = "ResourceBackedMetadataResolver")}) @Audited @AuditOverride(forClass = AbstractAuditable.class) -public class MetadataResolver extends AbstractAuditable { +public class MetadataResolver extends AbstractAuditable implements IActivatable { @JsonProperty("@type") @Transient @@ -84,6 +87,28 @@ public class MetadataResolver extends AbstractAuditable { @Transient private Integer version; + public void addFilter(MetadataFilter metadataFilter) { + //To make sure that Spring Data auditing infrastructure recognizes update and "touched" modifiedDate + markAsModified(); + this.metadataFilters.add(metadataFilter); + } + + public void entityAttributesFilterIntoTransientRepresentation() { + //expose explicit API to call to convert into transient representation + //used in unit/integration tests where JPA's @PostLoad callback execution engine is not available + this.metadataFilters + .stream() + .filter(EntityAttributesFilter.class::isInstance) + .map(EntityAttributesFilter.class::cast) + .forEach(EntityAttributesFilter::intoTransientRepresentation); + } + + @Override + @JsonIgnore + public ActivatableType getActivatableType() { + return METADATA_RESOLVER; + } + public Boolean getDoInitialization() { return doInitialization == null ? false : doInitialization; } @@ -96,23 +121,7 @@ public int getVersion() { return this.hashCode(); } - public void addFilter(MetadataFilter metadataFilter) { - //To make sure that Spring Data auditing infrastructure recognizes update and "touched" modifiedDate - markAsModified(); - this.metadataFilters.add(metadataFilter); - } - public void markAsModified() { this.versionModifiedTimestamp = System.currentTimeMillis(); } - - public void entityAttributesFilterIntoTransientRepresentation() { - //expose explicit API to call to convert into transient representation - //used in unit/integration tests where JPA's @PostLoad callback execution engine is not available - this.metadataFilters - .stream() - .filter(EntityAttributesFilter.class::isInstance) - .map(EntityAttributesFilter.class::cast) - .forEach(EntityAttributesFilter::intoTransientRepresentation); - } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java index 6769aaa5f..4d0009523 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java @@ -4,4 +4,4 @@ public class EntityNotFoundException extends Exception { public EntityNotFoundException(String message) { super(message); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InitializationException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InitializationException.java new file mode 100644 index 000000000..f18483176 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InitializationException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class InitializationException extends Exception { + public InitializationException(Exception e) { + super(e); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java new file mode 100644 index 000000000..d576e0630 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java @@ -0,0 +1,74 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.controller; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IRolesService; + +@RestController +@RequestMapping("/api/admin/roles") +public class RolesController { + @Autowired + private IRolesService rolesService; + + @Secured("ROLE_ADMIN") + @PostMapping + @Transactional + public ResponseEntity create(@RequestBody Role role) throws RoleExistsConflictException { + Role result = rolesService.createRole(role); + return ResponseEntity.status(HttpStatus.CREATED).body(result); + } + + @Secured("ROLE_ADMIN") + @DeleteMapping("/{resourceId}") + @Transactional + public ResponseEntity delete(@PathVariable String resourceId) throws EntityNotFoundException, RoleDeleteException { + rolesService.deleteDefinition(resourceId); + return ResponseEntity.noContent().build(); + } + + @GetMapping + @Transactional(readOnly = true) + public ResponseEntity getAll() { + return ResponseEntity.ok(rolesService.findAll()); + } + + @GetMapping("/{resourceId}") + @Transactional(readOnly = true) + public ResponseEntity getOne(@PathVariable String resourceId) throws EntityNotFoundException { + Role role = rolesService.findByResourceId(resourceId); + return ResponseEntity.ok(role); + } + + @Secured("ROLE_ADMIN") + @PutMapping(path = {"/", "/{resourceId}" }) + @Transactional + public ResponseEntity update(@RequestBody Role incomingRoleDetail, @PathVariable Optional resourceId) throws EntityNotFoundException { + Role updateRole; + if (resourceId.isPresent()) { + updateRole = rolesService.findByResourceId(resourceId.get()); + } else { + updateRole = rolesService.findByResourceId(incomingRoleDetail.getResourceId()); + } + updateRole.setName(incomingRoleDetail.getName()); + Role result = rolesService.updateRole(updateRole); + return ResponseEntity.ok(result); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesExceptionHandler.java new file mode 100644 index 000000000..e4b840f1a --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesExceptionHandler.java @@ -0,0 +1,38 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.controller; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import edu.internet2.tier.shibboleth.admin.ui.controller.ErrorResponse; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; + +@ControllerAdvice(assignableTypes = {RolesController.class}) +public class RolesExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler({ EntityNotFoundException.class }) + public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(HttpStatus.NOT_FOUND, e.getMessage())); + } + + @ExceptionHandler({ RoleDeleteException.class }) + public ResponseEntity handleForbiddenAccess(RoleDeleteException e, WebRequest request) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/admin/role/{resourceId}").build().toUri()); + return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers) + .body(new ErrorResponse(String.valueOf(HttpStatus.CONFLICT.value()), e.getMessage())); + + } + + @ExceptionHandler({RoleExistsConflictException.class}) + public ResponseEntity handleRoleExistsConflictException(RoleExistsConflictException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponse(HttpStatus.CONFLICT, e.getMessage())); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleDeleteException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleDeleteException.java new file mode 100644 index 000000000..a7a35ab4f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleDeleteException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.exception; + +public class RoleDeleteException extends Exception { + public RoleDeleteException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleExistsConflictException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleExistsConflictException.java new file mode 100644 index 000000000..f5364c6ff --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleExistsConflictException.java @@ -0,0 +1,9 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.exception; + +public class RoleExistsConflictException extends Exception { + + public RoleExistsConflictException(String message) { + super(message); + } + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java index c56c403ab..29016d8cc 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 @@ -29,7 +29,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/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 f6ae4dfd8..df200f482 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java @@ -1,18 +1,10 @@ package edu.internet2.tier.shibboleth.admin.ui.security.service; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; - -import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException; -import org.apache.commons.lang.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; +import edu.internet2.tier.shibboleth.admin.ui.domain.IActivatable; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException; import edu.internet2.tier.shibboleth.admin.ui.security.exception.OwnershipConflictException; import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownable; @@ -23,48 +15,89 @@ import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository; import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository; import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; +import static edu.internet2.tier.shibboleth.admin.ui.security.service.UserAccess.ADMIN; +import static edu.internet2.tier.shibboleth.admin.ui.security.service.UserAccess.GROUP; +import static edu.internet2.tier.shibboleth.admin.ui.security.service.UserAccess.NONE; import lombok.NoArgsConstructor; +import org.apache.commons.lang.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; @Service @NoArgsConstructor public class UserService { @Autowired private IGroupService groupService; - + @Autowired private OwnershipRepository ownershipRepository; - + @Autowired private RoleRepository roleRepository; - + @Autowired private UserRepository userRepository; - + public UserService(IGroupService groupService, OwnershipRepository ownershipRepository, RoleRepository roleRepository, UserRepository userRepository) { this.groupService = groupService; this.ownershipRepository = ownershipRepository; this.roleRepository = roleRepository; this.userRepository = userRepository; } - + + public boolean currentUserCanEnable(IActivatable activatableObject) { + if (currentUserIsAdmin()) { return true; } + switch (activatableObject.getActivatableType()) { + case ENTITY_DESCRIPTOR: { + return currentUserHasExpectedRole(Arrays.asList("ROLE_ENABLE" )) && getCurrentUserGroup().getOwnerId().equals(((EntityDescriptor) activatableObject).getIdOfOwner()); + } + // Currently filters and providers dont have ownership, so we just look for the right role + case FILTER: + case METADATA_RESOLVER: + return currentUserHasExpectedRole(Arrays.asList("ROLE_ENABLE" )); + default: + return false; + } + } + + /** + * This basic logic assumes users only have a single role (despite users having a list of roles, we assume only 1 currently) + */ + private boolean currentUserHasExpectedRole(List acceptedRoles) { + User user = getCurrentUser(); + return acceptedRoles.contains(user.getRole()); + } + public boolean currentUserIsAdmin() { User user = getCurrentUser(); return user != null && user.getRole().equals("ROLE_ADMIN"); } - + @Transactional public void delete(String username) throws EntityNotFoundException, OwnershipConflictException { Optional userToRemove = userRepository.findByUsername(username); if (userToRemove.isEmpty()) throw new EntityNotFoundException("User does not exist"); - if (!ownershipRepository.findOwnedByUser(username).isEmpty()) throw new OwnershipConflictException("User ["+username+"] has ownership of entities in the system. Please remove all items before attemtping to delete the user."); - + if (!ownershipRepository.findOwnedByUser(username).isEmpty()) throw new OwnershipConflictException("User ["+username+"] has ownership of entities in the system. Please remove all items before attempting to delete the user."); + // ok, user exists and doesn't own anything in the system, so delete them // If the user is owned by anything, clear that first ownershipRepository.clearUsersGroups(username); - User user = userToRemove.get(); + User user = userToRemove.get(); userRepository.delete(user); } - + + public Optional findByUsername(String username) { + return userRepository.findByUsername(username); + } + public User getCurrentUser() { //TODO: Consider returning an Optional here User user = null; @@ -83,17 +116,17 @@ public User getCurrentUser() { public UserAccess getCurrentUserAccess() { User user = getCurrentUser(); if (user == null) { - return UserAccess.NONE; + return NONE; } if (user.getRole().equals("ROLE_ADMIN")) { - return UserAccess.ADMIN; + return ADMIN; } - if (user.getRole().equals("ROLE_USER")) { - return UserAccess.GROUP; + if (user.getRole().equals("ROLE_USER") || user.getRole().equals("ROLE_ENABLE")) { + return GROUP; } - return UserAccess.NONE; + return NONE; } - + public Group getCurrentUserGroup() { switch (getCurrentUserAccess()) { case ADMIN: @@ -102,16 +135,15 @@ public Group getCurrentUserGroup() { return getCurrentUser().getGroup(); } } - + public Set getUserRoles(String username) { Optional user = userRepository.findByUsername(username); HashSet result = new HashSet<>(); - if (user.isPresent() ) { - user.get().getRoles().forEach(role -> result.add(role.getName())); - } + user.ifPresent(value -> value.getRoles().forEach(role -> result.add(role.getName()))); return result; } + // @TODO - probably delegate this out to something plugable at some point public boolean isAuthorizedFor(Ownable ownableObject) { switch (getCurrentUserAccess()) { case ADMIN: // Pure admin is authorized to do anything @@ -129,7 +161,7 @@ public boolean isAuthorizedFor(Ownable ownableObject) { return false; } } - + /** * Creating users should always have a group. If the user isn't assigned to a group, create one based on their name. * If the user has the ADMIN role, they are always solely assigned to the admin group. @@ -164,7 +196,7 @@ public User save(User user) { if (g == null) { try { Group newGroup = ug; - Ownership o = ownershipRepository.saveAndFlush(new Ownership(newGroup, user)); + ownershipRepository.saveAndFlush(new Ownership(newGroup, user)); g = groupService.createGroup(newGroup); } catch (GroupExistsConflictException | InvalidGroupRegexException e) { @@ -177,14 +209,13 @@ public User save(User user) { } return userRepository.saveAndFlush(user); } - + /** * Given a user with a defined User.role, update the User.roles collection with that role. * * This currently exists because users should only ever have one role in the system at this time. However, user * roles are persisted as a set of roles (for future-proofing). Once we start allowing a user to have multiple roles, * this method and User.role can go away. - * @param user */ public void updateUserRole(User user) { if (StringUtils.isNotBlank(user.getRole())) { @@ -197,7 +228,7 @@ public void updateUserRole(User user) { throw new RuntimeException(String.format("User with username [%s] is defined with role [%s] which does not exist in the system!", user.getUsername(), user.getRole())); } } else { - throw new RuntimeException(String.format("User with username [%s] has no role defined and therefor cannot be updated!", user.getUsername())); + throw new RuntimeException(String.format("User with username [%s] has no role defined and therefore cannot be updated!", user.getUsername())); } } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java index 0d070d43d..9928eefc4 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java @@ -70,7 +70,7 @@ EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepres * @return a list of EntityDescriptorRepresentations that a user has the rights to access */ List getAllRepresentationsBasedOnUserAccess() throws ForbiddenException; - + /** * Given a list of attributes, generate an AttributeReleaseList * @@ -113,4 +113,5 @@ EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRepresent */ void updateDescriptorFromRepresentation(final org.opensaml.saml.saml2.metadata.EntityDescriptor entityDescriptor, final EntityDescriptorRepresentation representation); + EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String resourceId, boolean status) throws EntityNotFoundException, ForbiddenException; } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java index 2ef9ab08e..6d752928b 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java @@ -1,7 +1,12 @@ package edu.internet2.tier.shibboleth.admin.ui.service; +import javax.script.ScriptException; + import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; /** * Main backend facade API that defines operations pertaining to manipulating {@link EntityAttributesFilter} objects. @@ -25,4 +30,6 @@ public interface FilterService { * @return FilterRepresentation front end representation */ FilterRepresentation createRepresentationFromFilter(final EntityAttributesFilter entityAttributesFilter); + + MetadataFilter updateFilterEnabledStatus(String metadataResolverId, String resourceId, boolean status) throws EntityNotFoundException, ForbiddenException, ScriptException; } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java index 4fbdbe004..04ac48d02 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java @@ -1,7 +1,21 @@ package edu.internet2.tier.shibboleth.admin.ui.service; -import edu.internet2.tier.shibboleth.admin.ui.domain.*; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.*; +import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributes; +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; +import edu.internet2.tier.shibboleth.admin.ui.domain.IRelyingPartyOverrideProperty; +import edu.internet2.tier.shibboleth.admin.ui.domain.KeyDescriptor; +import edu.internet2.tier.shibboleth.admin.ui.domain.UIInfo; +import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean; +import edu.internet2.tier.shibboleth.admin.ui.domain.XSInteger; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ContactRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.LogoutEndpointRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.MduiRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSsoDescriptorRepresentation; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; @@ -9,41 +23,56 @@ import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Owner; +import edu.internet2.tier.shibboleth.admin.ui.security.model.OwnerType; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership; import edu.internet2.tier.shibboleth.admin.ui.security.model.User; import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository; import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.setupACSs; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.setupContacts; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.setupLogout; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.setupOrganization; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.setupRelyingPartyOverrides; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.setupSPSSODescriptor; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.setupSecurity; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.setupUIInfo; import edu.internet2.tier.shibboleth.admin.util.MDDCConstants; import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; +import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getStringListOfAttributeValues; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.util.*; +import java.util.ArrayList; +import java.util.ConcurrentModificationException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; -import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.*; -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getStringListOfAttributeValues; - @Slf4j @Service public class JPAEntityDescriptorServiceImpl implements EntityDescriptorService { @Autowired - EntityDescriptorRepository entityDescriptorRepository; - + private EntityDescriptorRepository entityDescriptorRepository; + @Autowired IGroupService groupService; - + @Autowired private OpenSamlObjects openSamlObjects; - + @Autowired private OwnershipRepository ownershipRepository; @Autowired - UserService userService; - + private UserService userService; + private EntityDescriptor buildDescriptorFromRepresentation(final EntityDescriptor ed, final EntityDescriptorRepresentation representation) { ed.setEntityID(representation.getEntityId()); ed.setIdOfOwner(representation.getIdOfOwner()); @@ -77,14 +106,14 @@ public EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, EntityIdExistsException, InvalidPatternMatchException { return createNew(createRepresentationFromDescriptor(ed)); } - + @Override public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) throws ForbiddenException, EntityIdExistsException, InvalidPatternMatchException { if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { throw new ForbiddenException("You do not have the permissions necessary to enable this service."); } - + if (entityDescriptorRepository.findByEntityID(edRep.getEntityId()) != null) { throw new EntityIdExistsException(edRep.getEntityId()); } @@ -95,7 +124,10 @@ public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation e validateEntityIdAndACSUrls(edRep); EntityDescriptor ed = (EntityDescriptor) createDescriptorFromRepresentation(edRep); - ed.setIdOfOwner(ownerId); + ed.setIdOfOwner(userService.getCurrentUserGroup().getOwnerId()); + + ownershipRepository.deleteEntriesForOwnedObject(ed); + ownershipRepository.save(new Ownership(userService.getCurrentUserGroup(), ed)); return createRepresentationFromDescriptor(entityDescriptorRepository.save(ed)); } @@ -378,7 +410,7 @@ public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRe if (existingEd == null) { throw new EntityNotFoundException(String.format("The entity descriptor with entity id [%s] was not found for update.", edRep.getId())); } - if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { + if (edRep.isServiceEnabled() && !userService.currentUserCanEnable(existingEd)) { throw new ForbiddenException("You do not have the permissions necessary to enable this service."); } if (StringUtils.isEmpty(edRep.getIdOfOwner())) { @@ -394,10 +426,17 @@ public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRe validateEntityIdAndACSUrls(edRep); updateDescriptorFromRepresentation(existingEd, edRep); - return createRepresentationFromDescriptor(entityDescriptorRepository.save(existingEd)); + existingEd = entityDescriptorRepository.save(existingEd); + ownershipRepository.deleteEntriesForOwnedObject(existingEd); + ownershipRepository.save(new Ownership(new Owner() { + public String getOwnerId() { return edRep.getIdOfOwner(); } + public OwnerType getOwnerType() { return OwnerType.GROUP; } + }, existingEd)); + return createRepresentationFromDescriptor(existingEd); } @Override + // This should be private, but we use it in a couple different test classes not sure we should keep... public void updateDescriptorFromRepresentation(org.opensaml.saml.saml2.metadata.EntityDescriptor entityDescriptor, EntityDescriptorRepresentation representation) { if (!(entityDescriptor instanceof EntityDescriptor)) { throw new UnsupportedOperationException("not yet implemented"); @@ -405,6 +444,20 @@ public void updateDescriptorFromRepresentation(org.opensaml.saml.saml2.metadata. buildDescriptorFromRepresentation((EntityDescriptor) entityDescriptor, representation); } + @Override + public EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String resourceId, boolean status) throws EntityNotFoundException, ForbiddenException { + EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); + if (ed == null) { + throw new EntityNotFoundException("Entity with resourceid[" + resourceId + "] was not found for update"); + } + if (!userService.currentUserCanEnable(ed)) { + throw new ForbiddenException("You do not have the permissions necessary to change the enable status of this entity descriptor."); + } + ed.setServiceEnabled(status); + ed = entityDescriptorRepository.save(ed); + return createRepresentationFromDescriptor(ed); + } + private void validateEntityIdAndACSUrls(EntityDescriptorRepresentation edRep) throws InvalidPatternMatchException { // Check the entity id first if (!groupService.doesStringMatchGroupPattern(edRep.getIdOfOwner(), edRep.getEntityId())) { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java index 0693b538a..c42bd7cad 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java @@ -1,13 +1,26 @@ package edu.internet2.tier.shibboleth.admin.ui.service; +import edu.internet2.tier.shibboleth.admin.ui.domain.IActivatable; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; + import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.interceptor.TransactionAspectSupport; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Optional; + +import javax.script.ScriptException; /** * Default implementation of {@link FilterService} @@ -15,19 +28,29 @@ * @since 1.0 * @author Bill Smith (wsmith@unicon.net) */ +@Service public class JPAFilterServiceImpl implements FilterService { - - private static final Logger LOGGER = LoggerFactory.getLogger(JPAFilterServiceImpl.class); - @Autowired EntityDescriptorService entityDescriptorService; - + @Autowired EntityService entityService; + @Autowired + FilterRepository filterRepository; + @Autowired FilterTargetService filterTargetService; + + @Autowired + private MetadataResolverRepository metadataResolverRepository; + + @Autowired + private MetadataResolverService metadataResolverService; + @Autowired + private UserService userService; + @Override public EntityAttributesFilter createFilterFromRepresentation(FilterRepresentation representation) { //TODO? use OpenSamlObjects.buildDefaultInstanceOfType(EntityAttributesFilter.class)? @@ -66,4 +89,52 @@ public FilterRepresentation createRepresentationFromFilter(EntityAttributesFilte representation.setVersion(entityAttributesFilter.hashCode()); return representation; } -} + + private void reloadFiltersAndHandleScriptException(String resolverResourceId) throws ScriptException { + try { + metadataResolverService.reloadFilters(resolverResourceId); + } catch (Throwable ex) { + //explicitly mark transaction for rollback when we get ScriptException as we call reloadFilters + //after persistence call. Then re-throw the exception with pertinent message + if (ex instanceof ScriptException) { + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + throw new ScriptException("Caught invalid script parsing error when reloading filters. Please fix the script data"); + } + } + } + + /** + * Logic taken directly from the MetadataFiltersController and then modified slightly. + */ + @Override + public MetadataFilter updateFilterEnabledStatus(String metadataResolverId, String resourceId, boolean status) + throws EntityNotFoundException, ForbiddenException, ScriptException { + + MetadataResolver metadataResolver = metadataResolverRepository.findByResourceId(metadataResolverId); + // Now we operate directly on the filter attached to MetadataResolver, + // Instead of fetching filter separately, to accommodate correct envers versioning with uni-directional one-to-many + Optional filterTobeUpdatedOptional = metadataResolver.getMetadataFilters().stream() + .filter(it -> it.getResourceId().equals(resourceId)).findFirst(); + if (filterTobeUpdatedOptional.isEmpty()) { + throw new EntityNotFoundException("Filter with resource id[" + resourceId + "] not found"); + } + + MetadataFilter filterTobeUpdated = filterTobeUpdatedOptional.get(); + + if (!userService.currentUserCanEnable(filterTobeUpdated)) { + throw new ForbiddenException("You do not have the permissions necessary to change the enable status of this filter."); + } + + filterTobeUpdated.setFilterEnabled(status); + MetadataFilter persistedFilter = filterRepository.save(filterTobeUpdated); + + // To support envers versioning from MetadataResolver side + metadataResolver.markAsModified(); + metadataResolverRepository.save(metadataResolver); + + // TODO: do we need to reload filters here? + reloadFiltersAndHandleScriptException(metadataResolver.getResourceId()); + + return persistedFilter; + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java index ee5f8cec3..2343206a7 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverConverterServiceImpl.java @@ -22,6 +22,7 @@ import org.opensaml.saml.metadata.resolver.MetadataResolver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; import java.io.File; import java.io.FileNotFoundException; @@ -32,6 +33,7 @@ /** * @author Bill Smith (wsmith@unicon.net) */ +@Service public class MetadataResolverConverterServiceImpl implements MetadataResolverConverterService { @Autowired diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java index 376c34aa5..5fd205c20 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java @@ -2,8 +2,18 @@ import org.w3c.dom.Document; +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException; + public interface MetadataResolverService { - public void reloadFilters(String metadataResolverName); + public MetadataResolver findByResourceId(String resourceId) throws EntityNotFoundException; public Document generateConfiguration(); + + public void reloadFilters(String metadataResolverName); + + public MetadataResolver updateMetadataResolverEnabledStatus(MetadataResolver existingResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException; } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 83f2635e0..5b7b801f1 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -20,6 +20,7 @@ spring.datasource.platform=h2 spring.datasource.driverClassName=org.h2.Driver spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.h2.console.enabled=true +spring.h2.console.settings.web-allow-others=true # spring.jackson.default-property-inclusion=non_absent spring.jackson.default-property-inclusion=NON_NULL @@ -58,7 +59,10 @@ shibui.logout-url=/dashboard # spring.profiles.active=default -#shibui.default-password= +## Default root user can be set in application.yml or here - setting in both places can be undeterministic +## Default password must be set for the default user to be configured and setup +#shibui.default-password={noop}somepassword +shibui.default-rootuser=root shibui.metadata-sources-ui-schema-location=classpath:metadata-sources-ui-schema.json shibui.entity-attributes-filters-ui-schema-location=classpath:entity-attributes-filters-ui-schema.json @@ -87,7 +91,11 @@ shibui.mail.text-email-template-path-prefix=/mail/text/ shibui.mail.html.email-template-path-prefix=/mail/html/ shibui.mail.system-email-address=doNotReply@shibui.org -shibui.roles=ROLE_ADMIN,ROLE_USER,ROLE_NONE + +#ShibUIConfiguration slurps in these values and they are bootstrapped in on startup +shibui.roles=ROLE_ADMIN,ROLE_ENABLE,ROLE_USER,ROLE_NONE +#Authenticated access roles - used by Spring Security to allow access when authenticated +shibui.roles.authenticated=ADMIN,ENABLE,USER #In order to enable authentication via configured pac4j library (with external SAMl Idp, for example) #This property must be set to true and pac4j properties configured. For sample pac4j properties, see application.yml @@ -97,4 +105,4 @@ shibui.roles=ROLE_ADMIN,ROLE_USER,ROLE_NONE #This property must be set to true in order to enable posting stats to beacon endpoint. Furthermore, appropriate #environment variables must be set for beacon publisher to be used (the ones that are set when running shib-ui in #docker container -shibui.beacon-enabled=true +shibui.beacon-enabled=true \ No newline at end of file diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index e9301289a..74ae43689 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1,4 +1,8 @@ #shibui: +## Default password must be set for the default user to be configured and setup +# default-rootuser:root +## need to include the encoding for the password - be sure to quote the entire value as shown +# default-password: "{noop}foopassword" # pac4j-enabled: true # pac4j: # keystorePath: "/etc/shibui/samlKeystore.jks" diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 9100dd04c..54ad56fe2 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -66,6 +66,12 @@ action.add-attribute=Add Attribute action.custom-entity-attributes=Custom Entity Attributes action.groups=Groups action.source-group=Group +action.enable=Enable +action.disable=Disable + +action.add-new-role=Add new role +action.roles=Roles +action.source-role=Role value.enabled=Enabled value.disabled=Disabled @@ -100,7 +106,6 @@ value.dynamic-http-metadata-provider=DynamicHttpMetadataProvider value.entity-attributes-filter=EntityAttributes Filter value.spdescriptor=SPSSODescriptor value.attr-auth-descriptor=AttributeAuthorityDescriptor -value.local-dynamic-metadata-provider=LocalDynamicMetadataProvider value.md-query-protocol=MetadataQueryProtocol value.template=Template @@ -427,7 +432,6 @@ label.attribute-eduPersonUniqueId=eduPersonUniqueId label.attribute-employeeNumber=employeeNumber label.force-authn=Force AuthN -label.dynamic-attributes=Dynamic Attributes label.min-cache-duration=Min Cache Duration label.max-cache-duration=Max Cache Duration label.max-idle-entity-data=Max Idle Entity Data @@ -458,7 +462,6 @@ label.admin=Admin label.user-maintenance=User Maintenance label.user-id=UserId label.email=Email -label.role=Role label.delete=Delete? label.title=Title @@ -494,6 +497,16 @@ tooltip.url-validation-regex=URL validation regular expression message.user-role-admin-group=Cannot change group for ROLE_ADMIN users. +label.roles-management=Role Management +label.new-role=New Role +label.edit-role=Edit Role +label.role-name=Role Name +label.role-description=Role Description +label.role=Role + +message.delete-role-title=Delete Role? + +message.delete-role-body=You are requesting to delete a role. If you complete this process the role will be removed. This cannot be undone. Do you wish to continue? message.duration=Requires a valid ISO 8601 duration (ex. PT2D) message.delete-user-title=Delete User? @@ -690,4 +703,7 @@ tooltip.nameid-formats-value=Value tooltip.nameid-formats-type=Type tooltip.group-name=Group Name -tooltip.group-description=Group Description \ No newline at end of file +tooltip.group-description=Group Description + +tooltip.role-name=Role Name +tooltip.role-description=Role Description \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/ResetDatabase.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/ResetsDatabaseTrait.groovy similarity index 100% rename from backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/ResetDatabase.groovy rename to backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/ResetsDatabaseTrait.groovy diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy index 384cb9ce6..f46eb33d8 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy @@ -45,7 +45,7 @@ class TestConfiguration { final OpenSamlObjects openSamlObjects final MetadataResolverRepository metadataResolverRepository - final Logger logger = LoggerFactory.getLogger(TestConfiguration.class); + final Logger logger = LoggerFactory.getLogger(TestConfiguration.class) @Autowired private CustomEntityAttributeDefinitionRepository repository; @@ -91,12 +91,12 @@ class TestConfiguration { for (String entityId: this.getBackingStore().getIndexedDescriptors().keySet()) { Document document = new Document(); - document.add(new StringField("id", entityId, Field.Store.YES)); - document.add(new TextField("content", entityId, Field.Store.YES)); // TODO: change entityId to be content of entity descriptor block + document.add(new StringField("id", entityId, Field.Store.YES)) + document.add(new TextField("content", entityId, Field.Store.YES)) // TODO: change entityId to be content of entity descriptor block try { - indexWriter.addDocument(document); + indexWriter.addDocument(document) } catch (IOException e) { - logger.error(e.getMessage(), e); + logger.error(e.getMessage(), e) } } try { @@ -131,4 +131,4 @@ class TestConfiguration { return it } } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy index f83ab1fb3..c844c0070 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 @@ -81,7 +81,7 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { @Transactional def setup() { openSamlObjects.init() - + Group gb = new Group() gb.setResourceId("testingGroupBBB") gb.setName("Group BBB") 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 14165b411..51c786429 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy @@ -4,9 +4,13 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException +import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository @@ -85,6 +89,18 @@ class MetadataFiltersControllerTests extends AbstractBaseDataJpaTest { Document generateConfiguration() { return null } + + @Override + MetadataResolver updateMetadataResolverEnabledStatus(MetadataResolver existingResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException { + // This won't get called + return null + } + + @Override + MetadataResolver findByResourceId(String resourceId) throws EntityNotFoundException { + // This won't get called + return null + } }, chainingMetadataResolver: new OpenSamlChainingMetadataResolver().with { it.id = 'chain' diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy index a32b65f64..1348f9a36 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 @@ -7,7 +7,6 @@ import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.EntitiesVersioningConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConverterConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverValidationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.PlaceholderResolverComponentsConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration @@ -42,7 +41,6 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.MvcResult -import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.transaction.annotation.Transactional import spock.lang.Unroll diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy index 9916ae084..a3f223efb 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy @@ -7,15 +7,15 @@ import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl @Profile('test') class GroupServiceForTesting extends GroupServiceImpl { - public GroupServiceForTesting(GroupServiceImpl impl) { + GroupServiceForTesting(GroupServiceImpl impl) { this.groupRepository = impl.groupRepository this.ownershipRepository = impl.ownershipRepository } @Transactional - public void clearAllForTesting() { - groupRepository.deleteAll(); + void clearAllForTesting() { + groupRepository.deleteAll() ownershipRepository.clearAllOwnedByGroup() ensureAdminGroupExists() } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy index 26c7eb5f1..4c572e2ad 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy @@ -3,6 +3,7 @@ package edu.internet2.tier.shibboleth.admin.ui.service import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.configuration.JsonSchemaComponentsConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup import edu.internet2.tier.shibboleth.admin.ui.jsonschema.LowLevelJsonSchemaValidator import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.security.model.Group @@ -34,11 +35,6 @@ class AuxiliaryIntegrationTests extends Specification { def "SHIBUI-1723: after enabling saved entity descriptor, it should still have valid xml"() { given: - def group = new Group().with { - it.name = "foo" - it.resourceId = "foo" - it - } def entityDescriptor = openSamlObjects.unmarshalFromXml(this.class.getResource('/metadata/SHIBUI-1723-1.xml').bytes) as EntityDescriptor entityDescriptor.idOfOwner = "foo" @@ -52,7 +48,7 @@ class AuxiliaryIntegrationTests extends Specification { it } def json = objectMapper.writeValueAsString(entityDescriptorRepresentation) - def schemaUri = edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.metadataSourcesSchema(new JsonSchemaComponentsConfiguration().jsonSchemaResourceLocationRegistry(this.resourceLoader, this.objectMapper)).uri + def schemaUri = JsonSchemaLocationLookup.metadataSourcesSchema(new JsonSchemaComponentsConfiguration().jsonSchemaResourceLocationRegistry(this.resourceLoader, this.objectMapper)).uri when: LowLevelJsonSchemaValidator.validatePayloadAgainstSchema(new MockHttpInputMessage(json.bytes), schemaUri) @@ -60,4 +56,4 @@ class AuxiliaryIntegrationTests extends Specification { then: noExceptionThrown() } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy index ba7870566..d6235e632 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy @@ -1,7 +1,27 @@ package edu.internet2.tier.shibboleth.admin.ui.util -import edu.internet2.tier.shibboleth.admin.ui.domain.* -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.* + +import edu.internet2.tier.shibboleth.admin.ui.domain.ContactPerson +import edu.internet2.tier.shibboleth.admin.ui.domain.Description +import edu.internet2.tier.shibboleth.admin.ui.domain.DisplayName +import edu.internet2.tier.shibboleth.admin.ui.domain.EmailAddress +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.Extensions +import edu.internet2.tier.shibboleth.admin.ui.domain.GivenName +import edu.internet2.tier.shibboleth.admin.ui.domain.InformationURL +import edu.internet2.tier.shibboleth.admin.ui.domain.KeyDescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.Logo +import edu.internet2.tier.shibboleth.admin.ui.domain.NameIDFormat +import edu.internet2.tier.shibboleth.admin.ui.domain.PrivacyStatementURL +import edu.internet2.tier.shibboleth.admin.ui.domain.SPSSODescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.SingleLogoutService +import edu.internet2.tier.shibboleth.admin.ui.domain.UIInfo +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ContactRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.LogoutEndpointRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.MduiRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSsoDescriptorRepresentation import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils import org.opensaml.saml.common.xml.SAMLConstants @@ -39,7 +59,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 @@ -65,8 +85,8 @@ class EntityDescriptorConversionUtilsTests extends Specification { 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 == key2 @@ -585,7 +605,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 } ) @@ -609,7 +629,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 } ) @@ -618,8 +638,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 } ) @@ -642,8 +662,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 } ) @@ -652,7 +672,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 } ) @@ -672,8 +692,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 } ) @@ -693,8 +713,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 c379690c5..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 @@ -1,69 +1,60 @@ package net.unicon.shibui.pac4j; -import com.fasterxml.jackson.databind.ObjectMapper; -import edu.internet2.tier.shibboleth.admin.ui.controller.ErrorResponse; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; import edu.internet2.tier.shibboleth.admin.ui.security.model.User; import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository; -import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import edu.internet2.tier.shibboleth.admin.ui.service.EmailService; - +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; import org.pac4j.core.context.JEEContext; import org.pac4j.core.context.session.JEESessionStore; import org.pac4j.core.matching.matcher.Matcher; import org.pac4j.core.profile.CommonProfile; -import org.pac4j.saml.profile.SAML2Profile; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.bcrypt.BCrypt; import javax.mail.MessagingException; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; +import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.transaction.Transactional; - import java.io.IOException; -import java.util.List; -import java.util.Optional; - -import lombok.extern.slf4j.Slf4j; +import java.util.*; @Slf4j public class AddNewUserFilter implements Filter { private static final String ROLE_NONE = "ROLE_NONE"; private Optional emailService; + private IGroupService groupService; + private Matcher matcher; private Pac4jConfigurationProperties pac4jConfigurationProperties; private RoleRepository roleRepository; private Pac4jConfigurationProperties.SimpleProfileMapping simpleProfileMapping; - private UserRepository userRepository; - private Matcher matcher; + private UserService userService; - public AddNewUserFilter(Pac4jConfigurationProperties pac4jConfigurationProperties, UserRepository userRepository, RoleRepository roleRepository, Matcher matcher, Optional emailService) { - this.userRepository = userRepository; + public AddNewUserFilter(Pac4jConfigurationProperties pac4jConfigurationProperties, UserService userService, RoleRepository roleRepository, Matcher matcher, IGroupService groupService, Optional emailService) { + this.userService = userService; this.roleRepository = roleRepository; this.emailService = emailService; this.pac4jConfigurationProperties = pac4jConfigurationProperties; this.matcher = matcher; + this.groupService = groupService; simpleProfileMapping = this.pac4jConfigurationProperties.getSimpleProfileMapping(); } @Transactional - private User buildAndPersistNewUserFromProfile(CommonProfile profile) { + User buildAndPersistNewUserFromProfile(CommonProfile profile) { Optional noRole = roleRepository.findByName(ROLE_NONE); Role newUserRole; if (noRole.isEmpty()) { newUserRole = new Role(ROLE_NONE); - newUserRole = roleRepository.save(newUserRole); + roleRepository.save(newUserRole); } newUserRole = noRole.get(); @@ -74,7 +65,23 @@ private User buildAndPersistNewUserFromProfile(CommonProfile profile) { user.setFirstName(profile.getFirstName()); user.setLastName(profile.getFamilyName()); user.setEmailAddress(profile.getEmail()); - User persistedUser = userRepository.save(user); + + // get profile attribute for groups + Object obj = profile.getAttribute(simpleProfileMapping.getGroups()); + if (obj != null) { + final ArrayList groupNames = new ArrayList<>(); + if (obj instanceof String) { + groupNames.add(obj.toString()); + } + if (obj instanceof List) { + ((List)obj).forEach(val -> groupNames.add(val.toString())); + } + if (!groupNames.isEmpty()) { + user.setUserGroups(findOrCreateGroups(groupNames)); + } + } + + User persistedUser = userService.save(user); if (log.isDebugEnabled()) { log.debug("Persisted new user:\n" + user); } @@ -97,7 +104,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha if (profile != null) { String username = profile.getUsername(); if (username != null) { - Optional persistedUser = userRepository.findByUsername(username); + Optional persistedUser = userService.findByUsername(username); User user; if (persistedUser.isEmpty()) { user = buildAndPersistNewUserFromProfile(profile); @@ -122,7 +129,26 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } } + private Set findOrCreateGroups(ArrayList groupNames) { + final HashSet result = new HashSet<>(); + groupNames.forEach(name -> { + Group g = groupService.find(name); + if (g == null) { + g = new Group(); + g.setResourceId(name); + g.setName(name); + try { + groupService.createGroup(g); + } + catch (GroupExistsConflictException shouldntHappen) { + } + } + result.add(g); + }); + return result; + } + @Override public void init(FilterConfig filterConfig) throws ServletException { } -} +} \ No newline at end of file diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/BetterSAML2Profile.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/BetterSAML2Profile.java index 17a5edd64..38ec220f3 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/BetterSAML2Profile.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/BetterSAML2Profile.java @@ -1,10 +1,10 @@ package net.unicon.shibui.pac4j; -import org.pac4j.saml.profile.SAML2Profile; - import net.unicon.shibui.pac4j.Pac4jConfigurationProperties.SimpleProfileMapping; +import org.pac4j.saml.profile.SAML2Profile; import java.util.Collection; +import java.util.List; public class BetterSAML2Profile extends SAML2Profile { private SimpleProfileMapping profileMapping; @@ -28,6 +28,10 @@ public String getFirstName() { return (String) getAttribute(profileMapping.getFirstName()); } + public List getGroups() { + return (List) getAttribute(profileMapping.getGroups()); + } + @Override public String getUsername() { Object username = getAttribute(profileMapping.getUsername()); @@ -38,4 +42,4 @@ public String getUsername() { } } -} +} \ No newline at end of file diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java index 5e173f151..39f12035b 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfiguration.java @@ -1,5 +1,9 @@ package net.unicon.shibui.pac4j; +import com.google.common.collect.Lists; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.pac4j.core.client.Clients; import org.pac4j.core.config.Config; @@ -17,7 +21,6 @@ import org.pac4j.saml.config.SAML2Configuration; import org.pac4j.saml.credentials.authenticator.SAML2Authenticator; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.ErrorPageRegistrar; @@ -26,12 +29,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; -import com.google.common.collect.Lists; - -import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; -import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; -import lombok.extern.slf4j.Slf4j; - /** * Configuration setup here following readme from - https://github.com/pac4j/spring-security-pac4j/tree/5.0.x * NOTE: matchers are now done as part of the config and have been moved over from the WebSecurity.java class of this package @@ -85,7 +82,6 @@ public Config config(final Pac4jConfigurationProperties pac4jConfigProps, //saml2Config.setSpLogoutRequestBindingType(pac4jConfigProps.getSpLogoutRequestBindingType()); final SAML2Client saml2Client = new SAML2Client(saml2Config); - saml2Client.setName("Saml2Client"); saml2Client.addAuthorizationGenerator(saml2ModelAuthorizationGenerator); SAML2Authenticator saml2Authenticator = new SAML2Authenticator(saml2Config.getAttributeAsId(), saml2Config.getMappedAttributes()); saml2Authenticator.setProfileDefinition(new CommonProfileDefinition(p -> new BetterSAML2Profile(pac4jConfigProps.getSimpleProfileMapping()))); @@ -110,7 +106,7 @@ public void validate(Credentials credentials, WebContext context, SessionStore s throw new CredentialsException("Invalid Credentials object generated by HeaderClient"); } final CommonProfile profile = new CommonProfile(); - String token = ((TokenCredentials)credentials).getToken(); + String token = ((TokenCredentials)credentials).getToken(); profile.setId(token); profile.addAttribute("username", token); profile.setRoles(userService.getUserRoles(token)); @@ -134,4 +130,4 @@ private void registerErrorPages(ErrorPageRegistry registry) { registry.addErrorPages(new ErrorPage(HttpStatus.UNAUTHORIZED, "/unsecured/error.html")); registry.addErrorPages(new ErrorPage(HttpStatus.FORBIDDEN, "/unsecured/error.html")); } -} +} \ No newline at end of file diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java index 977164958..41f65d43b 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/Pac4jConfigurationProperties.java @@ -1,16 +1,12 @@ package net.unicon.shibui.pac4j; -import javax.servlet.Filter; - -import org.pac4j.springframework.security.web.CallbackFilter; +import lombok.Getter; +import lombok.Setter; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.stereotype.Component; -import lombok.Getter; -import lombok.Setter; - @Component @ConfigurationProperties(prefix = "shibui.pac4j") @EnableConfigurationProperties @@ -40,8 +36,8 @@ public class Pac4jConfigurationProperties { public static class SimpleProfileMapping { private String email; private String firstName; + private String groups; private String lastName; private String username; - } -} - + } +} \ No newline at end of file diff --git a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java index b16af86ba..ac7633afa 100644 --- a/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java +++ b/pac4j-module/src/main/java/net/unicon/shibui/pac4j/WebSecurity.java @@ -2,16 +2,13 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.auto.EmailConfiguration; import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository; -import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import edu.internet2.tier.shibboleth.admin.ui.service.EmailService; - -import org.apache.commons.lang3.StringUtils; import org.pac4j.core.config.Config; import org.pac4j.core.matching.matcher.Matcher; -import org.pac4j.core.matching.matcher.PathMatcher; import org.pac4j.springframework.security.web.CallbackFilter; import org.pac4j.springframework.security.web.SecurityFilter; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -24,11 +21,9 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.firewall.StrictHttpFirewall; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; - -import java.util.Optional; import javax.servlet.Filter; +import java.util.Optional; @Configuration @AutoConfigureOrder(-1) @@ -36,39 +31,43 @@ @AutoConfigureAfter(EmailConfiguration.class) public class WebSecurity { @Bean("webSecurityConfig") - public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter(final Config config, UserRepository userRepository, + public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter(final Config config, UserService userService, RoleRepository roleRepository, Optional emailService, - Pac4jConfigurationProperties pac4jConfigurationProperties) { - return new Pac4jWebSecurityConfigurerAdapter(config, userRepository, roleRepository, emailService, + Pac4jConfigurationProperties pac4jConfigurationProperties, IGroupService groupService) { + return new Pac4jWebSecurityConfigurerAdapter(config, userService, roleRepository, emailService, groupService, pac4jConfigurationProperties); } @Order(100) public static class Pac4jWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { private final Config config; - private UserRepository userRepository; - private RoleRepository roleRepository; private Optional emailService; + private IGroupService groupService; private Pac4jConfigurationProperties pac4jConfigurationProperties; + private RoleRepository roleRepository; + private UserService userService; - public Pac4jWebSecurityConfigurerAdapter(final Config config, UserRepository userRepository, RoleRepository roleRepository, - Optional emailService, Pac4jConfigurationProperties pac4jConfigurationProperties) { + public Pac4jWebSecurityConfigurerAdapter(final Config config, UserService userService, RoleRepository roleRepository, + Optional emailService, IGroupService groupService, Pac4jConfigurationProperties pac4jConfigurationProperties) { this.config = config; - this.userRepository = userRepository; + this.userService = userService; this.roleRepository = roleRepository; this.emailService = emailService; + this.groupService = groupService; this.pac4jConfigurationProperties = pac4jConfigurationProperties; } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/unsecured/**/*").permitAll(); - + + final SecurityFilter securityFilter = new SecurityFilter(this.config, "Saml2Client"); + // add filter based on auth type http.antMatcher("/**").addFilterBefore(getFilter(config, pac4jConfigurationProperties.getTypeOfAuth()), BasicAuthenticationFilter.class); - + http.antMatcher("/**").addFilterBefore(securityFilter, BasicAuthenticationFilter.class); // add the new user filter - http.addFilterAfter(new AddNewUserFilter(pac4jConfigurationProperties, userRepository, roleRepository, getPathMatcher("exclude-paths-matcher") , emailService), SecurityFilter.class); + http.addFilterAfter(new AddNewUserFilter(pac4jConfigurationProperties, userService, roleRepository, getPathMatcher("exclude-paths-matcher"), groupService, emailService), SecurityFilter.class); http.exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> response.sendRedirect("/unsecured/error.html")); http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS); @@ -110,4 +109,4 @@ public void configure(org.springframework.security.config.annotation.web.builder public AuditorAware pac4jAuditorAware() { return new Pac4jAuditorAware(); } -} +} \ No newline at end of file diff --git a/pac4j-module/src/main/resources/application.yml b/pac4j-module/src/main/resources/application.yml index ebba4fcd9..4dd28dc1f 100644 --- a/pac4j-module/src/main/resources/application.yml +++ b/pac4j-module/src/main/resources/application.yml @@ -1,7 +1,9 @@ shibui: + pac4j-enabled: true pac4j: simpleProfileMapping: username: urn:oid:0.9.2342.19200300.100.1.3 firstName: givenName lastName: sn - email: mail \ No newline at end of file + email: mail + groups: urn:oid:1.3.6.1.4.1.5923.1.5.1.1 # attributeId - isMemberOf \ No newline at end of file diff --git a/pac4j-module/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy b/pac4j-module/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy new file mode 100644 index 000000000..7773c2d20 --- /dev/null +++ b/pac4j-module/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy @@ -0,0 +1,17 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service + +import org.springframework.transaction.annotation.Transactional + +class GroupServiceForTesting extends GroupServiceImpl { + public GroupServiceForTesting(GroupServiceImpl impl) { + this.groupRepository = impl.groupRepository + this.ownershipRepository = impl.ownershipRepository + } + + @Transactional + public void clearAllForTesting() { + groupRepository.deleteAll(); + ownershipRepository.clearAllOwnedByGroup() + ensureAdminGroupExists() + } +} \ No newline at end of file diff --git a/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterMockTests.groovy b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterMockTests.groovy new file mode 100644 index 000000000..1f729beb7 --- /dev/null +++ b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterMockTests.groovy @@ -0,0 +1,101 @@ +package net.unicon.shibui.pac4j + +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import edu.internet2.tier.shibboleth.admin.ui.service.EmailService + +import org.pac4j.core.matching.matcher.PathMatcher +import org.pac4j.saml.profile.SAML2Profile +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder +import spock.lang.Specification +import spock.lang.Subject + +import javax.servlet.FilterChain +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@SpringBootTest(classes = [Pac4jConfigurationProperties]) +@EnableConfigurationProperties([Pac4jConfigurationProperties]) +class AddNewUserFilterMockTests extends Specification { + + UserService userService = Mock() + RoleRepository roleRepository = Mock() + EmailService emailService = Mock() + IGroupService groupService = Mock() + + HttpServletRequest request = Mock() + HttpServletResponse response = Mock() + FilterChain chain = Mock() + + SecurityContext securityContext = Mock() + Authentication authentication = Mock() + SAML2Profile saml2Profile = Mock() + + @Autowired + Pac4jConfigurationProperties pac4jConfigurationProperties + + Pac4jConfigurationProperties.SimpleProfileMapping profileMapping + + @Subject + AddNewUserFilter addNewUserFilter + + def setup() { + SecurityContextHolder.setContext(securityContext) + securityContext.getAuthentication() >> authentication + authentication.getPrincipal() >> saml2Profile + + addNewUserFilter = new AddNewUserFilter(pac4jConfigurationProperties, userService, roleRepository, new PathMatcher(), groupService, Optional.of(emailService)) + profileMapping = pac4jConfigurationProperties.simpleProfileMapping + } + + def "new users are redirected"() { + given: + ['Username': 'newUser', + 'FirstName': 'New', + 'LastName': 'User', + 'Email': 'newuser@institution.edu'].each { key, value -> + saml2Profile.getAttribute(profileMapping."get${key}"()) >> [value] + } + saml2Profile.getUsername() >> "newUser" + userService.findByUsername('newUser') >> Optional.empty() + roleRepository.findByName('ROLE_NONE') >> Optional.of(new Role('ROLE_NONE')) + + when: + addNewUserFilter.doFilter(request, response, chain) + + then: + 0 * roleRepository.save(_) + 1 * userService.save(_ as User) >> { User user -> user } + 1 * emailService.sendNewUserMail('newUser') + 1 * response.sendRedirect("/unsecured/error.html") + } + + def "existing users are not redirected"() { + given: + saml2Profile.getUsername() >> "existingUser" + userService.findByUsername('existingUser') >> Optional.of(new User().with { + it.username = 'existingUser' + it.roles = [new Role('ROLE_USER')] + it + }) + + when: + addNewUserFilter.doFilter(request, response, chain) + + then: + 0 * roleRepository.save(_) + 0 * userService.save(_) + 1 * chain.doFilter(_, _) + } +} \ No newline at end of file diff --git a/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy index d15b70f0a..9b8f6131d 100644 --- a/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy +++ b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/AddNewUserFilterTests.groovy @@ -2,37 +2,58 @@ package net.unicon.shibui.pac4j import edu.internet2.tier.shibboleth.admin.ui.security.model.Role import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository -import edu.internet2.tier.shibboleth.admin.ui.service.EmailService - +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceForTesting +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import org.pac4j.core.matching.matcher.PathMatcher -import org.pac4j.core.profile.CommonProfile import org.pac4j.saml.profile.SAML2Profile import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.context.properties.EnableConfigurationProperties -import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.security.core.Authentication import org.springframework.security.core.context.SecurityContext import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.annotation.Transactional import spock.lang.Specification import spock.lang.Subject import javax.servlet.FilterChain -import javax.servlet.ServletRequest import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse -/** - * @author Bill Smith (wsmith@unicon.net) - */ -@SpringBootTest(classes = [Pac4jConfigurationProperties]) +@DataJpaTest +@ContextConfiguration(classes=[Pac4JTestingConfig]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@DirtiesContext @EnableConfigurationProperties([Pac4jConfigurationProperties]) class AddNewUserFilterTests extends Specification { + @Subject + AddNewUserFilter addNewUserFilter + + @Autowired + GroupServiceForTesting groupService - UserRepository userRepository = Mock() - RoleRepository roleRepository = Mock() - EmailService emailService = Mock() + @Autowired + OwnershipRepository ownershipRepository + + @Autowired + RoleRepository roleRepository + + @Autowired + Pac4jConfigurationProperties pac4jConfigurationProperties + + @Autowired + UserRepository userRepository + + @Autowired + UserService userService HttpServletRequest request = Mock() HttpServletResponse response = Mock() @@ -42,24 +63,42 @@ class AddNewUserFilterTests extends Specification { Authentication authentication = Mock() SAML2Profile saml2Profile = Mock() - @Autowired - Pac4jConfigurationProperties pac4jConfigurationProperties - Pac4jConfigurationProperties.SimpleProfileMapping profileMapping - @Subject - AddNewUserFilter addNewUserFilter - + @Transactional def setup() { SecurityContextHolder.setContext(securityContext) securityContext.getAuthentication() >> authentication authentication.getPrincipal() >> saml2Profile - addNewUserFilter = new AddNewUserFilter(pac4jConfigurationProperties, userRepository, roleRepository, new PathMatcher(), Optional.of(emailService)) + addNewUserFilter = new AddNewUserFilter(pac4jConfigurationProperties, userService, roleRepository, new PathMatcher(), groupService, Optional.empty()) profileMapping = pac4jConfigurationProperties.simpleProfileMapping + + userRepository.findAll().forEach { + userService.delete(it.getUsername()) + } + userRepository.flush() + + roleRepository.deleteAll() + roleRepository.flush() + groupService.clearAllForTesting() //leaves us just the admingroup + + def roles = [new Role().with { + name = 'ROLE_ADMIN' + it + }, new Role().with { + name = 'ROLE_USER' + it + }, new Role().with { + name = 'ROLE_NONE' + it + }] + roles.each { + roleRepository.save(it) + } } - def "new users are redirected"() { + def "new user created"() { given: ['Username': 'newUser', 'FirstName': 'New', @@ -68,34 +107,34 @@ class AddNewUserFilterTests extends Specification { saml2Profile.getAttribute(profileMapping."get${key}"()) >> [value] } saml2Profile.getUsername() >> "newUser" - userRepository.findByUsername('newUser') >> Optional.empty() - roleRepository.findByName('ROLE_NONE') >> Optional.of(new Role('ROLE_NONE')) when: addNewUserFilter.doFilter(request, response, chain) then: - 0 * roleRepository.save(_) - 1 * userRepository.save(_ as User) >> { User user -> user } - 1 * emailService.sendNewUserMail('newUser') 1 * response.sendRedirect("/unsecured/error.html") + User user = userRepository.findByUsername("newUser").get() + user.getGroupId() == "newUser" } - def "existing users are not redirected"() { + def "new user created with group - assumes saml2profile got property for groups"() { given: - saml2Profile.getUsername() >> "existingUser" - userRepository.findByUsername('existingUser') >> Optional.of(new User().with { - it.username = 'existingUser' - it.roles = [new Role('ROLE_USER')] - it - }) + ['Username': 'newUser', + 'FirstName': 'New', + 'LastName': 'User', + 'Email': 'newuser@institution.edu', + 'Groups':'AAAGroup' + ].each { key, value -> + saml2Profile.getAttribute(profileMapping."get${key}"()) >> [value] + } + saml2Profile.getUsername() >> "newUser" when: addNewUserFilter.doFilter(request, response, chain) then: - 0 * roleRepository.save(_) - 0 * userRepository.save(_) - 1 * chain.doFilter(_, _) + 1 * response.sendRedirect("/unsecured/error.html") + User user = userRepository.findByUsername("newUser").get() + user.getGroupId() == "AAAGroup" } -} +} \ No newline at end of file diff --git a/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/Pac4JTestingConfig.groovy b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/Pac4JTestingConfig.groovy new file mode 100644 index 000000000..552aaca04 --- /dev/null +++ b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/Pac4JTestingConfig.groovy @@ -0,0 +1,52 @@ +package net.unicon.shibui.pac4j + +import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.GroupUpdatedEntityListener +import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.UserUpdatedEntityListener +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceForTesting +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Primary + +@Configuration +class Pac4JTestingConfig { + @Bean + @Primary + GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { + GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { + it.groupRepository = repo + it.ownershipRepository = ownershipRepository + return it + }) + result.ensureAdminGroupExists() + return result + } + + @Bean + @Primary + GroupUpdatedEntityListener groupUpdatedEntityListener(OwnershipRepository repo) { + GroupUpdatedEntityListener listener = new GroupUpdatedEntityListener() + listener.init(repo) + return listener + } + + @Bean + @Primary + UserUpdatedEntityListener userUpdatedEntityListener(OwnershipRepository repo, GroupsRepository groupRepo) { + UserUpdatedEntityListener listener = new UserUpdatedEntityListener() + listener.init(repo, groupRepo) + return listener + } + + @Bean + @Primary + UserService userService(IGroupService groupService, OwnershipRepository ownershipRepository, RoleRepository roleRepository, UserRepository userRepository) { + return new UserService(groupService, ownershipRepository, roleRepository, userRepository) + } +} \ No newline at end of file diff --git a/ui/public/assets/schema/roles/role.json b/ui/public/assets/schema/roles/role.json new file mode 100644 index 000000000..8145fae88 --- /dev/null +++ b/ui/public/assets/schema/roles/role.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "title": "label.role-name", + "description": "tooltip.role-name", + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } +} \ No newline at end of file diff --git a/ui/src/app/App.js b/ui/src/app/App.js index 2fb4797b6..e52a45f4a 100644 --- a/ui/src/app/App.js +++ b/ui/src/app/App.js @@ -26,6 +26,7 @@ import { NewProvider } from './metadata/new/NewProvider'; import { Filter } from './metadata/Filter'; import { Contention } from './metadata/contention/ContentionContext'; import { SessionModal } from './core/user/SessionModal'; +import { Roles } from './admin/Roles'; import Button from 'react-bootstrap/Button'; import { Groups } from './admin/Groups'; @@ -66,7 +67,7 @@ function App() { {(message, confirm, confirmCallback, setConfirm, getConfirmation) => - +
@@ -80,6 +81,7 @@ function App() { + diff --git a/ui/src/app/admin/Roles.js b/ui/src/app/admin/Roles.js new file mode 100644 index 000000000..08daed90d --- /dev/null +++ b/ui/src/app/admin/Roles.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { Switch, Route, useRouteMatch, Redirect } from 'react-router-dom'; +import { RolesProvider } from './hoc/RolesProvider'; +import { NewRole } from './container/NewRole'; +import { EditRole } from './container/EditRole'; +import { RoleList } from './container/RoleList'; + +export function Roles() { + + let { path } = useRouteMatch(); + + return ( + <> + + + + {(roles, onDelete) => + + } + + } /> + + + } /> + + + } /> + + + + ); +} \ No newline at end of file diff --git a/ui/src/app/admin/component/RoleForm.js b/ui/src/app/admin/component/RoleForm.js new file mode 100644 index 000000000..074cb5b09 --- /dev/null +++ b/ui/src/app/admin/component/RoleForm.js @@ -0,0 +1,68 @@ +import React from 'react'; +import Button from 'react-bootstrap/Button'; +import Form from '@rjsf/bootstrap-4'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSpinner, faSave } from '@fortawesome/free-solid-svg-icons'; +import Translate from '../../i18n/components/translate'; + +import { useRoleUiSchema } from '../hooks'; +import { fields, widgets } from '../../form/component'; +import { templates } from '../../form/component'; +import { FormContext, setFormDataAction, setFormErrorAction } from '../../form/FormManager'; + +function ErrorListTemplate() { + return (<>); +} + +export function RoleForm({ role = {}, errors = [], loading = false, schema, onSave, onCancel }) { + + const { dispatch } = React.useContext(FormContext); + const onChange = ({ formData, errors }) => { + dispatch(setFormDataAction(formData)); + dispatch(setFormErrorAction(errors)); + }; + + const uiSchema = useRoleUiSchema(); + + return (<> +
+
+ + + + +
+
+
+
+
onChange(form)} + schema={schema} + uiSchema={uiSchema} + FieldTemplate={templates.FieldTemplate} + ObjectFieldTemplate={templates.ObjectFieldTemplate} + ArrayFieldTemplate={templates.ArrayFieldTemplate} + fields={fields} + widgets={widgets} + liveValidate={true} + ErrorList={ErrorListTemplate}> + <> +
+
+
+
+ ) +} +/**/ \ No newline at end of file diff --git a/ui/src/app/admin/component/UserMaintenance.js b/ui/src/app/admin/component/UserMaintenance.js index c4542438f..e9a7b8c33 100644 --- a/ui/src/app/admin/component/UserMaintenance.js +++ b/ui/src/app/admin/component/UserMaintenance.js @@ -23,10 +23,10 @@ export default function UserMaintenance({ users, roles, loading, onDeleteUser, o UserId - Name + Name Email - Role - Group + Role + Group Delete? @@ -36,10 +36,10 @@ export default function UserMaintenance({ users, roles, loading, onDeleteUser, o {users.map((user, idx) => - {user.username} - {user.firstName} {user.lastName} - {user.emailAddress} - + {user.username} + {user.firstName} {user.lastName} + {user.emailAddress} + changeSourceGroup(model, event.target.value)} - value={model.idOfOwner} - disabled={loadingGroups} - disablevalidation="true"> - - {groups.map((g, ridx) => ( - - ))} - - - - } - + + + + } + } {children} - +

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

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

No Filters

-

No filters have been added to this Metadata Provider

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

No Filters

+

No filters have been added to this Metadata Provider

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