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 3c73bfbbf..14a65b52b 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 @@ -153,6 +153,6 @@ class SeleniumSIDETest extends Specification { 'SHIBUI-1744: Verify attribute bundles in entity attribute filters' | '/SHIBUI-1744-3.side' 'SHIBUI-2052: Logged in user & role appear on dashboard' | '/SHIBUI-2052.side' 'SHIBUI-2116: Verify entity attribute bundle highlights' | '/SHIBUI-2116.side' // Note that this script WILL NOT PASS in the Selenium IDE due to ${driver} not being set (it is provided by this groovy script). - + 'SHIBUI-2269: Verify XML generation of external filters' | '/SHIBUI-2269.side' } } diff --git a/backend/src/integration/resources/SHIBUI-2269.side b/backend/src/integration/resources/SHIBUI-2269.side new file mode 100644 index 000000000..f2848dddb --- /dev/null +++ b/backend/src/integration/resources/SHIBUI-2269.side @@ -0,0 +1,545 @@ +{ + "id": "1b31a551-eb09-4bd4-8db9-694bf1539a46", + "version": "2.0", + "name": "SHIBUI-2269", + "url": "http://localhost:10101", + "tests": [{ + "id": "841ade0e-83bd-4a4b-94f2-de6bd5c536b2", + "name": "SHIBUI-2269", + "commands": [{ + "id": "d6b23986-6d14-4b10-be7b-a7e6f576e3b2", + "comment": "", + "command": "open", + "target": "/login", + "targets": [], + "value": "" + }, { + "id": "f77ecd77-01c2-4463-944e-1a69600f5297", + "comment": "", + "command": "type", + "target": "name=username", + "targets": [ + ["name=username", "name"], + ["css=tr:nth-child(1) input", "css:finder"], + ["xpath=//input[@name='username']", "xpath:attributes"], + ["xpath=//input", "xpath:position"] + ], + "value": "admin" + }, { + "id": "c9bf0a22-faa9-494c-b2ed-6c9653248551", + "comment": "", + "command": "type", + "target": "name=password", + "targets": [ + ["name=password", "name"], + ["css=tr:nth-child(2) input", "css:finder"], + ["xpath=//input[@name='password']", "xpath:attributes"], + ["xpath=//tr[2]/td[2]/input", "xpath:position"] + ], + "value": "adminpass" + }, { + "id": "7ab1d854-3582-4101-bd19-f94b8f438090", + "comment": "", + "command": "sendKeys", + "target": "name=password", + "targets": [ + ["name=password", "name"], + ["css=tr:nth-child(2) input", "css:finder"], + ["xpath=//input[@name='password']", "xpath:attributes"], + ["xpath=//tr[2]/td[2]/input", "xpath:position"] + ], + "value": "${KEY_ENTER}" + }, { + "id": "4059cae7-b9f9-49d0-a213-343bcaba66d1", + "comment": "", + "command": "waitForElementVisible", + "target": "id=metadata-nav-dropdown-toggle", + "targets": [], + "value": "30000" + }, { + "id": "f03af8d5-5875-4a2c-b93a-c3ddcbd4b16a", + "comment": "", + "command": "open", + "target": "/api/heheheheheheheWipeout", + "targets": [], + "value": "" + }, { + "id": "081f495b-4d84-4758-824c-1e85b6311e7f", + "comment": "", + "command": "assertText", + "target": "css=body", + "targets": [], + "value": "yes, you did it" + }, { + "id": "9e912dd5-6ace-45be-bafd-2d1655906575", + "comment": "", + "command": "open", + "target": "/", + "targets": [], + "value": "" + }, { + "id": "3bb52950-667c-4852-a98f-6a6fb5632ba5", + "comment": "", + "command": "waitForElementEditable", + "target": "id=metadata-nav-dropdown-toggle", + "targets": [], + "value": "30000" + }, { + "id": "6af18279-b5ed-47d0-932b-cba97881b9bb", + "comment": "", + "command": "click", + "target": "id=metadata-nav-dropdown-toggle", + "targets": [ + ["id=metadata-nav-dropdown-toggle", "id"], + ["css=#metadata-nav-dropdown-toggle", "css:finder"], + ["xpath=//button[@id='metadata-nav-dropdown-toggle']", "xpath:attributes"], + ["xpath=//div[@id='metadata-nav-dropdown']/button", "xpath:idRelative"], + ["xpath=//div[2]/button", "xpath:position"], + ["xpath=//button[contains(.,'Add New')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "b07230f3-c268-4680-943f-5f8f81414002", + "comment": "", + "command": "click", + "target": "id=metadata-nav-dropdown-provider", + "targets": [ + ["id=metadata-nav-dropdown-provider", "id"], + ["linkText=Add a new metadata provider", "linkText"], + ["css=#metadata-nav-dropdown-provider", "css:finder"], + ["xpath=//a[contains(text(),'Add a new metadata provider')]", "xpath:link"], + ["xpath=//a[@id='metadata-nav-dropdown-provider']", "xpath:attributes"], + ["xpath=//div[@id='metadata-nav-dropdown']/div/a[2]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/metadata/provider/new')]", "xpath:href"], + ["xpath=//a[2]", "xpath:position"], + ["xpath=//a[contains(.,'Add a new metadata provider')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "004dbab9-263e-4f90-9c5f-9d2948d80ab5", + "comment": "", + "command": "waitForElementEditable", + "target": "name=type", + "targets": [], + "value": "30000" + }, { + "id": "d77381c8-2164-499d-91fa-e762792644ec", + "comment": "", + "command": "select", + "target": "name=type", + "targets": [], + "value": "label=ExternalMetadataResolver" + }, { + "id": "8edf9e5e-188e-435d-acbd-a0ee770df39d", + "comment": "", + "command": "type", + "target": "name=name", + "targets": [ + ["name=name", "name"], + ["css=.form-control", "css:finder"], + ["xpath=//input[@name='name']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/div/form/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "ExternalTest" + }, { + "id": "d652c1d5-44d2-448a-8dae-85d266b51638", + "comment": "", + "command": "click", + "target": "css=.direction", + "targets": [ + ["css=.direction", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/nav/ul/li[2]/button/span[2]", "xpath:idRelative"], + ["xpath=//span[2]", "xpath:position"], + ["xpath=//span[contains(.,'Next')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "d1294ac3-d91c-495a-ab0b-cc3183cc08d5", + "comment": "", + "command": "pause", + "target": "5000", + "targets": [], + "value": "" + }, { + "id": "b15dcd2a-9360-4831-a95c-8797eb235972", + "comment": "", + "command": "type", + "target": "id=root_xmlId", + "targets": [ + ["id=root_xmlId", "id"], + ["css=#root_xmlId", "css:finder"], + ["xpath=//input[@id='root_xmlId']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[2]/div/div/form/div/div/div/div/div[2]/div/div/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "1" + }, { + "id": "657ee88c-4777-485b-b3de-a467657e6fe4", + "comment": "", + "command": "type", + "target": "id=root_description", + "targets": [ + ["id=root_description", "id"], + ["css=#root_description", "css:finder"], + ["xpath=//textarea[@id='root_description']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[2]/div/div/form/div/div/div/div/div[2]/div[2]/div/div/textarea", "xpath:idRelative"], + ["xpath=//textarea", "xpath:position"] + ], + "value": "External Test" + }, { + "id": "5548997c-2f3a-4376-89a9-606b5b948ef1", + "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(.,'2. Finished!')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "826e7804-88f0-436e-9951-11e8065671ed", + "comment": "", + "command": "click", + "target": "css=.save", + "targets": [ + ["css=.save", "css:finder"], + ["xpath=(//button[@type='button'])[6]", "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": "ab15e915-02b9-4616-9f92-ffcb0386918c", + "comment": "", + "command": "click", + "target": "linkText=ExternalTest", + "targets": [ + ["linkText=ExternalTest", "linkText"], + ["css=.align-middle > a", "css:finder"], + ["xpath=//a[contains(text(),'ExternalTest')]", "xpath:link"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/div/table/tbody/tr/td[2]/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/metadata/provider/0e237f9c-3ffe-49da-a60d-289733126d92/configuration/options')]", "xpath:href"], + ["xpath=//td[2]/a", "xpath:position"], + ["xpath=//a[contains(.,'ExternalTest')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "3ece6089-c462-437e-9384-62cede8afc7d", + "comment": "", + "command": "click", + "target": "linkText=Add Filter", + "targets": [ + ["linkText=Add Filter", "linkText"], + ["css=#filters .btn", "css:finder"], + ["xpath=//div[@id='filters']/div/div/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/metadata/provider/0e237f9c-3ffe-49da-a60d-289733126d92/filter/new')]", "xpath:href"], + ["xpath=//div[3]/div/div/a", "xpath:position"], + ["xpath=//a[contains(.,' Add Filter')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "b409c3ea-3066-4d06-a660-4702cdace6ec", + "comment": "", + "command": "waitForElementEditable", + "target": "name=type", + "targets": [], + "value": "30000" + }, { + "id": "84111b1d-6927-4536-b883-0b057f13d898", + "comment": "", + "command": "select", + "target": "name=type", + "targets": [], + "value": "label=EntityAttributes" + }, { + "id": "ec27be24-7951-4d76-9457-4a5dbe5866ef", + "comment": "", + "command": "waitForElementEditable", + "target": "id=root_name", + "targets": [], + "value": "30000" + }, { + "id": "bea341ae-4cdf-432e-8032-c827e9c8a7b8", + "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[2]/div[2]/div/form/div/div/div/div/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "1" + }, { + "id": "79f5b0a9-57e9-4f6b-8980-4af6258a5aa4", + "comment": "", + "command": "click", + "target": "id=dropdown-label.filter-target-type", + "targets": [ + ["id=dropdown-label.filter-target-type", "id"], + ["css=#dropdown-label\\.filter-target-type", "css:finder"], + ["xpath=//button[@id='dropdown-label.filter-target-type']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div[2]/div[2]/div/form/div/div/div/div[6]/div/div/div/fieldset/div/div/div/div/button", "xpath:idRelative"], + ["xpath=//fieldset/div/div/div/div/button", "xpath:position"], + ["xpath=//button[contains(.,'Entity ID ')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "d01e44ab-802b-4d85-ac2e-e2e03a00c1c0", + "comment": "", + "command": "click", + "target": "css=.dropdown-item:nth-child(3)", + "targets": [ + ["css=.dropdown-item:nth-child(3)", "css:finder"], + ["xpath=(//button[@type='button'])[17]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div[2]/div[2]/div/form/div/div/div/div[6]/div/div/div/fieldset/div/div/div/div/div/button[3]", "xpath:idRelative"], + ["xpath=//div/button[3]", "xpath:position"], + ["xpath=//button[contains(.,'Script')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "f401438a-0154-4610-be40-580829ba76a9", + "comment": "", + "command": "type", + "target": "css=.npm__react-simple-code-editor__textarea", + "targets": [ + ["css=.npm__react-simple-code-editor__textarea", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div[2]/div[2]/div/form/div/div/div/div[6]/div/div/div/fieldset/div/div/div[2]/div/div/div/div/textarea", "xpath:idRelative"], + ["xpath=//textarea", "xpath:position"] + ], + "value": "true;" + }, { + "id": "75563d4f-487d-4c08-81d9-26a8ef989711", + "comment": "", + "command": "click", + "target": "css=.btn-info", + "targets": [ + ["css=.btn-info", "css:finder"], + ["xpath=(//button[@type='button'])[7]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div/div[2]/div/button", "xpath:idRelative"], + ["xpath=//div[2]/div/button", "xpath:position"], + ["xpath=//button[contains(.,' Save')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "21f5c73b-0aa2-41cd-a97c-35c05a892275", + "comment": "", + "command": "click", + "target": "linkText=Add Filter", + "targets": [ + ["linkText=Add Filter", "linkText"], + ["css=#filters > .numbered-header .btn", "css:finder"], + ["xpath=//div[@id='filters']/div/div/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/metadata/provider/0e237f9c-3ffe-49da-a60d-289733126d92/filter/new')]", "xpath:href"], + ["xpath=//div[3]/div/div/a", "xpath:position"], + ["xpath=//a[contains(.,' Add Filter')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "6eb76a2f-370a-4547-b05e-f0dc8bdb60ba", + "comment": "", + "command": "select", + "target": "name=type", + "targets": [], + "value": "label=NameIDFormat" + }, { + "id": "fcacadd8-7b9e-4878-b1cc-3d5da56e6660", + "comment": "", + "command": "click", + "target": "css=.col-12 > .mb-3 > div > div", + "targets": [ + ["css=.col-12 > .mb-3 > div > div", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div[2]/div[2]/div/form/div/div/div/div/div/div/div/div", "xpath:idRelative"], + ["xpath=//div/div/div/div/div/div/div/div", "xpath:position"] + ], + "value": "" + }, { + "id": "4d206821-ace7-483e-876e-99c7a0490144", + "comment": "", + "command": "waitForElementEditable", + "target": "id=root_name", + "targets": [], + "value": "30000" + }, { + "id": "62afd5f1-e565-4bb8-a142-9eda83e98a1c", + "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[2]/div[2]/div/form/div/div/div/div/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "2" + }, { + "id": "da90db0c-c7e6-4dc5-b643-13a3c2029f05", + "comment": "", + "command": "click", + "target": "id=dropdown-label.filter-target-type", + "targets": [ + ["id=dropdown-label.filter-target-type", "id"], + ["css=#dropdown-label\\.filter-target-type", "css:finder"], + ["xpath=//button[@id='dropdown-label.filter-target-type']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div[2]/div[2]/div/form/div/div/div/div[3]/div/div/div/fieldset/div/div/div/div/button", "xpath:idRelative"], + ["xpath=//fieldset/div/div/div/div/button", "xpath:position"], + ["xpath=//button[contains(.,'Entity ID ')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "bce0cd29-246e-4f6e-a860-0eade5c73850", + "comment": "", + "command": "click", + "target": "css=.dropdown-item:nth-child(3)", + "targets": [ + ["css=.dropdown-item:nth-child(3)", "css:finder"], + ["xpath=(//button[@type='button'])[16]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div[2]/div[2]/div/form/div/div/div/div[3]/div/div/div/fieldset/div/div/div/div/div/button[3]", "xpath:idRelative"], + ["xpath=//button[3]", "xpath:position"], + ["xpath=//button[contains(.,'Script')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "2923ab38-d8ff-4d3f-b247-1d977d06a0dd", + "comment": "", + "command": "click", + "target": "id=dropdown-label.filter-target-type", + "targets": [ + ["id=dropdown-label.filter-target-type", "id"], + ["css=#dropdown-label\\.filter-target-type", "css:finder"], + ["xpath=//button[@id='dropdown-label.filter-target-type']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div[2]/div[2]/div/form/div/div/div/div[3]/div/div/div/fieldset/div/div/div/div/button", "xpath:idRelative"], + ["xpath=//fieldset/div/div/div/div/button", "xpath:position"], + ["xpath=//button[contains(.,'Script ')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "7141ca97-429a-4d1f-9efb-68a23fc92748", + "comment": "", + "command": "click", + "target": "css=.show > .dropdown-item:nth-child(2)", + "targets": [ + ["css=.show > .dropdown-item:nth-child(2)", "css:finder"], + ["xpath=(//button[@type='button'])[15]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div[2]/div[2]/div/form/div/div/div/div[3]/div/div/div/fieldset/div/div/div/div/div/button[2]", "xpath:idRelative"], + ["xpath=//fieldset/div/div/div/div/div/button[2]", "xpath:position"], + ["xpath=//button[contains(.,'Regex')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "e15eab26-07bd-45e9-ba8e-ee2a400baee9", + "comment": "", + "command": "type", + "target": "id=targetInput", + "targets": [ + ["id=targetInput", "id"], + ["name=script", "name"], + ["css=#targetInput", "css:finder"], + ["xpath=//input[@id='targetInput']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div[2]/div[2]/div[2]/div/form/div/div/div/div[3]/div/div/div/fieldset/div/div/div[2]/div/div/input", "xpath:idRelative"], + ["xpath=//div[2]/div/div/input", "xpath:position"] + ], + "value": ".*unicon.*" + }, { + "id": "55cc5ef6-1b05-48a9-bc9d-58551b751feb", + "comment": "", + "command": "click", + "target": "css=.fa-floppy-disk > path", + "targets": [ + ["css=.fa-floppy-disk > path", "css:finder"] + ], + "value": "" + }, { + "id": "802be014-0d04-4bda-93d1-ca7a5d7f802d", + "comment": "", + "command": "waitForElementVisible", + "target": "css=.text:nth-child(2)", + "targets": [ + ["css=.text:nth-child(2)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div/section/div/div/h2/span[2]", "xpath:idRelative"], + ["xpath=//h2/span[2]", "xpath:position"], + ["xpath=//span[contains(.,'Common Attributes')]", "xpath:innerText"] + ], + "value": "30000" + }, { + "id": "7e3e7d65-e0ff-4a2d-a0e9-5080bfefd4df", + "comment": "", + "command": "click", + "target": "css=.btn-outline-secondary", + "targets": [ + ["css=.btn-outline-secondary", "css:finder"], + ["xpath=(//button[@type='button'])[5]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div/div/div/div/div/button", "xpath:idRelative"], + ["xpath=//div/div/div/div/button", "xpath:position"], + ["xpath=//button[contains(.,'Enable')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "42929ec9-7860-467a-a52b-946df9965de5", + "comment": "", + "command": "click", + "target": "xpath=//div[@id='filters']/ul/li/div/span[3]/div/input", + "targets": [ + ["id=customSwitch-df84f9b7-6cbe-4727-b71e-733eb9b46ca0", "id"], + ["css=#customSwitch-df84f9b7-6cbe-4727-b71e-733eb9b46ca0", "css:finder"], + ["xpath=//input[@id='customSwitch-df84f9b7-6cbe-4727-b71e-733eb9b46ca0']", "xpath:attributes"], + ["xpath=//div[@id='filters']/ul/li/div/span[3]/div/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "" + }, { + "id": "3bb95cdd-23aa-425d-af75-e4d69b819235", + "comment": "", + "command": "click", + "target": "xpath=//div[@id='filters']/ul/li[2]/div/span[3]/div/input", + "targets": [ + ["id=customSwitch-be9ce5a5-1242-4884-882a-7a977cd7bbd5", "id"], + ["css=#customSwitch-be9ce5a5-1242-4884-882a-7a977cd7bbd5", "css:finder"], + ["xpath=//input[@id='customSwitch-be9ce5a5-1242-4884-882a-7a977cd7bbd5']", "xpath:attributes"], + ["xpath=//div[@id='filters']/ul/li[2]/div/span[3]/div/input", "xpath:idRelative"], + ["xpath=//li[2]/div/span[3]/div/input", "xpath:position"] + ], + "value": "" + }, { + "id": "038ab74e-cab9-486e-bbfc-25a7c1cfad82", + "comment": "", + "command": "open", + "target": "/api/MetadataResolvers/External", + "targets": [], + "value": "" + }, { + "id": "9e6390c9-3122-4038-8dce-61ae0c157e4a", + "comment": "", + "command": "assertText", + "target": "css=#folder4 > .opened > .line > span", + "targets": [ + ["css=#folder4 > .opened > .line > span", "css:finder"] + ], + "value": "" + }, { + "id": "8c32b412-dc63-4be5-98fb-8c3cd67895af", + "comment": "", + "command": "assertText", + "target": "css=#folder7 > .opened > .line > span", + "targets": [ + ["css=#folder7 > .opened > .line > span", "css:finder"] + ], + "value": "" + }] + }], + "suites": [{ + "id": "d2caeac4-7520-4e3c-96b1-840610b6983c", + "name": "Default Suite", + "persistSession": false, + "parallel": false, + "timeout": 300, + "tests": ["841ade0e-83bd-4a4b-94f2-de6bd5c536b2"] + }], + "urls": ["http://localhost:10101/"], + "plugins": [] +} \ No newline at end of file diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolverUiDefinitionController.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolverUiDefinitionController.groovy index 35815d31e..fe4a54434 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolverUiDefinitionController.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolverUiDefinitionController.groovy @@ -13,11 +13,11 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.dynamicHttpMetadataProviderSchema +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.externalMetadataProviderSchema import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.filesystemMetadataProviderSchema import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.localDynamicMetadataProviderSchema import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR - /** * Controller implementing REST resource responsible for exposing structure definition for metadata resolvers user * interface in terms of JSON schema. @@ -50,6 +50,9 @@ class MetadataResolverUiDefinitionController { case SchemaType.DYNAMIC_HTTP_METADATA_RESOLVER: jsonSchemaLocation = dynamicHttpMetadataProviderSchema(this.jsonSchemaResourceLocationRegistry) break + case SchemaType.EXTERNAL_METADATA_RESOLVER: + jsonSchemaLocation = externalMetadataProviderSchema(this.jsonSchemaResourceLocationRegistry) + break default: throw new UnsupportedOperationException("Json schema for an unsupported metadata resolver (" + resolverType + ") was requested") } 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 4c1f3a118..170a45fa8 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 @@ -11,6 +11,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilF import edu.internet2.tier.shibboleth.admin.ui.domain.filters.SignatureValidationFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.opensaml.OpenSamlNameIdFormatFilter import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ExternalMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FilesystemMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver @@ -286,6 +287,12 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } + void constructXmlNodeForResolver(ExternalMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { + markupBuilderDelegate.MetadataFilters(providerRef: resolver.getXmlId()) { + childNodes() + } + } + void constructXmlNodeForResolver(FileBackedHttpMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { markupBuilderDelegate.MetadataProvider(id: resolver.xmlId, 'xsi:type': 'FileBackedHTTPMetadataProvider', @@ -449,11 +456,11 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { '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)) { + // We do not currently marshall the internal incommon chaining resolver (with BaseMetadataResolver type) + // We do not want to include the custom type: ExternalMetadataResolver + if ((mr.type != 'BaseMetadataResolver') && (mr.type != 'ExternalMetadataResolver') && (mr.enabled)) { constructXmlNodeForResolver(mr, delegate) { //TODO: enhance def didNamespaceProtectionFilter = !(shibUIConfiguration.protectedAttributeNamespaces && shibUIConfiguration.protectedAttributeNamespaces.size() > 0) @@ -478,6 +485,42 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } + @Override + Document generateExternalMetadataFilterConfiguration() { + // TODO: this can probably be a better writer + new StringWriter().withCloseable { writer -> + def xml = new MarkupBuilder(writer) + xml.omitEmptyAttributes = true + xml.omitNullAttributes = true + + // https://shibboleth.atlassian.net/wiki/spaces/IDP4/pages/1279033515/ByReferenceFilter + xml.MetadataFilter( + 'xsi:type': 'ByReference', + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:schemaLocation': 'urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd', + 'xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata', + 'xmlns': 'urn:mace:shibboleth:2.0:metadata', + 'xmlns:security': 'urn:mace:shibboleth:2.0:security', + 'xmlns:saml2': 'urn:oasis:names:tc:SAML:2.0:assertion' + ) { + resolversPositionOrderContainerService.allMetadataResolversInDefinedOrderOrUnordered.each { + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr -> + // Only include the custom type: ExternalMetadataResolver + if ((mr.type == 'ExternalMetadataResolver') && (mr.enabled)) { + constructXmlNodeForResolver(mr, delegate) { + mr.metadataFilters.each { edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter filter -> + if (filter.isFilterEnabled()) { + constructXmlNodeForFilter(filter, delegate) + } + } + } + } + } + } + return DOMBuilder.newInstance().parseText(writer.toString()) + } + } + private String generateJavaScriptRegexScript(String regex) { return """ "use strict"; 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 c897a31b5..fdc85e20f 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 @@ -6,6 +6,7 @@ import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolversPositionOrderContainerRepository; import edu.internet2.tier.shibboleth.admin.ui.scheduled.EntityDescriptorFilesScheduledTasks; +import edu.internet2.tier.shibboleth.admin.ui.scheduled.ExternalMetadataProvidersScheduledTasks; import edu.internet2.tier.shibboleth.admin.ui.scheduled.MetadataProvidersScheduledTasks; import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.GroupUpdatedEntityListener; import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.UserUpdatedEntityListener; @@ -88,6 +89,14 @@ public MetadataProvidersScheduledTasks metadataProvidersScheduledTasks( return new MetadataProvidersScheduledTasks(resource, metadataResolverService, fileWritingService()); } + @Bean + @ConditionalOnProperty(name = "shibui.external.metadataProviders.target") + public ExternalMetadataProvidersScheduledTasks externalMetadataProvidersScheduledTasks( + @Value("${shibui.external.metadataProviders.target}") final Resource resource, + final MetadataResolverService metadataResolverService) { + return new ExternalMetadataProvidersScheduledTasks(resource, metadataResolverService, fileWritingService()); + } + @Bean public EntityIdsSearchService entityIdsSearchService(LuceneUtility luceneUtility, Analyzer fullTokenAnalyzer) { return new EntityIdsSearchServiceImpl(luceneUtility, fullTokenAnalyzer); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java index d483319f3..34c5c1eaf 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java @@ -14,6 +14,7 @@ import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.*; import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.ENTITY_ATTRIBUTES_FILTERS; +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.EXTERNAL_METADATA_RESOLVER; import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.METADATA_SOURCES; import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.FILESYSTEM_METADATA_RESOLVER; import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.LOCAL_DYNAMIC_METADATA_RESOLVER; @@ -57,6 +58,8 @@ public class JsonSchemaComponentsConfiguration { @Setter private String nameIdFormatFilterUiSchemaLocation = "classpath:nameid-filter.schema.json"; + private String externalMetadataResolverUiSchemaLocation = "classpath:external.schema.json"; + @Bean public JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry(ResourceLoader resourceLoader, ObjectMapper jacksonMapper) { return JsonSchemaResourceLocationRegistry.inMemory() @@ -90,6 +93,12 @@ public JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry(Res .jacksonMapper(jacksonMapper) .detectMalformedJson(true) .build()) + .register(EXTERNAL_METADATA_RESOLVER, JsonSchemaLocationBuilder.with() + .jsonSchemaLocation(externalMetadataResolverUiSchemaLocation) + .resourceLoader(resourceLoader) + .jacksonMapper(jacksonMapper) + .detectMalformedJson(true) + .build()) .register(NAME_ID_FORMAT_FILTER, JsonSchemaLocationBuilder.with() .jsonSchemaLocation(nameIdFormatFilterUiSchemaLocation) .resourceLoader(resourceLoader) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java index 0f7b92338..6a11f07a2 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java @@ -82,7 +82,7 @@ public ResponseEntity unableToParseJson(Exception ex) { return ResponseEntity.badRequest().body(new ErrorResponse(HttpStatus.BAD_REQUEST.toString(), ex.getMessage(), ex.getCause().getMessage())); } - @GetMapping("/MetadataResolvers") + @GetMapping(value = "/MetadataResolvers", produces = "application/json") @Transactional(readOnly = true) public ResponseEntity getAll() { List resolvers = positionOrderContainerService.getAllMetadataResolversInDefinedOrderOrUnordered(); @@ -92,7 +92,6 @@ public ResponseEntity getAll() { @GetMapping(value = "/MetadataResolvers", produces = "application/xml") @Transactional(readOnly = true) public ResponseEntity getXml() throws IOException, TransformerException { - // TODO: externalize try (StringWriter writer = new StringWriter()) { Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); @@ -103,6 +102,19 @@ public ResponseEntity getXml() throws IOException, TransformerException { } } + @GetMapping(value = "/MetadataResolvers/External", produces = "application/xml") + @Transactional(readOnly = true) + public ResponseEntity getExternalXml() throws IOException, TransformerException { + try (StringWriter writer = new StringWriter()) { + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + + transformer.transform(new DOMSource(metadataResolverService.generateExternalMetadataFilterConfiguration()), new StreamResult(writer)); + return ResponseEntity.ok(writer.toString()); + } + } + @GetMapping("/MetadataResolvers/{resourceId}") @Transactional(readOnly = true) public ResponseEntity getOne(@PathVariable String resourceId) { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ExternalMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ExternalMetadataResolver.java new file mode 100644 index 000000000..cf488be82 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ExternalMetadataResolver.java @@ -0,0 +1,28 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.envers.Audited; + +import javax.persistence.Column; +import javax.persistence.Entity; + +@Entity +@EqualsAndHashCode(callSuper = true) +@Getter +@Setter +@ToString +@Audited +public class ExternalMetadataResolver extends MetadataResolver { + @Column + private String description; + + @Column + String name; + + public ExternalMetadataResolver() { + type = "ExternalMetadataResolver"; + } +} \ 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 63a04d764..19c96e753 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 @@ -43,7 +43,8 @@ @JsonSubTypes.Type(value = FileBackedHttpMetadataResolver.class, name = "FileBackedHttpMetadataResolver"), @JsonSubTypes.Type(value = DynamicHttpMetadataResolver.class, name = "DynamicHttpMetadataResolver"), @JsonSubTypes.Type(value = FilesystemMetadataResolver.class, name = "FilesystemMetadataResolver"), - @JsonSubTypes.Type(value = ResourceBackedMetadataResolver.class, name = "ResourceBackedMetadataResolver")}) + @JsonSubTypes.Type(value = ResourceBackedMetadataResolver.class, name = "ResourceBackedMetadataResolver"), + @JsonSubTypes.Type(value = ExternalMetadataResolver.class, name = "ExternalMetadataResolver")}) @Audited @AuditOverride(forClass = AbstractAuditable.class) public class MetadataResolver extends AbstractAuditable implements IActivatable { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaLocationLookup.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaLocationLookup.java index 71986eea7..b44e4e7ce 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaLocationLookup.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaLocationLookup.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.jsonschema; import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.ENTITY_ATTRIBUTES_FILTERS; +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.EXTERNAL_METADATA_RESOLVER; import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.METADATA_SOURCES; import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.FILESYSTEM_METADATA_RESOLVER; import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.LOCAL_DYNAMIC_METADATA_RESOLVER; @@ -66,6 +67,19 @@ public static JsonSchemaResourceLocation localDynamicMetadataProviderSchema(Json .orElseThrow(() -> new IllegalStateException("JSON schema resource location for local dynamic metadata resolver is not registered.")); } + /** + * Searches external metadata resolver JSON schema resource location object in the given location registry. + * + * @param resourceLocationRegistry + * @return external metadata resolver JSON schema resource location object + * @throws IllegalStateException if schema is not found in the given registry + */ + public static JsonSchemaResourceLocation externalMetadataProviderSchema(JsonSchemaResourceLocationRegistry resourceLocationRegistry) { + return resourceLocationRegistry + .lookup(EXTERNAL_METADATA_RESOLVER) + .orElseThrow(() -> new IllegalStateException("JSON schema resource location for external metadata resolver is not registered.")); + } + /** * Searches dynamic http metadata resolver JSON schema resource location object in the given location registry. * @@ -75,8 +89,8 @@ public static JsonSchemaResourceLocation localDynamicMetadataProviderSchema(Json */ public static JsonSchemaResourceLocation dynamicHttpMetadataProviderSchema(JsonSchemaResourceLocationRegistry resourceLocationRegistry) { return resourceLocationRegistry - .lookup(DYNAMIC_HTTP_METADATA_RESOLVER) - .orElseThrow(() -> new IllegalStateException("JSON schema resource location for dynamic http metadata resolver is not registered.")); + .lookup(DYNAMIC_HTTP_METADATA_RESOLVER) + .orElseThrow(() -> new IllegalStateException("JSON schema resource location for dynamic http metadata resolver is not registered.")); } /** @@ -91,4 +105,4 @@ public static JsonSchemaResourceLocation nameIdFormatFilterSchema(JsonSchemaReso .lookup(NAME_ID_FORMAT_FILTER) .orElseThrow(() -> new IllegalStateException("JSON schema resource location for name id format filter is not registered.")); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocation.java index 2d25fac0b..1b9054cd3 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocation.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocation.java @@ -104,7 +104,8 @@ public enum SchemaType { FILE_BACKED_HTTP_METADATA_RESOLVER("FileBackedHttpMetadataResolver"), FILESYSTEM_METADATA_RESOLVER("FilesystemMetadataResolver"), LOCAL_DYNAMIC_METADATA_RESOLVER("LocalDynamicMetadataResolver"), - DYNAMIC_HTTP_METADATA_RESOLVER("DynamicHttpMetadataResolver"); + DYNAMIC_HTTP_METADATA_RESOLVER("DynamicHttpMetadataResolver"), + EXTERNAL_METADATA_RESOLVER("ExternalMetadataResolver"); String jsonType; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/scheduled/ExternalMetadataProvidersScheduledTasks.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/scheduled/ExternalMetadataProvidersScheduledTasks.java new file mode 100644 index 000000000..b2e3c7d37 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/scheduled/ExternalMetadataProvidersScheduledTasks.java @@ -0,0 +1,53 @@ +package edu.internet2.tier.shibboleth.admin.ui.scheduled; + +import edu.internet2.tier.shibboleth.admin.ui.service.FileWritingService; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.core.io.WritableResource; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.transaction.annotation.Transactional; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.IOException; +import java.io.StringWriter; + +@Configuration +@ConditionalOnProperty("shibui.external.metadataProviders.target") +public class ExternalMetadataProvidersScheduledTasks { + private static final Logger logger = LoggerFactory.getLogger(ExternalMetadataProvidersScheduledTasks.class); + + private final Resource target; + private final MetadataResolverService metadataResolverService; + private final FileWritingService fileWritingService; + + public ExternalMetadataProvidersScheduledTasks(Resource target, MetadataResolverService metadataResolverService, FileWritingService fileWritingService) { + this.target = target; + this.metadataResolverService = metadataResolverService; + this.fileWritingService = fileWritingService; + } + + @Scheduled(fixedRateString = "${shibui.external.metadataProviders.taskRunRate:30000}") + @Transactional(readOnly = true) + public void generateMetadataProvidersFile() { + try (StringWriter os = new StringWriter()) { + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + + + transformer.transform(new DOMSource(metadataResolverService.generateExternalMetadataFilterConfiguration()), new StreamResult(os)); + this.fileWritingService.write((WritableResource)this.target, os.toString()); + } catch (IOException | TransformerException e) { + logger.error(e.getLocalizedMessage(), e); + } + } +} \ No newline at end of file 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 5fd205c20..d851cd021 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 @@ -7,6 +7,7 @@ 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 org.w3c.dom.Node; public interface MetadataResolverService { public MetadataResolver findByResourceId(String resourceId) throws EntityNotFoundException; @@ -16,4 +17,6 @@ public interface MetadataResolverService { public void reloadFilters(String metadataResolverName); public MetadataResolver updateMetadataResolverEnabledStatus(MetadataResolver existingResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException; -} + + public Document generateExternalMetadataFilterConfiguration(); +} \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index f6af0610e..109e7c30f 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -84,6 +84,12 @@ shibui.nameid-filter-ui-schema-location=classpath:nameid-filter.schema.json # shibui.metadataProviders.target=file:/opt/shibboleth-idp/conf/shibui-metadata-providers.xml # shibui.metadataProviders.taskRunRate=30000 +# Set the following property to periodically write out external metadata providers (ie metadata-filters) configuration. +# There is no default value; the following is just an example +# @see - https://shibboleth.atlassian.net/wiki/spaces/IDP4/pages/1279033515/ByReferenceFilter +# shibui.external.metadataProviders.target=file:/opt/shibboleth-idp/conf/metadata-filters.xml +# shibui.external.metadataProviders.taskRunRate=30000 + # Email configuration (local mailhog) # spring.mail.host=mailhog # spring.mail.port=1025 diff --git a/backend/src/main/resources/external.schema.json b/backend/src/main/resources/external.schema.json new file mode 100644 index 000000000..daa8c4503 --- /dev/null +++ b/backend/src/main/resources/external.schema.json @@ -0,0 +1,43 @@ +{ + "type": "object", + "required": [ + "name", + "@type", + "xmlId", + "description" + ], + "properties": { + "name": { + "title": "label.metadata-provider-name-dashboard-display-only", + "description": "tooltip.metadata-provider-name-dashboard-display-only", + "type": "string", + "widget": { + "id": "string", + "help": "message.must-be-unique" + } + }, + "@type": { + "title": "label.metadata-provider-type", + "description": "tooltip.metadata-provider-type", + "type": "string", + "const": "FilesystemMetadataResolver" + }, + "xmlId": { + "title": "label.metadata-resolver-id", + "description": "tooltip.xml-id", + "type": "string", + "minLength": 1 + }, + "enabled": { + "title": "label.enable-provider-upon-saving", + "description": "tooltip.enable-provider-upon-saving", + "type": "boolean", + "default": false + }, + "description": { + "title": "label.description", + "description": "tooltip.description", + "type": "string" + } + } +} \ No newline at end of file diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 994df5e8d..d5202280e 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -344,6 +344,7 @@ label.default-authentication-methods=Default Authentication Method(s) label.new-of-type=New { type } label.filters=Filters label.attributes=Attributes +label.metadata-resolver-id=Metadata Resolver ID label.metadata-filter-name=Metadata Filter Name (Dashboard Display Only) label.filter-enable=Enable this Filter? 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 c0a7d0e68..82bee21b2 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 @@ -96,6 +96,11 @@ class MetadataFiltersControllerTests extends AbstractBaseDataJpaTest { return null } + @Override + Document generateExternalMetadataFilterConfiguration() { + return null + } + @Override MetadataResolver findByResourceId(String resourceId) throws EntityNotFoundException { // This won't get called diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy index 0a53d5173..9905dbcea 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy @@ -1,14 +1,19 @@ package edu.internet2.tier.shibboleth.admin.ui.service +import com.google.common.collect.Lists import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.configuration.PlaceholderResolverComponentsConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeValue +import edu.internet2.tier.shibboleth.admin.ui.domain.XSString import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilFilter import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ClasspathMetadataResource import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ExternalMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataQueryProtocolScheme import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.RegexScheme @@ -18,6 +23,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSaml import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import edu.internet2.tier.shibboleth.admin.util.AttributeUtility import groovy.xml.DOMBuilder import groovy.xml.MarkupBuilder import net.shibboleth.ext.spring.resource.ResourceHelper @@ -31,6 +37,8 @@ import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean import org.springframework.core.io.ClassPathResource import org.springframework.test.context.ContextConfiguration +import org.w3c.dom.Document +import org.w3c.dom.Node import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input import spock.lang.Ignore @@ -67,6 +75,7 @@ class JPAMetadataResolverServiceImplTests extends AbstractBaseDataJpaTest { @Autowired TestObjectGenerator testObjectGenerator + AttributeUtility attributeUtility DOMBuilder domBuilder = DOMBuilder.newInstance() StringWriter writer = new StringWriter() MarkupBuilder markupBuilder @@ -75,6 +84,7 @@ class JPAMetadataResolverServiceImplTests extends AbstractBaseDataJpaTest { markupBuilder = new MarkupBuilder(writer) markupBuilder.omitNullAttributes = true markupBuilder.omitEmptyAttributes = true + attributeUtility = new AttributeUtility(openSamlObjects) } def cleanup() { @@ -82,6 +92,43 @@ class JPAMetadataResolverServiceImplTests extends AbstractBaseDataJpaTest { writer.close() } + def 'test generating ExternalMetadataResolver XML'() { + given: + def resolver = new ExternalMetadataResolver().with { + it.setEnabled(true) + it.setName("testme") + it.setXmlId("InCommonMD") + it.setDescription("some description that won't appear in the xml") + it.addFilter(new EntityAttributesFilter().with { + it.name = 'EntityAttributes' + EntityAttributesFilterTarget filterTarget = testObjectGenerator.buildEntityAttributesFilterTarget() + filterTarget.setSingleValue("https://sp.example.org/shibboleth") + it.setEntityAttributesFilterTarget(filterTarget) + def attribute = attributeUtility.createAttributeWithStringValues('http://shibboleth.net/ns/attributes/releaseAllValues', null, 'eduPersonPrincipalName') + attribute.nameFormat = 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri' + attribute.namespacePrefix = 'saml2' + attribute.attributeValues.each { val -> + ((XSString)val).namespacePrefix = 'saml2' + + '' + } + it.attributes = [attribute] + it.intoTransientRepresentation() + it.enabled = true; + it + }) + it + } + metadataResolverRepository.save(resolver) + metadataResolverService.reloadFilters("testme") + + when: + Document doc = JPAMetadataResolverServiceImpl.cast(metadataResolverService).generateExternalMetadataFilterConfiguration() + Node node = doc.getFirstChild() + + then: + generatedXmlIsTheSameAsExpectedXml('/conf/2269.xml', node) + } + def 'test adding a filter'() { given: def expectedXML = ''' diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy index f50263663..e2d67412e 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy @@ -3,20 +3,20 @@ package edu.internet2.tier.shibboleth.admin.ui.util import edu.internet2.tier.shibboleth.admin.ui.security.model.User import groovy.xml.XmlUtil import junit.framework.Assert -import javax.xml.transform.Source; -import javax.xml.transform.Transformer -import javax.xml.transform.TransformerException -import javax.xml.transform.TransformerFactory -import javax.xml.transform.dom.DOMSource -import javax.xml.transform.stream.StreamResult - import org.apache.commons.lang.StringUtils -import org.springframework.security.core.context.SecurityContextHolder import org.w3c.dom.Document +import org.w3c.dom.Node import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input import org.xmlunit.builder.Input.Builder +import javax.xml.transform.Source +import javax.xml.transform.Transformer +import javax.xml.transform.TransformerException +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult + /** * @author Bill Smith (wsmith@unicon.net) */ @@ -50,6 +50,19 @@ class TestHelpers { Assert.assertFalse(myDiff.toString(), myDiff.hasDifferences()); } + static void generatedXmlIsTheSameAsExpectedXml(String expectedXmlResource, Node generatedXml) { + def Builder builder = Input.fromNode(generatedXml) + def Source source = builder.build() + def myDiff = DiffBuilder.compare(Input.fromStream(TestHelpers.getResourceAsStream(expectedXmlResource))) + .withTest(builder) + .withAttributeFilter({attribute -> !attribute.name.equals("sourceDirectory")}) + .ignoreComments() + .ignoreWhitespace() + .build() + System.out.println("@@@ \n" + getString(source) + "\n") + Assert.assertFalse(myDiff.toString(), myDiff.hasDifferences()); + } + public static String getString(DOMSource domSource) throws TransformerException { StringWriter writer = new StringWriter(); StreamResult result = new StreamResult(writer); @@ -67,4 +80,4 @@ class TestHelpers { def user = new User(username: username, role: rolename) Optional.of(user) } -} +} \ No newline at end of file diff --git a/backend/src/test/resources/conf/2269.xml b/backend/src/test/resources/conf/2269.xml new file mode 100644 index 000000000..f4c166e29 --- /dev/null +++ b/backend/src/test/resources/conf/2269.xml @@ -0,0 +1,21 @@ + + + + + + + eduPersonPrincipalName + + https://sp.example.org/shibboleth + + + + \ No newline at end of file diff --git a/ui/public/assets/schema/provider/external.schema.json b/ui/public/assets/schema/provider/external.schema.json new file mode 100644 index 000000000..daa8c4503 --- /dev/null +++ b/ui/public/assets/schema/provider/external.schema.json @@ -0,0 +1,43 @@ +{ + "type": "object", + "required": [ + "name", + "@type", + "xmlId", + "description" + ], + "properties": { + "name": { + "title": "label.metadata-provider-name-dashboard-display-only", + "description": "tooltip.metadata-provider-name-dashboard-display-only", + "type": "string", + "widget": { + "id": "string", + "help": "message.must-be-unique" + } + }, + "@type": { + "title": "label.metadata-provider-type", + "description": "tooltip.metadata-provider-type", + "type": "string", + "const": "FilesystemMetadataResolver" + }, + "xmlId": { + "title": "label.metadata-resolver-id", + "description": "tooltip.xml-id", + "type": "string", + "minLength": 1 + }, + "enabled": { + "title": "label.enable-provider-upon-saving", + "description": "tooltip.enable-provider-upon-saving", + "type": "boolean", + "default": false + }, + "description": { + "title": "label.description", + "description": "tooltip.description", + "type": "string" + } + } +} \ No newline at end of file diff --git a/ui/src/app/metadata/domain/provider/definition/ExternalMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/definition/ExternalMetadataProviderDefinition.js new file mode 100644 index 000000000..a72d3601d --- /dev/null +++ b/ui/src/app/metadata/domain/provider/definition/ExternalMetadataProviderDefinition.js @@ -0,0 +1,92 @@ +import defaultsDeep from 'lodash/defaultsDeep'; +import API_BASE_PATH from "../../../../App.constant"; +import { BaseProviderDefinition } from "./BaseProviderDefinition"; + +export const ExternalMetadataProviderWizard = { + ...BaseProviderDefinition, + label: 'ExternalMetadataProvider', + type: 'ExternalMetadataResolver', + // schema: 'assets/schema/provider/external.schema.json', + schema: `${API_BASE_PATH}/ui/MetadataResolver/ExternalMetadataResolver`, + steps: [ + ...BaseProviderDefinition.steps, + { + id: 'common', + label: 'label.common-attributes', + index: 2, + initialValues: [], + fields: [ + 'xmlId', + 'metadataFile', + 'description' + ] + }, + { + id: 'summary', + label: 'label.finished', + index: 2, + initialValues: [], + fields: [ + 'enabled', + ] + }, + ], + uiSchema: defaultsDeep({ + layout: { + groups: [ + { + size: 8, + classNames: 'bg-light border rounded px-4 pt-4 pb-3 mb-4', + fields: [ + 'name', + '@type' + ] + }, + { + size: 8, + fields: [ + 'xmlId', + 'description', + ] + } + ] + }, + description: { + 'ui:widget': 'textarea' + } + }, BaseProviderDefinition.uiSchema) +}; + + +export const ExternalMetadataProviderEditor = { + ...ExternalMetadataProviderWizard, + steps: [ + { + id: 'common', + label: 'label.common-attributes', + index: 1, + initialValues: [], + fields: [ + 'name', + 'xmlId', + '@type', + 'description', + 'enabled' + ], + override: { + '@type': { + type: 'string', + readOnly: true, + widget: 'string', + oneOf: [{ enum: ['ExternalMetadataResolver'], + description: 'value.file-system-metadata-provider' }] + } + } + } + ], + uiSchema: defaultsDeep({ + '@type': { + 'ui:readonly': true + } + }, ExternalMetadataProviderWizard.uiSchema) +}; diff --git a/ui/src/app/metadata/domain/provider/index.js b/ui/src/app/metadata/domain/provider/index.js index 05c0e057d..9368df466 100644 --- a/ui/src/app/metadata/domain/provider/index.js +++ b/ui/src/app/metadata/domain/provider/index.js @@ -2,22 +2,26 @@ import { FileBackedHttpMetadataProviderWizard, FileBackedHttpMetadataProviderEdi import { DynamicHttpMetadataProviderWizard, DynamicHttpMetadataProviderEditor } from './definition/DynamicHttpMetadataProviderDefinition'; import { LocalDynamicMetadataProviderWizard, LocalDynamicMetadataProviderEditor } from './definition/LocalDynamicMetadataProviderDefinition'; import { FileSystemMetadataProviderWizard, FileSystemMetadataProviderEditor } from './definition/FileSystemMetadataProviderDefinition'; +import { ExternalMetadataProviderWizard, ExternalMetadataProviderEditor } from './definition/ExternalMetadataProviderDefinition'; export const MetadataProviderWizardTypes = [ FileBackedHttpMetadataProviderWizard, DynamicHttpMetadataProviderWizard, FileSystemMetadataProviderWizard, - LocalDynamicMetadataProviderWizard + LocalDynamicMetadataProviderWizard, + ExternalMetadataProviderWizard, ]; export const MetadataProviderEditorTypes = [ FileBackedHttpMetadataProviderEditor, DynamicHttpMetadataProviderEditor, LocalDynamicMetadataProviderEditor, - FileSystemMetadataProviderEditor + FileSystemMetadataProviderEditor, + ExternalMetadataProviderEditor, ]; export const FilterableProviders = [ FileBackedHttpMetadataProviderEditor.type, - DynamicHttpMetadataProviderEditor.type + DynamicHttpMetadataProviderEditor.type, + ExternalMetadataProviderEditor.type, ]; diff --git a/ui/src/app/metadata/new/NewProvider.js b/ui/src/app/metadata/new/NewProvider.js index 43a9ed715..e181747b8 100644 --- a/ui/src/app/metadata/new/NewProvider.js +++ b/ui/src/app/metadata/new/NewProvider.js @@ -7,7 +7,7 @@ import { MetadataProviderTypeSelector } from '../wizard/MetadataProviderTypeSele export function NewProvider() { - const { data } = useMetadataProviderTypes({}, []); + const { data = [] } = useMetadataProviderTypes({}, []); return (
@@ -20,7 +20,7 @@ export function NewProvider() {
- + {(data, onRestart) =>