diff --git a/.gitignore b/.gitignore index b298de799..160d8bdd0 100644 --- a/.gitignore +++ b/.gitignore @@ -415,3 +415,8 @@ beacon/spring/out /a.xml /application.yml /backend/src/test/resources/conf/deletem.xml +/testbed/authentication/shibui/saml-signing-cert.crt +/testbed/authentication/shibui/saml-signing-cert.key +/testbed/authentication/shibui/saml-signing-cert.pem +/testbed/authentication/shibui/samlKeystore.jks +/testbed/authentication/shibui/sp-metadata.xml diff --git a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolverControllerVersionEndpointsIntegrationTests.groovy b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolverControllerVersionEndpointsIntegrationTests.groovy index 92d53ff65..74821fcd2 100644 --- a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolverControllerVersionEndpointsIntegrationTests.groovy +++ b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolverControllerVersionEndpointsIntegrationTests.groovy @@ -9,7 +9,6 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFil import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityRoleWhiteListFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilterTarget -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.SignatureValidationFilter import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FilesystemMetadataResolver @@ -19,13 +18,9 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.RegexScheme import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.TemplateScheme import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository - import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverVersionService import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility - -import org.apache.commons.lang3.RandomStringUtils - import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.web.client.TestRestTemplate diff --git a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/MetadataResolverEnversVersioningTests.groovy b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/MetadataResolverEnversVersioningTests.groovy index 6d976a033..d828b8e19 100644 --- a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/MetadataResolverEnversVersioningTests.groovy +++ b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/MetadataResolverEnversVersioningTests.groovy @@ -27,7 +27,10 @@ import javax.persistence.EntityManager import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.HttpMetadataResolverAttributes.HttpCachingType.file import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.HttpMetadataResolverAttributes.HttpCachingType.none -import static edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.* +import static edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.getModifiedEntityNames +import static edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.getRevisionEntityForRevisionIndex +import static edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.getTargetEntityForRevisionIndex +import static edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.updateAndGetRevisionHistoryOfMetadataResolver /** * Testing metadata resolver envers versioning diff --git a/backend/src/integration/groovy/com/sebuilder/interpreter/webdriverfactory/Firefox.java b/backend/src/integration/groovy/com/sebuilder/interpreter/webdriverfactory/Firefox.java index a7ddb5217..6f3c9290b 100644 --- a/backend/src/integration/groovy/com/sebuilder/interpreter/webdriverfactory/Firefox.java +++ b/backend/src/integration/groovy/com/sebuilder/interpreter/webdriverfactory/Firefox.java @@ -16,8 +16,6 @@ package com.sebuilder.interpreter.webdriverfactory; -import java.io.File; -import java.util.HashMap; import org.openqa.selenium.firefox.FirefoxBinary; import org.openqa.selenium.firefox.FirefoxDriver; import org.openqa.selenium.firefox.FirefoxOptions; @@ -25,6 +23,9 @@ import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.RemoteWebDriver; +import java.io.File; +import java.util.HashMap; + public class Firefox implements WebDriverFactory { /** * @param config Key/value pairs treated as required capabilities, with the exception of: @@ -50,4 +51,4 @@ public RemoteWebDriver make(HashMap config) { options.setBinary(fb); return new FirefoxDriver(options); } -} +} \ 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 fed08da45..1455db029 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 @@ -156,11 +156,12 @@ class SeleniumSIDETest extends Specification { 'SHIBUI-2267: Verify new RPO CRUD' | '/SHIBUI-2267.side' 'SHIBUI-2380: OIDC metadata source CRUD' | '/SHIBUI-2380.side' 'SHIBUI-1674: Verify metadata source tooltips' | '/SHIBUI-1674-1.side' -// 'SHIBUI-1674: Verify metadata provider tooltips' | '/SHIBUI-1674-2.side' + 'SHIBUI-1674: Verify metadata provider tooltips' | '/SHIBUI-1674-2.side' 'SHIBUI-1674: Verify advanced menu tooltips' | '/SHIBUI-1674-3.side' 'SHIBUI-2270: Verify property set CRUD' | '/SHIBUI-2270-1.side' 'SHIBUI-2270: Verify full property set' | '/SHIBUI-2270-2.side' + 'SHIBUI-2394: Multiple levels of approval' | '/SHIBUI-2394.side' 'SHIBUI-2268: Verify Algorithm Filter' | '/SHIBUI-2268.side' - 'SHIBUI-2269: Verify XML generation of external filters' | '/SHIBUI-2269.side' + 'SHIBUI-2269: Verify XML generation of external filters' | '/SHIBUI-2269.side' // Leave this as the last test in order to keep the suite running without strange errors. } -} \ No newline at end of file +} diff --git a/backend/src/integration/groovy/jp/vmi/selenium/selenese/Runner.java b/backend/src/integration/groovy/jp/vmi/selenium/selenese/Runner.java index e866b06ac..acc94ad8d 100644 --- a/backend/src/integration/groovy/jp/vmi/selenium/selenese/Runner.java +++ b/backend/src/integration/groovy/jp/vmi/selenium/selenese/Runner.java @@ -1,38 +1,7 @@ package jp.vmi.selenium.selenese; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintStream; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Deque; -import java.util.EnumSet; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.output.NullOutputStream; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.time.FastDateFormat; -import org.openqa.selenium.Alert; -import org.openqa.selenium.HasCapabilities; -import org.openqa.selenium.JavascriptExecutor; -import org.openqa.selenium.OutputType; -import org.openqa.selenium.TakesScreenshot; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.remote.Augmenter; -import org.openqa.selenium.remote.RemoteWebDriver; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.assertthat.selenium_shutterbug.core.Shutterbug; import com.assertthat.selenium_shutterbug.utils.web.ScrollStrategy; - import jp.vmi.html.result.HtmlResult; import jp.vmi.html.result.HtmlResultHolder; import jp.vmi.junit.result.JUnitResult; @@ -55,9 +24,38 @@ import jp.vmi.selenium.selenese.utils.MouseUtils; import jp.vmi.selenium.selenese.utils.PathUtils; import jp.vmi.selenium.webdriver.WebDriverPreparator; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.output.NullOutputStream; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.FastDateFormat; +import org.openqa.selenium.Alert; +import org.openqa.selenium.HasCapabilities; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.remote.Augmenter; +import org.openqa.selenium.remote.RemoteWebDriver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Deque; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; -import static jp.vmi.selenium.selenese.result.Unexecuted.*; -import static org.openqa.selenium.remote.CapabilityType.*; +import static jp.vmi.selenium.selenese.result.Unexecuted.UNEXECUTED; +import static org.openqa.selenium.remote.CapabilityType.TAKES_SCREENSHOT; /** * Provide Java API to run Selenese script. @@ -851,4 +849,4 @@ public void unhighlight() { void setupMaxTimeTimer(long maxTime) { this.maxTimeTimer = new MaxTimeActiveTimer(maxTime); } -} +} \ No newline at end of file diff --git a/backend/src/integration/groovy/jp/vmi/selenium/selenese/command/AttachFile.java b/backend/src/integration/groovy/jp/vmi/selenium/selenese/command/AttachFile.java index 02bf54d65..3b82c0298 100644 --- a/backend/src/integration/groovy/jp/vmi/selenium/selenese/command/AttachFile.java +++ b/backend/src/integration/groovy/jp/vmi/selenium/selenese/command/AttachFile.java @@ -25,25 +25,24 @@ package jp.vmi.selenium.selenese.command; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; - -import org.apache.commons.io.FilenameUtils; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.io.TemporaryFilesystem; - import com.google.common.io.Resources; - import jp.vmi.selenium.selenese.Context; import jp.vmi.selenium.selenese.result.Error; import jp.vmi.selenium.selenese.result.Result; import jp.vmi.selenium.selenese.result.Warning; +import org.apache.commons.io.FilenameUtils; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.io.TemporaryFilesystem; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; -import static jp.vmi.selenium.selenese.command.ArgumentType.*; -import static jp.vmi.selenium.selenese.result.Success.*; +import static jp.vmi.selenium.selenese.command.ArgumentType.LOCATOR; +import static jp.vmi.selenium.selenese.command.ArgumentType.VALUE; +import static jp.vmi.selenium.selenese.result.Success.SUCCESS; /** * Re-implementation of AttachFile. @@ -109,4 +108,4 @@ protected Result executeImpl(Context context, String... curArgs) { element.sendKeys(outputTo.getAbsolutePath()); return SUCCESS; } -} +} \ No newline at end of file diff --git a/backend/src/integration/resources/SHIBUI-1334-1.side b/backend/src/integration/resources/SHIBUI-1334-1.side index 28458a417..56a095d0b 100644 --- a/backend/src/integration/resources/SHIBUI-1334-1.side +++ b/backend/src/integration/resources/SHIBUI-1334-1.side @@ -2748,7 +2748,7 @@ "id": "bde2bbbb-df66-4e07-a770-ec9125fe3e81", "comment": "", "command": "pause", - "target": "7000", + "target": "10000", "targets": [], "value": "" }, { diff --git a/backend/src/integration/resources/SHIBUI-1407-1.side b/backend/src/integration/resources/SHIBUI-1407-1.side index 7ab010496..b86ffc627 100644 --- a/backend/src/integration/resources/SHIBUI-1407-1.side +++ b/backend/src/integration/resources/SHIBUI-1407-1.side @@ -2461,7 +2461,7 @@ "id": "4dbf4b6f-7de9-49e1-a23f-ff748f5a986b", "comment": "", "command": "pause", - "target": "7000", + "target": "10000", "targets": [], "value": "" }, { @@ -2514,7 +2514,7 @@ "id": "39637add-5eb4-40d0-b840-8eb1972ede0f", "comment": "", "command": "pause", - "target": "10000", + "target": "15000", "targets": [], "value": "" }, { diff --git a/backend/src/integration/resources/SHIBUI-1503-1.side b/backend/src/integration/resources/SHIBUI-1503-1.side index cb0be998d..35736f797 100644 --- a/backend/src/integration/resources/SHIBUI-1503-1.side +++ b/backend/src/integration/resources/SHIBUI-1503-1.side @@ -509,6 +509,22 @@ ["xpath=//div[4]/a", "xpath:position"] ], "value": "" + }, { + "id": "a43898de-b92d-443e-8686-fba526f403ec", + "comment": "", + "command": "click", + "target": "id=enable-btn", + "targets": [ + ["id=enable-btn", "id"], + ["linkText=Enable Metadata Sources1", "linkText"], + ["css=#enable-btn", "css:finder"], + ["xpath=//a[@id='enable-btn']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/dashboard/admin/actions/enable')]", "xpath:href"], + ["xpath=//div[2]/div/div/a", "xpath:position"], + ["xpath=//a[contains(.,'Enable Metadata Sources1')]", "xpath:innerText"] + ], + "value": "" }, { "id": "b1a8c4b1-d164-4f32-adb3-6cfb76951f28", "comment": "", diff --git a/backend/src/integration/resources/SHIBUI-1503-2.side b/backend/src/integration/resources/SHIBUI-1503-2.side index 65ccf98a9..febe7bd02 100644 --- a/backend/src/integration/resources/SHIBUI-1503-2.side +++ b/backend/src/integration/resources/SHIBUI-1503-2.side @@ -101,6 +101,22 @@ "target": "css=.nav-item > .d-flex", "targets": [], "value": "" + }, { + "id": "1f9a1cc1-00d5-4ecd-a0d1-3cec1683286d", + "comment": "", + "command": "click", + "target": "id=user-access-btn", + "targets": [ + ["id=user-access-btn", "id"], + ["linkText=User Access Request2", "linkText"], + ["css=#user-access-btn", "css:finder"], + ["xpath=//a[@id='user-access-btn']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div[3]/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/dashboard/admin/actions/useraccess')]", "xpath:href"], + ["xpath=//div[2]/div/div[3]/a", "xpath:position"], + ["xpath=//a[contains(.,'User Access Request2')]", "xpath:innerText"] + ], + "value": "" }, { "id": "a98143b5-647f-4e7e-b920-f6e6875d7372", "comment": "", diff --git a/backend/src/integration/resources/SHIBUI-1503-3.side b/backend/src/integration/resources/SHIBUI-1503-3.side index 60250abbd..3eaaf09e2 100644 --- a/backend/src/integration/resources/SHIBUI-1503-3.side +++ b/backend/src/integration/resources/SHIBUI-1503-3.side @@ -102,14 +102,14 @@ "id": "659e4909-239b-4895-aa54-8bf3a6bd57cd", "comment": "", "command": "waitForElementVisible", - "target": "xpath=//table/tbody/tr[td[.='none2']]", + "target": "xpath=//table/tbody/tr[td[.='Approver']]", "targets": [], "value": "30000" }, { "id": "dc06ff49-c076-4f60-95d1-a42514cc6038", "comment": "", "command": "select", - "target": "xpath=//table/tbody/tr[td[.='none2']]/td[4]/select", + "target": "xpath=//table/tbody/tr[td[.='Approver']]/td[4]/select", "targets": [], "value": "label=ROLE_USER" }, { @@ -168,7 +168,7 @@ ["xpath=//input[@name='username']", "xpath:attributes"], ["xpath=//input", "xpath:position"] ], - "value": "none2" + "value": "Approver" }, { "id": "c8bf8ea5-1f75-4a40-aca4-9dfa6a6056dc", "comment": "", @@ -180,7 +180,7 @@ ["xpath=//input[@name='password']", "xpath:attributes"], ["xpath=//tr[2]/td[2]/input", "xpath:position"] ], - "value": "none2pass" + "value": "password" }, { "id": "ba66c45f-2436-4fe7-a5a9-31b55ffe8118", "comment": "", @@ -214,21 +214,21 @@ ["xpath=//a[contains(.,'Metadata Sources')]", "xpath:innerText"] ], "value": "Metadata Sources" - },{ - "id": "4ec2c493-85e4-403b-9b09-031c5728f498", - "comment": "", - "command": "open", - "target": "/api/heheheheheheheWipeout", - "targets": [], - "value": "" - }, { - "id": "e074980a-8f21-4c22-8412-c4b6fcdcd1a4", - "comment": "", - "command": "assertText", - "target": "css=body", - "targets": [], - "value": "yes, you did it" - }] + }, { + "id": "4ec2c493-85e4-403b-9b09-031c5728f498", + "comment": "", + "command": "open", + "target": "/api/heheheheheheheWipeout", + "targets": [], + "value": "" + }, { + "id": "e074980a-8f21-4c22-8412-c4b6fcdcd1a4", + "comment": "", + "command": "assertText", + "target": "css=body", + "targets": [], + "value": "yes, you did it" + }] }], "suites": [{ "id": "173aaf44-c763-416e-ab3c-d5afd5ffcd29", diff --git a/backend/src/integration/resources/SHIBUI-1674-2.side b/backend/src/integration/resources/SHIBUI-1674-2.side index 7e24d99e2..b220ee571 100644 --- a/backend/src/integration/resources/SHIBUI-1674-2.side +++ b/backend/src/integration/resources/SHIBUI-1674-2.side @@ -689,13 +689,6 @@ "target": "css=div[role=\"tooltip\"]", "targets": [], "value": "The minimum duration for which metadata will be cached before it is refreshed." - }, { - "id": "6afe9417-450d-4820-8ed4-300188f38196", - "comment": "", - "command": "mouseOut", - "target": "css=.row:nth-child(2) path", - "targets": [], - "value": "" }, { "id": "da0ce3d8-75e6-4b84-b3a7-8b423399044e", "comment": "", @@ -931,43 +924,6 @@ "target": "css=div[role=\"tooltip\"]", "targets": [], "value": "The minimum duration for which metadata will be cached before it is refreshed." - }, { - "id": "687d2359-86f5-45de-bbfd-6557f62fd6d1", - "comment": "", - "command": "mouseOut", - "target": "css=.row:nth-child(2) path", - "targets": [], - "value": "" - }, { - "id": "fba778ef-2d9a-4fe9-9e6a-291560e3d807", - "comment": "", - "command": "click", - "target": "css=body", - "targets": [], - "value": "" - }, { - "id": "86512870-b695-44b6-a112-ea60375586f4", - "comment": "", - "command": "pause", - "target": "1000", - "targets": [], - "value": "" - }, { - "id": "8f77dc16-2b54-46d7-b6bd-bf6fd046e8b6", - "comment": "", - "command": "mouseOver", - "target": "css=.row:nth-child(8) .svg-inline--fa", - "targets": [ - ["css=.row:nth-child(8) .svg-inline--fa", "css:finder"] - ], - "value": "" - }, { - "id": "3fd29e97-1178-4bb5-9e76-22e89bca717c", - "comment": "", - "command": "assertText", - "target": "css=div[role=\"tooltip\"]", - "targets": [], - "value": "Flag indicating whether the system should initialize from the persistent cache in the background. Initializing from the cache in the background will improve IdP startup times." }, { "id": "d1ee0afc-651b-4da5-bd99-eac47bbceb78", "comment": "", @@ -980,22 +936,6 @@ ["xpath=//li[2]/button", "xpath:position"] ], "value": "" - }, { - "id": "aea0e033-111e-4a5d-8038-ec222786a695", - "comment": "", - "command": "mouseOver", - "target": "css=.row:nth-child(4) .svg-inline--fa:nth-child(2)", - "targets": [ - ["css=.row:nth-child(4) .svg-inline--fa:nth-child(2)", "css:finder"] - ], - "value": "" - }, { - "id": "80e0d456-3951-4858-8423-7e04d6debb96", - "comment": "", - "command": "assertText", - "target": "css=div[role=\"tooltip\"]", - "targets": [], - "value": "Controls whether to keep entities descriptors that contain no entity descriptors. Note: If this attribute is set to false, the resulting output may not be schema-valid since an element must include at least one child element, either an element or an element." }, { "id": "148a84ef-0353-425d-9a63-79ccaa01478d", "comment": "", @@ -1264,22 +1204,6 @@ ["xpath=//select", "xpath:position"] ], "value": "label=EntityAttributes" - }, { - "id": "e2b1a582-a3ce-4adb-b10c-0804f44a56ae", - "comment": "", - "command": "mouseOver", - "target": "css=.justify-content-start > .btn path", - "targets": [ - ["css=.justify-content-start > .btn path", "css:finder"] - ], - "value": "" - }, { - "id": "c42fe40a-496c-491b-834e-694181da46d1", - "comment": "", - "command": "assertText", - "target": "css=div[role=\"tooltip\"]", - "targets": [], - "value": "Indicates the type of search to be performed." }, { "id": "e4596fdd-7ffc-4636-95d3-870d6bd51f20", "comment": "", @@ -1322,6 +1246,29 @@ ["xpath=//select", "xpath:position"] ], "value": "label=NameIDFormat" + }, { + "id": "47c3d355-4218-456a-9441-2d5d2ffa4d34", + "comment": "", + "command": "waitForElementVisible", + "target": "css=.ms-2 > path", + "targets": [], + "value": "30000" + }, { + "id": "a6dcb984-5ad2-4004-8129-931adfc54a87", + "comment": "", + "command": "mouseOver", + "target": "css=.ms-2 > path", + "targets": [ + ["css=.ms-2 > path", "css:finder"] + ], + "value": "" + }, { + "id": "cda8d83d-d242-4def-b4cf-f696864fa806", + "comment": "", + "command": "mouseOut", + "target": "css=.ms-2 > path", + "targets": [], + "value": "" }, { "id": "b358a7c0-87a5-4b74-8013-c0ab379278c2", "comment": "", @@ -1337,7 +1284,7 @@ "command": "assertText", "target": "css=div[role=\"tooltip\"]", "targets": [], - "value": "Whether to remove any existing formats from a role if any are added by the filter (unmodified roles will be untouched regardless of this setting)" + "value": "Whether to use the SHA1 Signing Algorithm. In cryptography, SHA-1 (Secure Hash Algorithm 1) is cryptographically broken but still widely used. It takes an input and produces a 160-bit (20-byte) hash value." }, { "id": "59d268fc-f9ba-4c9c-b412-f17ca72b67d1", "comment": "", diff --git a/backend/src/integration/resources/SHIBUI-1740-1.side b/backend/src/integration/resources/SHIBUI-1740-1.side index f2b1ed457..776bc06ea 100644 --- a/backend/src/integration/resources/SHIBUI-1740-1.side +++ b/backend/src/integration/resources/SHIBUI-1740-1.side @@ -59,6 +59,27 @@ "target": "id=advanced-nav-dropdown-toggle", "targets": [], "value": "30000" + }, { + "id": "dc2bc3fa-631d-43be-b9b9-d92bcf1619ec", + "comment": "", + "command": "open", + "target": "/api/heheheheheheheWipeout", + "targets": [], + "value": "" + }, { + "id": "e3ef0c57-5d19-4f25-b1b3-63f1520fbf07", + "comment": "", + "command": "assertText", + "target": "css=body", + "targets": [], + "value": "yes, you did it" + }, { + "id": "7a7878a7-4258-42b9-b3d9-6b67d582faa6", + "comment": "", + "command": "open", + "target": "/", + "targets": [], + "value": "" }, { "id": "0bdcd2aa-3e9e-41be-96d2-abf567538990", "comment": "", @@ -186,9 +207,9 @@ "id": "62b9e743-cc16-4931-9064-06c15b057318", "comment": "", "command": "click", - "target": "xpath=//td[contains(.,'Test Group')]/parent::*/td[3]/a", + "target": "xpath=//td[contains(.,'Test Group')]/parent::*/td[4]/a", "targets": [ - ["css=tr:nth-child(6) .text-primary path", "css:finder"] + ["css=tr:nth-child(10) .text-primary path", "css:finder"] ], "value": "" }, { @@ -279,7 +300,7 @@ "id": "3f463655-29df-4c52-bc53-b96b60b845fd", "comment": "", "command": "click", - "target": "xpath=//td[contains(.,'Test Group')]/parent::*/td[3]/button", + "target": "xpath=//td[contains(.,'Test Group')]/parent::*/td[4]/button", "targets": [ ["css=tr:nth-child(6) .text-danger > .svg-inline--fa", "css:finder"] ], @@ -302,21 +323,21 @@ "target": "xpath=//li[contains(.,'Deleted group successfully.')]", "targets": [], "value": "Deleted group successfully." - },{ - "id": "4ec2c493-85e4-403b-9b09-031c5728f498", - "comment": "", - "command": "open", - "target": "/api/heheheheheheheWipeout", - "targets": [], - "value": "" - }, { - "id": "e074980a-8f21-4c22-8412-c4b6fcdcd1a4", - "comment": "", - "command": "assertText", - "target": "css=body", - "targets": [], - "value": "yes, you did it" - }] + }, { + "id": "4ec2c493-85e4-403b-9b09-031c5728f498", + "comment": "", + "command": "open", + "target": "/api/heheheheheheheWipeout", + "targets": [], + "value": "" + }, { + "id": "e074980a-8f21-4c22-8412-c4b6fcdcd1a4", + "comment": "", + "command": "assertText", + "target": "css=body", + "targets": [], + "value": "yes, you did it" + }] }], "suites": [{ "id": "bb170239-568b-4e90-991e-1a5882465aaa", diff --git a/backend/src/integration/resources/SHIBUI-1740-2.side b/backend/src/integration/resources/SHIBUI-1740-2.side index c1a7fedd5..196be44df 100644 --- a/backend/src/integration/resources/SHIBUI-1740-2.side +++ b/backend/src/integration/resources/SHIBUI-1740-2.side @@ -99,11 +99,11 @@ "id": "bfae23fa-f6ab-4cb1-a056-e16e309194d2", "comment": "", "command": "assertText", - "target": "css=tr:nth-child(4) > td:nth-child(1)", + "target": "css=tr:nth-child(7) > td:nth-child(1)", "targets": [ - ["css=tr:nth-child(4) > td:nth-child(1)", "css:finder"], - ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div[2]/table/tbody/tr[4]/td", "xpath:idRelative"], - ["xpath=//tr[4]/td", "xpath:position"], + ["css=tr:nth-child(7) > td:nth-child(1)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div[2]/table/tbody/tr[7]/td", "xpath:idRelative"], + ["xpath=//tr[7]/td", "xpath:position"], ["xpath=//td[contains(.,'nonadmin')]", "xpath:innerText"] ], "value": "nonadmin" @@ -116,9 +116,9 @@ ["css=tr:nth-child(5) > td:nth-child(1)", "css:finder"], ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div[2]/table/tbody/tr[5]/td", "xpath:idRelative"], ["xpath=//tr[5]/td", "xpath:position"], - ["xpath=//td[contains(.,'none')]", "xpath:innerText"] + ["xpath=//td[contains(.,'YYY')]", "xpath:innerText"] ], - "value": "none" + "value": "YYY" }, { "id": "346cec03-bbdd-435b-bdf4-ffaffd159b12", "comment": "", @@ -130,16 +130,17 @@ ["xpath=//tr[4]/td[2]", "xpath:position"], ["xpath=//td[contains(.,'default user-group')]", "xpath:innerText"] ], - "value": "default user-group" + "value": "XXX" }, { "id": "9fb41858-7d3d-4454-a07b-8a88ce2d726e", "comment": "", "command": "assertText", - "target": "css=tr:nth-child(5) > td:nth-child(2)", + "target": "css=tr:nth-child(7) > td:nth-child(2)", "targets": [ - ["css=tr:nth-child(5) > td:nth-child(2)", "css:finder"], - ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div[2]/table/tbody/tr[5]/td[2]", "xpath:idRelative"], - ["xpath=//tr[5]/td[2]", "xpath:position"] + ["css=tr:nth-child(7) > td:nth-child(2)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div[2]/table/tbody/tr[7]/td[2]", "xpath:idRelative"], + ["xpath=//tr[7]/td[2]", "xpath:position"], + ["xpath=//td[contains(.,'default user-group')]", "xpath:innerText"] ], "value": "default user-group" }, { @@ -211,43 +212,57 @@ "id": "70bb9b86-12d0-493e-815a-9fae0ea6e2d3", "comment": "", "command": "assertValue", - "target": "id=role-none", + "target": "id=role-Approver", "targets": [ - ["id=role-none", "id"], - ["name=role-none", "name"], - ["css=#role-none", "css:finder"], - ["xpath=//select[@id='role-none']", "xpath:attributes"], - ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[3]/td[3]/select", "xpath:idRelative"], - ["xpath=//tr[3]/td[3]/select", "xpath:position"] + ["id=role-Approver", "id"], + ["name=role-Approver", "name"], + ["css=#role-Approver", "css:finder"], + ["xpath=//select[@id='role-Approver']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[3]/td[4]/select", "xpath:idRelative"], + ["xpath=//tr[3]/td[4]/select", "xpath:position"] ], "value": "ROLE_NONE" }, { "id": "3cdd6916-5fad-4fb7-a422-b10cc38f1fb2", "comment": "", "command": "assertEditable", - "target": "id=role-none", - "targets": [], + "target": "id=role-Approver", + "targets": [ + ["id=role-Approver", "id"], + ["name=role-Approver", "name"], + ["css=#role-Approver", "css:finder"], + ["xpath=//select[@id='role-Approver']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[3]/td[4]/select", "xpath:idRelative"], + ["xpath=//tr[3]/td[4]/select", "xpath:position"] + ], "value": "" }, { "id": "d90dd945-c2f4-428c-8bb9-8557a1a93ef9", "comment": "", "command": "assertValue", - "target": "id=group-none", + "target": "id=group-Approver", "targets": [ - ["id=group-none", "id"], - ["name=group-none", "name"], - ["css=#group-none", "css:finder"], - ["xpath=//select[@id='group-none']", "xpath:attributes"], - ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[3]/td[4]/span/select", "xpath:idRelative"], - ["xpath=//tr[3]/td[4]/span/select", "xpath:position"] + ["id=group-Approver", "id"], + ["name=group-Approver", "name"], + ["css=#group-Approver", "css:finder"], + ["xpath=//select[@id='group-Approver']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[3]/td[5]/span/select", "xpath:idRelative"], + ["xpath=//tr[3]/td[5]/span/select", "xpath:position"] ], - "value": "none" + "value": "Approver" }, { "id": "d6d6cae9-acb7-46de-9ec7-0a76a8f6b61e", "comment": "", "command": "assertEditable", - "target": "id=group-none", - "targets": [], + "target": "id=group-Approver", + "targets": [ + ["id=group-Approver", "id"], + ["name=group-Approver", "name"], + ["css=#group-Approver", "css:finder"], + ["xpath=//select[@id='group-Approver']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/table/tbody/tr[3]/td[5]/span/select", "xpath:idRelative"], + ["xpath=//tr[3]/td[5]/span/select", "xpath:position"] + ], "value": "" }, { "id": "7d9099ef-bb52-4abd-b6b4-5ed70fdc56db", @@ -298,21 +313,21 @@ ["xpath=//tr[4]/td[4]/span/select", "xpath:position"] ], "value": "" - },{ - "id": "4ec2c493-85e4-403b-9b09-031c5728f498", - "comment": "", - "command": "open", - "target": "/api/heheheheheheheWipeout", - "targets": [], - "value": "" - }, { - "id": "e074980a-8f21-4c22-8412-c4b6fcdcd1a4", - "comment": "", - "command": "assertText", - "target": "css=body", - "targets": [], - "value": "yes, you did it" - }] + }, { + "id": "4ec2c493-85e4-403b-9b09-031c5728f498", + "comment": "", + "command": "open", + "target": "/api/heheheheheheheWipeout", + "targets": [], + "value": "" + }, { + "id": "e074980a-8f21-4c22-8412-c4b6fcdcd1a4", + "comment": "", + "command": "assertText", + "target": "css=body", + "targets": [], + "value": "yes, you did it" + }] }], "suites": [{ "id": "16a0d393-c257-46c4-9a0f-dc2a84dfc23b", diff --git a/backend/src/integration/resources/SHIBUI-1743-2.side b/backend/src/integration/resources/SHIBUI-1743-2.side index 88fd04ec4..95930e493 100644 --- a/backend/src/integration/resources/SHIBUI-1743-2.side +++ b/backend/src/integration/resources/SHIBUI-1743-2.side @@ -126,9 +126,9 @@ "id": "a9865fff-afe0-4786-8a32-18eb1f920424", "comment": "", "command": "click", - "target": "css=tr:nth-child(4) .text-primary path", + "target": "css=tr:nth-child(7) .text-primary path", "targets": [ - ["css=tr:nth-child(4) .text-primary path", "css:finder"] + ["css=tr:nth-child(7) .text-primary path", "css:finder"] ], "value": "" }, { diff --git a/backend/src/integration/resources/SHIBUI-2269.side b/backend/src/integration/resources/SHIBUI-2269.side index f2848dddb..3b2265392 100644 --- a/backend/src/integration/resources/SHIBUI-2269.side +++ b/backend/src/integration/resources/SHIBUI-2269.side @@ -211,6 +211,39 @@ ["xpath=//li[3]/button", "xpath:position"] ], "value": "" + }, { + "id": "836b08a5-1a08-4482-aff2-455fb02f3eb6", + "comment": "", + "command": "waitForElementEditable", + "target": "id=enable-switch-0", + "targets": [ + ["id=enable-switch-0", "id"], + ["css=#enable-switch-0", "css:finder"], + ["xpath=//input[@id='enable-switch-0']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/div/table/tbody/tr/td[6]/span/div/input", "xpath:idRelative"], + ["xpath=//span/div/input", "xpath:position"] + ], + "value": "30000" + }, { + "id": "29aa153e-4f7d-4468-bbef-db0b3f9de6eb", + "comment": "", + "command": "click", + "target": "id=enable-switch-0", + "targets": [ + ["id=enable-switch-0", "id"], + ["css=#enable-switch-0", "css:finder"], + ["xpath=//input[@id='enable-switch-0']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/div/table/tbody/tr/td[6]/span/div/input", "xpath:idRelative"], + ["xpath=//span/div/input", "xpath:position"] + ], + "value": "" + }, { + "id": "4753e8d9-eca2-4616-879b-64c6308c2b94", + "comment": "", + "command": "waitForElementVisible", + "target": "css=.alert", + "targets": [], + "value": "30000" }, { "id": "ab15e915-02b9-4616-9f92-ffcb0386918c", "comment": "", @@ -454,31 +487,6 @@ ["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": "", diff --git a/backend/src/integration/resources/SHIBUI-2394.side b/backend/src/integration/resources/SHIBUI-2394.side new file mode 100644 index 000000000..76e5bdcdd --- /dev/null +++ b/backend/src/integration/resources/SHIBUI-2394.side @@ -0,0 +1,783 @@ +{ + "id": "1b31a551-eb09-4bd4-8db9-694bf1539a46", + "version": "2.0", + "name": "SHIBUI-2394", + "url": "http://localhost:10101", + "tests": [{ + "id": "841ade0e-83bd-4a4b-94f2-de6bd5c536b2", + "name": "SHIBUI-2394", + "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": "f7421850-3d87-45ad-94f0-d6eac919426a", + "comment": "", + "command": "waitForElementVisible", + "target": "linkText=Admin", + "targets": [], + "value": "30000" + }, { + "id": "abc99561-21e2-45f1-acd2-33d91aee8c3f", + "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": "ed1dab3e-cf01-420d-a3d4-d1290a1bb62f", + "comment": "", + "command": "waitForElementEditable", + "target": "id=role-Approver", + "targets": [], + "value": "30000" + }, { + "id": "984a6cca-85e2-4dc3-9ece-495f227e4406", + "comment": "", + "command": "select", + "target": "id=role-Approver", + "targets": [], + "value": "label=ROLE_USER" + }, { + "id": "9a84712c-6df1-4d3e-af23-d9cb5daef3be", + "comment": "", + "command": "waitForElementPresent", + "target": "css=.alert", + "targets": [], + "value": "30000" + }, { + "id": "89c95061-fe7a-4223-a8b4-4fccc3a986c4", + "comment": "", + "command": "waitForElementNotPresent", + "target": "css=.alert", + "targets": [], + "value": "30000" + }, { + "id": "626494b7-b86f-423b-a027-81f079674ee2", + "comment": "", + "command": "select", + "target": "id=role-Submitter", + "targets": [], + "value": "label=ROLE_USER" + }, { + "id": "2cb31ab6-5ffb-4178-a8a5-6cb5078367fd", + "comment": "", + "command": "waitForElementPresent", + "target": "css=.alert", + "targets": [], + "value": "30000" + }, { + "id": "a03d3b8f-ddcf-4df2-96aa-957491089527", + "comment": "", + "command": "waitForElementNotPresent", + "target": "css=.alert", + "targets": [], + "value": "30000" + }, { + "id": "d7896b5a-dca8-4f01-a9f1-0f82c9e054e3", + "comment": "", + "command": "click", + "target": "id=advanced-nav-dropdown-toggle", + "targets": [ + ["id=advanced-nav-dropdown-toggle", "id"], + ["css=#advanced-nav-dropdown-toggle", "css:finder"], + ["xpath=//button[@id='advanced-nav-dropdown-toggle']", "xpath:attributes"], + ["xpath=//div[@id='advanced-nav-dropdown']/button", "xpath:idRelative"], + ["xpath=//div[3]/button", "xpath:position"], + ["xpath=//button[contains(.,'Advanced')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "7663a98a-d968-4df7-87f3-e48456061845", + "comment": "", + "command": "click", + "target": "id=advanced-nav-dropdown-groups", + "targets": [ + ["id=advanced-nav-dropdown-groups", "id"], + ["linkText=Groups", "linkText"], + ["css=#advanced-nav-dropdown-groups", "css:finder"], + ["xpath=//a[contains(text(),'Groups')]", "xpath:link"], + ["xpath=//a[@id='advanced-nav-dropdown-groups']", "xpath:attributes"], + ["xpath=//div[@id='advanced-nav-dropdown']/div/a[3]", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/groups')]", "xpath:href"], + ["xpath=//a[3]", "xpath:position"], + ["xpath=//a[contains(.,'Groups')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "80002f7f-2783-4fc5-b058-168f4c71276a", + "comment": "", + "command": "waitForElementVisible", + "target": "id=group-edit-8", + "targets": [ + ["id=group-edit-8", "id"], + ["css=#group-edit-8", "css:finder"], + ["xpath=//a[@id='group-edit-8']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div[2]/table/tbody/tr[9]/td[4]/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/groups/Submitter/edit')]", "xpath:href"], + ["xpath=//tr[9]/td[4]/a", "xpath:position"] + ], + "value": "30000" + }, { + "id": "08f9aae8-9e5c-4adc-8d07-237dddc651bf", + "comment": "", + "command": "click", + "target": "id=group-edit-8", + "targets": [ + ["id=group-edit-8", "id"], + ["css=#group-edit-8", "css:finder"], + ["xpath=//a[@id='group-edit-8']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div[2]/table/tbody/tr[9]/td[4]/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/groups/Submitter/edit')]", "xpath:href"], + ["xpath=//tr[9]/td[4]/a", "xpath:position"] + ], + "value": "" + }, { + "id": "dca4241d-63c1-4733-9a26-09e790ed8a89", + "comment": "", + "command": "waitForElementEditable", + "target": "id=option-selector-root_approversList", + "targets": [], + "value": "30000" + }, { + "id": "417f4a3a-a814-41ac-8293-10bb2be644cf", + "comment": "", + "command": "click", + "target": "id=option-selector-root_approversList", + "targets": [ + ["id=option-selector-root_approversList", "id"], + ["css=#option-selector-root_approversList", "css:finder"], + ["xpath=//input[@id='option-selector-root_approversList']", "xpath:attributes"], + ["xpath=//div[@id='root_approversList-group']/div/div/div/div/div/div/input", "xpath:idRelative"], + ["xpath=//div[4]/div/div/div/div/div/div/div/div/input", "xpath:position"] + ], + "value": "" + }, { + "id": "e0fa4c5c-8c6c-4237-8cae-f3613b94ef6a", + "comment": "", + "command": "click", + "target": "id=option-selector-items-root_approversList-item-7", + "targets": [ + ["id=option-selector-items-root_approversList-item-7", "id"], + ["linkText=Approver", "linkText"], + ["css=#option-selector-items-root_approversList-item-7", "css:finder"], + ["xpath=//a[contains(text(),'Approver')]", "xpath:link"], + ["xpath=//a[@id='option-selector-items-root_approversList-item-7']", "xpath:attributes"], + ["xpath=//div[@id='option-selector-items-root_approversList']/a[8]", "xpath:idRelative"], + ["xpath=(//a[contains(@href, '#')])[8]", "xpath:href"], + ["xpath=//a[8]", "xpath:position"], + ["xpath=//a[contains(.,'Approver')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "0b6d4c07-9df7-400b-b645-39f7d0a11385", + "comment": "", + "command": "click", + "target": "css=.btn-info", + "targets": [ + ["css=.btn-info", "css:finder"], + ["xpath=(//button[@type='button'])[5]", "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": "213f133c-33ba-40ea-a591-44a229734d28", + "comment": "", + "command": "assertElementPresent", + "target": "css=.alert", + "targets": [], + "value": "" + }, { + "id": "203b1353-030b-4e38-8e6a-62a677b6d6a1", + "comment": "", + "command": "click", + "target": "id=user-nav-dropdown-toggle", + "targets": [ + ["id=user-nav-dropdown-toggle", "id"], + ["css=#user-nav-dropdown-toggle", "css:finder"], + ["xpath=//button[@id='user-nav-dropdown-toggle']", "xpath:attributes"], + ["xpath=//div[@id='user-nav-dropdown']/button", "xpath:idRelative"], + ["xpath=//div[4]/button", "xpath:position"], + ["xpath=//button[contains(.,'Logged in as admin')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "7ade666b-0709-4e0e-a981-b8a90636438a", + "comment": "", + "command": "click", + "target": "id=user-nav-dropdown-logout", + "targets": [ + ["id=user-nav-dropdown-logout", "id"], + ["linkText=Logout", "linkText"], + ["css=#user-nav-dropdown-logout", "css:finder"], + ["xpath=//a[contains(text(),'Logout')]", "xpath:link"], + ["xpath=//a[@id='user-nav-dropdown-logout']", "xpath:attributes"], + ["xpath=//div[@id='user-nav-dropdown']/div/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/logout')]", "xpath:href"], + ["xpath=//div[4]/div/a", "xpath:position"], + ["xpath=//a[contains(.,'Logout')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "3ebf1f87-4cab-4035-b301-6987a45a69f8", + "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": "Submitter" + }, { + "id": "ee90c4fe-b436-4c90-9101-66baa97935e4", + "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": "password" + }, { + "id": "84f481c0-3d4d-4ebb-9aa3-3f2f4955a744", + "comment": "", + "command": "click", + "target": "css=.btn", + "targets": [ + ["css=.btn", "css:finder"], + ["xpath=//button[@type='submit']", "xpath:attributes"], + ["xpath=//button", "xpath:position"], + ["xpath=//button[contains(.,'Sign in')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "661c9b32-d218-469d-b4ab-2a410a675ec5", + "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": "46522a0f-30bd-4dfd-b03f-5f42b923bc18", + "comment": "", + "command": "click", + "target": "id=metadata-nav-dropdown-source", + "targets": [ + ["id=metadata-nav-dropdown-source", "id"], + ["linkText=Add a new metadata source", "linkText"], + ["css=#metadata-nav-dropdown-source", "css:finder"], + ["xpath=//a[contains(text(),'Add a new metadata source')]", "xpath:link"], + ["xpath=//a[@id='metadata-nav-dropdown-source']", "xpath:attributes"], + ["xpath=//div[@id='metadata-nav-dropdown']/div/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/metadata/source/new')]", "xpath:href"], + ["xpath=//div[2]/div/a", "xpath:position"], + ["xpath=//a[contains(.,'Add a new metadata source')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "d7520ced-f7fc-45ea-8e68-aa4258c38e0e", + "comment": "", + "command": "click", + "target": "id=root_serviceProviderName", + "targets": [ + ["id=root_serviceProviderName", "id"], + ["name=serviceProviderName", "name"], + ["css=#root_serviceProviderName", "css:finder"], + ["xpath=//input[@id='root_serviceProviderName']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[2]/div/div/form/div[2]/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "" + }, { + "id": "3b39ef8b-1098-4dd8-8b5d-699de7f1e01e", + "comment": "", + "command": "type", + "target": "id=root_serviceProviderName", + "targets": [ + ["id=root_serviceProviderName", "id"], + ["name=serviceProviderName", "name"], + ["css=#root_serviceProviderName", "css:finder"], + ["xpath=//input[@id='root_serviceProviderName']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[2]/div/div/form/div[2]/input", "xpath:idRelative"], + ["xpath=//input", "xpath:position"] + ], + "value": "Test Submission" + }, { + "id": "1cc4c76c-27fc-479d-b687-ca0cfcc07e54", + "comment": "", + "command": "type", + "target": "id=root_entityId", + "targets": [ + ["id=root_entityId", "id"], + ["name=entityId", "name"], + ["css=#root_entityId", "css:finder"], + ["xpath=//input[@id='root_entityId']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div[2]/div/div/form/div[3]/input", "xpath:idRelative"], + ["xpath=//div[3]/input", "xpath:position"] + ], + "value": "Test Submission" + }, { + "id": "39554cca-152d-472c-8326-78933f68bbdd", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.nav-link", "css:finder"], + ["xpath=(//button[@type='button'])[4]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/nav/ul/li[2]/button", "xpath:idRelative"], + ["xpath=//li[2]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "79e85a6a-bd8b-41d1-a5ab-b82dac168315", + "comment": "", + "command": "click", + "target": "css=.next", + "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(.,'3. User Interface / MDUI Information')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "63073aef-1699-4609-944b-f281d10a7361", + "comment": "", + "command": "click", + "target": "css=.next", + "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(.,'4. SP SSO Descriptor Information')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "bab40b15-e72c-4784-8829-759e4a676cce", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[5]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button", "xpath:idRelative"], + ["xpath=//li[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "e34c681d-33d5-4767-ab70-01a268151cb5", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[5]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button", "xpath:idRelative"], + ["xpath=//li[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "b5b6b6d7-1882-4e3a-8ef4-9ca9a9359e09", + "comment": "", + "command": "click", + "target": "css=.next", + "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": "31739113-fd33-4576-a30e-b2dd27f9a444", + "comment": "", + "command": "click", + "target": "css=.next", + "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": "3f8139ae-6fe2-49a8-8c3f-0b1cea3ef8eb", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[5]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button", "xpath:idRelative"], + ["xpath=//li[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "7289bc3b-7931-4db8-a5d5-0f2669416453", + "comment": "", + "command": "click", + "target": "css=.next", + "targets": [ + ["css=.next", "css:finder"], + ["xpath=(//button[@type='button'])[5]", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button", "xpath:idRelative"], + ["xpath=//li[3]/button", "xpath:position"] + ], + "value": "" + }, { + "id": "93069c65-356c-46fe-a37f-823ad26f1b89", + "comment": "", + "command": "click", + "target": "css=.save", + "targets": [ + ["css=.direction:nth-child(2)", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div[2]/div/div/nav/ul/li[3]/button/span[2]", "xpath:idRelative"], + ["xpath=//li[3]/button/span[2]", "xpath:position"] + ], + "value": "" + }, { + "id": "660ec22b-b4e2-43e4-93b0-9ffa72f6d7bc", + "comment": "", + "command": "waitForElementVisible", + "target": "css=.text-center:nth-child(6) .badge", + "targets": [], + "value": "30000" + }, { + "id": "c84d5506-b904-4c41-9043-b42257197694", + "comment": "", + "command": "assertText", + "target": "css=.text-center:nth-child(6) .badge", + "targets": [ + ["css=.text-center:nth-child(6) .badge", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/div/table/tbody/tr/td[6]/span/span", "xpath:idRelative"], + ["xpath=//span/span", "xpath:position"] + ], + "value": "Not Approved" + }, { + "id": "a5c2a126-2c73-4563-bfc6-0ce2d22c62b6", + "comment": "", + "command": "click", + "target": "id=user-nav-dropdown-toggle", + "targets": [ + ["id=user-nav-dropdown-toggle", "id"], + ["css=#user-nav-dropdown-toggle", "css:finder"], + ["xpath=//button[@id='user-nav-dropdown-toggle']", "xpath:attributes"], + ["xpath=//div[@id='user-nav-dropdown']/button", "xpath:idRelative"], + ["xpath=//div[3]/button", "xpath:position"], + ["xpath=//button[contains(.,'Logged in as Submitter')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "b4797afc-c71c-46b4-9b04-32a828296582", + "comment": "", + "command": "click", + "target": "id=user-nav-dropdown-logout", + "targets": [ + ["id=user-nav-dropdown-logout", "id"], + ["linkText=Logout", "linkText"], + ["css=#user-nav-dropdown-logout", "css:finder"], + ["xpath=//a[contains(text(),'Logout')]", "xpath:link"], + ["xpath=//a[@id='user-nav-dropdown-logout']", "xpath:attributes"], + ["xpath=//div[@id='user-nav-dropdown']/div/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/logout')]", "xpath:href"], + ["xpath=//div[3]/div/a", "xpath:position"], + ["xpath=//a[contains(.,'Logout')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "3c57ebfb-3dba-4142-b151-162faa2b4a2d", + "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": "Approver" + }, { + "id": "793d3b6e-015b-4da8-abf1-b3dfad61d0ea", + "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": "password" + }, { + "id": "51f72872-6774-4519-aa45-c9b8cd69b0b5", + "comment": "", + "command": "click", + "target": "css=.btn", + "targets": [ + ["css=.btn", "css:finder"], + ["xpath=//button[@type='submit']", "xpath:attributes"], + ["xpath=//button", "xpath:position"], + ["xpath=//button[contains(.,'Sign in')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "7689ee17-7d09-43a8-b055-54f0bdf3e402", + "comment": "", + "command": "assertText", + "target": "css=.ms-1", + "targets": [ + ["css=.ms-1", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/div/div[2]/a/span", "xpath:idRelative"], + ["xpath=//div[2]/a/span", "xpath:position"], + ["xpath=//span[contains(.,'1')]", "xpath:innerText"] + ], + "value": "1" + }, { + "id": "d2067ebc-6312-47ac-a734-b4e5b38d2455", + "comment": "", + "command": "click", + "target": "xpath=//a[text() = 'Action Required']", + "targets": [ + ["css=.align-items-center", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/div/div[2]/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/dashboard/admin/actions')]", "xpath:href"], + ["xpath=//div[2]/a", "xpath:position"] + ], + "value": "" + }, { + "id": "45eb3c5f-d2ac-4479-83ad-fc75bc2c8575", + "comment": "", + "command": "click", + "target": "id=approve-switch-0", + "targets": [ + ["id=approve-switch-0", "id"], + ["css=#approve-switch-0", "css:finder"], + ["xpath=//button[@id='approve-switch-0']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[3]/div/div/div/table/tbody/tr/td[6]/span/button", "xpath:idRelative"], + ["xpath=//span/button", "xpath:position"] + ], + "value": "" + }, { + "id": "956859e4-e726-48e7-aad3-cab4bdf67dd0", + "comment": "", + "command": "waitForElementNotPresent", + "target": "id=approve-switch-0", + "targets": [ + ["id=approve-switch-0", "id"], + ["css=#approve-switch-0", "css:finder"], + ["xpath=//button[@id='approve-switch-0']", "xpath:attributes"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[3]/div/div/div/table/tbody/tr/td[6]/span/button", "xpath:idRelative"], + ["xpath=//span/button", "xpath:position"] + ], + "value": "30000" + }, { + "id": "5c522f44-7b6d-4497-bfde-648a924ee6e8", + "comment": "", + "command": "assertText", + "target": "css=.ms-1", + "targets": [ + ["css=.ms-1", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/div/div[2]/a/span", "xpath:idRelative"], + ["xpath=//div[2]/a/span", "xpath:position"], + ["xpath=//span[contains(.,'0')]", "xpath:innerText"] + ], + "value": "0" + }, { + "id": "4b60b7bc-0678-4212-aecc-985a0d7a7b91", + "comment": "", + "command": "click", + "target": "id=user-nav-dropdown-toggle", + "targets": [ + ["id=user-nav-dropdown-toggle", "id"], + ["css=#user-nav-dropdown-toggle", "css:finder"], + ["xpath=//button[@id='user-nav-dropdown-toggle']", "xpath:attributes"], + ["xpath=//div[@id='user-nav-dropdown']/button", "xpath:idRelative"], + ["xpath=//div[3]/button", "xpath:position"], + ["xpath=//button[contains(.,'Logged in as Approver')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "ed735637-7965-4ce6-87ef-6dbdcb7fdb36", + "comment": "", + "command": "click", + "target": "id=user-nav-dropdown-logout", + "targets": [ + ["id=user-nav-dropdown-logout", "id"], + ["linkText=Logout", "linkText"], + ["css=#user-nav-dropdown-logout", "css:finder"], + ["xpath=//a[contains(text(),'Logout')]", "xpath:link"], + ["xpath=//a[@id='user-nav-dropdown-logout']", "xpath:attributes"], + ["xpath=//div[@id='user-nav-dropdown']/div/a", "xpath:idRelative"], + ["xpath=//a[contains(@href, '/logout')]", "xpath:href"], + ["xpath=//div[3]/div/a", "xpath:position"], + ["xpath=//a[contains(.,'Logout')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "582ede3c-9b8b-47df-b591-92b06b6ef034", + "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": "Submitter" + }, { + "id": "bd753141-97ab-442c-96e5-931a8bb37cb1", + "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": "password" + }, { + "id": "4c043d66-93fa-4852-af23-198fafdb2efe", + "comment": "", + "command": "click", + "target": "css=.btn", + "targets": [ + ["css=.btn", "css:finder"], + ["xpath=//button[@type='submit']", "xpath:attributes"], + ["xpath=//button", "xpath:position"], + ["xpath=//button[contains(.,'Sign in')]", "xpath:innerText"] + ], + "value": "" + }, { + "id": "a6dd1ab6-85e9-4261-934c-865b7c2620b1", + "comment": "", + "command": "assertText", + "target": "css=.bg-success", + "targets": [ + ["css=.bg-success", "css:finder"], + ["xpath=//div[@id='root']/div/main/div/section/div/div[2]/div/div/div/table/tbody/tr/td[6]/span/span", "xpath:idRelative"], + ["xpath=//span/span", "xpath:position"] + ], + "value": "Approved" + }, { + "id": "59391390-17bc-4e84-8254-85a0eefbfd8f", + "comment": "", + "command": "open", + "target": "/api/heheheheheheheWipeout", + "targets": [], + "value": "" + }, { + "id": "7154e95e-fe6d-4e47-a808-d1302646ed48", + "comment": "", + "command": "assertText", + "target": "css=body", + "targets": [], + "value": "yes, you did it" + }] + }], + "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/configuration/DevConfig.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy index e08392454..53007322a 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy @@ -12,9 +12,11 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ReloadableMetadat import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.security.model.Approvers 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.ApproversRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository @@ -38,6 +40,7 @@ class DevConfig { private final OpenSamlObjects openSamlObjects private final RoleRepository roleRepository private final UserRepository userRepository + private final ApproversRepository approversRepository @Autowired private UserService userService @@ -48,7 +51,8 @@ class DevConfig { RoleRepository roleRepository, EntityDescriptorRepository entityDescriptorRepository, OpenSamlObjects openSamlObjects, - IGroupService groupService) { + IGroupService groupService, + ApproversRepository approversRepository) { this.userRepository = adminUserRepository this.metadataResolverRepository = metadataResolverRepository @@ -56,7 +60,7 @@ class DevConfig { this.entityDescriptorRepository = entityDescriptorRepository this.openSamlObjects = openSamlObjects this.groupsRepository = groupsRepository - + this.approversRepository = approversRepository groupService.ensureAdminGroupExists() } @@ -84,7 +88,29 @@ class DevConfig { } } groupsRepository.flush() - + + List apprGroups = new ArrayList<>() + String[] groupNames = ['XXX', 'YYY', 'ZZZ'] + groupNames.each {name -> { + Group group = new Group().with({ + it.name = name + it.description = name + it.resourceId = name + it + }) + if (name != "ZZZ") { + apprGroups.add(groupsRepository.save(group)) + } else { + Approvers approvers = new Approvers() + approvers.setApproverGroups(apprGroups) + List apprList = new ArrayList<>() + apprList.add(approversRepository.save(approvers)) + group.setApproversList(apprList) + groupsRepository.save(group) + } + }} + groupsRepository.flush() + if (roleRepository.count() == 0) { def roles = [new Role().with { name = 'ROLE_ADMIN' @@ -122,16 +148,16 @@ class DevConfig { roles.add(roleRepository.findByName('ROLE_USER').get()) it }, new User().with { - username = 'none' - password = '{noop}nonepass' + username = 'Approver' + password = '{noop}password' firstName = 'Bad' lastName = 'robot' emailAddress = 'badboy@institution.edu' roles.add(roleRepository.findByName('ROLE_NONE').get()) it }, new User().with { - username = 'none2' - password = '{noop}none2pass' + username = 'Submitter' + password = '{noop}password' firstName = 'Bad' lastName = 'robot2' emailAddress = 'badboy2@institution.edu' 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 0ca482f4c..146fa38d6 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 @@ -31,6 +31,8 @@ import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound 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.permission.IShibUiPermissionEvaluator +import edu.internet2.tier.shibboleth.admin.ui.security.permission.PermissionType import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import edu.internet2.tier.shibboleth.admin.util.OpenSamlChainingMetadataResolverUtil import groovy.util.logging.Slf4j @@ -79,6 +81,9 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { @Autowired private ShibUIConfiguration shibUIConfiguration + @Autowired + private IShibUiPermissionEvaluator shibUiService; + @Autowired private UserService userService @@ -733,11 +738,13 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } - 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.") + public edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver updateMetadataResolverEnabledStatus(String resourceId, boolean status) throws ForbiddenException, MetadataFileNotFoundException, InitializationException { + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver updatedResolver = findByResourceId(resourceId); + if (!shibUiService.hasPermission(userService.getCurrentUserAuthentication(), updatedResolver, PermissionType.enable)) { + throw new ForbiddenException("You do not have the permissions necessary to change the enable status of this resolver.") } + updatedResolver.setEnabled(status); edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver persistedResolver = metadataResolverRepository.save(updatedResolver) if (persistedResolver.getDoInitialization()) { 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 53332dba4..607615adc 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 @@ -10,6 +10,8 @@ import edu.internet2.tier.shibboleth.admin.ui.scheduled.MetadataProvidersScheduledTasks; import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.GroupUpdatedEntityListener; import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.UserUpdatedEntityListener; +import edu.internet2.tier.shibboleth.admin.ui.security.permission.IShibUiPermissionEvaluator; +import edu.internet2.tier.shibboleth.admin.ui.security.permission.ShibUiPermissionDelegate; 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; @@ -218,9 +220,9 @@ public EntityDescriptorConversionUtils EntityDescriptorConverstionUtilsInit(Enti } @Bean - public GroupUpdatedEntityListener groupUpdatedEntityListener(OwnershipRepository repo) { + public GroupUpdatedEntityListener groupUpdatedEntityListener(OwnershipRepository repo, GroupsRepository groupsRepository) { GroupUpdatedEntityListener listener = new GroupUpdatedEntityListener(); - listener.init(repo); + listener.init(repo, groupsRepository); return listener; } @@ -230,4 +232,10 @@ public UserUpdatedEntityListener userUpdatedEntityListener(OwnershipRepository r listener.init(repo, groupRepo); return listener; } + + @Bean + public IShibUiPermissionEvaluator shibUiPermissionEvaluator(EntityDescriptorRepository entityDescriptorRepository, UserService userService) { + // TODO: @jj define type to return for Grouper integration + return new ShibUiPermissionDelegate(entityDescriptorRepository, userService); + } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java index f04f2f716..f2135109e 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 @@ -67,6 +67,7 @@ private HttpFirewall allowUrlEncodedSlashHttpFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); firewall.setAllowUrlEncodedSlash(true); firewall.setAllowUrlEncodedDoubleSlash(true); + firewall.setAllowSemicolon(true); return firewall; } @@ -87,7 +88,7 @@ protected void configure(HttpSecurity http) throws Exception { .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .and() .authorizeRequests() - .antMatchers("/unsecured/**/*").permitAll() + .antMatchers("/unsecured/**/*","/entities/**/*").permitAll() .anyRequest().hasAnyRole(acceptedAuthenticationRoles) .and() .exceptionHandling().accessDeniedHandler((request, response, accessDeniedException) -> response.sendRedirect("/unsecured/error.html")) 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 index 30ccaf6b6..49c9e0d90 100644 --- 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 @@ -38,8 +38,7 @@ public class ActivateController { @PatchMapping(path = "/entityDescriptor/{resourceId}/{mode}") @Transactional - public ResponseEntity enableEntityDescriptor(@PathVariable String resourceId, @PathVariable String mode) throws - PersistentEntityNotFound, ForbiddenException { + public ResponseEntity enableEntityDescriptor(@PathVariable String resourceId, @PathVariable String mode) throws PersistentEntityNotFound, ForbiddenException { boolean status = "enable".equalsIgnoreCase(mode); EntityDescriptorRepresentation edr = entityDescriptorService.updateEntityDescriptorEnabledStatus(resourceId, status); return ResponseEntity.ok(edr); @@ -47,8 +46,7 @@ public ResponseEntity enableEntityDescriptor(@PathVariable String resourceId, @PatchMapping(path = "/MetadataResolvers/{metadataResolverId}/Filter/{resourceId}/{mode}") @Transactional - public ResponseEntity enableFilter(@PathVariable String metadataResolverId, @PathVariable String resourceId, @PathVariable String mode) throws - PersistentEntityNotFound, ForbiddenException, ScriptException { + public ResponseEntity enableFilter(@PathVariable String metadataResolverId, @PathVariable String resourceId, @PathVariable String mode) throws PersistentEntityNotFound, ForbiddenException, ScriptException { boolean status = "enable".equalsIgnoreCase(mode); MetadataFilter persistedFilter = filterService.updateFilterEnabledStatus(metadataResolverId, resourceId, status); return ResponseEntity.ok(persistedFilter); @@ -56,13 +54,9 @@ public ResponseEntity enableFilter(@PathVariable String metadataResolverId, @ @PatchMapping("/MetadataResolvers/{resourceId}/{mode}") @Transactional - public ResponseEntity enableProvider(@PathVariable String resourceId, @PathVariable String mode) throws - PersistentEntityNotFound, ForbiddenException, MetadataFileNotFoundException, InitializationException { + public ResponseEntity enableProvider(@PathVariable String resourceId, @PathVariable String mode) throws PersistentEntityNotFound, ForbiddenException, MetadataFileNotFoundException, InitializationException { boolean status = "enable".equalsIgnoreCase(mode); - MetadataResolver existingResolver = metadataResolverService.findByResourceId(resourceId); - existingResolver.setEnabled(status); - existingResolver = metadataResolverService.updateMetadataResolverEnabledStatus(existingResolver); - - return ResponseEntity.ok(existingResolver); + MetadataResolver metadataResolver = metadataResolverService.updateMetadataResolverEnabledStatus(resourceId, status); + return ResponseEntity.ok(metadataResolver); } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ApprovalController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ApprovalController.java new file mode 100644 index 000000000..32bf84afb --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ApprovalController.java @@ -0,0 +1,31 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.tags.Tags; +import org.springframework.beans.factory.annotation.Autowired; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/approve") +@Tags(value = {@Tag(name = "approve")}) +public class ApprovalController { + @Autowired + private EntityDescriptorService entityDescriptorService; + + @PatchMapping(path = "/entityDescriptor/{resourceId}/{mode}") + @Transactional + public ResponseEntity approveEntityDescriptor(@PathVariable String resourceId, @PathVariable String mode) throws PersistentEntityNotFound, ForbiddenException { + boolean status = "approve".equalsIgnoreCase(mode); + EntityDescriptorRepresentation edr = entityDescriptorService.changeApproveStatusOfEntityDescriptor(resourceId, status); + return ResponseEntity.ok(edr); + } +} \ No newline at end of file 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/ApproveAndActivateExceptionHandler.java similarity index 93% rename from backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java rename to backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ApproveAndActivateExceptionHandler.java index 2015febcb..0c8efc28f 100644 --- 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/ApproveAndActivateExceptionHandler.java @@ -15,8 +15,8 @@ import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; -@ControllerAdvice(assignableTypes = {ActivateController.class}) -public class ActivateExceptionHandler extends ResponseEntityExceptionHandler { +@ControllerAdvice(assignableTypes = {ActivateController.class, ApprovalController.class}) +public class ApproveAndActivateExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({ PersistentEntityNotFound.class }) public ResponseEntity handleEntityNotFoundException(PersistentEntityNotFound e, WebRequest request) { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/DangerController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/DangerController.java index 7af217eb2..15c4a4a0a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/DangerController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/DangerController.java @@ -9,6 +9,7 @@ import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolversPositionOrderContainerRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.ShibPropertySetRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.ShibPropertySettingRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.ApproversRepository; 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.UserRepository; @@ -28,6 +29,9 @@ @Profile("very-dangerous") @Slf4j public class DangerController { + @Autowired + private ApproversRepository approversRepositry; + @Autowired private CustomEntityAttributeDefinitionRepository attributeRepository; @@ -105,6 +109,7 @@ private void clearShibSettings() { } private void clearUsersAndGroups() { + approversRepositry.deleteAll(); groupRepository.deleteAll(); ownershipRepository.clearAllOwnedByGroup(); userRepository.findAll().forEach(user -> { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java index e6560bf7d..e8498a6c8 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java @@ -85,6 +85,12 @@ public ResponseEntity getAll() throws ForbiddenException { return ResponseEntity.ok(entityDescriptorService.getAllEntityDescriptorProjectionsBasedOnUserAccess()); } + @GetMapping("/EntityDescriptors/needsApproval") + @Transactional + public ResponseEntity getAllNeedingApproval() throws ForbiddenException { + return ResponseEntity.ok(entityDescriptorService.getAllEntityDescriptorProjectionsNeedingApprovalBasedOnUserAccess()); + } + @GetMapping("/EntityDescriptor/{resourceId}/Versions") @Transactional public ResponseEntity getAllVersions(@PathVariable String resourceId) throws PersistentEntityNotFound, ForbiddenException { @@ -93,11 +99,14 @@ public ResponseEntity getAllVersions(@PathVariable String resourceId) throws return ResponseEntity.ok(versionService.findVersionsForEntityDescriptor(ed.getResourceId())); } + /** + * @throws ForbiddenException This call is used for the admin needs action list, therefore the user must be an admin + */ @Secured("ROLE_ADMIN") @Transactional - @GetMapping(value = "/EntityDescriptor/disabledNonAdmin") - public Iterable getDisabledAndNotOwnedByAdmin() throws ForbiddenException { - return entityDescriptorService.getAllDisabledAndNotOwnedByAdmin(); + @GetMapping(value = "/EntityDescriptor/disabledSources") + public ResponseEntity getDisabledMetadataSources() throws ForbiddenException { + return ResponseEntity.ok(entityDescriptorService.getDisabledMetadataSources()); } @GetMapping("/EntityDescriptor/{resourceId}") @@ -115,8 +124,7 @@ public ResponseEntity getOneXml(@PathVariable String resourceId) throws Marsh } @GetMapping("/EntityDescriptor/{resourceId}/Versions/{versionId}") - public ResponseEntity getSpecificVersion(@PathVariable String resourceId, @PathVariable String versionId) throws - PersistentEntityNotFound, ForbiddenException { + public ResponseEntity getSpecificVersion(@PathVariable String resourceId, @PathVariable String versionId) throws PersistentEntityNotFound, ForbiddenException { // this "get by resource id" verifies that both the ED exists and the user has proper access, so needs to remain EntityDescriptor ed = entityDescriptorService.getEntityDescriptorByResourceId(resourceId); EntityDescriptorRepresentation result = versionService.findSpecificVersionOfEntityDescriptor(ed.getResourceId(), versionId); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/RootUiViewController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/RootUiViewController.java index 0af16ada6..80c861898 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/RootUiViewController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/RootUiViewController.java @@ -1,6 +1,10 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.info.InfoEndpoint; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletRequest; @@ -15,6 +19,13 @@ @Controller public class RootUiViewController { + @Autowired InfoEndpoint infoEndpoint; + + @GetMapping(value = "/info") + public ResponseEntity getInfo() { + return ResponseEntity.ok(infoEndpoint.info()); + } + @RequestMapping("/") public String index() { return "redirect:/index.html"; 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 8a4133ea6..1d85e8158 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 @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.common.base.MoreObjects; import com.google.common.collect.Lists; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownable; import edu.internet2.tier.shibboleth.admin.ui.security.model.OwnableType; import lombok.EqualsAndHashCode; @@ -15,7 +16,9 @@ import javax.annotation.Nullable; import javax.persistence.CascadeType; +import javax.persistence.ElementCollection; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.JoinColumn; import javax.persistence.OneToMany; import javax.persistence.OneToOne; @@ -34,7 +37,7 @@ @Entity @EqualsAndHashCode(callSuper = true) @Audited -public class EntityDescriptor extends AbstractDescriptor implements org.opensaml.saml.saml2.metadata.EntityDescriptor, Ownable, IActivatable { +public class EntityDescriptor extends AbstractDescriptor implements org.opensaml.saml.saml2.metadata.EntityDescriptor, Ownable, IActivatable, IApprovable { @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "entitydesc_addlmetdatlocations_id") @OrderColumn @@ -45,10 +48,19 @@ public class EntityDescriptor extends AbstractDescriptor implements org.opensaml @NotAudited private AffiliationDescriptor affiliationDescriptor; + @Getter + @Setter + private boolean approved; + @OneToOne(cascade = CascadeType.ALL) @NotAudited private AttributeAuthorityDescriptor attributeAuthorityDescriptor; + @ElementCollection (fetch = FetchType.EAGER) + @EqualsAndHashCode.Exclude + @Getter + private List approvedBy = new ArrayList<>(); + @OneToOne(cascade = CascadeType.ALL) @NotAudited private AuthnAuthorityDescriptor authnAuthorityDescriptor; @@ -59,19 +71,22 @@ public class EntityDescriptor extends AbstractDescriptor implements org.opensaml private String entityID; + @Getter + @Setter + private String idOfOwner; + private String localId; @OneToOne(cascade = CascadeType.ALL) private Organization organization; - @Getter - @Setter - private String idOfOwner; - @OneToOne(cascade = CascadeType.ALL) @NotAudited private PDPDescriptor pdpDescriptor; + @Setter + private EntityDescriptorProtocol protocol = EntityDescriptorProtocol.SAML; + private String resourceId; @OneToMany(cascade = CascadeType.ALL) @@ -85,18 +100,27 @@ public class EntityDescriptor extends AbstractDescriptor implements org.opensaml @EqualsAndHashCode.Exclude private Long versionModifiedTimestamp; - @Setter - private EntityDescriptorProtocol protocol = EntityDescriptorProtocol.SAML; - public EntityDescriptor() { super(); this.resourceId = UUID.randomUUID().toString(); } + public void addApproval(Group group) { + approvedBy.add(group.getName()); + } + public void addContactPerson(ContactPerson contactPerson) { this.contactPersons.add(contactPerson); } + public int approvedCount() { + return approvedBy.size(); + } + + @Override public ActivatableType getActivatableType() { + return ENTITY_DESCRIPTOR; + } + @Override public List getAdditionalMetadataLocations() { return Lists.newArrayList(additionalMetadataLocations); @@ -142,6 +166,10 @@ public IDPSSODescriptor getIDPSSODescriptor(String s) { .orElse(null); } + public String getObjectId() { + return entityID; + } + @Transient public Optional getOptionalSPSSODescriptor() { return this.getOptionalSPSSODescriptor(""); @@ -175,6 +203,10 @@ public org.opensaml.saml.saml2.metadata.Organization getOrganization() { return organization; } + public OwnableType getOwnableType() { + return OwnableType.ENTITY_DESCRIPTOR; + } + public EntityDescriptorProtocol getProtocol() { return protocol == null ? EntityDescriptorProtocol.SAML : protocol; } @@ -228,10 +260,33 @@ public SPSSODescriptor getSPSSODescriptor(String s) { .orElse(null); } + @JsonIgnore + public boolean hasKeyDescriptors() { + SPSSODescriptor spssoDescriptor = getSPSSODescriptor(""); + return spssoDescriptor != null && spssoDescriptor.getKeyDescriptors().size() > 0; + } + + @JsonIgnore + public boolean isAuthnRequestsSigned() { + SPSSODescriptor spssoDescriptor = getSPSSODescriptor(""); + return spssoDescriptor != null && spssoDescriptor.isAuthnRequestsSigned() != null && spssoDescriptor.isAuthnRequestsSigned(); + } + + @JsonIgnore + public boolean isOidcProtocol() { + return getSPSSODescriptor("") != null && getProtocol() == EntityDescriptorProtocol.OIDC; + } + public boolean isServiceEnabled() { return serviceEnabled; } + public void removeLastApproval() { + if (!approvedBy.isEmpty()) { + approvedBy.remove(approvedBy.size() - 1); + } + } + public void setAdditionalMetadataLocations(List additionalMetadataLocations) { this.additionalMetadataLocations = additionalMetadataLocations; } @@ -300,42 +355,14 @@ public void setVersionModifiedTimestamp(Long versionModifiedTimestamp) { public String toString() { return MoreObjects.toStringHelper(this) .add("entityID", entityID) + // .add("organization", organization) .add("id", id) .toString(); } - public String getObjectId() { - return entityID; - } - - public OwnableType getOwnableType() { - return OwnableType.ENTITY_DESCRIPTOR; - } - - @Override public ActivatableType getActivatableType() { - return ENTITY_DESCRIPTOR; - } - @JsonIgnore public boolean wantsAssertionsSigned() { SPSSODescriptor spssoDescriptor = getSPSSODescriptor(""); return spssoDescriptor != null && spssoDescriptor.getWantAssertionsSigned() != null && spssoDescriptor.getWantAssertionsSigned(); } - - @JsonIgnore - public boolean isAuthnRequestsSigned() { - SPSSODescriptor spssoDescriptor = getSPSSODescriptor(""); - return spssoDescriptor != null && spssoDescriptor.isAuthnRequestsSigned() != null && spssoDescriptor.isAuthnRequestsSigned(); - } - - @JsonIgnore - public boolean isOidcProtocol() { - return getSPSSODescriptor("") != null && getProtocol() == EntityDescriptorProtocol.OIDC; - } - - @JsonIgnore - public boolean hasKeyDescriptors() { - SPSSODescriptor spssoDescriptor = getSPSSODescriptor(""); - return spssoDescriptor != null && spssoDescriptor.getKeyDescriptors().size() > 0; - } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IApprovable.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IApprovable.java new file mode 100644 index 000000000..f2e163b0b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IApprovable.java @@ -0,0 +1,5 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +public interface IApprovable { + String getIdOfOwner(); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java index 27a11d890..f58fc6e0d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java @@ -18,6 +18,11 @@ public class EntityDescriptorRepresentation implements Serializable { private static final long serialVersionUID = 7753435553892353966L; + + @Setter + @Getter + private boolean approved; + private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS"); private List assertionConsumerServices; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorProjection.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorProjection.java index ecf6a5f2c..94dada3d8 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorProjection.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorProjection.java @@ -2,31 +2,34 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptorProtocol; import lombok.Getter; +import org.hibernate.criterion.Projection; import java.time.LocalDateTime; public class EntityDescriptorProjection { @Getter - String id; + boolean approved; + @Getter + String createdBy; + @Getter + LocalDateTime createdDate; String entityID; String entityId; @Getter - String resourceId; - @Getter - String serviceProviderName; + String id; @Getter - String createdBy; + String idOfOwner; @Getter - LocalDateTime createdDate; + String resourceId; @Getter boolean serviceEnabled; @Getter - String idOfOwner; - + String serviceProviderName; EntityDescriptorProtocol protocol; public EntityDescriptorProjection(String entityID, String resourceId, String serviceProviderName, String createdBy, - LocalDateTime createdDate, boolean serviceEnabled, String idOfOwner, EntityDescriptorProtocol edp) { + LocalDateTime createdDate, boolean serviceEnabled, String idOfOwner, + EntityDescriptorProtocol edp, boolean approved) { this.entityID = entityID; this.entityId = entityID; this.resourceId = resourceId; @@ -37,6 +40,7 @@ public EntityDescriptorProjection(String entityID, String resourceId, String ser this.serviceEnabled = serviceEnabled; this.idOfOwner = idOfOwner; this.protocol = edp == null ? EntityDescriptorProtocol.SAML : edp; + this.approved = approved; } public String getEntityID() { @@ -50,4 +54,4 @@ public String getEntityId() { public EntityDescriptorProtocol getProtocol() { return protocol == null ? EntityDescriptorProtocol.SAML : protocol; } -} \ No newline at end of file +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java index 4c8d4ad30..1cfc5ca0b 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java @@ -13,13 +13,17 @@ * Repository to manage {@link EntityDescriptor} instances. */ public interface EntityDescriptorRepository extends JpaRepository { + + @Query(value="SELECT e.resourceId FROM EntityDescriptor e WHERE e.idOfOwner = :groupId AND e.serviceEnabled = false") + List findAllResourceIdsByIdOfOwnerAndNotEnabled(@Param("groupId") String groupId); + @Query(value = "select new edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorProjection(e.entityID, e.resourceId, e.serviceProviderName, e.createdBy, " + - "e.createdDate, e.serviceEnabled, e.idOfOwner, e.protocol) " + - "from EntityDescriptor e") + "e.createdDate, e.serviceEnabled, e.idOfOwner, e.protocol, e.approved) " + + "from EntityDescriptor e") List findAllReturnProjections(); @Query(value = "select new edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorProjection(e.entityID, e.resourceId, e.serviceProviderName, e.createdBy, " + - "e.createdDate, e.serviceEnabled, e.idOfOwner, e.protocol) " + + "e.createdDate, e.serviceEnabled, e.idOfOwner, e.protocol, e.approved) " + "from EntityDescriptor e " + "where e.idOfOwner = :ownerId") List findAllByIdOfOwner(@Param("ownerId") String ownerId); @@ -35,9 +39,12 @@ public interface EntityDescriptorRepository extends JpaRepository findAllStreamByIdOfOwner(String ownerId); - @Query("select e from EntityDescriptor e, User u join u.roles r " + - "where e.createdBy = u.username and e.serviceEnabled = false and r.name in ('ROLE_USER', 'ROLE_NONE')") - Stream findAllDisabledAndNotOwnedByAdmin(); + @Query(value = "select new edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorProjection(e.entityID, e.resourceId, e.serviceProviderName, e.createdBy, " + + "e.createdDate, e.serviceEnabled, e.idOfOwner, e.protocol, e.approved) " + + " from EntityDescriptor e " + + " where e.serviceEnabled = false" + ) + List getEntityDescriptorsNeedingEnabling(); /** * SHIBUI-1740 This is here to aid in migration of systems using the SHIBUI prior to group functionality being added @@ -45,4 +52,13 @@ public interface EntityDescriptorRepository extends JpaRepository findAllByIdOfOwnerIsNull(); + + @Query(value = "select new edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorProjection(e.entityID, e.resourceId, e.serviceProviderName, e.createdBy, " + + "e.createdDate, e.serviceEnabled, e.idOfOwner, e.protocol, e.approved) " + + " from EntityDescriptor e " + + " where e.idOfOwner in (:groupIds)" + + " and e.serviceEnabled = false" + + " and e.approved = false") + List getEntityDescriptorsNeedingApproval(@Param("groupIds") List groupIds); + } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupController.java index 8293c9b04..bb283b140 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupController.java @@ -6,8 +6,10 @@ import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException; import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tags; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -29,6 +31,9 @@ public class GroupController { @Autowired private IGroupService groupService; + @Autowired + private EntityDescriptorService entityDescriptorService; + @Secured("ROLE_ADMIN") @PostMapping @Transactional @@ -66,6 +71,7 @@ public ResponseEntity getOne(@PathVariable String resourceId) throws Persiste @Transactional public ResponseEntity update(@RequestBody Group group) throws PersistentEntityNotFound, InvalidGroupRegexException { Group result = groupService.updateGroup(group); + entityDescriptorService.checkApprovalStatusOfEntitiesForGroup(result); return ResponseEntity.ok(result); } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Approvers.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Approvers.java new file mode 100644 index 000000000..670fc8407 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Approvers.java @@ -0,0 +1,42 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.ManyToMany; +import javax.persistence.Transient; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Data +@NoArgsConstructor +@Entity(name = "approvers") +public class Approvers { + @Id + @Column(name = "resource_id") + @JsonIgnore + private String resourceId = UUID.randomUUID().toString(); + + @ManyToMany + @JsonIgnore + private List approverGroups = new ArrayList<>(); + + @Transient + private List approverGroupIds = new ArrayList<>(); + + public List getApproverGroupIds() { + if (approverGroupIds.isEmpty()) { + approverGroups.forEach(group -> approverGroupIds.add(group.getResourceId())); + } + return approverGroupIds; + } + + public void setApproverGroups(List appGroups) { + this.approverGroups = appGroups; + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java index 3274de7cc..316ed8b6a 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 @@ -10,9 +10,13 @@ import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EntityListeners; +import javax.persistence.FetchType; import javax.persistence.Id; +import javax.persistence.OneToMany; import javax.persistence.Transient; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.UUID; @@ -27,6 +31,10 @@ public class Group implements Owner { @JsonIgnore public static Group ADMIN_GROUP; + @Transient + @JsonIgnore + List approveForList = new ArrayList<>(); + @Column(name = "group_description") String description; @@ -49,6 +57,9 @@ public class Group implements Owner { @Column(name = "validation_regex") private String validationRegex; + @OneToMany(fetch = FetchType.EAGER) + private List approversList = new ArrayList<>(); + /** * Define a Group object based on the user */ @@ -78,4 +89,26 @@ public Set getOwnedItems() { } return ownedItems; } + + @Override + public int hashCode() { + return resourceId.hashCode(); + } + + @Override + public boolean equals(Object o) { + return o instanceof Group && this.resourceId.equals(((Group)o).resourceId); + } + + public List getApproveForList() { + if (lazyLoaderHelper != null) { + lazyLoaderHelper.loadApproveForList(this); + } + return approveForList; + } + + @Override + public String toString() { + return "Group resourceId=" + resourceId; + } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/User.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/User.java index 523a5ad4d..c1cd10e91 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/User.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/User.java @@ -147,4 +147,19 @@ public void setGroups(Set groups) { public void registerLoader(ILazyLoaderHelper lazyLoaderHelper) { this.lazyLoaderHelper = lazyLoaderHelper; } + + /** + * @return true if the user belongs to any group that can approve for other groups + */ + public boolean getCanApprove() { + if (Group.ADMIN_GROUP.equals(getGroup())) { + return true; + } + for (Group group : getUserGroups()) { + if (!group.getApproveForList().isEmpty()) { + return true; + } + } + return false; + } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/GroupUpdatedEntityListener.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/GroupUpdatedEntityListener.java index d477ae78c..5fa01c570 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/GroupUpdatedEntityListener.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/GroupUpdatedEntityListener.java @@ -2,6 +2,7 @@ import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository; import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository; import org.springframework.beans.factory.annotation.Autowired; @@ -9,17 +10,20 @@ import javax.persistence.PostPersist; import javax.persistence.PostRemove; import javax.persistence.PostUpdate; +import java.util.List; import java.util.Set; public class GroupUpdatedEntityListener implements ILazyLoaderHelper { + private static GroupsRepository groupsRepository; private static OwnershipRepository ownershipRepository; /** * @see https://stackoverflow.com/questions/12155632/injecting-a-spring-dependency-into-a-jpa-entitylistener */ @Autowired - public static void init(OwnershipRepository repo) { - GroupUpdatedEntityListener.ownershipRepository = repo; + public static void init(OwnershipRepository ownershipRepository, GroupsRepository groupsRepository) { + GroupUpdatedEntityListener.ownershipRepository = ownershipRepository; + GroupUpdatedEntityListener.groupsRepository = groupsRepository; } @PostPersist @@ -38,4 +42,14 @@ public void loadOwnedItems(Group group) { group.setOwnedItems(ownedItems); } + @Override + public void loadApproveForList(Group group) { + List result = group.getResourceId().equals(Group.ADMIN_GROUP.getResourceId()) ? + groupsRepository.findAllGroupIds() : + groupsRepository.getGroupIdsOfGroupsToApproveFor(group.getResourceId()); + if (result != null) { + group.setApproveForList(result); + } + } + } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/ILazyLoaderHelper.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/ILazyLoaderHelper.java index 689306932..b669845aa 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/ILazyLoaderHelper.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/listener/ILazyLoaderHelper.java @@ -4,6 +4,8 @@ import edu.internet2.tier.shibboleth.admin.ui.security.model.User; public interface ILazyLoaderHelper { + default void loadApproveForList(Group group) { } + default void loadOwnedItems(Group g) { } default void loadGroups(User u) { } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/permission/IShibUiPermissionEvaluator.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/permission/IShibUiPermissionEvaluator.java new file mode 100644 index 000000000..9351fac71 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/permission/IShibUiPermissionEvaluator.java @@ -0,0 +1,29 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.permission; + +import edu.internet2.tier.shibboleth.admin.ui.domain.Auditable; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import liquibase.pro.packaged.T; +import org.apache.commons.lang.NotImplementedException; +import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.core.Authentication; + +import java.util.Collection; + +public interface IShibUiPermissionEvaluator extends PermissionEvaluator { + + /** + * Return a Collection of items matching the type describing those types that can be asked for and for which the authenticated + * user has the correct permission to access + * @param authentication The security Authorization + * @param type The permissible type that should be returned in the collection. This is an abstraction + * @param permissionType The type of permissions the user should have to access the items returned in the collection. Determining + * the relationship is up to the implementation + * @return Collection of objects representing the type described by the ShibUiPermissibleType enumeration + * @throws ForbiddenException if the user does not have the correct authority required + */ + Collection getPersistentEntities(Authentication authentication, ShibUiPermissibleType type, PermissionType permissionType) throws ForbiddenException; + + default Collection getAuditableEntities(Authentication authentication, + Class auditableType, + PermissionType permissionType) throws ForbiddenException {throw new NotImplementedException();} +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/permission/PermissionType.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/permission/PermissionType.java new file mode 100644 index 000000000..b807ecf32 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/permission/PermissionType.java @@ -0,0 +1,5 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.permission; + +public enum PermissionType { + admin, approve, enable, fetch, viewOrEdit; +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/permission/ShibUiPermissibleType.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/permission/ShibUiPermissibleType.java new file mode 100644 index 000000000..5db069c5c --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/permission/ShibUiPermissibleType.java @@ -0,0 +1,5 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.permission; + +public enum ShibUiPermissibleType { + entityDescriptorProjection // represents EntityDescriptorProjections +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/permission/ShibUiPermissionDelegate.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/permission/ShibUiPermissionDelegate.java new file mode 100644 index 000000000..0560b569b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/permission/ShibUiPermissionDelegate.java @@ -0,0 +1,116 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.permission; + +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.domain.IApprovable; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorProjection; +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownable; +import edu.internet2.tier.shibboleth.admin.ui.security.model.User; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserAccess; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import lombok.AllArgsConstructor; +import org.springframework.security.core.Authentication; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** + * The ShibUiPermissionDelegate is the default service for SHIBUI, which delegates calls (primarily) to the the UserService to determine + * whether a user has the correct abilty to act a particular way (possibly on certain objects). Because the Authentication being + * supplied to this implmentation comes from the user service, we ignore it and defer to the UserService (which is ultimately using + * the Authentication from the security context anyway). + * + */ +@AllArgsConstructor +public class ShibUiPermissionDelegate implements IShibUiPermissionEvaluator { + private EntityDescriptorRepository entityDescriptorRepository; + + private UserService userService; + + @Override + public Collection getPersistentEntities(Authentication ignored, ShibUiPermissibleType shibUiType, PermissionType permissionType) throws ForbiddenException { + switch (shibUiType) { + case entityDescriptorProjection: + switch (permissionType) { + case approve: + return getAllEntityDescriptorProjectionsNeedingApprovalBasedOnUserAccess(); + case enable: + // This particular list is used for an admin function, so the user must be an ADMIN + if (!hasPermission(ignored, null, PermissionType.admin)) { + throw new ForbiddenException(); + } + return entityDescriptorRepository.getEntityDescriptorsNeedingEnabling(); + case fetch: + if (!hasPermission(ignored, null, PermissionType.fetch)) { + throw new ForbiddenException("User has no access rights to get a list of Metadata Sources"); + } + return getAllEntityDescriptorProjectionsBasedOnUserAccess(); + } + } + return null; + } + + private List getAllEntityDescriptorProjectionsBasedOnUserAccess() { + if (userService.currentUserIsAdmin()) { + return entityDescriptorRepository.findAllReturnProjections(); + } else { + return entityDescriptorRepository.findAllByIdOfOwner(userService.getCurrentUser().getGroup().getOwnerId()); + } + } + + private List getAllEntityDescriptorProjectionsNeedingApprovalBasedOnUserAccess() { + List groupsToApprove = userService.getGroupsCurrentUserCanApprove(); + List result = entityDescriptorRepository.getEntityDescriptorsNeedingApproval(groupsToApprove); + return result; + } + + @Override + public boolean hasPermission(Authentication ignored, Object targetDomainObject, Object permission) { + switch ((PermissionType) permission) { + case admin: // we don't care about the object - the user is an admin or not + return userService.currentUserIsAdmin(); + case approve: + if (userService.currentUserIsAdmin()) { return true; } + return targetDomainObject instanceof IApprovable ? userService.getGroupsCurrentUserCanApprove().contains(((IApprovable)targetDomainObject).getIdOfOwner()) : false; + case enable: + return targetDomainObject instanceof IActivatable ? currentUserCanEnable((IActivatable) targetDomainObject) : false; + case fetch: + return userService.currentUserIsAdmin() || userService.getCurrentUserAccess().equals(UserAccess.GROUP); + case viewOrEdit: + return userService.canViewOrEditTarget((Ownable) targetDomainObject); + default: return false; + } + } + + @Override + public boolean hasPermission(Authentication authentication, Serializable targetId, String target, Object permission) { + return false; // Unused and Unimplemented - we don't need for this implementation to lookup objects + } + + private boolean currentUserCanEnable(IActivatable activatableObject) { + if (userService.currentUserIsAdmin()) { return true; } + switch (activatableObject.getActivatableType()) { + case ENTITY_DESCRIPTOR: { + return currentUserHasExpectedRole(Arrays.asList("ROLE_ENABLE" )) && userService.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 = userService.getCurrentUser(); + return acceptedRoles.contains(user.getRole()); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/ApproversRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/ApproversRepository.java new file mode 100644 index 000000000..313d24a9f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/ApproversRepository.java @@ -0,0 +1,23 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.repository; + +import edu.internet2.tier.shibboleth.admin.ui.security.model.Approvers; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ApproversRepository extends JpaRepository { + @Modifying + @Query(nativeQuery = true, value="DELETE FROM approvers_user_groups WHERE approver_groups_resource_id IN (:ids)") + void deleteGroupAssociationsForIds(@Param("ids") List ids); + + Approvers findByResourceId(String resourceId); + + @Query(nativeQuery = true, + value = "SELECT approvers_list_resource_id " + + " FROM user_groups_approvers " + + " WHERE user_groups_resource_id = :resourceId") + List getApproverIdsForGroup(@Param("resourceId") String resourceId); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepository.java index bd9c0c30c..9d877a62c 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 @@ -2,9 +2,29 @@ import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface GroupsRepository extends JpaRepository { + @Modifying + @Query(nativeQuery = true, value = "DELETE FROM user_groups_approvers WHERE approvers_list_resource_id IN (:approverIds)") + void clearApproversByApproverIds(@Param("approverIds") List approverIds); + void deleteByResourceId(String resourceId); Group findByResourceId(String id); + + @Query(nativeQuery = true, value = "SELECT resource_id FROM user_groups") + List findAllGroupIds(); + + @Query(nativeQuery = true, + value = "SELECT DISTINCT user_groups_resource_id " + + " FROM user_groups_approvers " + + " WHERE approvers_list_resource_id IN (SELECT approvers_resource_id " + + " FROM approvers_user_groups " + + " WHERE approver_groups_resource_id = :resourceId)") + List getGroupIdsOfGroupsToApproveFor(@Param("resourceId") String resourceId); } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java index f329a5be2..2e18e48ed 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java @@ -4,7 +4,9 @@ import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupDeleteException; import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException; import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Approvers; import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.ApproversRepository; import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository; import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository; import lombok.NoArgsConstructor; @@ -16,7 +18,9 @@ import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; @Service @NoArgsConstructor @@ -25,28 +29,24 @@ public class GroupServiceImpl implements IGroupService { private static final String REGEX_MATCHER = "function validate(r, s){ return RegExp(r).test(s);};validate(rgx, str);"; private final ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript"); + @Autowired + protected ApproversRepository approversRepository; + @Autowired protected GroupsRepository groupRepository; @Autowired protected OwnershipRepository ownershipRepository; - public GroupServiceImpl(GroupsRepository repo, OwnershipRepository ownershipRepository) { - this.groupRepository = repo; - this.ownershipRepository = ownershipRepository; - } - @Override @Transactional public Group createGroup(Group group) throws GroupExistsConflictException, InvalidGroupRegexException { - Group foundGroup = find(group.getResourceId()); // If already defined, we don't want to create a new one, nor do we want this call update the definition - if (foundGroup != null) { - throw new GroupExistsConflictException( - String.format("Call update (PUT) to modify the group with resource id: [%s] and name: [%s]", - foundGroup.getResourceId(), foundGroup.getName())); + if (groupRepository.existsById(group.getResourceId())) { + throw new GroupExistsConflictException(String.format("Call update (PUT) to modify the group with resource id: [%s] and name: [%s]", group.getResourceId(), group.getName())); } validateGroupRegex(group); + manageApproversList(group); return groupRepository.save(group); } @@ -59,6 +59,7 @@ public void deleteDefinition(String resourceId) throws PersistentEntityNotFound, "Unable to delete group with resource id: [%s] - remove all items owned by / associated with the group first", resourceId)); } + approversRepository.deleteAll(group.getApproversList()); groupRepository.delete(group); } @@ -115,15 +116,49 @@ public List findAll() { return groupRepository.findAll(); } + private List getGroupListFromIds(List approverGroupIds) { + List result = new ArrayList<>(); + for (String id : approverGroupIds) { + Group g = find(id); + result.add(g); + } + return result; + } + + private void manageApproversList(Group group) { + AtomicInteger approversCount = new AtomicInteger(); + group.getApproversList().forEach(a -> approversCount.addAndGet(a.getApproverGroupIds().size())); + if (approversCount.intValue() == 0) { + // Need to manually manage the join tables + List ids = approversRepository.getApproverIdsForGroup(group.getResourceId()); + groupRepository.clearApproversByApproverIds(ids); + approversRepository.deleteGroupAssociationsForIds(ids); + approversRepository.deleteAllById(ids); + group.setApproversList(new ArrayList<>()); + return; + } + List updatedApprovers = new ArrayList<>(); + group.getApproversList().forEach(approvers -> { + Approvers savedApprovers = approversRepository.findByResourceId(approvers.getResourceId()); + savedApprovers = savedApprovers == null ? approversRepository.save(approvers) : savedApprovers; + savedApprovers.setApproverGroups(getGroupListFromIds(approvers.getApproverGroupIds())); + Approvers updatedApp = approversRepository.save(savedApprovers); + updatedApprovers.add(updatedApp); + }); + group.setApproversList(updatedApprovers); + } + @Override public Group updateGroup(Group group) throws PersistentEntityNotFound, InvalidGroupRegexException { + manageApproversList(group); // have to make sure that approvers have been saved before a fetch or we can get data integrity errors on lookup... Group g = find(group.getResourceId()); if (g == null) { throw new PersistentEntityNotFound(String.format("Unable to find group with resource id: [%s] and name: [%s]", group.getResourceId(), group.getName())); } validateGroupRegex(group); - return groupRepository.save(group); + Group result = groupRepository.save(group); + return result; } /** 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 670f60c39..097f745fb 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 @@ -18,10 +18,12 @@ import lombok.NoArgsConstructor; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -54,29 +56,10 @@ public UserService(IGroupService groupService, OwnershipRepository ownershipRepo 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) + * @deprecated don't call this, call the ShibUiPermissionDelegate method hasPermission(...) */ - private boolean currentUserHasExpectedRole(List acceptedRoles) { - User user = getCurrentUser(); - return acceptedRoles.contains(user.getRole()); - } - + @Deprecated public boolean currentUserIsAdmin() { User user = getCurrentUser(); return user != null && user.getRole().equals("ROLE_ADMIN"); @@ -99,6 +82,10 @@ public Optional findByUsername(String username) { return userRepository.findByUsername(username); } + public Authentication getCurrentUserAuthentication() { + return SecurityContextHolder.getContext().getAuthentication(); + } + public User getCurrentUser() { //TODO: Consider returning an Optional here User user = null; @@ -137,6 +124,19 @@ public Group getCurrentUserGroup() { } } + /** + * @return a list of ALL groups that the user can approve for (checks ALL the users groups) + */ + public List getGroupsCurrentUserCanApprove() { + HashSet fullSet = new HashSet<>(); + for (Group g : getCurrentUser().getUserGroups()) { + fullSet.addAll(g.getApproveForList()); + } + ArrayList result = new ArrayList<>(); + result.addAll(fullSet); + return result; + } + public Set getUserRoles(String username) { Optional user = userRepository.findByUsername(username); HashSet result = new HashSet<>(); @@ -145,15 +145,18 @@ public Set getUserRoles(String username) { } // @TODO - probably delegate this out to something plugable at some point - public boolean isAuthorizedFor(Ownable ownableObject) { + public boolean canViewOrEditTarget(Ownable ownableObject) { switch (getCurrentUserAccess()) { case ADMIN: // Pure admin is authorized to do anything return true; - case GROUP: // if the current user's group matches the object's group we are good. + case GROUP: // if the current user's group matches the object's group OR the user is an approver to the object Set owners = ownershipRepository.findOwnableObjectOwners(ownableObject); String currentUsersGroupId = getCurrentUser().getGroupId(); + List userApproveForGroups = getCurrentUser().getGroup().getApproveForList(); + // Check user is part of the owner's group for (Ownership owner : owners) { - if (currentUsersGroupId.equals(owner.getOwnerId()) && OwnerType.valueOf(owner.getOwnerType()) == OwnerType.GROUP) { + boolean isGroupOwner = OwnerType.valueOf(owner.getOwnerType()) == OwnerType.GROUP; + if (isGroupOwner && (currentUsersGroupId.equals(owner.getOwnerId())) || userApproveForGroups.contains(owner.getOwnerId())) { return true; } } 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 fa8fc62f8..c71dfe971 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 @@ -8,6 +8,7 @@ import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorProjection; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import java.util.ConcurrentModificationException; import java.util.List; @@ -19,6 +20,8 @@ * @since 1.0 */ public interface EntityDescriptorService { + void checkApprovalStatusOfEntitiesForGroup(Group group); + /** * Map from front-end data representation of entity descriptor to opensaml implementation of entity descriptor model * @@ -65,7 +68,7 @@ EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepres * "admin" * @throws ForbiddenException - If user is not an ADMIN */ - Iterable getAllDisabledAndNotOwnedByAdmin() throws ForbiddenException; + Iterable getDisabledMetadataSources() throws ForbiddenException; /** * @return a list of EntityDescriptorProjections that a user has the rights to access @@ -122,4 +125,8 @@ EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String resour boolean entityExists(String entityID); EntityDescriptorRepresentation updateGroupForEntityDescriptor(String resourceId, String groupId); + + EntityDescriptorRepresentation changeApproveStatusOfEntityDescriptor(String resourceId, boolean status) throws PersistentEntityNotFound, ForbiddenException; + + List getAllEntityDescriptorProjectionsNeedingApprovalBasedOnUserAccess() throws ForbiddenException; } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EnversEntityDescriptorVersionService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EnversEntityDescriptorVersionService.java index 5857ac283..398517a51 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EnversEntityDescriptorVersionService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EnversEntityDescriptorVersionService.java @@ -32,8 +32,7 @@ public List findVersionsForEntityDescriptor(String resourceId) throws P } @Override - public EntityDescriptorRepresentation findSpecificVersionOfEntityDescriptor(String resourceId, String versionId) throws - PersistentEntityNotFound { + public EntityDescriptorRepresentation findSpecificVersionOfEntityDescriptor(String resourceId, String versionId) throws PersistentEntityNotFound { Object edObject = enversVersionServiceSupport.findSpecificVersionOfPersistentEntity(resourceId, versionId, EntityDescriptor.class); if (edObject == null) { throw new PersistentEntityNotFound("Unable to find specific version requested - version: " + versionId); 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 f5fcbd0ad..93a1dbce6 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 @@ -28,11 +28,14 @@ import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorProjection; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Approvers; 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.permission.IShibUiPermissionEvaluator; +import edu.internet2.tier.shibboleth.admin.ui.security.permission.PermissionType; +import edu.internet2.tier.shibboleth.admin.ui.security.permission.ShibUiPermissibleType; 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; @@ -81,6 +84,9 @@ public class JPAEntityDescriptorServiceImpl implements EntityDescriptorService { @Autowired private OwnershipRepository ownershipRepository; + @Autowired + private IShibUiPermissionEvaluator shibUiAuthorizationDelegate; + @Autowired private UserService userService; @@ -172,53 +178,69 @@ private Map buildOAuthRPExtensionsMap(EntityDescriptor ed) { } @Override - public EntityDescriptor createDescriptorFromRepresentation(final EntityDescriptorRepresentation representation) { - EntityDescriptor ed = openSamlObjects.buildDefaultInstanceOfType(EntityDescriptor.class); - return buildDescriptorFromRepresentation(ed, representation); - } - - @Override - public EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, ObjectIdExistsException, InvalidPatternMatchException { - return createNew(createRepresentationFromDescriptor(ed)); + public EntityDescriptorRepresentation changeApproveStatusOfEntityDescriptor(String resourceId, boolean status) throws PersistentEntityNotFound, ForbiddenException { + EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); + if (ed == null) { + throw new PersistentEntityNotFound("Entity with resourceid[" + resourceId + "] was not found for approval"); + } + return changeApproveStatusOfEntityDescriptor(ed, status); } - @Override - public EntityDescriptorRepresentation createNewEntityDescriptorFromXMLOrigin(EntityDescriptor ed) { - ed.setIdOfOwner(userService.getCurrentUserGroup().getOwnerId()); - ed.setProtocol(determineEntityDescriptorProtocol(ed)); - if (ed.getProtocol() == EntityDescriptorProtocol.OIDC) { - ed.getSPSSODescriptor("").addSupportedProtocol("http://openid.net/specs/openid-connect-core-1_0.html"); + private EntityDescriptorRepresentation changeApproveStatusOfEntityDescriptor(EntityDescriptor ed, boolean status) throws PersistentEntityNotFound, ForbiddenException { + if (!shibUiAuthorizationDelegate.hasPermission(userService.getCurrentUserAuthentication(), ed, PermissionType.approve)) { + throw new ForbiddenException("You do not have the permissions necessary to approve this entity descriptor."); } - EntityDescriptor savedEntity = entityDescriptorRepository.save(ed); - return createRepresentationFromDescriptor(savedEntity); + if (status) { // approve + int approvedCount = ed.approvedCount(); // total number of approvals so far + List theApprovers = groupService.find(ed.getIdOfOwner()).getApproversList(); + if (theApprovers.size() > approvedCount) { // don't add if we already have enough approvals + ed.addApproval(userService.getCurrentUserGroup()); + } + ed.setApproved(ed.approvedCount() >= theApprovers.size()); // future check for multiple approvals needed + ed = entityDescriptorRepository.save(ed); + } else { // un-approve + ed.removeLastApproval(); + Group ownerGroup = groupService.find(ed.getIdOfOwner()); + ed.setApproved(ed.approvedCount() >= ownerGroup.getApproversList().size()); // safe check in case of weird race conditions from the UI + ed = entityDescriptorRepository.save(ed); + } + return createRepresentationFromDescriptor(ed); } - private EntityDescriptorProtocol determineEntityDescriptorProtocol(EntityDescriptor ed) { - boolean oidcType = ed.getSPSSODescriptor("") != null && ed.getSPSSODescriptor("").isOidcType(); - return oidcType ? EntityDescriptorProtocol.OIDC : EntityDescriptorProtocol.SAML; + /** + * Update the approval status of entities that were in some approval state but the group approvers were added/removed. + */ + @Override + public void checkApprovalStatusOfEntitiesForGroup(Group group) { + entityDescriptorRepository.findAllResourceIdsByIdOfOwnerAndNotEnabled(group.getResourceId()).forEach(id -> { + EntityDescriptor ed = entityDescriptorRepository.findByResourceId(id); + int approvedCount = ed.approvedCount(); // total number of approvals so far + List theApprovers = groupService.find(ed.getIdOfOwner()).getApproversList(); + ed.setApproved(approvedCount >= theApprovers.size()); + ed = entityDescriptorRepository.save(ed); + }); } @Override - public boolean entityExists(String entityID) { - return entityDescriptorRepository.findByEntityID(entityID) != null ; + public EntityDescriptor createDescriptorFromRepresentation(final EntityDescriptorRepresentation representation) { + EntityDescriptor ed = openSamlObjects.buildDefaultInstanceOfType(EntityDescriptor.class); + return buildDescriptorFromRepresentation(ed, representation); } @Override - public EntityDescriptorRepresentation updateGroupForEntityDescriptor(String resourceId, String groupId) { - EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); - ed.setIdOfOwner(groupId); - EntityDescriptor savedEntity = entityDescriptorRepository.save(ed); - return createRepresentationFromDescriptor(savedEntity); + public EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, ObjectIdExistsException, InvalidPatternMatchException { + return createNew(createRepresentationFromDescriptor(ed)); } @Override public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) throws ForbiddenException, ObjectIdExistsException, InvalidPatternMatchException { - if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { - throw new ForbiddenException("You do not have the permissions necessary to enable this service."); + if (entityExists(edRep.getEntityId())) { + throw new ObjectIdExistsException(edRep.getEntityId()); } - if (entityDescriptorRepository.findByEntityID(edRep.getEntityId()) != null) { - throw new ObjectIdExistsException(edRep.getEntityId()); + EntityDescriptor ed = (EntityDescriptor) createDescriptorFromRepresentation(edRep); + if (ed.isServiceEnabled() && !shibUiAuthorizationDelegate.hasPermission(userService.getCurrentUserAuthentication(), ed, PermissionType.enable)) { + throw new ForbiddenException("You do not have the permissions necessary to enable this entity descriptor."); } // "Create new" will use the current user's group as the owner @@ -226,8 +248,11 @@ public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation e edRep.setIdOfOwner(ownerId); validateEntityIdAndACSUrls(edRep); - EntityDescriptor ed = (EntityDescriptor) createDescriptorFromRepresentation(edRep); ed.setIdOfOwner(userService.getCurrentUserGroup().getOwnerId()); + if (shibUiAuthorizationDelegate.hasPermission(userService.getCurrentUserAuthentication(), null, PermissionType.admin) || + userService.getCurrentUserGroup().getApproversList().isEmpty()) { + ed.setApproved(true); + } ownershipRepository.deleteEntriesForOwnedObject(ed); ownershipRepository.save(new Ownership(userService.getCurrentUserGroup(), ed)); @@ -235,6 +260,23 @@ public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation e return createRepresentationFromDescriptor(entityDescriptorRepository.save(ed)); } + @Override + public EntityDescriptorRepresentation createNewEntityDescriptorFromXMLOrigin(EntityDescriptor ed) { + ed.setIdOfOwner(userService.getCurrentUserGroup().getOwnerId()); + ownershipRepository.deleteEntriesForOwnedObject(ed); + ownershipRepository.save(new Ownership(userService.getCurrentUserGroup(), ed)); + ed.setProtocol(determineEntityDescriptorProtocol(ed)); + if (ed.getProtocol() == EntityDescriptorProtocol.OIDC) { + ed.getSPSSODescriptor("").addSupportedProtocol("http://openid.net/specs/openid-connect-core-1_0.html"); + } + if (shibUiAuthorizationDelegate.hasPermission(userService.getCurrentUserAuthentication(), null, PermissionType.admin) || + userService.getCurrentUserGroup().getApproversList().isEmpty()) { + ed.setApproved(true); + } + EntityDescriptor savedEntity = entityDescriptorRepository.save(ed); + return createRepresentationFromDescriptor(savedEntity); + } + @Override public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.opensaml.saml.saml2.metadata.EntityDescriptor entityDescriptor) { EntityDescriptor ed = (EntityDescriptor) entityDescriptor; @@ -251,6 +293,7 @@ public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.ope representation.setCurrent(ed.isCurrent()); representation.setIdOfOwner(ed.getIdOfOwner()); representation.setProtocol(ed.getProtocol()); + representation.setApproved(isEntityDescriptorApproved(ed)); // Set up SPSSODescriptor if (ed.getSPSSODescriptor("") != null && ed.getSPSSODescriptor("").getSupportedProtocols().size() > 0) { @@ -261,7 +304,7 @@ public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.ope if (ed.getSPSSODescriptor("") != null && ed.getSPSSODescriptor("").getNameIDFormats().size() > 0) { ServiceProviderSsoDescriptorRepresentation serviceProviderSsoDescriptorRepresentation = representation.getServiceProviderSsoDescriptor(true); serviceProviderSsoDescriptorRepresentation.setNameIdFormats( - ed.getSPSSODescriptor("").getNameIDFormats().stream().map(p -> p.getURI()).collect(Collectors.toList()) + ed.getSPSSODescriptor("").getNameIDFormats().stream().map(p -> p.getURI()).collect(Collectors.toList()) ); } @@ -371,40 +414,40 @@ public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.ope IRelyingPartyOverrideProperty overrideProperty = (IRelyingPartyOverrideProperty)override.get(); Object attributeValues = null; switch (ModelRepresentationConversions.AttributeTypes.valueOf(overrideProperty.getDisplayType().toUpperCase())) { - case STRING: - case LONG: - case DOUBLE: - case DURATION: - case SPRING_BEAN_ID: - if (jpaAttribute.getAttributeValues().size() != 1) { - throw new RuntimeException("Multiple/No values detected where one is expected for override: " + jpaAttribute.getName()); - } - attributeValues = ModelRepresentationConversions.getValueFromXMLObject(jpaAttribute.getAttributeValues().get(0)); - break; - case INTEGER: - if (jpaAttribute.getAttributeValues().size() != 1) { - throw new RuntimeException("Multiple/No values detected where one is expected for override: " + jpaAttribute.getName()); - } - attributeValues = ((XSInteger)jpaAttribute.getAttributeValues().get(0)).getValue(); - break; - case BOOLEAN: - if (jpaAttribute.getAttributeValues().size() != 1) { - throw new RuntimeException("Multiple/No values detected where one is expected!"); - } - if (overrideProperty.getPersistType() != null && - !overrideProperty.getPersistType().equals(overrideProperty.getDisplayType())) { - attributeValues = overrideProperty.getPersistValue().equals(ModelRepresentationConversions.getValueFromXMLObject(jpaAttribute.getAttributeValues().get(0))); - } else { - attributeValues = Boolean.valueOf(overrideProperty.getInvert()) ^ Boolean.valueOf(((XSBoolean) jpaAttribute.getAttributeValues() - .get(0)).getStoredValue()); - } - break; - case SET: - case LIST: - case SELECTION_LIST: - attributeValues = jpaAttribute.getAttributeValues().stream() - .map(attributeValue -> ModelRepresentationConversions.getValueFromXMLObject(attributeValue)) - .collect(Collectors.toList()); + case STRING: + case LONG: + case DOUBLE: + case DURATION: + case SPRING_BEAN_ID: + if (jpaAttribute.getAttributeValues().size() != 1) { + throw new RuntimeException("Multiple/No values detected where one is expected for override: " + jpaAttribute.getName()); + } + attributeValues = ModelRepresentationConversions.getValueFromXMLObject(jpaAttribute.getAttributeValues().get(0)); + break; + case INTEGER: + if (jpaAttribute.getAttributeValues().size() != 1) { + throw new RuntimeException("Multiple/No values detected where one is expected for override: " + jpaAttribute.getName()); + } + attributeValues = ((XSInteger)jpaAttribute.getAttributeValues().get(0)).getValue(); + break; + case BOOLEAN: + if (jpaAttribute.getAttributeValues().size() != 1) { + throw new RuntimeException("Multiple/No values detected where one is expected!"); + } + if (overrideProperty.getPersistType() != null && + !overrideProperty.getPersistType().equals(overrideProperty.getDisplayType())) { + attributeValues = overrideProperty.getPersistValue().equals(ModelRepresentationConversions.getValueFromXMLObject(jpaAttribute.getAttributeValues().get(0))); + } else { + attributeValues = Boolean.valueOf(overrideProperty.getInvert()) ^ Boolean.valueOf(((XSBoolean) jpaAttribute.getAttributeValues() + .get(0)).getStoredValue()); + } + break; + case SET: + case LIST: + case SELECTION_LIST: + attributeValues = jpaAttribute.getAttributeValues().stream() + .map(attributeValue -> ModelRepresentationConversions.getValueFromXMLObject(attributeValue)) + .collect(Collectors.toList()); } relyingPartyOverrides.put(((IRelyingPartyOverrideProperty) override.get()).getName(), attributeValues); } @@ -428,28 +471,57 @@ public void delete(String resourceId) throws ForbiddenException, PersistentEntit } - @Override - public Iterable getAllDisabledAndNotOwnedByAdmin() throws ForbiddenException { - if (!userService.currentUserIsAdmin()) { - throw new ForbiddenException(); + private EntityDescriptorProtocol determineEntityDescriptorProtocol(EntityDescriptor ed) { + boolean oidcType = ed.getSPSSODescriptor("") != null && ed.getSPSSODescriptor("").isOidcType(); + return oidcType ? EntityDescriptorProtocol.OIDC : EntityDescriptorProtocol.SAML; + } + + private KeyDescriptorRepresentation.ElementType determineKeyInfoType(KeyInfo keyInfo) { + List children = keyInfo.getOrderedChildren().stream().filter(xmlObj -> { + boolean xmlWeDoNotWant = xmlObj instanceof KeyName || xmlObj instanceof KeyValue || xmlObj == null; + return !xmlWeDoNotWant; + }).collect(Collectors.toList()); + if (children.size() < 1) { + return KeyDescriptorRepresentation.ElementType.unsupported; } - return entityDescriptorRepository.findAllDisabledAndNotOwnedByAdmin().map(ed -> createRepresentationFromDescriptor(ed)).collect(Collectors.toList()); + XMLObject xmlObject = children.get(0); + switch (xmlObject.getElementQName().getLocalPart()) { + case "X509Data": + return KeyDescriptorRepresentation.ElementType.X509Data; + case "ClientSecret": + return KeyDescriptorRepresentation.ElementType.clientSecret; + case "ClientSecretKeyReference": + return KeyDescriptorRepresentation.ElementType.clientSecretRef; + case "JwksData": + return KeyDescriptorRepresentation.ElementType.jwksData; + case "JwksUri": + return KeyDescriptorRepresentation.ElementType.jwksUri; + default: + return KeyDescriptorRepresentation.ElementType.unsupported; + } + } + + @Override + public boolean entityExists(String entityID) { + return entityDescriptorRepository.findByEntityID(entityID) != null ; } + /** + * Get the "short" detail list of entity descriptors that match the current user's group. The intent is the list will be those + * EDs that the user would see on the dashboard. + * @throws ForbiddenException + */ @Override public List getAllEntityDescriptorProjectionsBasedOnUserAccess() throws ForbiddenException { - switch (userService.getCurrentUserAccess()) { - case ADMIN: - List o = entityDescriptorRepository.findAllReturnProjections(); - return o; - case GROUP: - User user = userService.getCurrentUser(); - Group group = user.getGroup(); - List ed = entityDescriptorRepository.findAllByIdOfOwner(group.getOwnerId()); - return ed; - default: - throw new ForbiddenException(); - } + return (List) shibUiAuthorizationDelegate.getPersistentEntities(userService.getCurrentUserAuthentication(), ShibUiPermissibleType.entityDescriptorProjection, PermissionType.fetch); + } + + /** + * Based on the current users group, find those entities that the user can approve that need approval + */ + @Override + public List getAllEntityDescriptorProjectionsNeedingApprovalBasedOnUserAccess() throws ForbiddenException { + return (List) shibUiAuthorizationDelegate.getPersistentEntities(userService.getCurrentUserAuthentication(), ShibUiPermissibleType.entityDescriptorProjection, PermissionType.approve); } @Override @@ -461,16 +533,20 @@ public List getAttributeReleaseListFromAttributeList(List att return ModelRepresentationConversions.getAttributeReleaseListFromAttributeList(attributeList); } + @Override + public Iterable getDisabledMetadataSources() throws ForbiddenException { + return (List) shibUiAuthorizationDelegate.getPersistentEntities(userService.getCurrentUserAuthentication(), ShibUiPermissibleType.entityDescriptorProjection, PermissionType.enable); + } + @Override public EntityDescriptor getEntityDescriptorByResourceId(String resourceId) throws PersistentEntityNotFound, ForbiddenException { EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); if (ed == null) { throw new PersistentEntityNotFound(String.format("The entity descriptor with entity id [%s] was not found.", resourceId)); } - if (!userService.isAuthorizedFor(ed)) { + if (!shibUiAuthorizationDelegate.hasPermission(userService.getCurrentUserAuthentication(), ed, PermissionType.viewOrEdit)) { throw new ForbiddenException(); } - return ed; } @@ -479,6 +555,17 @@ public Map getRelyingPartyOverridesRepresentationFromAttributeLi return ModelRepresentationConversions.getRelyingPartyOverridesRepresentationFromAttributeList(attributeList); } + private boolean isEntityDescriptorApproved(EntityDescriptor ed) { + if (ed.isServiceEnabled()) { + return true; + } + Group ownerGroup = groupService.find(ed.getIdOfOwner()); + if (ownerGroup == null) { + ownerGroup = Group.ADMIN_GROUP; // This should only happen in the large number of tests that were written prior to group implementation + } + return ed.approvedCount() >= ownerGroup.getApproversList().size(); + } + private void setupSecurityRepresentationFromEntityDescriptor(EntityDescriptor ed, EntityDescriptorRepresentation representation) { SecurityInfoRepresentation securityInfoRepresentation = representation.getSecurityInfo(); if (ed.wantsAssertionsSigned()) { @@ -532,44 +619,19 @@ private void setupSecurityRepresentationFromEntityDescriptor(EntityDescriptor ed } } - private KeyDescriptorRepresentation.ElementType determineKeyInfoType(KeyInfo keyInfo) { - List children = keyInfo.getOrderedChildren().stream().filter(xmlObj -> { - boolean xmlWeDoNotWant = xmlObj instanceof KeyName || xmlObj instanceof KeyValue || xmlObj == null; - return !xmlWeDoNotWant; - }).collect(Collectors.toList()); - if (children.size() < 1) { - return KeyDescriptorRepresentation.ElementType.unsupported; - } - XMLObject xmlObject = children.get(0); - switch (xmlObject.getElementQName().getLocalPart()) { - case "X509Data": - return KeyDescriptorRepresentation.ElementType.X509Data; - case "ClientSecret": - return KeyDescriptorRepresentation.ElementType.clientSecret; - case "ClientSecretKeyReference": - return KeyDescriptorRepresentation.ElementType.clientSecretRef; - case "JwksData": - return KeyDescriptorRepresentation.ElementType.jwksData; - case "JwksUri": - return KeyDescriptorRepresentation.ElementType.jwksUri; - default: - return KeyDescriptorRepresentation.ElementType.unsupported; - } - } - @Override public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRep) throws ForbiddenException, PersistentEntityNotFound, InvalidPatternMatchException { EntityDescriptor existingEd = entityDescriptorRepository.findByResourceId(edRep.getId()); if (existingEd == null) { throw new PersistentEntityNotFound(String.format("The entity descriptor with entity id [%s] was not found for update.", edRep.getId())); } - if (edRep.isServiceEnabled() && !userService.currentUserCanEnable(existingEd)) { + if (edRep.isServiceEnabled() && !shibUiAuthorizationDelegate.hasPermission(userService.getCurrentUserAuthentication(), existingEd, PermissionType.enable)) { throw new ForbiddenException("You do not have the permissions necessary to enable this service."); } if (StringUtils.isEmpty(edRep.getIdOfOwner())) { edRep.setIdOfOwner(StringUtils.isNotEmpty(existingEd.getIdOfOwner()) ? existingEd.getIdOfOwner() : userService.getCurrentUserGroup().getOwnerId()); } - if (!userService.isAuthorizedFor(existingEd)) { + if (!shibUiAuthorizationDelegate.hasPermission(userService.getCurrentUserAuthentication(), existingEd, PermissionType.viewOrEdit)) { throw new ForbiddenException(); } // Verify we're the only one attempting to update the EntityDescriptor @@ -598,20 +660,47 @@ public void updateDescriptorFromRepresentation(org.opensaml.saml.saml2.metadata. } @Override - public EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String resourceId, boolean status) throws - PersistentEntityNotFound, ForbiddenException { + public EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String resourceId, boolean enabled) throws PersistentEntityNotFound, ForbiddenException { EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); if (ed == null) { throw new PersistentEntityNotFound("Entity with resourceid[" + resourceId + "] was not found for update"); } - if (!userService.currentUserCanEnable(ed)) { + if (!shibUiAuthorizationDelegate.hasPermission(userService.getCurrentUserAuthentication(), ed, PermissionType.enable)) { throw new ForbiddenException("You do not have the permissions necessary to change the enable status of this entity descriptor."); } - ed.setServiceEnabled(status); + // check to see if approvals have been completed + int approvedCount = ed.approvedCount(); + List approversList = groupService.find(ed.getIdOfOwner()).getApproversList(); + if (enabled == true && + !ed.isServiceEnabled() && + !shibUiAuthorizationDelegate.hasPermission(userService.getCurrentUserAuthentication(), null, PermissionType.admin) && + approversList.size() > approvedCount) { + throw new ForbiddenException("Approval must be completed before you can change the enable status of this entity descriptor."); + } + ed.setServiceEnabled(enabled); + if (enabled == true) { + ed.setApproved(true); + } ed = entityDescriptorRepository.save(ed); return createRepresentationFromDescriptor(ed); } + @Override + public EntityDescriptorRepresentation updateGroupForEntityDescriptor(String resourceId, String groupId) { + EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); + ed.setIdOfOwner(groupId); + Group group = groupService.find(groupId); + ownershipRepository.deleteEntriesForOwnedObject(ed); + ownershipRepository.save(new Ownership(group, ed)); + // check and see if we need to update the approved status + if (!ed.isServiceEnabled()) { + int numApprovers = group.getApproversList().size(); + ed.setApproved(!(numApprovers > 0 && ed.approvedCount() < numApprovers)); + } + EntityDescriptor savedEntity = entityDescriptorRepository.save(ed); + return createRepresentationFromDescriptor(savedEntity); + } + 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 928ad2607..03a72602e 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 @@ -8,6 +8,8 @@ import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound; 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.permission.IShibUiPermissionEvaluator; +import edu.internet2.tier.shibboleth.admin.ui.security.permission.PermissionType; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -44,6 +46,9 @@ public class JPAFilterServiceImpl implements FilterService { @Autowired private MetadataResolverService metadataResolverService; + @Autowired + private IShibUiPermissionEvaluator shibUiService; + @Autowired private UserService userService; @@ -117,7 +122,7 @@ public MetadataFilter updateFilterEnabledStatus(String metadataResolverId, Strin MetadataFilter filterTobeUpdated = filterTobeUpdatedOptional.get(); - if (!userService.currentUserCanEnable(filterTobeUpdated)) { + if (!shibUiService.hasPermission(userService.getCurrentUserAuthentication(), filterTobeUpdated, PermissionType.enable)) { throw new ForbiddenException("You do not have the permissions necessary to change the enable status of this filter."); } 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 6cccc3dd0..07dd94510 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 @@ -16,7 +16,7 @@ public interface MetadataResolverService { public void reloadFilters(String metadataResolverName); - public MetadataResolver updateMetadataResolverEnabledStatus(MetadataResolver existingResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException; + public MetadataResolver updateMetadataResolverEnabledStatus(String resourceId, boolean status) 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 ed752b5f5..42f801894 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -48,9 +48,11 @@ spring.liquibase.change-log=db/changelog/changelog.sql spring.jpa.hibernate.ddl-auto=update spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl spring.jpa.show-sql=false +spring.jpa.properties.hibernate.show_sql=false spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.check_nullability=true spring.jpa.hibernate.use-new-id-generator-mappings=true +spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true #Envers versioning spring.jpa.properties.org.hibernate.envers.store_data_at_delete=true diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index a2ecaf585..08950568c 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -192,6 +192,7 @@ label.new-group=New Group label.new-attribute=New Custom Entity Attribute label.edit-group=Edit Group +label.approved=Approved label.metadata-source=Metadata Source label.metadata-sources=Metadata Sources label.metadata-provider=Metadata Provider @@ -496,6 +497,7 @@ label.disable=Disable label.enabled=Enabled label.disabled=Disabled label.enable-metadata-sources=Enable Metadata Sources +label.approve-metadata-sources=Approve Metadata Sources label.title=Title label.author=Author label.creation-date=Creation Date @@ -516,6 +518,9 @@ label.compare-selected=Compare Selected label.restore-version=Restore Version ({ date }) label.group=Group +label.approvers-list=Approvers +tooltip.approvers-list=List of groups who are able to approve Metadata Sources. + label.saved=Saved label.by=By @@ -637,6 +642,7 @@ message.session-timeout-heading=Session timed out message.session-timeout-body=Your session has timed out. Please login again. message.session-timeout=An error has occurred while saving. Your session may have timed out. +tooltip.approved=Metadata Source is approved and can be enabled by authorized enabler tooltip.entity-id=An entityID is the SAML identifier that uniquely names a service provider. tooltip.service-provider-name=Used only in the Shibboleth IDP UI, this name is used to distinguish the service provider in the dashboard. tooltip.source-protocol=Authentication Protocol to use for this Metadata Source. (SAML, OIDC, CAS, etc) @@ -804,6 +810,13 @@ value.algorithm-cbc-tripledes=CBC (TRIPLEDES) - http://www.w3.org/2001/04/xmlenc message.algorithms-unique=Each algorithm may only be used once. +label.approve=Approve +label.disapprove=Unapprove +label.approval=Approved +value.approved=Approved +value.disapproved=Not Approved +label.group-approvers=Approvers + label.source-protocol=Authentication Protocol label.key-descriptors=Key Descriptors label.certificate-value=Value diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/AbstractBaseDataJpaTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/AbstractBaseDataJpaTest.groovy index da12d8bc2..791cbbc2e 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/AbstractBaseDataJpaTest.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/AbstractBaseDataJpaTest.groovy @@ -1,9 +1,12 @@ package edu.internet2.tier.shibboleth.admin.ui +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository import edu.internet2.tier.shibboleth.admin.ui.security.model.Role import edu.internet2.tier.shibboleth.admin.ui.security.model.User import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.GroupUpdatedEntityListener import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.UserUpdatedEntityListener +import edu.internet2.tier.shibboleth.admin.ui.security.repository.ApproversRepository 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 @@ -33,6 +36,12 @@ import javax.persistence.EntityManager @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") abstract class AbstractBaseDataJpaTest extends Specification implements ResetsDatabaseTrait { + @Autowired + ApproversRepository approversRepository + + @Autowired + EntityDescriptorRepository entityDescriptorRepository + @Autowired EntityManager entityManager @@ -83,7 +92,7 @@ abstract class AbstractBaseDataJpaTest extends Specification implements ResetsDa } createAdminUser() - GroupUpdatedEntityListener.init(ownershipRepository) + GroupUpdatedEntityListener.init(ownershipRepository, groupRepository) UserUpdatedEntityListener.init(ownershipRepository, groupRepository) } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/BaseDataJpaTestConfiguration.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/BaseDataJpaTestConfiguration.groovy index 2c246889a..9c5b88df1 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/BaseDataJpaTestConfiguration.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/BaseDataJpaTestConfiguration.groovy @@ -9,12 +9,16 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.StringTrimModule import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository 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.permission.IShibUiPermissionEvaluator +import edu.internet2.tier.shibboleth.admin.ui.security.permission.ShibUiPermissionDelegate import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository import edu.internet2.tier.shibboleth.admin.ui.security.service.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.JPAEntityServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility @@ -54,9 +58,9 @@ class BaseDataJpaTestConfiguration { } @Bean - GroupUpdatedEntityListener groupUpdatedEntityListener(OwnershipRepository ownershipRepository) { + GroupUpdatedEntityListener groupUpdatedEntityListener(OwnershipRepository ownershipRepository, GroupsRepository groupsRepository) { GroupUpdatedEntityListener listener = new GroupUpdatedEntityListener() - listener.init(ownershipRepository) + listener.init(ownershipRepository, groupsRepository) return listener } @@ -104,4 +108,9 @@ class BaseDataJpaTestConfiguration { listener.init(ownershipRepository, groupRepo) return listener } + + @Bean + public IShibUiPermissionEvaluator shibUiPermissionEvaluator(EntityDescriptorRepository entityDescriptorRepository, UserService userService) { + return new ShibUiPermissionDelegate(entityDescriptorRepository, userService); + } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestConfiguration.groovy index 1a044baf2..e443888dc 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 @@ -12,9 +12,6 @@ import edu.internet2.tier.shibboleth.admin.ui.service.CustomEntityAttributesDefi import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService import net.shibboleth.ext.spring.resource.ResourceHelper import net.shibboleth.utilities.java.support.component.ComponentInitializationException - -import javax.persistence.EntityManager - import org.apache.lucene.document.Document import org.apache.lucene.document.Field import org.apache.lucene.document.StringField @@ -27,14 +24,14 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Profile import org.springframework.core.io.ClassPathResource import org.springframework.data.domain.AuditorAware import org.springframework.mail.javamail.JavaMailSender import org.springframework.mail.javamail.JavaMailSenderImpl +import javax.persistence.EntityManager + /** * NOT A TEST - this is configuration FOR tests */ diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateControllerTests.groovy new file mode 100644 index 000000000..8dfd0cf29 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateControllerTests.groovy @@ -0,0 +1,157 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository +import edu.internet2.tier.shibboleth.admin.ui.security.model.Approvers +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService +import edu.internet2.tier.shibboleth.admin.ui.service.EntityService +import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import spock.lang.Subject + +import javax.transaction.Transactional + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +// TODO: This is only checking activation for EntityDescriptors. Expanding for resolvers not included +class ActivateControllerTests extends AbstractBaseDataJpaTest { + @Subject + def controller + + @Autowired + ObjectMapper mapper + + @Autowired + EntityService entityService + + @Autowired + EntityDescriptorRepository entityDescriptorRepository + + @Autowired + private EntityDescriptorService entDescriptorService; + + @Autowired + OpenSamlObjects openSamlObjects + + def defaultEntityDescriptorResourceId + def mockMvc + + @Transactional + def setup() { + controller = new ActivateController() + controller.entityDescriptorService = entDescriptorService + mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + + EntityDescriptorConversionUtils.setOpenSamlObjects(openSamlObjects) + EntityDescriptorConversionUtils.setEntityService(entityService) + + groupService.clearAllForTesting() + List apprGroups = new ArrayList<>() + String[] groupNames = ['BBB', 'CCC', 'AAA'] + groupNames.each {name -> { + Group group = new Group().with({ + it.name = name + it.description = name + it.resourceId = name + it + }) + if (name != "AAA") { + apprGroups.add(groupRepository.save(group)) + } else { + Approvers approvers = new Approvers() + approvers.setApproverGroups(apprGroups) + List apprList = new ArrayList<>() + apprList.add(approversRepository.save(approvers)) + group.setApproversList(apprList) + groupRepository.save(group) + } + }} + Group group = new Group().with({ + it.name = 'DDD' + it.description = 'DDD' + it.resourceId = 'DDD' + it + }) + groupRepository.save(group) + entityManager.flush() + entityManager.clear() + + Optional userRole = roleRepository.findByName("ROLE_ENABLE") + User user = new User(username: "AUser", roles:[userRole.get()], password: "foo") + user.setGroup(groupRepository.findByResourceId("AAA")) + userService.save(user) + user = new User(username: "BUser", roles:[userRole.get()], password: "foo") + user.setGroup(groupRepository.findByResourceId("BBB")) + userService.save(user) + user = new User(username: "DUser", roles:[userRole.get()], password: "foo") + user.setGroup(groupRepository.findByResourceId("DDD")) + userService.save(user) + + entityManager.flush() + entityManager.clear() + + EntityDescriptor entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false, idOfOwner: 'AAA') + entityDescriptor = entityDescriptorRepository.save(entityDescriptor) + + defaultEntityDescriptorResourceId = entityDescriptor.getResourceId() + } + + @WithMockUser(value = "AUser", roles = ["USER"]) + def 'Owner group cannot activate their own entity descriptor without approvals'() { + expect: + try { + mockMvc.perform(patch("/api/activate/entityDescriptor/" + defaultEntityDescriptorResourceId + "/enable")) + } + catch (Exception e) { + e instanceof ForbiddenException + } + } + + @WithMockUser(value = "DUser", roles = ["USER"]) + def 'non-owner group cannot activate entity descriptor'() { + expect: + try { + mockMvc.perform(patch("/api/activate/entityDescriptor/" + defaultEntityDescriptorResourceId + "/enable")) + } + catch (Exception e) { + e instanceof ForbiddenException + } + } + + @WithMockAdmin + def 'Admin can activate an entity descriptor without approval'() { + when: + def result = mockMvc.perform(patch("/api/activate/entityDescriptor/" + defaultEntityDescriptorResourceId + "/enable")) + + then: + result.andExpect(status().isOk()) + .andExpect(jsonPath("\$.id").value(defaultEntityDescriptorResourceId)) + .andExpect(jsonPath("\$.serviceEnabled").value(true)) + } + + @WithMockUser(value = "AUser", roles = ["USER"]) + def 'Owner group can enable their own entity descriptor with approvals'() { + when: + EntityDescriptor entityDescriptor = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp1', serviceEnabled: false, idOfOwner: 'AAA') + entityDescriptor.addApproval(groupService.find("CCC")) + entityDescriptorRepository.save(entityDescriptor) + + def result = mockMvc.perform(patch("/api/activate/entityDescriptor/uuid-2/enable")) + + then: + result.andExpect(status().isOk()) + .andExpect(jsonPath("\$.id").value('uuid-2')) + .andExpect(jsonPath("\$.serviceEnabled").value(true)) + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ApproveControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ApproveControllerTests.groovy new file mode 100644 index 000000000..eaf337064 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ApproveControllerTests.groovy @@ -0,0 +1,176 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository +import edu.internet2.tier.shibboleth.admin.ui.security.model.Approvers +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService +import edu.internet2.tier.shibboleth.admin.ui.service.EntityService +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import spock.lang.Subject + +import javax.transaction.Transactional + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +class ApproveControllerTests extends AbstractBaseDataJpaTest { + @Subject + def controller + + @Autowired + ObjectMapper mapper + + @Autowired + EntityService entityService + + @Autowired + EntityDescriptorRepository entityDescriptorRepository + + @Autowired + private EntityDescriptorService entDescriptorService; + + @Autowired + OpenSamlObjects openSamlObjects + + def defaultEntityDescriptorResourceId + def mockMvc + + @Transactional + def setup() { + controller = new ApprovalController() + controller.entityDescriptorService = entDescriptorService + mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + + EntityDescriptorConversionUtils.setOpenSamlObjects(openSamlObjects) + EntityDescriptorConversionUtils.setEntityService(entityService) + + groupService.clearAllForTesting() + List apprGroups = new ArrayList<>() + String[] groupNames = ['BBB', 'CCC', 'AAA'] + groupNames.each {name -> { + Group group = new Group().with({ + it.name = name + it.description = name + it.resourceId = name + it + }) + if (name != "AAA") { + apprGroups.add(groupRepository.save(group)) + } else { + Approvers approvers = new Approvers() + approvers.setApproverGroups(apprGroups) + List apprList = new ArrayList<>() + apprList.add(approversRepository.save(approvers)) + group.setApproversList(apprList) + groupRepository.save(group) + } + }} + Group group = new Group().with({ + it.name = 'DDD' + it.description = 'DDD' + it.resourceId = 'DDD' + it + }) + groupRepository.save(group) + entityManager.flush() + entityManager.clear() + + Optional userRole = roleRepository.findByName("ROLE_USER") + User user = new User(username: "AUser", roles:[userRole.get()], password: "foo") + user.setGroup(groupRepository.findByResourceId("AAA")) + userService.save(user) + user = new User(username: "BUser", roles:[userRole.get()], password: "foo") + user.setGroup(groupRepository.findByResourceId("BBB")) + userService.save(user) + user = new User(username: "DUser", roles:[userRole.get()], password: "foo") + user.setGroup(groupRepository.findByResourceId("DDD")) + userService.save(user) + + entityManager.flush() + entityManager.clear() + + EntityDescriptor entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false, idOfOwner: 'AAA') + entityDescriptor = entityDescriptorRepository.save(entityDescriptor) + + defaultEntityDescriptorResourceId = entityDescriptor.getResourceId() + } + + @WithMockUser(value = "AUser", roles = ["USER"]) + def 'Owner group cannot approve their own entity descriptor'() { + expect: + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() == false + try { + mockMvc.perform(patch("/api/approve/entityDescriptor/" + defaultEntityDescriptorResourceId + "/approve")) + } + catch (Exception e) { + e instanceof ForbiddenException + } + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() == false + } + + @WithMockUser(value = "DUser", roles = ["USER"]) + def 'non-approver group cannot approve entity descriptor'() { + expect: + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() == false + try { + mockMvc.perform(patch("/api/approve/entityDescriptor/" + defaultEntityDescriptorResourceId + "/approve")) + } + catch (Exception e) { + e instanceof ForbiddenException + } + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() == false + } + + @WithMockUser(value = "BUser", roles = ["USER"]) + def 'Approver group can approve an entity descriptor'() { + expect: + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() == false + + when: + def result = mockMvc.perform(patch("/api/approve/entityDescriptor/" + defaultEntityDescriptorResourceId + "/approve")) + + then: + result.andExpect(status().isOk()) + .andExpect(jsonPath("\$.id").value(defaultEntityDescriptorResourceId)) + .andExpect(jsonPath("\$.serviceEnabled").value(false)) + .andExpect(jsonPath("\$.approved").value(true)) + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() + } + + @WithMockUser(value = "BUser", roles = ["USER"]) + def 'Approver can approve and un-approve an entity descriptor'() { + expect: + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() == false + + when: + def result = mockMvc.perform(patch("/api/approve/entityDescriptor/" + defaultEntityDescriptorResourceId + "/approve")) + + then: + result.andExpect(status().isOk()) + .andExpect(jsonPath("\$.id").value(defaultEntityDescriptorResourceId)) + .andExpect(jsonPath("\$.serviceEnabled").value(false)) + .andExpect(jsonPath("\$.approved").value(true)) + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() + + when: + def result2 = mockMvc.perform(patch("/api/approve/entityDescriptor/" + defaultEntityDescriptorResourceId + "/unapprove")) + + then: + result2.andExpect(status().isOk()) + .andExpect(jsonPath("\$.id").value(defaultEntityDescriptorResourceId)) + .andExpect(jsonPath("\$.serviceEnabled").value(false)) + .andExpect(jsonPath("\$.approved").value(false)) + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() == false + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy index 567639f36..9fa91edfc 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy @@ -1,11 +1,10 @@ package edu.internet2.tier.shibboleth.admin.ui.controller - import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle -import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException +import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound import edu.internet2.tier.shibboleth.admin.ui.repository.AttributeBundleRepository import edu.internet2.tier.shibboleth.admin.ui.service.AttributeBundleService import org.springframework.beans.factory.annotation.Autowired diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/BadJSONMetadataSourcesUiDefinitionControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/BadJSONMetadataSourcesUiDefinitionControllerIntegrationTests.groovy index 1bcf387b2..029bc7a53 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/BadJSONMetadataSourcesUiDefinitionControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/BadJSONMetadataSourcesUiDefinitionControllerIntegrationTests.groovy @@ -12,7 +12,7 @@ import org.springframework.core.io.ResourceLoader import org.springframework.test.context.ActiveProfiles import spock.lang.Specification -import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.* +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.JsonSchemaLocationBuilder import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.ALGORITHM_FILTER import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.DYNAMIC_HTTP_METADATA_RESOLVER import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.ENTITY_ATTRIBUTES_FILTERS diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy index d64fa8861..2843711c0 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy @@ -1,183 +1,237 @@ package edu.internet2.tier.shibboleth.admin.ui.controller +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException +import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository -import net.shibboleth.ext.spring.resource.ResourceHelper -import net.shibboleth.utilities.java.support.resolver.CriteriaSet -import org.opensaml.core.criterion.EntityIdCriterion -import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver -import org.opensaml.saml.metadata.resolver.MetadataResolver -import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain -import org.opensaml.saml.metadata.resolver.impl.ResourceBackedMetadataResolver -import org.spockframework.spring.SpringBean +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionService +import edu.internet2.tier.shibboleth.admin.ui.service.EntityService +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator +import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import org.springframework.core.io.ClassPathResource -import org.springframework.test.context.ActiveProfiles -import org.springframework.test.web.reactive.server.WebTestClient -import org.xmlunit.builder.DiffBuilder -import org.xmlunit.builder.Input -import org.xmlunit.diff.DefaultNodeMatcher -import org.xmlunit.diff.ElementSelectors -import spock.lang.Specification - -import java.time.Instant - -/** - * @author Bill Smith (wsmith@unicon.net) - */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@ActiveProfiles("no-auth") -class EntitiesControllerIntegrationTests extends Specification { +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.client.RestTemplate +import spock.lang.Subject +import javax.persistence.EntityManager + +import static org.springframework.http.MediaType.APPLICATION_XML +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +class EntitiesControllerIntegrationTests extends AbstractBaseDataJpaTest { @Autowired - private WebTestClient webClient + EntityDescriptorRepository entityDescriptorRepository - def openSamlObjects = new OpenSamlObjects().with { - init() - it - } + @Autowired + EntityManager entityManager - def resource = ResourceHelper.of(new ClassPathResource("/metadata/aggregate.xml")) + @Autowired + EntityService entityService + + @Autowired + TestObjectGenerator generator + + @Autowired + ObjectMapper mapper + + @Autowired + OpenSamlObjects openSamlObjects + + @Autowired + JPAEntityDescriptorServiceImpl jpaEntityDescriptorService - def metadataResolver = new ResourceBackedMetadataResolver(resource).with { - it.id = 'test' - it.parserPool = openSamlObjects.parserPool - initialize() - it + RandomGenerator randomGenerator + def mockRestTemplate = Mock(RestTemplate) + def mockMvc + + @Subject + def controller + + EntityDescriptorVersionService versionService = Mock() + + @Transactional + def setup() { + openSamlObjects.init() + + Group gb = new Group() + gb.setResourceId("testingGroupBBB") + gb.setName("Group BBB") + gb.setValidationRegex("^(?:https?:\\/\\/)?(?:[^.]+\\.)?shib\\.org(\\/.*)?\$") + gb = groupService.createGroup(gb) + + randomGenerator = new RandomGenerator() + + controller = new EntitiesController() + controller.openSamlObjects = openSamlObjects + controller.entityDescriptorService = jpaEntityDescriptorService + controller.entityDescriptorRepository = entityDescriptorRepository + + mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + + Optional userRole = roleRepository.findByName("ROLE_USER") + User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") + user.setGroup(gb) + userService.save(user) + + EntityDescriptorConversionUtils.setOpenSamlObjects(openSamlObjects) + EntityDescriptorConversionUtils.setEntityService(entityService) + } + + @WithMockAdmin + def 'GET /entities/{resourceId} non-existent'() { + expect: + try { + mockMvc.perform(get("/api/entities/uuid-1")) + } + catch (Exception e) { + e instanceof PersistentEntityNotFound + } } - - // This stub will spit out the results from the resolver instead of actually finding them in the DB - @SpringBean - EntityDescriptorRepository edr = Stub(EntityDescriptorRepository) { - findByEntityID("http://test.scaldingspoon.org/test1") >> metadataResolver.resolveSingle(new CriteriaSet(new EntityIdCriterion("http://test.scaldingspoon.org/test1"))) - findByEntityID("test") >> metadataResolver.resolveSingle(new CriteriaSet(new EntityIdCriterion("test"))) + + @WithMockAdmin + def 'GET /entities/{resourceId} existing'() { + given: + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "admingroup") + entityDescriptorRepository.save(entityDescriptorOne) + entityManager.flush() + entityManager.clear() + + when: + def result = mockMvc.perform(get("/api/entities/eid1")) + + then: + result.andExpect(status().isOk()) + .andExpect(jsonPath("\$.entityId").value("eid1")) + .andExpect(jsonPath("\$.serviceProviderName").value("sp1")) + .andExpect(jsonPath("\$.serviceEnabled").value(true)) + .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) } - //todo review - def "GET /api/entities returns the proper json"() { + @WithMockUser(value = "someUser", roles = ["USER"]) + def 'GET /entities/{resourceId} existing, validate group access'() { given: - def expectedBody = ''' - { - "entityId":"http://test.scaldingspoon.org/test1", - "serviceProviderSsoDescriptor": { - "protocolSupportEnum":"SAML 2", - "nameIdFormats":["urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"] - }, - "assertionConsumerServices":[ - {"locationUrl":"https://test.scaldingspoon.org/test1/acs","binding":"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST","makeDefault":false} - ], - "serviceEnabled":false, - "attributeRelease":["givenName","employeeNumber"] - } - ''' + Group g = userService.getCurrentUserGroup() + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "someUser") + def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) + + entityDescriptorRepository.saveAndFlush(entityDescriptorOne) + entityDescriptorRepository.saveAndFlush(entityDescriptorTwo) + + ownershipRepository.saveAndFlush(new Ownership(g, entityDescriptorOne)) + ownershipRepository.saveAndFlush(new Ownership(Group.ADMIN_GROUP, entityDescriptorTwo)) when: - def result = this.webClient - .get() - .uri("/api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1") - .exchange() // someday, I'd like to know why IntelliJ "cannot resolve symbol 'exchange'" + def result = mockMvc.perform(get("/api/entities/eid1")) then: - result.expectStatus().isOk() - .expectBody() - .json(expectedBody) + result.andExpect(status().isOk()) + .andExpect(jsonPath("\$.entityId").value("eid1")) + .andExpect(jsonPath("\$.serviceProviderName").value("sp1")) + .andExpect(jsonPath("\$.serviceEnabled").value(true)) + .andExpect(jsonPath("\$.idOfOwner").value("someUser")) } - def "GET /api/entities/test is not found"() { + @WithMockUser(value = "someUser", roles = ["USER"]) + def 'GET /entities/{resourceId} existing, owned by some other user'() { when: - def result = this.webClient - .get() - .uri("/api/entities/test") - .exchange() + Group g = userService.getCurrentUserGroup() + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) + def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) + + entityDescriptorRepository.saveAndFlush(entityDescriptorOne) + entityDescriptorRepository.saveAndFlush(entityDescriptorTwo) + + ownershipRepository.saveAndFlush(new Ownership(g, entityDescriptorOne)) + ownershipRepository.saveAndFlush(new Ownership(Group.ADMIN_GROUP, entityDescriptorTwo)) then: - result.expectStatus().isNotFound() + try { + mockMvc.perform(get("/api/entities/eid2")) + } + catch (Exception e) { + e instanceof ForbiddenException + } } - def "GET /api/entities/test XML is not found"() { + @WithMockAdmin + def 'GET /entities/{resourceId} existing (xml)'() { + given: + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true) + entityDescriptorOne.setElementLocalName("EntityDescriptor") + entityDescriptorOne.setNamespacePrefix("md") + entityDescriptorOne.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") + entityDescriptorRepository.save(entityDescriptorOne) + entityManager.flush() + + def expectedXML = """ +""" + when: - def result = this.webClient - .get() - .uri("/api/entities/test") - .header('Accept', 'application/xml') - .exchange() + def result = mockMvc.perform(get("/api/entities/eid1").accept(APPLICATION_XML)) then: - result.expectStatus().isNotFound() + result.andExpect(status().isOk()).andExpect(content().xml(expectedXML)) } - def "GET /api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1 XML returns proper XML"() { + @WithMockUser(value = "someUser", roles = ["USER"]) + def 'GET /entities/{resourceId} existing (xml), user-owned'() { given: - def expectedBody = ''' - - - - - internal - - - givenName - employeeNumber - - - - - urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified - - - -''' + Group g = userService.getCurrentUserGroup() + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) + entityDescriptorOne.setElementLocalName("EntityDescriptor") + entityDescriptorOne.setNamespacePrefix("md") + entityDescriptorOne.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") + entityDescriptorOne = entityDescriptorRepository.saveAndFlush(entityDescriptorOne) + ownershipRepository.saveAndFlush(new Ownership(g,entityDescriptorOne)) + + def expectedXML = """ +""" + when: - def result = this.webClient - .get() - .uri("/api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1") - .header('Accept', 'application/xml') - .exchange() + def result = mockMvc.perform(get("/api/entities/eid1").accept(APPLICATION_XML)) then: - def resultBody = result.expectStatus().isOk() - //.expectHeader().contentType("application/xml;charset=ISO-8859-1") // should this really be ISO-8859-1? - // expectedBody encoding is UTF-8... - .expectHeader().contentType("application/xml;charset=UTF-8") - .expectBody(String.class) - .returnResult() - def diff = DiffBuilder.compare(Input.fromString(expectedBody)).withTest(Input.fromString(resultBody.getResponseBody())).ignoreComments().checkForSimilar().ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)).build() - !diff.hasDifferences() + result.andExpect(status().isOk()).andExpect(content().xml(expectedXML)) } + @WithMockUser(value = "someUser", roles = ["USER"]) + def 'GET /entities/{resourceId} existing (xml), other user-owned'() { + when: + Group g = Group.ADMIN_GROUP + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) + entityDescriptorOne.setElementLocalName("EntityDescriptor") + entityDescriptorOne.setNamespacePrefix("md") + entityDescriptorOne.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") + entityDescriptorRepository.save(entityDescriptorOne) + entityManager.flush() - @TestConfiguration - static class Config { - @Autowired - OpenSamlObjects openSamlObjects - - @Bean - MetadataResolver metadataResolver() { - def resource = ResourceHelper.of(new ClassPathResource("/metadata/aggregate.xml")) - def aggregate = new ResourceBackedMetadataResolver(resource){ - @Override - Instant getLastRefresh() { - return null - } - } - - aggregate.with { - it.metadataFilter = new MetadataFilterChain() - it.id = 'testme' - it.parserPool = openSamlObjects.parserPool - it.initialize() - it - } - - return new ChainingMetadataResolver().with { - it.id = 'chain' - it.resolvers = [aggregate] - it.initialize() - it - } + then: + try { + mockMvc.perform(get("/api/entities/eid1").accept(APPLICATION_XML)) + } + catch (Exception e) { + e instanceof ForbiddenException } } } \ 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 33de12c2f..e5f542e3a 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 @@ -48,7 +48,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.xpath class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { @Autowired @@ -132,6 +131,25 @@ class EntityDescriptorControllerTests extends AbstractBaseDataJpaTest { entityDescriptorRepository.findAll().size() == 0 } + @WithMockUser(value = "someUser", roles = ["USER"]) + def 'DELETE as non-admin'() { + given: + def entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false) + entityDescriptorRepository.save(entityDescriptor) + + when: 'pre-check' + entityManager.flush() + + then: + entityDescriptorRepository.findAll().size() == 1 + try { + result = mockMvc.perform(delete("/api/EntityDescriptor/uuid-1")) + } + catch (Exception e) { + e instanceof ForbiddenException + } + } + @WithMockAdmin def 'GET /EntityDescriptors with empty repository as admin'() { given: 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 index 6462482d0..718621ee6 100644 --- 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 @@ -1,13 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.controller -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.SerializationFeature -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule + import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest -import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects @@ -15,37 +9,18 @@ import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorReposit import edu.internet2.tier.shibboleth.admin.ui.security.model.Group import edu.internet2.tier.shibboleth.admin.ui.security.model.Role import edu.internet2.tier.shibboleth.admin.ui.security.model.User -import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository -import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository -import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceForTesting -import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl -import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionService import edu.internet2.tier.shibboleth.admin.ui.service.EntityService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Primary -import org.springframework.context.annotation.Profile -import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.security.test.context.support.WithMockUser -import org.springframework.test.context.ActiveProfiles -import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.transaction.annotation.Transactional import org.springframework.web.client.RestTemplate -import spock.lang.Specification import spock.lang.Stepwise import spock.lang.Subject -import javax.persistence.EntityManager - import static org.springframework.http.MediaType.APPLICATION_JSON import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorVersionControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorVersionControllerTests.groovy index 0dbb40471..fd8838b4e 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorVersionControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorVersionControllerTests.groovy @@ -43,9 +43,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @ContextConfiguration(classes=[EDCLocalConfig]) class EntityDescriptorVersionControllerTests extends AbstractBaseDataJpaTest { - @Autowired - EntityDescriptorRepository entityDescriptorRepository - @Autowired private TestEntityManager testEntityManager @@ -166,4 +163,4 @@ class EntityDescriptorVersionControllerTests extends AbstractBaseDataJpaTest { return new EnversVersionServiceSupport(entityManager) } } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy index 6b54c7a0d..1107af074 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 @@ -8,9 +8,9 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotF 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.PersistentEntityNotFound 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.exception.PersistentEntityNotFound 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 @@ -96,7 +96,7 @@ class MetadataFiltersControllerTests extends AbstractBaseDataJpaTest { } @Override - MetadataResolver updateMetadataResolverEnabledStatus(MetadataResolver existingResolver) throws ForbiddenException, MetadataFileNotFoundException, InitializationException { + MetadataResolver updateMetadataResolverEnabledStatus(String id, boolean status) throws ForbiddenException, MetadataFileNotFoundException, InitializationException { // This won't get called return null } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersPositionOrderControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersPositionOrderControllerIntegrationTests.groovy index 4d163a660..6e6ec83cb 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersPositionOrderControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersPositionOrderControllerIntegrationTests.groovy @@ -3,15 +3,12 @@ package edu.internet2.tier.shibboleth.admin.ui.controller 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 org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.test.context.ActiveProfiles - import spock.lang.Specification - /** * @author Dmitriy Kopylenko */ diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ShibPropertiesControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ShibPropertiesControllerTests.groovy index 8545362c4..3a96695a7 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ShibPropertiesControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ShibPropertiesControllerTests.groovy @@ -4,8 +4,8 @@ import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.domain.shib.properties.ShibPropertySet import edu.internet2.tier.shibboleth.admin.ui.domain.shib.properties.ShibPropertySetting -import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException +import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound import edu.internet2.tier.shibboleth.admin.ui.repository.ShibPropertySetRepository import edu.internet2.tier.shibboleth.admin.ui.repository.ShibPropertySettingRepository import edu.internet2.tier.shibboleth.admin.ui.service.ShibConfigurationService diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTargetTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTargetTests.groovy index 2ff346ee7..b2b55fcc6 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTargetTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTargetTests.groovy @@ -1,6 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.filters -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget + import spock.lang.Specification /** @@ -33,4 +33,4 @@ class EntityAttributesFilterTargetTests extends Specification { filterTarget.value.size() == 1 filterTarget.value == expectedList } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/PolymorphicFiltersJacksonHandlingTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/PolymorphicFiltersJacksonHandlingTests.groovy index fa9239ec5..5e36544e5 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/PolymorphicFiltersJacksonHandlingTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/PolymorphicFiltersJacksonHandlingTests.groovy @@ -1,7 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.filters 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.util.TestObjectGenerator diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/OAuthRPExtensionsTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/OAuthRPExtensionsTest.groovy index 806d5fa6d..6f5e8d473 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/OAuthRPExtensionsTest.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/OAuthRPExtensionsTest.groovy @@ -4,16 +4,12 @@ import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest 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.service.EntityService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl -import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.json.JacksonTester import org.springframework.context.annotation.PropertySource -import org.springframework.transaction.annotation.Transactional import javax.persistence.EntityManager diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationServiceTests.groovy index f0aaf8aa4..04e8657a4 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationServiceTests.groovy @@ -4,7 +4,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver import spock.lang.Specification import spock.lang.Subject -import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator.* +import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator.ValidationResult /** * @author Dmitriy Kopylenko diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/ResourceBackedMetadataValidatorTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/ResourceBackedMetadataValidatorTests.groovy index 66102f4fe..11cb27938 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/ResourceBackedMetadataValidatorTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/ResourceBackedMetadataValidatorTests.groovy @@ -4,8 +4,6 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ClasspathMetadata import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.SvnMetadataResource -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.ResourceBackedIMetadataResolverValidator import spock.lang.Specification class ResourceBackedMetadataValidatorTests extends Specification { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy index 0db6a9555..46818a442 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy @@ -1,6 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.repository -import com.fasterxml.jackson.databind.MapperFeature + import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/ShibPropertySetRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/ShibPropertySetRepositoryTests.groovy index edcf106d9..fc7fd9501 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/ShibPropertySetRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/ShibPropertySetRepositoryTests.groovy @@ -2,7 +2,6 @@ package edu.internet2.tier.shibboleth.admin.ui.repository import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.domain.shib.properties.ShibPropertySet -import edu.internet2.tier.shibboleth.admin.ui.domain.shib.properties.ShibPropertySetting import org.springframework.beans.factory.annotation.Autowired import javax.persistence.EntityManager diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy index b9e9856dd..f25d67682 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy @@ -28,7 +28,7 @@ class EntityDescriptorFilesScheduledTasksTests extends AbstractBaseDataJpaTest { def directory - def entityDescriptorRepository = Mock(EntityDescriptorRepository) + def entityDescriptorRepo = Mock(EntityDescriptorRepository) def entityDescriptorFilesScheduledTasks @@ -38,7 +38,7 @@ class EntityDescriptorFilesScheduledTasksTests extends AbstractBaseDataJpaTest { randomGenerator = new RandomGenerator() tempPath = tempPath + randomGenerator.randomRangeInt(10000, 20000) EntityDescriptorConversionUtils.setOpenSamlObjects(openSamlObjects) - entityDescriptorFilesScheduledTasks = new EntityDescriptorFilesScheduledTasks(tempPath, entityDescriptorRepository, openSamlObjects, new FileCheckingFileWritingService()) + entityDescriptorFilesScheduledTasks = new EntityDescriptorFilesScheduledTasks(tempPath, entityDescriptorRepo, openSamlObjects, new FileCheckingFileWritingService()) directory = new File(tempPath) directory.mkdir() } @@ -74,7 +74,7 @@ class EntityDescriptorFilesScheduledTasksTests extends AbstractBaseDataJpaTest { } it }) - 1 * entityDescriptorRepository.findAllStreamByServiceEnabled(true) >> [entityDescriptor].stream() + 1 * entityDescriptorRepo.findAllStreamByServiceEnabled(true) >> [entityDescriptor].stream() when: if (directory.exists()) { @@ -107,7 +107,7 @@ class EntityDescriptorFilesScheduledTasksTests extends AbstractBaseDataJpaTest { def file = new File(directory, randomGenerator.randomId() + ".xml") file.text = "Delete me!" - 1 * entityDescriptorRepository.findAllStreamByServiceEnabled(true) >> [entityDescriptor].stream() + 1 * entityDescriptorRepo.findAllStreamByServiceEnabled(true) >> [entityDescriptor].stream() when: entityDescriptorFilesScheduledTasks.removeDanglingEntityDescriptorFiles() diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy index bb4613f6b..49b0b1f19 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy @@ -4,10 +4,13 @@ import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupDeleteException import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException +import edu.internet2.tier.shibboleth.admin.ui.security.model.Approvers 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.service.JPAEntityDescriptorServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin import groovy.json.JsonOutput import org.springframework.beans.factory.annotation.Autowired @@ -17,14 +20,22 @@ import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.transaction.annotation.Transactional -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @Rollback class GroupsControllerIntegrationTests extends AbstractBaseDataJpaTest { @Autowired GroupsRepository groupsRepository + @Autowired + JPAEntityDescriptorServiceImpl service + static RESOURCE_URI = '/api/admin/groups' MockMvc mockMvc @@ -33,6 +44,7 @@ class GroupsControllerIntegrationTests extends AbstractBaseDataJpaTest { def setup() { GroupController groupController = new GroupController().with ({ it.groupService = this.groupService + it.entityDescriptorService = this.service it }) mockMvc = MockMvcBuilders.standaloneSetup(groupController).build() @@ -47,36 +59,65 @@ class GroupsControllerIntegrationTests extends AbstractBaseDataJpaTest { @WithMockAdmin def 'POST new group persists properly'() { given: - def newGroup = [name: 'Foo', - description: 'Bar', - resourceId: 'FooBar'] - - def expectedJson = """ - { - "name":"Foo", - "description":"Bar", - "resourceId":"FooBar" - } -""" + def newGroup = [name: 'Foo', description: 'Bar', resourceId: 'FooBar'] + when: - def result = mockMvc.perform(post(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON) - .content(JsonOutput.toJson(newGroup)).accept(MediaType.APPLICATION_JSON)) + def result = mockMvc.perform(post(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON).content(JsonOutput.toJson(newGroup)).accept(MediaType.APPLICATION_JSON)) then: result.andExpect(status().isCreated()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(expectedJson, false)) - + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("\$.name").value("Foo")) + .andExpect(jsonPath("\$.resourceId").value("FooBar")) + .andExpect(jsonPath("\$.description").value("Bar")) - //'Try to create with an existing resource id' + //'Try to create with an existing resource id' try { mockMvc.perform(post(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON) .content(JsonOutput.toJson(newGroup)) .accept(MediaType.APPLICATION_JSON)) - false + false // failure if the call didn't throw } catch (Throwable expected) { expected instanceof GroupExistsConflictException } + + when: "POST new group with approvers" + groupService.clearAllForTesting() + String[] groupNames = ['AAA', 'BBB', 'CCC', 'DDD'] + groupNames.each {name -> { + Group group = new Group().with({ + it.name = name + it.description = name + it.resourceId = name + it + }) + groupRepository.save(group) + }} + entityManager.flush() + entityManager.clear() + + List apprGroups = new ArrayList<>() + groupNames.each {name ->{ + if (!name.equals('AAA')) { + apprGroups.add(name) + } + }} + Approvers approvers = new Approvers() + approvers.setApproverGroupIds(apprGroups) + def apprList = new ArrayList<>() + apprList.add(approvers) + def newGroup2 = [name: 'Foo', description: 'Bar', resourceId: 'FooBar', approversList: apprList] + def result2 = mockMvc.perform(post(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON).content(JsonOutput.toJson(newGroup2)).accept(MediaType.APPLICATION_JSON)) + + then: + result2.andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("\$.name").value("Foo")) + .andExpect(jsonPath("\$.resourceId").value("FooBar")) + .andExpect(jsonPath("\$.description").value("Bar")) + .andExpect(jsonPath("\$.approversList[0].approverGroupIds[0]").value("BBB")) + .andExpect(jsonPath("\$.approversList[0].approverGroupIds[1]").value("CCC")) + .andExpect(jsonPath("\$.approversList[0].approverGroupIds[2]").value("DDD")) } @WithMockAdmin @@ -94,8 +135,7 @@ class GroupsControllerIntegrationTests extends AbstractBaseDataJpaTest { groupAAA.setName("NOT AAA") when: - def result = mockMvc.perform(put(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON) - .content(JsonOutput.toJson(groupAAA)).accept(MediaType.APPLICATION_JSON)) + def result = mockMvc.perform(put(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON).content(JsonOutput.toJson(groupAAA)).accept(MediaType.APPLICATION_JSON)) then: result.andExpect(status().isOk()) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/permission/ShibUiPermissionDelegateTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/permission/ShibUiPermissionDelegateTests.groovy new file mode 100644 index 000000000..f3c3ab8fd --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/permission/ShibUiPermissionDelegateTests.groovy @@ -0,0 +1,202 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.permission + +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorProjection +import edu.internet2.tier.shibboleth.admin.ui.security.model.Approvers +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.annotation.Rollback +import org.springframework.transaction.annotation.Transactional + +@Rollback +class ShibUiPermissionDelegateTests extends AbstractBaseDataJpaTest { + ShibUiPermissionDelegate delegate + + @Autowired + JPAEntityDescriptorServiceImpl jpaEntityDescriptorService + + def entityDescriptor + def entityDescriptor2 + def entityDescriptor3 + + @Transactional + def setup() { + delegate = new ShibUiPermissionDelegate(entityDescriptorRepository, userService) + createDevUsersAndGroups() + } + + def createDevUsersAndGroups() { + def groups = [ + new Group().with { + it.name = "A1" + it.description = "AAA Group" + it.resourceId = "AAA" + it + }, + new Group().with { + it.name = "B1" + it.description = "BBB Group" + it.resourceId = "BBB" + it + }] + groups.each { + try { + groupRepository.save(it) + } catch (Throwable e) { + // Must already exist (from a unit test) + } + } + groupRepository.flush() + + List apprGroups = new ArrayList<>() + String[] groupNames = ['XXX', 'YYY', 'ZZZ'] + groupNames.each {name -> { + Group group = new Group().with({ + it.name = name + it.description = name + it.resourceId = name + it + }) + if (name != "ZZZ") { + apprGroups.add(groupRepository.save(group)) + } else { + Approvers approvers = new Approvers() + approvers.setApproverGroups(apprGroups) + List apprList = new ArrayList<>() + apprList.add(approversRepository.save(approvers)) + group.setApproversList(apprList) + groupRepository.save(group) + } + }} + groupRepository.flush() + + if (roleRepository.count() == 0) { + def roles = [new Role().with { + name = 'ROLE_ADMIN' + it + }, new Role().with { + name = 'ROLE_USER' + it + }, new Role().with { + name = 'ROLE_NONE' + it + }, new Role().with { + name = 'ROLE_ENABLE' + it + }] + roles.each { + roleRepository.save(it) + } + } + roleRepository.flush() + if (userRepository.count() < 2) { + userRepository.deleteAll() + def users = [new User().with { + username = 'admin' + password = '{noop}adminpass' + firstName = 'Joe' + lastName = 'Doe' + emailAddress = 'joe@institution.edu' + roles.add(roleRepository.findByName('ROLE_ADMIN').get()) + it + }, new User().with { + username = 'enableZ' + password = '{noop}nonadminpass' + firstName = 'Peter' + lastName = 'Vandelay' + emailAddress = 'peter@institution.edu' + setGroupId('ZZZ') + roles.add(roleRepository.findByName('ROLE_ENABLE').get()) + it + }, new User().with { + username = 'Approver' + password = '{noop}password' + firstName = 'Bad' + lastName = 'robot' + emailAddress = 'badboy@institution.edu' + setGroupId('XXX') + roles.add(roleRepository.findByName('ROLE_USER').get()) + it + }, new User().with { + username = 'Submitter' + password = '{noop}password' + firstName = 'Bad' + lastName = 'robot2' + emailAddress = 'badboy2@institution.edu' + setGroupId('ZZZ') + roles.add(roleRepository.findByName('ROLE_NONE').get()) + it + }] + users.each { + userService.save(it) + } + } + entityManager.flush() + entityManager.clear() + + entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false, idOfOwner: 'ZZZ') + def edid = jpaEntityDescriptorService.createNew(entityDescriptor).getId() + entityManager.flush() + entityDescriptor2 = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: 'XXX') + def edid2 = jpaEntityDescriptorService.createNew(entityDescriptor2).getId() + entityManager.flush() + entityDescriptor3 = new EntityDescriptor(resourceId: 'uuid-3', entityID: 'eid3', serviceProviderName: 'sp3', serviceEnabled: false, idOfOwner: 'YYY') + def edid3 = jpaEntityDescriptorService.createNew(entityDescriptor3).getId() + entityManager.flush() + + jpaEntityDescriptorService.updateGroupForEntityDescriptor(edid, 'ZZZ') + jpaEntityDescriptorService.updateGroupForEntityDescriptor(edid2, 'XXX') + jpaEntityDescriptorService.updateGroupForEntityDescriptor(edid3, 'YYY') + entityManager.flush() + } + + @WithMockAdmin + def testAdmin() { + expect: + delegate.hasPermission(userService.getCurrentUserAuthentication(), "doesn't matter", PermissionType.admin) + delegate.hasPermission(null, "doesn't matter", PermissionType.admin) + } + + @WithMockUser(username = "Approver", roles = ["USER"]) + def testApproverPerms() { + expect: + userRepository.findAll().size() == 4 + !delegate.hasPermission(null, "doesn't matter", PermissionType.admin) + !delegate.hasPermission(null, entityDescriptor, PermissionType.enable) + !delegate.hasPermission(null, entityDescriptor2, PermissionType.enable) + !delegate.hasPermission(null, entityDescriptor3, PermissionType.enable) + + delegate.hasPermission(null, entityDescriptor, PermissionType.approve) + !delegate.hasPermission(null, entityDescriptor2, PermissionType.approve) + !delegate.hasPermission(null, entityDescriptor3, PermissionType.approve) + + delegate.hasPermission(null, entityDescriptor, PermissionType.viewOrEdit) + delegate.hasPermission(null, entityDescriptor2, PermissionType.viewOrEdit) + !delegate.hasPermission(null, entityDescriptor3, PermissionType.viewOrEdit) + + when: + def Collection fetch = delegate.getPersistentEntities(null, ShibUiPermissibleType.entityDescriptorProjection, PermissionType.fetch) + def Collection approve = delegate.getPersistentEntities(null, ShibUiPermissibleType.entityDescriptorProjection, PermissionType.approve) + + then: + fetch.size() == 1 + ((EntityDescriptorProjection)fetch.iterator().next()).getEntityID().equals("eid2") + + approve.size() == 1 + ((EntityDescriptorProjection)approve.iterator().next()).getEntityID().equals("eid1") + + when: + delegate.getPersistentEntities(null, ShibUiPermissibleType.entityDescriptorProjection, PermissionType.enable) + + then: + thrown (ForbiddenException) + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepositoryTests.groovy index a81443166..92e69aa8c 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepositoryTests.groovy @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.security.repository import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.security.model.Approvers import edu.internet2.tier.shibboleth.admin.ui.security.model.Group import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership import org.springframework.beans.factory.annotation.Autowired @@ -8,8 +9,6 @@ import org.springframework.dao.DataIntegrityViolationException import org.springframework.test.annotation.Rollback import org.springframework.transaction.annotation.Transactional -import javax.persistence.EntityManager - /** * Tests to validate the repo and model for groups * @author chasegawa @@ -176,11 +175,11 @@ class GroupsRepositoryTests extends AbstractBaseDataJpaTest { def groupFromDb = gList.get(0) as Group groupFromDb == group - // update check + // update check - equality for groups should be by id groupFromDb.with { it.description = "some new text that wasn't there before" } - groupFromDb.equals(group) == false + groupFromDb.equals(group) == true when: groupsRepo.save(groupFromDb) @@ -189,7 +188,7 @@ class GroupsRepositoryTests extends AbstractBaseDataJpaTest { def gList2 = groupsRepo.findAll() gList2.size() == 1 def groupFromDb2 = gList2.get(0) as Group - groupFromDb2.equals(group) == false + groupFromDb2.equals(group) == true groupFromDb2 == groupFromDb // delete tests @@ -205,4 +204,52 @@ class GroupsRepositoryTests extends AbstractBaseDataJpaTest { then: nothingThere == null } + + def "get list of groups that a group can approve for"() { + when: + groupService.clearAllForTesting() + List apprGroups = new ArrayList<>() + String[] groupNames = ['BBB', 'CCC', 'EEE', 'AAA'] + groupNames.each {name -> { + Group group = new Group().with({ + it.name = name + it.description = name + it.resourceId = name + it + }) + if (name != "AAA") { + apprGroups.add(groupRepository.save(group)) + } else { + Approvers approvers = new Approvers() + approvers.setApproverGroups(apprGroups) + List apprList = new ArrayList<>() + apprList.add(approversRepository.save(approvers)) + group.setApproversList(apprList) + groupRepository.save(group) + } + }} + Group group = new Group().with({ + it.name = 'DDD' + it.description = 'DDD' + it.resourceId = 'DDD' + it + }) + Approvers approvers = new Approvers() + apprGroups = new ArrayList<>() + apprGroups.add(groupRepository.findByResourceId('BBB')) + approvers.setApproverGroups(apprGroups) + List apprList = new ArrayList<>() + apprList.add(approversRepository.save(approvers)) + group.setApproversList(apprList) + groupRepository.save(group) + entityManager.flush() + entityManager.clear() + + then: + def result = groupRepository.getGroupIdsOfGroupsToApproveFor('BBB') + result.size() == 2 + result.contains('AAA') + result.contains('DDD') + groupRepository.findAllGroupIds().size() == 6 + } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceForTesting.groovy index a3f223efb..6e2243e67 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 @@ -3,8 +3,6 @@ package edu.internet2.tier.shibboleth.admin.ui.security.service import org.springframework.context.annotation.Profile import org.springframework.transaction.annotation.Transactional -import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl - @Profile('test') class GroupServiceForTesting extends GroupServiceImpl { GroupServiceForTesting(GroupServiceImpl impl) { @@ -14,6 +12,7 @@ class GroupServiceForTesting extends GroupServiceImpl { @Transactional void clearAllForTesting() { + approversRepository.deleteAll() groupRepository.deleteAll() ownershipRepository.clearAllOwnedByGroup() ensureAdminGroupExists() diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy index c88838875..6da1ff232 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy @@ -2,6 +2,7 @@ package edu.internet2.tier.shibboleth.admin.ui.security.service import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException +import edu.internet2.tier.shibboleth.admin.ui.security.model.Approvers import edu.internet2.tier.shibboleth.admin.ui.security.model.Group import org.springframework.test.annotation.Rollback @@ -80,4 +81,69 @@ class GroupServiceTests extends AbstractBaseDataJpaTest { !groupService.doesStringMatchGroupPattern("AAA", "something") groupService.doesStringMatchGroupPattern("AAA", "/foobar/") } + + def "CRUD operations - approver groups" () { + given: + groupService.clearAllForTesting() + List apprGroups = new ArrayList<>() + String[] groupNames = ['AAA', 'BBB', 'CCC', 'DDD'] + groupNames.each {name -> { + Group group = new Group().with({ + it.name = name + it.description = name + it.resourceId = name + it + }) + groupRepository.save(group) + }} + entityManager.flush() + entityManager.clear() + + when: "Adding approval list to a group" + groupNames.each {name ->{ + if (!name.equals('AAA')) { + apprGroups.add(name) + } + }} + Approvers approvers = new Approvers() + approvers.setApproverGroupIds(apprGroups) + List apprList = new ArrayList<>() + apprList.add(approvers) + Group aaaGroup = groupService.find('AAA') + aaaGroup.setApproversList(apprList) + groupService.updateGroup(aaaGroup) + + then: + def lookupGroup = groupService.find('AAA') + lookupGroup.getApproversList().size() == 1 + def approvalGroups = lookupGroup.getApproversList().get(0).getApproverGroups() + approvalGroups.size() == 3 + approvalGroups.each {group -> { + assert apprGroups.contains(group.getResourceId())} + } + + when: "removing approver group from existing list" + approvers.setApproverGroupIds(Arrays.asList("CCC", "DDD")) + apprList = new ArrayList<>() + apprList.add(approvers) + aaaGroup.setApproversList(apprList) + groupService.updateGroup(aaaGroup) + + then: + def lookupGroup2 = groupService.find('AAA') + lookupGroup2.getApproversList().size() == 1 + def approvalGroups2 = lookupGroup2.getApproversList().get(0).getApproverGroups() + approvalGroups2.size() == 2 + approvalGroups2.forEach(group -> group.getResourceId().equals("CCC") || group.getResourceId().equals("DDD")) + + when: "removing all approver groups" + apprList = new ArrayList<>() + aaaGroup.setApproversList(apprList) + groupService.updateGroup(aaaGroup) + + then: + def lookupGroup3 = groupService.find('AAA') + lookupGroup3.getApproversList().isEmpty() == true + } + } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy index d40e2bd1c..364fd14e1 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy @@ -5,6 +5,7 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.DevConfig import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.ApproversRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository @@ -13,9 +14,7 @@ import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.ComponentScan import org.springframework.security.core.userdetails.UsernameNotFoundException -import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration /** @@ -74,9 +73,10 @@ class AdminUserServiceTests extends AbstractBaseDataJpaTest { @Bean DevConfig devConfig(UserRepository adminUserRepository, GroupsRepository groupsRepository, IGroupService groupService, MetadataResolverRepository metadataResolverRepository, OpenSamlObjects openSamlObjects, UserService userService, - RoleRepository roleRepository, EntityDescriptorRepository entityDescriptorRepository) { + RoleRepository roleRepository, EntityDescriptorRepository entityDescriptorRepository, + ApproversRepository approversRepository) { DevConfig dc = new DevConfig( adminUserRepository, groupsRepository, metadataResolverRepository, roleRepository, - entityDescriptorRepository, openSamlObjects, groupService).with { + entityDescriptorRepository, openSamlObjects, groupService, approversRepository).with { it.userService = userService it } 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 465bd4186..61fc89acd 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,10 +3,10 @@ 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.JsonSchemaResourceLocation 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.service.IGroupService import org.springframework.core.io.DefaultResourceLoader import org.springframework.core.io.ResourceLoader import org.springframework.mock.http.MockHttpInputMessage @@ -26,12 +26,17 @@ class AuxiliaryIntegrationTests extends Specification { JPAEntityDescriptorServiceImpl entityDescriptorService ObjectMapper objectMapper ResourceLoader resourceLoader + IGroupService mockGroupService = Stub() { + getApproversList() >> new ArrayList<>() + } void setup() { entityDescriptorService = new JPAEntityDescriptorServiceImpl() + entityDescriptorService.groupService = mockGroupService entityDescriptorService.openSamlObjects = openSamlObjects objectMapper = new ObjectMapper() resourceLoader = new DefaultResourceLoader() + mockGroupService } def "SHIBUI-1723: after enabling saved entity descriptor, it should still have valid xml"() { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EmailServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EmailServiceImplTests.groovy index 148def965..f99b5c1d6 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EmailServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EmailServiceImplTests.groovy @@ -2,10 +2,10 @@ package edu.internet2.tier.shibboleth.admin.ui.service import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.DevConfig -import edu.internet2.tier.shibboleth.admin.ui.configuration.auto.EmailConfiguration 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.configuration.auto.EmailConfiguration import groovy.json.JsonOutput import groovy.json.JsonSlurper import org.springframework.beans.factory.annotation.Autowired @@ -66,4 +66,4 @@ class EmailServiceImplTests extends Specification { expectedNewUserEmailSubject == resultJson.items[0].Content.Headers.Subject[0] resultJson.items[0].Content.Body.contains(expectedTextEmailBody) } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchServiceTests.groovy index a36e7c4ae..e51dd33a0 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchServiceTests.groovy @@ -1,9 +1,9 @@ package edu.internet2.tier.shibboleth.admin.ui.service -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy index 14ad669c5..e1a8f4f59 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy @@ -2,6 +2,7 @@ package edu.internet2.tier.shibboleth.admin.ui.service import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +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.domain.EntityDescriptorProtocol import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation @@ -14,7 +15,14 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepres import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSsoDescriptorRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.OAuthRPExtensions +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation +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.repository.EntityDescriptorRepository +import edu.internet2.tier.shibboleth.admin.ui.security.model.Approvers +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.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils @@ -22,12 +30,21 @@ import org.skyscreamer.jsonassert.JSONAssert import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.json.JacksonTester import org.springframework.context.annotation.PropertySource +import org.springframework.core.io.DefaultResourceLoader +import org.springframework.mock.http.MockHttpInputMessage +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.transaction.annotation.Transactional import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input import org.xmlunit.diff.DefaultNodeMatcher import org.xmlunit.diff.ElementSelectors import spock.lang.Ignore +import java.time.LocalDateTime + +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.metadataSourcesOIDCSchema +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.metadataSourcesSAMLSchema + @PropertySource("classpath:application.yml") class JPAEntityDescriptorServiceImplTests extends AbstractBaseDataJpaTest { @Autowired @@ -48,14 +65,64 @@ class JPAEntityDescriptorServiceImplTests extends AbstractBaseDataJpaTest { RandomGenerator generator JacksonTester jacksonTester - def setup() { + @Autowired + EntityDescriptorRepository entityDescriptorRepository + + @Transactional + def setup() { JacksonTester.initFields(this, mapper) generator = new RandomGenerator() EntityDescriptorConversionUtils.openSamlObjects = openSamlObjects EntityDescriptorConversionUtils.entityService = entityService openSamlObjects.init() + + groupService.clearAllForTesting() + List apprGroups = new ArrayList<>() + String[] groupNames = ['BBB', 'CCC', 'EEE', 'AAA'] + groupNames.each {name -> { + Group group = new Group().with({ + it.name = name + it.description = name + it.resourceId = name + it + }) + if (name != "AAA") { + apprGroups.add(groupRepository.save(group)) + } else { + Approvers approvers = new Approvers() + approvers.setApproverGroups(apprGroups) + List apprList = new ArrayList<>() + apprList.add(approversRepository.save(approvers)) + group.setApproversList(apprList) + groupRepository.save(group) + } + }} + Group group = new Group().with({ + it.name = 'DDD' + it.description = 'DDD' + it.resourceId = 'DDD' + it + }) + Approvers approvers = new Approvers() + apprGroups = new ArrayList<>() + apprGroups.add(groupRepository.findByResourceId('BBB')) + approvers.setApproverGroups(apprGroups) + List apprList = new ArrayList<>() + apprList.add(approversRepository.save(approvers)) + group.setApproversList(apprList) + groupRepository.save(group) + + Optional userRole = roleRepository.findByName("ROLE_USER") + User user = new User(username: "bbUser", roles:[userRole.get()], password: "foo") + user.setGroup(groupRepository.findByResourceId("BBB")) + userService.save(user) + + entityManager.flush() + entityManager.clear() } + + def "simple Entity Descriptor"() { when: def expected = ''' schemaLocations = new HashMap<>() + def jsonSchemaResourceLocationRegistry = new JsonSchemaComponentsConfiguration().jsonSchemaResourceLocationRegistry(new DefaultResourceLoader(), this.mapper) + schemaLocations.put("SAML", metadataSourcesSAMLSchema(jsonSchemaResourceLocationRegistry)) + schemaLocations.put("OIDC", metadataSourcesOIDCSchema(jsonSchemaResourceLocationRegistry)) + + when: + LowLevelJsonSchemaValidator.validateMetadataSourcePayloadAgainstSchema(new MockHttpInputMessage(json.bytes), schemaLocations) + + then: + noExceptionThrown() + } + + @WithMockUser(value = "bbUser", roles = ["USER"]) + def "get list of entity descriptors that a group can approve"() { + when: + def ed = openSamlObjects.unmarshalFromXml(this.class.getResource('/metadata/SHIBUI-553.2.xml').bytes) as EntityDescriptor + ed.with { + it.idOfOwner = 'AAA' // BBB can approve + it.enabled = false + it + } + entityDescriptorRepository.save(ed) + def ed2 = openSamlObjects.unmarshalFromXml(this.class.getResource('/metadata/SHIBUI-1772.xml').bytes) as EntityDescriptor + ed2.with { + it.idOfOwner = 'CCC' // BBB can not approve + it.enabled = false + it + } + entityDescriptorRepository.save(ed2) + + entityManager.flush() + entityManager.clear() + + then: + def result = service.getAllEntityDescriptorProjectionsNeedingApprovalBasedOnUserAccess() + result.size() == 1 + + when: + service.changeApproveStatusOfEntityDescriptor(result.get(0).getResourceId(), true); + + then: + def result2 = service.getAllEntityDescriptorProjectionsNeedingApprovalBasedOnUserAccess() + result2.isEmpty() == true + } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImplTests.groovy index d60792b22..51401e187 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImplTests.groovy @@ -3,7 +3,6 @@ package edu.internet2.tier.shibboleth.admin.ui.service import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation -import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterTargetServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterTargetServiceImplTests.groovy index 376e23732..dd2587725 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterTargetServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterTargetServiceImplTests.groovy @@ -4,7 +4,6 @@ import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import org.springframework.beans.factory.annotation.Autowired -import spock.lang.Specification /** * @author Bill Smith (wsmith@unicon.net) 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 f5f37da1f..a270c9519 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 @@ -7,11 +7,11 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.AlgorithmDigestMethod import edu.internet2.tier.shibboleth.admin.ui.domain.EncryptionMethod import edu.internet2.tier.shibboleth.admin.ui.domain.SignatureDigestMethod import edu.internet2.tier.shibboleth.admin.ui.domain.XSString +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.AlgorithmFilterTarget 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.filters.AlgorithmFilterTarget 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 diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/TokenPlaceholderValueResolvingServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/TokenPlaceholderValueResolvingServiceTests.groovy index 8e3ba09f7..69fd1a81b 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/TokenPlaceholderValueResolvingServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/TokenPlaceholderValueResolvingServiceTests.groovy @@ -5,7 +5,6 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.util.TestPropertyValues import org.springframework.core.env.ConfigurableEnvironment import org.springframework.test.context.ContextConfiguration - import spock.lang.Specification import spock.lang.Subject @@ -78,4 +77,4 @@ class TokenPlaceholderValueResolvingServiceTests extends Specification { then: 'Correct combined property values resolution is performed' combinedValue == "$IDP_HOME AND $REFRESH_INTERVAL" } -} +} \ 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 204ffaf52..e1038f2e0 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,6 +1,5 @@ package edu.internet2.tier.shibboleth.admin.ui.util - 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 @@ -9,7 +8,6 @@ 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 diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy index 972120ab4..bfdf5ee63 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy @@ -8,6 +8,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.LocalizedName import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationDisplayName import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationName import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationURL +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.AlgorithmFilter 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.EntityAttributesFilterTarget.EntityAttributesFilterTargetType @@ -17,7 +18,6 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilterTarget import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.SignatureValidationFilter -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.AlgorithmFilter import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterTargetRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ClasspathMetadataResource 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 cc5ce8e25..afc7ae437 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 @@ -64,7 +64,7 @@ public Pac4jWebSecurityConfigurerAdapter(final Config config, UserService userSe @Override protected void configure(HttpSecurity http) throws Exception { - http.authorizeRequests().antMatchers("/unsecured/**/*").permitAll(); + http.authorizeRequests().antMatchers("/unsecured/**/*","/entities/**/*").permitAll(); // adding the authorizer bypasses the default behavior of checking CSRF in Pac4J's default securitylogic+defaultauthorizationchecker final SecurityFilter securityFilter = new SecurityFilter(this.config, PAC4J_CLIENT_NAME, DefaultAuthorizers.IS_AUTHENTICATED); @@ -116,10 +116,11 @@ public void configure(org.springframework.security.config.annotation.web.builder StrictHttpFirewall firewall = new StrictHttpFirewall(); firewall.setAllowUrlEncodedSlash(true); firewall.setAllowUrlEncodedDoubleSlash(true); + firewall.setAllowSemicolon(true); web.httpFirewall(firewall); // These don't need to be secured - web.ignoring().antMatchers("/favicon.ico", "/unsecured/**/*", "/assets/**/*.png", "/static/**/*", "/**/*.css"); + web.ignoring().antMatchers("/favicon.ico", "/unsecured/**/*", "/assets/**/*.png", "/static/**/*", "/**/*.css", "/entities/**/*"); } } 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 index c2eb94ca7..9969741ff 100644 --- a/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/Pac4JTestingConfig.groovy +++ b/pac4j-module/src/test/groovy/net/unicon/shibui/pac4j/Pac4JTestingConfig.groovy @@ -31,9 +31,9 @@ class Pac4JTestingConfig { @Bean @Primary - GroupUpdatedEntityListener groupUpdatedEntityListener(OwnershipRepository repo) { + GroupUpdatedEntityListener groupUpdatedEntityListener(OwnershipRepository repo, GroupsRepository groupsRepository) { GroupUpdatedEntityListener listener = new GroupUpdatedEntityListener() - listener.init(repo) + listener.init(repo, groupsRepository) return listener } diff --git a/ui/public/assets/schema/groups/group.json b/ui/public/assets/schema/groups/group.json index 518293851..200ce4529 100644 --- a/ui/public/assets/schema/groups/group.json +++ b/ui/public/assets/schema/groups/group.json @@ -22,6 +22,14 @@ "title": "label.url-validation-regex", "description": "tooltip.url-validation-regex", "type": "string" + }, + "approversList": { + "title": "label.approvers-list", + "description": "tooltip.approvers-list", + "type": "array", + "items": { + "type": "string" + } } } } \ No newline at end of file diff --git a/ui/public/assets/schema/source/metadata-source-saml.json b/ui/public/assets/schema/source/metadata-source-saml.json index 005c2830b..6d6c99985 100644 --- a/ui/public/assets/schema/source/metadata-source-saml.json +++ b/ui/public/assets/schema/source/metadata-source-saml.json @@ -1,7 +1,15 @@ { "type": "object", - "required": ["serviceProviderName", "entityId"], + "required": [ + "serviceProviderName", + "entityId" + ], "properties": { + "approved": { + "title": "label.approved", + "description": "tooltip.approved", + "type": "boolean" + }, "protocol": { "title": "label.source-protocol", "description": "tooltip.source-protocol", @@ -26,17 +34,25 @@ "description": "tooltip.enable-this-service-upon-saving", "type": "boolean" }, - "organization": { "$ref": "#/definitions/Organization" }, + "organization": { + "$ref": "#/definitions/Organization" + }, "contacts": { "title": "label.contact-information", "description": "tooltip.contact-information", "type": "array", - "items": { "$ref": "#/definitions/Contact" } + "items": { + "$ref": "#/definitions/Contact" + } + }, + "mdui": { + "$ref": "#/definitions/MDUI" }, - "mdui": { "$ref": "#/definitions/MDUI" }, "securityInfo": { "type": "object", - "widget": { "id": "fieldset" }, + "widget": { + "id": "fieldset" + }, "dependencies": { "authenticationRequestsSigned": { "oneOf": [ @@ -45,7 +61,9 @@ "authenticationRequestsSigned": { "enum": [true] }, - "x509Certificates": { "minItems": 1 } + "x509Certificates": { + "minItems": 1 + } } }, { @@ -53,7 +71,9 @@ "authenticationRequestsSigned": { "enum": [false] }, - "x509Certificates": { "minItems": 0 } + "x509Certificates": { + "minItems": 0 + } } } ] @@ -64,19 +84,27 @@ "title": "label.authentication-requests-signed", "description": "tooltip.authentication-requests-signed", "type": "boolean", - "enumNames": ["value.true", "value.false"] + "enumNames": [ + "value.true", + "value.false" + ] }, "wantAssertionsSigned": { "title": "label.want-assertions-signed", "description": "tooltip.want-assertions-signed", "type": "boolean", - "enumNames": ["value.true", "value.false"] + "enumNames": [ + "value.true", + "value.false" + ] }, "keyDescriptors": { "title": "label.key-descriptors", "description": "tooltip.key-descriptors", "type": "array", - "items": { "$ref": "#/definitions/Certificate" } + "items": { + "$ref": "#/definitions/Certificate" + } } } }, @@ -84,7 +112,9 @@ "title": "label.assertion-consumer-service-endpoints", "description": "tooltip.assertion-consumer-service-endpoints", "type": "array", - "items": { "$ref": "#/definitions/AssertionConsumerService" } + "items": { + "$ref": "#/definitions/AssertionConsumerService" + } }, "serviceProviderSsoDescriptor": { "type": "object", @@ -93,20 +123,36 @@ "title": "label.protocol-support-enumeration", "description": "tooltip.protocol-support-enumeration", "type": "string", - "widget": { "id": "select" }, + "widget": { + "id": "select" + }, "oneOf": [ - { "enum": ["SAML 2"], "description": "SAML 2" }, - { "enum": ["SAML 1.1"], "description": "SAML 1.1" } + { + "enum": [ + "SAML 2" + ], + "description": "SAML 2" + }, + { + "enum": [ + "SAML 1.1" + ], + "description": "SAML 1.1" + } ] }, - "nameIdFormats": { "$ref": "#/definitions/nameIdFormats" } + "nameIdFormats": { + "$ref": "#/definitions/nameIdFormats" + } } }, "logoutEndpoints": { "title": "label.logout-endpoints", "description": "tooltip.logout-endpoints", "type": "array", - "items": { "$ref": "#/definitions/LogoutEndpoint" } + "items": { + "$ref": "#/definitions/LogoutEndpoint" + } }, "relyingPartyOverrides": { "type": "object", @@ -211,7 +257,11 @@ "definitions": { "Contact": { "type": "object", - "required": ["name", "type", "emailAddress"], + "required": [ + "name", + "type", + "emailAddress" + ], "properties": { "name": { "title": "label.contact-name", @@ -233,7 +283,9 @@ "description": "value.technical" }, { - "enum": ["administrative"], + "enum": [ + "administrative" + ], "description": "value.administrative" }, { "enum": ["other"], "description": "value.other" } @@ -251,7 +303,10 @@ }, "Certificate": { "type": "object", - "required": ["type", "value", "elementType"], + "required": [ + "type", + "value" + ], "properties": { "name": { "title": "label.certificate-name-display-only", @@ -268,7 +323,10 @@ "type": { "title": "label.certificate-type", "type": "string", - "widget": { "id": "radio", "class": "form-check-inline" }, + "widget": { + "id": "radio", + "class": "form-check-inline" + }, "oneOf": [ { "enum": ["signing"], "description": "value.signing" }, { @@ -289,13 +347,19 @@ }, "AssertionConsumerService": { "type": "object", - "required": ["locationUrl", "binding"], + "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" }, + "widget": { + "id": "string", + "help": "message.valid-url" + }, "minLength": 1, "maxLength": 255 }, @@ -405,11 +469,17 @@ }, "MDUI": { "type": "object", - "widget": { "id": "fieldset" }, + "widget": { + "id": "fieldset" + }, "fieldsets": [ { "type": "group", - "fields": ["displayName", "informationUrl", "description"] + "fields": [ + "displayName", + "informationUrl", + "description" + ] }, { "type": "group", @@ -447,7 +517,9 @@ "title": "label.description", "description": "tooltip.mdui-description", "type": "string", - "widget": { "id": "textarea" }, + "widget": { + "id": "textarea" + }, "minLength": 1, "maxLength": 255 }, diff --git a/ui/src/app/App.constant.js b/ui/src/app/App.constant.js index afa7a688c..ed133f11d 100644 --- a/ui/src/app/App.constant.js +++ b/ui/src/app/App.constant.js @@ -16,7 +16,7 @@ export const getActuatorPath = () => { export const BASE_PATH = getBasePath(); export const API_BASE_PATH = `${BASE_PATH}api`; -export const ACTUATOR_PATH = getActuatorPath(); +export const ACTUATOR_PATH = getBasePath(); export const FILTER_PLUGIN_TYPES = ['RequiredValidUntil', 'SignatureValidation', 'EntityRoleWhiteList']; diff --git a/ui/src/app/admin/Groups.js b/ui/src/app/admin/Groups.js index 8326f9cd1..0e2e062c5 100644 --- a/ui/src/app/admin/Groups.js +++ b/ui/src/app/admin/Groups.js @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import { Switch, Route, useRouteMatch, Redirect } from 'react-router-dom'; import { GroupsProvider } from './hoc/GroupsProvider'; import { NewGroup } from './container/NewGroup'; import { EditGroup } from './container/EditGroup'; import { GroupsList } from './container/GroupsList'; +import Spinner from '../core/components/Spinner'; export function Groups() { @@ -20,10 +21,18 @@ export function Groups() { } /> - + + {(groups, onDelete, loading) => + { loading ?
: }
+ } +
} /> - + + {(groups, onDelete, loading) => + { loading ?
: }
+ } +
} /> diff --git a/ui/src/app/admin/component/GroupForm.js b/ui/src/app/admin/component/GroupForm.js index d4390d6d8..1490091bc 100644 --- a/ui/src/app/admin/component/GroupForm.js +++ b/ui/src/app/admin/component/GroupForm.js @@ -4,11 +4,12 @@ import Form from '../../form/Form'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSpinner, faSave } from '@fortawesome/free-solid-svg-icons'; import Translate from '../../i18n/components/translate'; +import set from 'lodash/set'; -import { useGroupUiSchema, useGroupUiValidator } from '../hooks'; +import { useGroupUiSchema, useGroupUiValidator, useGroupParser, useGroupFormatter} from '../hooks'; import { FormContext, setFormDataAction, setFormErrorAction } from '../../form/FormManager'; -export function GroupForm ({group = {}, errors = [], loading = false, schema, onSave, onCancel}) { +export function GroupForm ({group = {}, errors = [], context = {}, loading = false, schema, onSave, onCancel}) { const { dispatch } = React.useContext(FormContext); const onChange = ({formData, errors}) => { @@ -18,6 +19,18 @@ export function GroupForm ({group = {}, errors = [], loading = false, schema, on const uiSchema = useGroupUiSchema(); const validator = useGroupUiValidator(); + const groupSchema = React.useMemo(() => { + const filtered = context.groups.filter(g => !([group.resourceId].indexOf(g.resourceId) > -1)); + const enumList = filtered.map(g => g.resourceId); + const enumNames = filtered.map(g => g.name); + let s = { ...schema }; + s = set(s, 'properties.approversList.items.enum', enumList); + s = set(s, 'properties.approversList.items.enumNames', enumNames); + return s; + }, [schema, context.groups, group.resourceId]); + + const parser = useGroupParser(); + const formatter = useGroupFormatter(); return (<>
@@ -41,11 +54,11 @@ export function GroupForm ({group = {}, errors = [], loading = false, schema, on
-
onChange(form)} + onChange={(form) => onChange({ ...form, formData: parser(form.formData) })} validate={validator} - schema={schema} + schema={groupSchema} uiSchema={uiSchema} liveValidate={true}> <> diff --git a/ui/src/app/admin/container/EditGroup.js b/ui/src/app/admin/container/EditGroup.js index ad06368fb..175ac4e69 100644 --- a/ui/src/app/admin/container/EditGroup.js +++ b/ui/src/app/admin/container/EditGroup.js @@ -13,7 +13,7 @@ import { createNotificationAction, NotificationTypes, useNotificationDispatcher import { useTranslator } from '../../i18n/hooks'; import { BASE_PATH } from '../../App.constant'; -export function EditGroup() { +export function EditGroup({ groups }) { const { id } = useParams(); @@ -74,6 +74,7 @@ export function EditGroup() { {(data, errors) => Group Description + + Approvers + Actions @@ -51,14 +54,16 @@ export function GroupsList({ groups, onDelete }) { {group.name} {group.description || ''} + {group.approversList?.length > 0 ? group.approversList[0].approverGroupIds.join(', ') : '-'} - + Edit -