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/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..b0b65dbb5 100644 --- a/backend/src/integration/resources/SHIBUI-1407-1.side +++ b/backend/src/integration/resources/SHIBUI-1407-1.side @@ -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..725b2dee1 100644 --- a/backend/src/integration/resources/SHIBUI-1674-2.side +++ b/backend/src/integration/resources/SHIBUI-1674-2.side @@ -984,9 +984,12 @@ "id": "aea0e033-111e-4a5d-8038-ec222786a695", "comment": "", "command": "mouseOver", - "target": "css=.row:nth-child(4) .svg-inline--fa:nth-child(2)", + "target": "css=#root_metadataFilters_2_removeEmptyEntitiesDescriptors-group .info-icon", "targets": [ - ["css=.row:nth-child(4) .svg-inline--fa:nth-child(2)", "css:finder"] + ["css=#root_metadataFilters_2_removeEmptyEntitiesDescriptors-group .info-icon", "css:finder"], + ["xpath=(//button[@type='button'])[13]", "xpath:attributes"], + ["xpath=//div[@id='root_metadataFilters_2_removeEmptyEntitiesDescriptors-group']/div/div/div/label/span/button", "xpath:idRelative"], + ["xpath=//div[4]/div/div/div/div/div/label/span/button", "xpath:position"] ], "value": "" }, { @@ -994,7 +997,9 @@ "comment": "", "command": "assertText", "target": "css=div[role=\"tooltip\"]", - "targets": [], + "targets": [ + ["css=#root_metadataFilters_2_removeRolelessEntityDescriptors-group path", "css:finder"] + ], "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", @@ -1337,7 +1342,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/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/controller/DynamicRegistrationUiDefinitionController.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DynamicRegistrationUiDefinitionController.groovy new file mode 100644 index 000000000..b2a75350b --- /dev/null +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DynamicRegistrationUiDefinitionController.groovy @@ -0,0 +1,58 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocationRegistry +import edu.internet2.tier.shibboleth.admin.ui.service.JsonSchemaBuilderService +import groovy.util.logging.Slf4j +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.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +import javax.annotation.PostConstruct + +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.algorithmFilterSchema +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.dynamicRegistrationSchema +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR + +/** + * Controller implementing REST resource responsible for exposing structure definition for dynamic registration user + * interface in terms of JSON schema. + */ +@RestController +@RequestMapping('/api/ui/DynamicRegistration') +@Slf4j +@Tags(value = [@Tag(name = "ui")]) +class DynamicRegistrationUiDefinitionController { + + @Autowired + JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry + + JsonSchemaResourceLocation jsonSchemaLocation + + @Autowired + ObjectMapper jacksonObjectMapper + + @Autowired + JsonSchemaBuilderService jsonSchemaBuilderService + + @GetMapping + ResponseEntity getUiDefinitionJsonSchema() { + try { + def parsedJson = jacksonObjectMapper.readValue(this.jsonSchemaLocation.url, Map) + return ResponseEntity.ok(parsedJson) + } catch (Exception e) { + log.error(e.getMessage(), e) + return ResponseEntity.status(INTERNAL_SERVER_ERROR).body([jsonParseError : e.getMessage(), sourceUiSchemaDefinitionFile: this.jsonSchemaLocation.url]) + } + } + + @PostConstruct + void init() { + this.jsonSchemaLocation = dynamicRegistrationSchema(this.jsonSchemaResourceLocationRegistry) + } +} \ No newline at end of file diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy index 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..e9865c93b 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,9 @@ 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.DynamicRegistrationInfoRepository; 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; @@ -19,12 +22,14 @@ import edu.internet2.tier.shibboleth.admin.ui.service.DefaultMetadataResolversPositionOrderContainerService; import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryService; import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryServiceImpl; +import edu.internet2.tier.shibboleth.admin.ui.service.DynamicRegistrationService; import edu.internet2.tier.shibboleth.admin.ui.service.EntityIdsSearchService; import edu.internet2.tier.shibboleth.admin.ui.service.EntityIdsSearchServiceImpl; import edu.internet2.tier.shibboleth.admin.ui.service.EntityService; import edu.internet2.tier.shibboleth.admin.ui.service.FileCheckingFileWritingService; import edu.internet2.tier.shibboleth.admin.ui.service.FileWritingService; import edu.internet2.tier.shibboleth.admin.ui.service.FilterTargetService; +import edu.internet2.tier.shibboleth.admin.ui.service.JPADynamicRegistrationServiceImpl; import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl; import edu.internet2.tier.shibboleth.admin.ui.service.JPAFilterTargetServiceImpl; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; @@ -218,9 +223,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 +235,15 @@ public UserUpdatedEntityListener userUpdatedEntityListener(OwnershipRepository r listener.init(repo, groupRepo); return listener; } + + @Bean + public IShibUiPermissionEvaluator shibUiPermissionEvaluator(EntityDescriptorRepository entityDescriptorRepository, UserService userService, DynamicRegistrationInfoRepository driRepo) { + // TODO: @jj define type to return for Grouper integration + return new ShibUiPermissionDelegate(driRepo, entityDescriptorRepository, userService); + } + + @Bean + public DynamicRegistrationService dynamicRegistrationService(DynamicRegistrationInfoRepository driRepo, OwnershipRepository ownershipRepo, IShibUiPermissionEvaluator permissionEvaluator, UserService userService, IGroupService groupService) { + return new JPADynamicRegistrationServiceImpl(groupService, driRepo, ownershipRepo, permissionEvaluator, userService); + } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java index fa8f5db18..c310154d7 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java @@ -13,6 +13,7 @@ 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.DYNAMIC_REGISTRATION; import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.ENTITY_ATTRIBUTES_FILTERS; import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.EXTERNAL_METADATA_RESOLVER; import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.FILESYSTEM_METADATA_RESOLVER; @@ -73,6 +74,11 @@ public class JsonSchemaComponentsConfiguration { @Setter private String algorithmFilterUiSchemaLocation = "classpath:algorithm-filter.schema.json"; + //Configured via @ConfigurationProperties (using setter method) with 'shibui.dynamic-registration-ui-schema-location' property and + // default value set here if that property is not explicitly set in application.properties + @Setter + private String dynamicRegistrationUiSchemaLocation = "classpath:dynamic-registration.schema.json"; + @Bean public JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry(ResourceLoader resourceLoader, ObjectMapper jacksonMapper) { return JsonSchemaResourceLocationRegistry.inMemory() @@ -129,6 +135,12 @@ public JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry(Res .resourceLoader(resourceLoader) .jacksonMapper(jacksonMapper) .detectMalformedJson(true) + .build()) + .register(DYNAMIC_REGISTRATION, JsonSchemaLocationBuilder.with() + .jsonSchemaLocation(dynamicRegistrationUiSchemaLocation) + .resourceLoader(resourceLoader) + .jacksonMapper(jacksonMapper) + .detectMalformedJson(true) .build()); } 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..1fc52d3c4 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 @@ -2,11 +2,14 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.DynamicRegistrationRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.exception.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.exception.UnsupportedShibUiOperationException; +import edu.internet2.tier.shibboleth.admin.ui.service.DynamicRegistrationService; import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; import edu.internet2.tier.shibboleth.admin.ui.service.FilterService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; @@ -26,6 +29,8 @@ @RequestMapping("/api/activate") @Tags(value = {@Tag(name = "activate")}) public class ActivateController { + @Autowired + private DynamicRegistrationService dynamicRegistrationService; @Autowired private EntityDescriptorService entityDescriptorService; @@ -36,19 +41,27 @@ public class ActivateController { @Autowired private MetadataResolverService metadataResolverService; + @PatchMapping(path = "/DynamicRegistration/{resourceId}/{mode}") + @Transactional + public ResponseEntity enableDynamicRegistration(@PathVariable String resourceId, @PathVariable String mode) throws PersistentEntityNotFound, ForbiddenException, UnsupportedShibUiOperationException { + if ("enable".equalsIgnoreCase(mode)) { + DynamicRegistrationRepresentation drr = dynamicRegistrationService.enableDynamicRegistration(resourceId); + return ResponseEntity.ok(drr); + } + throw new UnsupportedShibUiOperationException("Disable is not a valid operation for Dynamic Registrations at this time"); + } + @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); } - + @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 +69,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..0798f12d8 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ApprovalController.java @@ -0,0 +1,44 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.DynamicRegistrationRepresentation; +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.DynamicRegistrationService; +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 DynamicRegistrationService dynamicRegistrationService; + + @Autowired + private EntityDescriptorService entityDescriptorService; + + @PatchMapping(path = "/DynamicRegistration/{resourceId}/{mode}") + @Transactional + public ResponseEntity approveDynamicRegistration(@PathVariable String resourceId, @PathVariable String mode) throws PersistentEntityNotFound, ForbiddenException { + boolean status = "approve".equalsIgnoreCase(mode); + DynamicRegistrationRepresentation drr = dynamicRegistrationService.approveDynamicRegistration(resourceId, status); + return ResponseEntity.ok(drr); + } + + @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 79% 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..48436eba5 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 @@ -4,6 +4,7 @@ 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.exception.UnsupportedShibUiOperationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -15,8 +16,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) { @@ -42,6 +43,9 @@ public ResponseEntity handleMetadataFileNotFoundException(MetadataFileNotFoun public ResponseEntity handleScriptException(ScriptException e, WebRequest request) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), e.getMessage())); } - - + + @ExceptionHandler({ UnsupportedShibUiOperationException.class }) + public ResponseEntity handleUnsupportedShibUiOperationException(UnsupportedShibUiOperationException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED).body(new ErrorResponse(String.valueOf(HttpStatus.NOT_IMPLEMENTED.value()), e.getMessage())); + } } \ No newline at end of file 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/DynamicRegistrationController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/DynamicRegistrationController.java new file mode 100644 index 000000000..c72ec3652 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/DynamicRegistrationController.java @@ -0,0 +1,97 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.DynamicRegistrationRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +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.service.DynamicRegistrationService; +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.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; +import java.util.ConcurrentModificationException; + +@RestController +@RequestMapping("/api") +@Tags(value = {@Tag(name = "oidc")}) +public class DynamicRegistrationController { + private static URI getResourceUriFor(String resourceId) { + return ServletUriComponentsBuilder + .fromCurrentServletMapping().path("/api/DynamicRegistration") + .pathSegment(resourceId) + .build() + .toUri(); + } + + @Autowired + DynamicRegistrationService dynamicRegistrationService; + + @PostMapping("/DynamicRegistration") + @Transactional + public ResponseEntity create(@RequestBody DynamicRegistrationRepresentation dynRegRepresentation) throws ObjectIdExistsException { + DynamicRegistrationRepresentation persisted = dynamicRegistrationService.createNew(dynRegRepresentation); + return ResponseEntity.created(getResourceUriFor(persisted.getResourceId())).body(persisted); + } + + @GetMapping(value = "/DynamicRegistrations", produces = "application/json") + @Transactional(readOnly = true) + public ResponseEntity getAll() throws ForbiddenException { + return ResponseEntity.ok(dynamicRegistrationService.getAllDynamicRegistrationsBasedOnUserAccess()); + } + + @GetMapping("/DynamicRegistrations/needsApproval") + @Transactional + public ResponseEntity getAllNeedingApproval() throws ForbiddenException { + return ResponseEntity.ok(dynamicRegistrationService.getAllDynamicRegistrationsNeedingApprovalBasedOnUserAccess()); + } + + /** + * @throws ForbiddenException This call is used for the admin needs action list, therefore the user must be an admin + */ + @Transactional + @GetMapping(value = "/DynamicRegistrations/disabledSources") + public ResponseEntity getDisabledMetadataSources() throws ForbiddenException { + return ResponseEntity.ok(dynamicRegistrationService.getDisabledDynamicRegistrations()); + } + + @GetMapping(value = "/DynamicRegistration/{resourceId}", produces = "application/json") + @Transactional(readOnly = true) + public ResponseEntity getOne(@PathVariable String resourceId) throws ForbiddenException { + return ResponseEntity.ok(dynamicRegistrationService.getOne(resourceId)); + } + + @DeleteMapping(value = "/DynamicRegistration/{resourceId}") + @Transactional + public ResponseEntity deleteOne(@PathVariable String resourceId) throws ForbiddenException, PersistentEntityNotFound { + dynamicRegistrationService.delete(resourceId); + return ResponseEntity.noContent().build(); + } + + @PutMapping("/DynamicRegistration/{resourceId}") + @Transactional + public ResponseEntity update(@RequestBody DynamicRegistrationRepresentation dynRegRepresentation, @PathVariable String resourceId) throws ForbiddenException, ConcurrentModificationException, PersistentEntityNotFound { + dynRegRepresentation.setResourceId(resourceId); // This should be the same already, but just to be safe... + DynamicRegistrationRepresentation result = dynamicRegistrationService.update(dynRegRepresentation); + return ResponseEntity.ok().body(result); + } + + @PutMapping("/DynamicRegistration/{resourceId}/changeGroup/{groupId}") + @Transactional + public ResponseEntity updateGroupForEntityDescriptor(@PathVariable String resourceId, @PathVariable String groupId) throws ForbiddenException, PersistentEntityNotFound { + DynamicRegistrationRepresentation result = dynamicRegistrationService.updateGroupForDynamicRegistration(resourceId, groupId); + return ResponseEntity.ok().body(result); + } + +} \ No newline at end of file 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..f88ae5d90 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); @@ -151,8 +159,7 @@ public ResponseEntity update(@RequestBody EntityDescriptorRepresentation edRe @PutMapping("/EntityDescriptor/{resourceId}/changeGroup/{groupId}") @Transactional - public ResponseEntity updateGroupForEntityDescriptor(@PathVariable String resourceId, @PathVariable String groupId) - throws ForbiddenException, ConcurrentModificationException, PersistentEntityNotFound, InvalidPatternMatchException { + public ResponseEntity updateGroupForEntityDescriptor(@PathVariable String resourceId, @PathVariable String groupId) throws ConcurrentModificationException { EntityDescriptorRepresentation result = entityDescriptorService.updateGroupForEntityDescriptor(resourceId, groupId); return ResponseEntity.ok().body(result); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/PersistentEntityControllerExceptionHandler.java similarity index 92% rename from backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java rename to backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/PersistentEntityControllerExceptionHandler.java index e6f46b5fe..eeb7223f4 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/PersistentEntityControllerExceptionHandler.java @@ -14,8 +14,8 @@ import java.util.ConcurrentModificationException; -@ControllerAdvice(assignableTypes = {EntityDescriptorController.class}) -public class EntityDescriptorControllerExceptionHandler extends ResponseEntityExceptionHandler { +@ControllerAdvice(assignableTypes = {EntityDescriptorController.class, DynamicRegistrationController.class}) +public class PersistentEntityControllerExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({ ConcurrentModificationException.class }) public ResponseEntity handleConcurrentModificationException(ConcurrentModificationException e, WebRequest request) { @@ -43,7 +43,7 @@ public ResponseEntity handleObjectIdExistsException(ObjectIdExistsException e headers.setLocation(EntityDescriptorController.getResourceUriFor(e.getMessage())); return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers).body(new ErrorResponse( String.valueOf(HttpStatus.CONFLICT.value()), - String.format("The entity descriptor with entity id [%s] already exists.", e.getMessage()))); + String.format("The persistent entity with id [%s] already exists.", e.getMessage()))); } } \ No newline at end of file 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/ActivatableType.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ActivatableType.java index 8042d56d4..e3a9a7387 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ActivatableType.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ActivatableType.java @@ -1,5 +1,5 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; public enum ActivatableType { - ENTITY_DESCRIPTOR, METADATA_RESOLVER, FILTER + ENTITY_DESCRIPTOR, METADATA_RESOLVER, FILTER, DYNAMIC_REGISTRATION } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java index 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..541d86fa5 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IApprovable.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +public interface IApprovable { + String getIdOfOwner(); + + void removeLastApproval(); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/DynamicRegistrationRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/DynamicRegistrationRepresentation.java new file mode 100644 index 000000000..133bac4d2 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/DynamicRegistrationRepresentation.java @@ -0,0 +1,115 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; + +import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.DynamicRegistrationInfo; +import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.GrantType; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@NoArgsConstructor +@Getter +@Setter +public class DynamicRegistrationRepresentation { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS"); + + private String applicationType; + private boolean approved; + private String contacts; + private LocalDateTime createdDate; + private boolean enabled; + private GrantType grantType; + private String idOfOwner; + private String jwks; + private String logoUri; + private String name; + private LocalDateTime modifiedDate; + private String policyUri; + private String redirectUris; + private String resourceId; + private String responseTypes; + private String scope; + private String subjectType; + private String tokenEndpointAuthMethod; + private String tosUri; + private int version; + + public DynamicRegistrationRepresentation(DynamicRegistrationInfo dri) { + applicationType = dri.getApplicationType(); + approved = dri.isApproved(); + contacts = dri.getContacts(); + createdDate = dri.getCreatedDate(); + enabled = dri.isEnabled(); + grantType = dri.getGrantType(); + idOfOwner = dri.getIdOfOwner(); + jwks = dri.getJwks(); + logoUri = dri.getLogoUri(); + name = dri.getName(); + modifiedDate = dri.getModifiedDate(); + policyUri = dri.getPolicyUri(); + redirectUris = dri.getRedirectUris(); + resourceId = dri.getResourceId(); + responseTypes = dri.getResponseTypes(); + scope = dri.getScope(); + subjectType = dri.getSubjectType(); + tokenEndpointAuthMethod = dri.getTokenEndpointAuthMethod(); + tosUri = dri.getTosUri(); + version = dri.hashCode(); + } + + public DynamicRegistrationInfo buildDynamicRegistrationInfo() { + // Approved and enabled shouldn't be handled from here, and owner shouldn't come from the UI, so we ignore all those + + DynamicRegistrationInfo dri = new DynamicRegistrationInfo(); + dri.setApplicationType(applicationType); +// dri.setApproved(approved); + dri.setContacts(contacts); +// dri.setEnabled(enabled); + dri.setGrantType(grantType); +// dri.setIdOfOwner(idOfOwner); + dri.setJwks(jwks); + dri.setLogoUri(logoUri); + dri.setName(name); + dri.setPolicyUri(policyUri); + dri.setRedirectUris(redirectUris); + dri.setResourceId(resourceId); + dri.setResponseTypes(responseTypes); + dri.setScope(scope); + dri.setSubjectType(subjectType); + dri.setTokenEndpointAuthMethod(tokenEndpointAuthMethod); + dri.setTosUri(tosUri); + return dri; + } + + public String getCreatedDate() { + return createdDate != null ? DATE_TIME_FORMATTER.format(createdDate) : null; + } + + public String getModifiedDate() { + return modifiedDate != null ? DATE_TIME_FORMATTER.format(modifiedDate) : null; + } + + /** + * Do not update approved or change the group here + */ + public DynamicRegistrationInfo updateExistingWithRepValues(DynamicRegistrationInfo dri) { + dri.setApplicationType(applicationType); + dri.setContacts(contacts); + dri.setEnabled(enabled); + dri.setGrantType(grantType); + dri.setJwks(jwks); + dri.setLogoUri(logoUri); + dri.setName(name); + dri.setPolicyUri(policyUri); + dri.setRedirectUris(redirectUris); + dri.setResourceId(resourceId); + dri.setResponseTypes(responseTypes); + dri.setScope(scope); + dri.setSubjectType(subjectType); + dri.setTokenEndpointAuthMethod(tokenEndpointAuthMethod); + dri.setTosUri(tosUri); + return dri; + } +} \ 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/domain/oidc/DynamicRegistrationInfo.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/DynamicRegistrationInfo.java new file mode 100644 index 000000000..207d9e1a2 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/DynamicRegistrationInfo.java @@ -0,0 +1,88 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.oidc; + +import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; +import edu.internet2.tier.shibboleth.admin.ui.domain.ActivatableType; +import edu.internet2.tier.shibboleth.admin.ui.domain.IActivatable; +import edu.internet2.tier.shibboleth.admin.ui.domain.IApprovable; +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.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; + +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Entity +@Data +@Audited +public class DynamicRegistrationInfo extends AbstractAuditable implements Ownable, IActivatable, IApprovable { + private String applicationType; + private boolean approved; + private String contacts; + private boolean enabled; + private GrantType grantType; + private String idOfOwner; + private String jwks; + private String logoUri; + private String name; + private String policyUri; + private String redirectUris; + private String resourceId; + private String responseTypes; + private String scope; + private String subjectType; + private String tokenEndpointAuthMethod; + private String tosUri; + + @ElementCollection(fetch = FetchType.EAGER) + @EqualsAndHashCode.Exclude + private List approvedBy = new ArrayList<>(); + + @Override + public ActivatableType getActivatableType() { + return ActivatableType.DYNAMIC_REGISTRATION; + } + + @Override + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + @Override + public String getObjectId() { + return getResourceId(); + } + + public String getResourceId() { + if (resourceId == null) { + resourceId = UUID.randomUUID().toString(); + } + return resourceId; + } + + @Override + public OwnableType getOwnableType() { + return OwnableType.DYNAMIC_REGISTRATION; + } + + @Override + public void removeLastApproval() { + if (!approvedBy.isEmpty()) { + approvedBy.remove(approvedBy.size() - 1); + } + } + + public int approvedCount() { + return approvedBy.size(); + } + + public void addApproval(Group currentUserGroup) { + approvedBy.add(currentUserGroup.getName()); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/GrantType.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/GrantType.java new file mode 100644 index 000000000..65e28c7fe --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/oidc/GrantType.java @@ -0,0 +1,5 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.oidc; + +public enum GrantType { + authorization_code, implicit, refresh_token +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/UnsupportedShibUiOperationException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/UnsupportedShibUiOperationException.java new file mode 100644 index 000000000..e90508131 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/UnsupportedShibUiOperationException.java @@ -0,0 +1,11 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class UnsupportedShibUiOperationException extends Exception { + public UnsupportedShibUiOperationException() { + super("Operation unsupport in ShibUI at this time"); + } + + public UnsupportedShibUiOperationException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaLocationLookup.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaLocationLookup.java index a5e5406ef..dc28fc1ad 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaLocationLookup.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaLocationLookup.java @@ -2,6 +2,7 @@ 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.DYNAMIC_REGISTRATION; import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.ENTITY_ATTRIBUTES_FILTERS; import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.EXTERNAL_METADATA_RESOLVER; import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocation.SchemaType.FILESYSTEM_METADATA_RESOLVER; @@ -132,4 +133,15 @@ public static JsonSchemaResourceLocation algorithmFilterSchema(JsonSchemaResourc return resourceLocationRegistry.lookup(ALGORITHM_FILTER) .orElseThrow(() -> new IllegalStateException("JSON schema resource location for algorithm filter is not registered.")); } + + /** + * Searches algorithm filter JSON schema resource location object in the given location registry. + * + * @param resourceLocationRegistry + * @return algorithm filter JSON schema resource location object + * @throws IllegalStateException if schema is not found in the given registry + */ + public static JsonSchemaResourceLocation dynamicRegistrationSchema(JsonSchemaResourceLocationRegistry resourceLocationRegistry) { + return resourceLocationRegistry.lookup(DYNAMIC_REGISTRATION).orElseThrow(() -> new IllegalStateException("JSON schema resource location for dynamic registration is not registered.")); + } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocation.java index 58b1e2d66..dd3262888 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocation.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/jsonschema/JsonSchemaResourceLocation.java @@ -96,6 +96,7 @@ public enum SchemaType { // common types METADATA_SOURCES_SAML("MetadataSourcesSAML"), METADATA_SOURCES_OIDC("MetadataSourcesOIDC"), + DYNAMIC_REGISTRATION("DynamicRegistration"), // filter types ENTITY_ATTRIBUTES_FILTERS("EntityAttributesFilters"), 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..5b60fd5b3 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 @@ -7,26 +7,28 @@ 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 +39,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() { 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..7fb999568 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,11 @@ 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 +51,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..6352e01ca 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,6 +6,7 @@ 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.springframework.beans.factory.annotation.Autowired; @@ -29,6 +30,9 @@ public class GroupController { @Autowired private IGroupService groupService; + @Autowired + private EntityDescriptorService entityDescriptorService; + @Secured("ROLE_ADMIN") @PostMapping @Transactional @@ -66,6 +70,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/OwnableType.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/OwnableType.java index 0cf82714c..ed1fdb8bf 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/OwnableType.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/OwnableType.java @@ -1,5 +1,5 @@ package edu.internet2.tier.shibboleth.admin.ui.security.model; public enum OwnableType { - USER, ENTITY_DESCRIPTOR, METADATA_PROVIDER -} + USER, ENTITY_DESCRIPTOR, METADATA_PROVIDER, DYNAMIC_REGISTRATION +} \ 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..53eddc63e --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/permission/IShibUiPermissionEvaluator.java @@ -0,0 +1,28 @@ +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 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();} +} \ No newline at end of file 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..0cd22d7a1 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/permission/ShibUiPermissibleType.java @@ -0,0 +1,6 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.permission; + +public enum ShibUiPermissibleType { + entityDescriptorProjection, // represents EntityDescriptorProjections + dynamicRegistrationInfo +} \ 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..3b290a31b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/permission/ShibUiPermissionDelegate.java @@ -0,0 +1,151 @@ +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.domain.oidc.DynamicRegistrationInfo; +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.repository.DynamicRegistrationInfoRepository; +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 DynamicRegistrationInfoRepository dynamicRegistrationInfoRepository; + + 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 : " + shibUiType); + } + return getAllEntityDescriptorProjectionsBasedOnUserAccess(); + } + case dynamicRegistrationInfo: + switch (permissionType) { + case approve: + if (!hasPermission(ignored, null, PermissionType.approve)) { + throw new ForbiddenException("User has no access rights to get a list of : " + shibUiType); + } + return getAllDynamicRegistrationInfoObjectsNeedingApprovalBasedOnUserAccess(); + case enable: + if (!hasPermission(ignored, null, PermissionType.enable)) { + throw new ForbiddenException("User has no access rights to get a list of : " + shibUiType); + } + return dynamicRegistrationInfoRepository.getDynamicRegistrationsNeedingEnabling(); + case fetch: + if (!hasPermission(ignored, null, PermissionType.fetch)) { + throw new ForbiddenException("User has no access rights to get a list of : " + shibUiType); + } + return getAllDynamicRegistrationInfoObjectsBasedOnUserAccess(); + } + } + return null; + } + + private List getAllDynamicRegistrationInfoObjectsNeedingApprovalBasedOnUserAccess() { + List groupsToApprove = userService.getGroupsCurrentUserCanApprove(); + return dynamicRegistrationInfoRepository.getAllNeedingApproval(groupsToApprove); + } + + private List getAllDynamicRegistrationInfoObjectsBasedOnUserAccess() { + if (userService.currentUserIsAdmin()) { + return dynamicRegistrationInfoRepository.findAll(); + } else { + return dynamicRegistrationInfoRepository.findAllByIdOfOwner(userService.getCurrentUser().getGroup().getOwnerId()); + } + } + + 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/DynamicRegistrationInfoRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/DynamicRegistrationInfoRepository.java new file mode 100644 index 000000000..40eecf6c6 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/DynamicRegistrationInfoRepository.java @@ -0,0 +1,23 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.repository; + +import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.DynamicRegistrationInfo; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface DynamicRegistrationInfoRepository extends JpaRepository { + List findAllByIdOfOwner(String idOfOwner); + + DynamicRegistrationInfo findByResourceId(String id); + + @Query(value = "SELECT dri FROM DynamicRegistrationInfo dri " + + " WHERE dri.idOfOwner IN (:groupIds)" + + " AND dri.enabled = false" + + " AND dri.approved = false") + List getAllNeedingApproval(@Param("groupIds") List groupIds); + + @Query(value = "SELECT dri FROM DynamicRegistrationInfo dri WHERE dri.enabled = false") + List getDynamicRegistrationsNeedingEnabling(); +} \ 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..c26526df8 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java @@ -1,7 +1,5 @@ package edu.internet2.tier.shibboleth.admin.ui.security.service; -import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; -import edu.internet2.tier.shibboleth.admin.ui.domain.IActivatable; import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound; import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException; import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException; @@ -18,11 +16,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.Arrays; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Optional; @@ -54,29 +53,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 +79,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 +121,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 +142,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/DynamicRegistrationService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DynamicRegistrationService.java new file mode 100644 index 000000000..ef95ce70b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DynamicRegistrationService.java @@ -0,0 +1,31 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.DynamicRegistrationRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; +import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound; + +import java.util.List; + +public interface DynamicRegistrationService { + DynamicRegistrationRepresentation approveDynamicRegistration(String resourceId, boolean status) + throws PersistentEntityNotFound, ForbiddenException; + + DynamicRegistrationRepresentation createNew(DynamicRegistrationRepresentation dynRegRepresentation) throws ObjectIdExistsException; + + void delete(String resourceId) throws ForbiddenException, PersistentEntityNotFound; + + DynamicRegistrationRepresentation enableDynamicRegistration(String resourceId) throws PersistentEntityNotFound, ForbiddenException; + + List getAllDynamicRegistrationsBasedOnUserAccess() throws ForbiddenException; + + List getAllDynamicRegistrationsNeedingApprovalBasedOnUserAccess() throws ForbiddenException; + + List getDisabledDynamicRegistrations() throws ForbiddenException; + + DynamicRegistrationRepresentation getOne(String resourceId) throws ForbiddenException; + + DynamicRegistrationRepresentation update(DynamicRegistrationRepresentation dynRegRepresentation) throws PersistentEntityNotFound, ForbiddenException; + + DynamicRegistrationRepresentation updateGroupForDynamicRegistration(String resourceId, String groupId) throws ForbiddenException, PersistentEntityNotFound; +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java index 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/JPADynamicRegistrationServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPADynamicRegistrationServiceImpl.java new file mode 100644 index 000000000..9c804d42c --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPADynamicRegistrationServiceImpl.java @@ -0,0 +1,225 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.DynamicRegistrationRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.DynamicRegistrationInfo; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +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.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.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.DynamicRegistrationInfoRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.ConcurrentModificationException; +import java.util.List; + +@Slf4j +@Service +@AllArgsConstructor +@NoArgsConstructor +public class JPADynamicRegistrationServiceImpl implements DynamicRegistrationService { + @Autowired + IGroupService groupService; + + @Autowired + DynamicRegistrationInfoRepository repository; + + @Autowired + OwnershipRepository ownershipRepository; + + @Autowired + private IShibUiPermissionEvaluator shibUiAuthorizationDelegate; + + @Autowired + UserService userService; + + @Override + public DynamicRegistrationRepresentation approveDynamicRegistration(String resourceId, boolean status) throws PersistentEntityNotFound, ForbiddenException { + DynamicRegistrationInfo dri = repository.findByResourceId(resourceId); + if (dri == null) { + throw new PersistentEntityNotFound("Dynamic Registration with resourceid[ " + resourceId + " ] was not found for approval"); + } + return changeApproveStatusOfDynamicRepresentation(dri, status); + } + + private DynamicRegistrationRepresentation changeApproveStatusOfDynamicRepresentation(DynamicRegistrationInfo dri, boolean status) throws ForbiddenException { + if (!shibUiAuthorizationDelegate.hasPermission(userService.getCurrentUserAuthentication(), dri, PermissionType.approve)) { + throw new ForbiddenException("You do not have the permissions necessary to approve this dynamic registration."); + } + if (status) { // approve + int approvedCount = dri.approvedCount(); // total number of approvals so far + List theApprovers = groupService.find(dri.getIdOfOwner()).getApproversList(); + if (theApprovers.size() > approvedCount) { // don't add if we already have enough approvals + dri.addApproval(userService.getCurrentUserGroup()); + } + dri.setApproved(dri.approvedCount() >= theApprovers.size()); // future check for multiple approvals needed + dri = repository.save(dri); + } else { // un-approve + dri.removeLastApproval(); + Group ownerGroup = groupService.find(dri.getIdOfOwner()); + dri.setApproved(dri.approvedCount() >= ownerGroup.getApproversList().size()); // safe check in case of weird race conditions from the UI + dri = repository.save(dri); + } + return new DynamicRegistrationRepresentation(dri); + } + + @Override + public DynamicRegistrationRepresentation createNew(DynamicRegistrationRepresentation dynRegRepresentation) throws ObjectIdExistsException { + if (entityExists(dynRegRepresentation.getResourceId())) { + throw new ObjectIdExistsException(dynRegRepresentation.getResourceId()); + } + + DynamicRegistrationInfo dri = dynRegRepresentation.buildDynamicRegistrationInfo(); + dri.setEnabled(false); // cannot create as enabled + + // "Create new" will use the current user's group as the owner + String ownerId = userService.getCurrentUserGroup().getOwnerId(); + dri.setIdOfOwner(ownerId); + + if (shibUiAuthorizationDelegate.hasPermission(userService.getCurrentUserAuthentication(), null, PermissionType.admin) || + userService.getCurrentUserGroup().getApproversList().isEmpty()) { + dri.setApproved(true); + } + + ownershipRepository.deleteEntriesForOwnedObject(dri); + ownershipRepository.save(new Ownership(userService.getCurrentUserGroup(), dri)); + + return new DynamicRegistrationRepresentation(repository.save(dri)); + } + + private List convertToRepresentations(List temp) { + List result = new ArrayList<>(); + temp.forEach(dri -> result.add(new DynamicRegistrationRepresentation(dri))); + return result; + } + + @Override + public void delete(String resourceId) throws ForbiddenException, PersistentEntityNotFound { + if (!shibUiAuthorizationDelegate.hasPermission(userService.getCurrentUserAuthentication(), null, PermissionType.admin)) { + throw new ForbiddenException("Deleting a Dynamic Registration Source is only allowed by an admin."); + } + DynamicRegistrationInfo dri = repository.findByResourceId(resourceId); + if (dri == null) { + throw new PersistentEntityNotFound("Dynamic Registration not found for resource id: " + resourceId); + } + if (dri.isEnabled()) { + throw new ForbiddenException("Deleting an enabled Dynamic Registration Source is not allowed."); + } + ownershipRepository.deleteEntriesForOwnedObject(dri); + repository.delete(dri); + } + + @Override + public DynamicRegistrationRepresentation enableDynamicRegistration(String resourceId) throws PersistentEntityNotFound, ForbiddenException { + DynamicRegistrationInfo existingDri = repository.findByResourceId(resourceId); + if (existingDri == null) { + throw new PersistentEntityNotFound(String.format("The dynamic registration with id [%s] was not found for update.", existingDri.getResourceId())); + } + if (!shibUiAuthorizationDelegate.hasPermission(userService.getCurrentUserAuthentication(), existingDri, PermissionType.enable)) { + throw new ForbiddenException("You do not have the permissions necessary to enable this service"); + } + // TODO do something... + return new DynamicRegistrationRepresentation(existingDri); + } + + private boolean entityExists(String id) { + return repository.findByResourceId(id) != null ; + } + + @Override + public List getAllDynamicRegistrationsBasedOnUserAccess() throws ForbiddenException { + List temp = (List) shibUiAuthorizationDelegate.getPersistentEntities(userService.getCurrentUserAuthentication(), ShibUiPermissibleType.dynamicRegistrationInfo, PermissionType.fetch); + return convertToRepresentations(temp); + } + + @Override + public List getAllDynamicRegistrationsNeedingApprovalBasedOnUserAccess() throws ForbiddenException { + List temp = (List) shibUiAuthorizationDelegate.getPersistentEntities(userService.getCurrentUserAuthentication(), ShibUiPermissibleType.dynamicRegistrationInfo, PermissionType.approve); + return convertToRepresentations(temp); + } + + @Override + public List getDisabledDynamicRegistrations() throws ForbiddenException { + List temp = (List) shibUiAuthorizationDelegate.getPersistentEntities(userService.getCurrentUserAuthentication(), ShibUiPermissibleType.dynamicRegistrationInfo, PermissionType.enable); + return convertToRepresentations(temp); + } + + @Override + public DynamicRegistrationRepresentation getOne(String resourceId) throws ForbiddenException { + DynamicRegistrationInfo existingDri = repository.findByResourceId(resourceId); + if (!shibUiAuthorizationDelegate.hasPermission(userService.getCurrentUserAuthentication(), existingDri, PermissionType.viewOrEdit)) { + throw new ForbiddenException(); + } + return new DynamicRegistrationRepresentation(existingDri); + } + + @Override + public DynamicRegistrationRepresentation update(DynamicRegistrationRepresentation dynRegRepresentation) + throws PersistentEntityNotFound, ForbiddenException, ConcurrentModificationException { + DynamicRegistrationInfo existingDri = repository.findByResourceId(dynRegRepresentation.getResourceId()); + if (existingDri == null) { + throw new PersistentEntityNotFound(String.format("The dynamic registration with id [%s] was not found for update.", existingDri.getResourceId())); + } + if (dynRegRepresentation.isEnabled() && !shibUiAuthorizationDelegate.hasPermission(userService.getCurrentUserAuthentication(), existingDri, PermissionType.enable)) { + throw new ForbiddenException("You do not have the permissions necessary to enable this service."); + } + if (StringUtils.isEmpty(dynRegRepresentation.getIdOfOwner())) { + dynRegRepresentation.setIdOfOwner(StringUtils.isNotEmpty(existingDri.getIdOfOwner()) ? existingDri.getIdOfOwner() : userService.getCurrentUserGroup().getOwnerId()); + } + if (!shibUiAuthorizationDelegate.hasPermission(userService.getCurrentUserAuthentication(), existingDri, PermissionType.viewOrEdit)) { + throw new ForbiddenException(); + } + // Verify we're the only one attempting to update the EntityDescriptor + if (dynRegRepresentation.getVersion() != existingDri.hashCode()) { + throw new ConcurrentModificationException(String.format("A concurrent modification has occured on entity descriptor with entity id [%s]. Please refresh and try again", dynRegRepresentation.getResourceId())); + } + existingDri = dynRegRepresentation.updateExistingWithRepValues(existingDri); + + existingDri = repository.save(existingDri); + ownershipRepository.deleteEntriesForOwnedObject(existingDri); + ownershipRepository.save(new Ownership(new Owner() { + public String getOwnerId() { return dynRegRepresentation.getIdOfOwner(); } + public OwnerType getOwnerType() { return OwnerType.GROUP; } + }, existingDri)); + return new DynamicRegistrationRepresentation(existingDri); + } + + @Override + public DynamicRegistrationRepresentation updateGroupForDynamicRegistration(String resourceId, String groupId) throws ForbiddenException, PersistentEntityNotFound { + DynamicRegistrationInfo existingDri = repository.findByResourceId(resourceId); + if (existingDri == null) { + throw new PersistentEntityNotFound(String.format("The dynamic registration with id [%s] was not found for update.", existingDri.getResourceId())); + } + if (!shibUiAuthorizationDelegate.hasPermission(userService.getCurrentUserAuthentication(), existingDri, PermissionType.admin)) { + throw new ForbiddenException("You do not have the permissions necessary to change the group for this service."); + } + existingDri.setIdOfOwner(groupId); + + Group group = groupService.find(groupId); + ownershipRepository.deleteEntriesForOwnedObject(existingDri); + ownershipRepository.save(new Ownership(group, existingDri)); + // check and see if we need to update the approved status + if (!existingDri.isEnabled()) { + int numApprovers = group.getApproversList().size(); + existingDri.setApproved(!(numApprovers > 0 && existingDri.approvedCount() < numApprovers)); + } + + DynamicRegistrationInfo savedEntity = repository.save(existingDri); + return new DynamicRegistrationRepresentation(savedEntity); + } +} \ No newline at end of file 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/dynamic-registration.schema.json b/backend/src/main/resources/dynamic-registration.schema.json new file mode 100644 index 000000000..2ff19e746 --- /dev/null +++ b/backend/src/main/resources/dynamic-registration.schema.json @@ -0,0 +1,101 @@ +{ + "type": "object", + "required": [ + "redirectUris" + ], + "properties": { + "applicationType": { + "title": "label.application-type", + "description": "tooltip.application-type", + "type": "string" + }, + "approved": { + "title": "label.approved", + "description": "tooltip.approved", + "type": "boolean" + }, + "contacts": { + "title": "label.contacts", + "description": "tooltip.contacts", + "type": "string" + }, + "enabled": { + "title": "label.enabled", + "description": "tooltip.enabled", + "type": "boolean" + }, + "grantType": { + "title": "label.grant-type", + "description": "tooltip.grant-type", + "type": "string", + "widget": "select", + "minLength": 1, + "oneOf": [ + { + "enum": ["authorization_code"], + "description": "value.authorization-code" + }, + { + "enum": ["implicit"], + "description": "value.implicit" + }, + { + "enum": ["refresh_token"], + "description": "value.refresh-token" + } + ] + }, + "jwks": { + "title": "label.jwks", + "description": "tooltip.jwks", + "type": "string" + }, + "logoUri": { + "title": "label.logo-uri", + "description": "tooltip.logo-uri", + "type": "string" + }, + "name": { + "title": "label.name", + "description": "tooltip.name", + "type": "string" + }, + "policyUri": { + "title": "label.policy-uri", + "description": "tooltip.policy-uri", + "type": "string" + }, + "redirectUris": { + "title": "label.redirect-uris", + "description": "tooltip.redirect-uris", + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "responseTypes": { + "title": "label.response-types", + "description": "tooltip.response-types", + "type": "string" + }, + "scope": { + "title": "label.scope", + "description": "tooltip.scope", + "type": "string" + }, + "subjectType": { + "title": "label.subject-type", + "description": "tooltip.subject-type", + "type": "string" + }, + "tokenEndpointAuthMethod": { + "title": "label.token-endpoint-auth-method", + "description": "tooltip.token-endpoint-auth-method", + "type": "string" + }, + "tosUri": { + "title": "label.tos-uri", + "description": "tooltip.tos-uri", + "type": "string" + } + } +} \ No newline at end of file diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 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..ab5b88d1c 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.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..97e826003 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,17 @@ 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.DynamicRegistrationInfoRepository 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 +59,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 +109,9 @@ class BaseDataJpaTestConfiguration { listener.init(ownershipRepository, groupRepo) return listener } + + @Bean + public IShibUiPermissionEvaluator shibUiPermissionEvaluator(DynamicRegistrationInfoRepository driRepo, EntityDescriptorRepository entityDescriptorRepository, UserService userService) { + return new ShibUiPermissionDelegate(driRepo, 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..6354f5538 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateControllerTests.groovy @@ -0,0 +1,235 @@ +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.domain.oidc.DynamicRegistrationInfo +import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.GrantType +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.security.repository.DynamicRegistrationInfoRepository +import edu.internet2.tier.shibboleth.admin.ui.service.DynamicRegistrationService +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 + DynamicRegistrationInfoRepository dynamicRegistrationInfoRepository + + @Autowired + DynamicRegistrationService dynamicRegistrationService + + @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 + controller.dynamicRegistrationService = dynamicRegistrationService + 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() + + def dynReg = new DynamicRegistrationInfo(resourceId: 'uuid-1', enabled: false, idOfOwner: "AAA", applicationType: 'apptype', + approved: true, contacts: 'contacts', jwks: 'jwks', logoUri: 'logouri', policyUri: 'policyuri', + redirectUris: 'redirecturis', responseTypes: 'responsetypes', scope: 'scope', subjectType: 'subjecttype', + tokenEndpointAuthMethod: 'token', tosUri: 'tosuri', grantType: GrantType.implicit) + dynamicRegistrationInfoRepository.saveAndFlush(dynReg) + } + + @WithMockUser(value = "AUser", roles = ["USER"]) + def 'Owner group cannot activate their own dynamic registration without approvals'() { + expect: + try { + mockMvc.perform(patch("/api/activate/DynamicRegistration/uuid-1/enable")) + } + catch (Exception e) { + e instanceof ForbiddenException + } + } + + @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 dynamic registration'() { + expect: + try { + mockMvc.perform(patch("/api/activate/DynamicRegistration/uuid-1/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 dynamic registration without approval'() { +// given: +// def dynReg = new DynamicRegistrationInfo(resourceId: 'uuid-2', enabled: false, applicationType: 'apptype', +// approved: true, contacts: 'contacts', jwks: 'jwks', logoUri: 'logouri', policyUri: 'policyuri', +// redirectUris: 'redirecturis', responseTypes: 'responsetypes', scope: 'scope', subjectType: 'subjecttype', +// tokenEndpointAuthMethod: 'token', tosUri: 'tosuri', grantType: GrantType.implicit) +// dynamicRegistrationService.createNew(new DynamicRegistrationRepresentation(dynReg)) +// +// when: +// def result = mockMvc.perform(patch("/api/activate/DynamicRegistration/uuid-2/enable")) +// +// then: +// result.andExpect(status().isOk()) +// .andExpect(jsonPath("\$.resourceId").value("uuid-2")) +// .andExpect(jsonPath("\$.enabled").value(true)) +// } + + @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 dynamic registration with approvals'() { +// when: +// def dynReg = new DynamicRegistrationInfo(resourceId: 'uuid-2', enabled: false, idOfOwner: "AAA", applicationType: 'apptype', +// approved: true, contacts: 'contacts', jwks: 'jwks', logoUri: 'logouri', policyUri: 'policyuri', +// redirectUris: 'redirecturis', responseTypes: 'responsetypes', scope: 'scope', subjectType: 'subjecttype', +// tokenEndpointAuthMethod: 'token', tosUri: 'tosuri', grantType: GrantType.implicit) +// dynReg = dynamicRegistrationService.createNew(dynReg) +// dynReg.addApproval(groupService.find("CCC")) +// dynamicRegistrationService.update(dynReg) +// entityManager.flush() +// +// def result = mockMvc.perform(patch("/api/activate/DynamicRegistration/uuid-2/enable")) +// +// then: +// result.andExpect(status().isOk()) +// .andExpect(jsonPath("\$.resourceId").value('uuid-2')) +// .andExpect(jsonPath("\$.enabled").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..b676ef724 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ApproveControllerTests.groovy @@ -0,0 +1,261 @@ +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.domain.oidc.DynamicRegistrationInfo +import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.GrantType +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.security.repository.DynamicRegistrationInfoRepository +import edu.internet2.tier.shibboleth.admin.ui.service.DynamicRegistrationService +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 + DynamicRegistrationInfoRepository dynamicRegistrationInfoRepository + + @Autowired + DynamicRegistrationService dynamicRegistrationService + + @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 + controller.dynamicRegistrationService = dynamicRegistrationService + 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() + + def dynReg = new DynamicRegistrationInfo(resourceId: 'uuid-1', enabled: false, idOfOwner: "AAA", applicationType: 'apptype', + approved: false, contacts: 'contacts', jwks: 'jwks', logoUri: 'logouri', policyUri: 'policyuri', + redirectUris: 'redirecturis', responseTypes: 'responsetypes', scope: 'scope', subjectType: 'subjecttype', + tokenEndpointAuthMethod: 'token', tosUri: 'tosuri', grantType: GrantType.implicit) + dynamicRegistrationInfoRepository.saveAndFlush(dynReg) + } + + @WithMockUser(value = "AUser", roles = ["USER"]) + def 'Owner group cannot approve their own dynamic registration'() { + expect: + dynamicRegistrationInfoRepository.findByResourceId("uuid-1").isApproved() == false + try { + mockMvc.perform(patch("/api/approve/DynamicRegistration/uuid-1/approve")) + } + catch (Exception e) { + e instanceof ForbiddenException + } + dynamicRegistrationInfoRepository.findByResourceId("uuid-1").isApproved() == false + } + + @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 dynamic registration'() { + expect: + dynamicRegistrationInfoRepository.findByResourceId("uuid-1").isApproved() == false + try { + mockMvc.perform(patch("/api/approve/DynamicRegistration/uuid-1/approve")) + } + catch (Exception e) { + e instanceof ForbiddenException + } + dynamicRegistrationInfoRepository.findByResourceId("uuid-1").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 dynamic registration'() { + expect: + dynamicRegistrationInfoRepository.findByResourceId("uuid-1").isApproved() == false + + when: + def result = mockMvc.perform(patch("/api/approve/DynamicRegistration/uuid-1/approve")) + + then: + result.andExpect(status().isOk()) + .andExpect(jsonPath("\$.resourceId").value("uuid-1")) + .andExpect(jsonPath("\$.enabled").value(false)) + .andExpect(jsonPath("\$.approved").value(true)) + dynamicRegistrationInfoRepository.findByResourceId("uuid-1").isApproved() + } + + @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 dynamic registration'() { + expect: + dynamicRegistrationInfoRepository.findByResourceId("uuid-1").isApproved() == false + + when: + def result = mockMvc.perform(patch("/api/approve/DynamicRegistration/uuid-1/approve")) + + then: + result.andExpect(status().isOk()) + .andExpect(jsonPath("\$.resourceId").value("uuid-1")) + .andExpect(jsonPath("\$.enabled").value(false)) + .andExpect(jsonPath("\$.approved").value(true)) + dynamicRegistrationInfoRepository.findByResourceId("uuid-1").isApproved() + + when: + def result2 = mockMvc.perform(patch("/api/approve/DynamicRegistration/uuid-1/unapprove")) + + then: + result2.andExpect(status().isOk()) + .andExpect(jsonPath("\$.resourceId").value("uuid-1")) + .andExpect(jsonPath("\$.enabled").value(false)) + .andExpect(jsonPath("\$.approved").value(false)) + dynamicRegistrationInfoRepository.findByResourceId("uuid-1").isApproved() == false + } + + @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/DynamicRegistrationControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DynamicRegistrationControllerTests.groovy new file mode 100644 index 000000000..d2a0d258d --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DynamicRegistrationControllerTests.groovy @@ -0,0 +1,527 @@ +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.frontend.DynamicRegistrationRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.DynamicRegistrationInfo +import edu.internet2.tier.shibboleth.admin.ui.domain.oidc.GrantType +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.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.DynamicRegistrationInfoRepository +import edu.internet2.tier.shibboleth.admin.ui.service.DynamicRegistrationService +import edu.internet2.tier.shibboleth.admin.ui.service.EntityService +import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin +import org.hamcrest.Matchers +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.web.servlet.result.MockMvcResultHandlers +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.transaction.annotation.Transactional +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.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 + +class DynamicRegistrationControllerTests extends AbstractBaseDataJpaTest { + @Autowired + DynamicRegistrationService dynamicRegistrationService; + + @Autowired + DynamicRegistrationInfoRepository repo + + @Autowired + EntityManager entityManager + + @Autowired + EntityService entityService + + @Autowired + ObjectMapper mapper + + def mockMvc + + @Subject + def controller + + @Transactional + def setup() { + Group ga = new Group() + ga.setResourceId("testingGroupAAA") + ga.setName("Group AAA") + ga = groupService.createGroup(ga) + + Group gb = new Group() + gb.setResourceId("testingGroupBBB") + gb.setName("Group BBB") + gb = groupService.createGroup(gb) + + controller = new DynamicRegistrationController() + controller.dynamicRegistrationService = dynamicRegistrationService + 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) + } + + @WithMockAdmin + def 'DELETE as admin'() { + given: + def dynReg = new DynamicRegistrationInfo(resourceId: 'uuid-1', enabled: false, idOfOwner: "admingroup", applicationType: 'apptype', + approved: true, contacts: 'contacts', jwks: 'jwks', logoUri: 'logouri', policyUri: 'policyuri', + redirectUris: 'redirecturis', responseTypes: 'responsetypes', scope: 'scope', subjectType: 'subjecttype', + tokenEndpointAuthMethod: 'token', tosUri: 'tosuri', grantType: GrantType.implicit) + repo.saveAndFlush(dynReg) + def dris = repo.findAll() + + expect: + dris.size() == 1 + + when: + def result = mockMvc.perform(delete("/api/DynamicRegistration/" + dris.get(0).getResourceId())) + + then: + result.andExpect(status().isNoContent()) + repo.findByResourceId("uuid-1") == null + repo.findAll().size() == 0 + + // persistent entity doesn't exist + try { + def result2 = mockMvc.perform(delete("/api/DynamicRegistration/uuid-1")) + } catch (Exception e) { + e instanceof PersistentEntityNotFound + } + } + + + @WithMockUser(value = "someUser", roles = ["USER"]) + def 'DELETE as non-admin'() { + given: + def dynReg = new DynamicRegistrationInfo(resourceId: 'uuid-1', enabled: false, idOfOwner: "admingroup", applicationType: 'apptype', + approved: true, contacts: 'contacts', jwks: 'jwks', logoUri: 'logouri', policyUri: 'policyuri', + redirectUris: 'redirecturis', responseTypes: 'responsetypes', scope: 'scope', subjectType: 'subjecttype', + tokenEndpointAuthMethod: 'token', tosUri: 'tosuri', grantType: GrantType.implicit) + repo.saveAndFlush(dynReg) + def dris = repo.findAll() + + expect: + dris.size() == 1 + try { + def result = mockMvc.perform(delete("/api/DynamicRegistration/" + dris.get(0).getResourceId())) + } catch (Exception e) { + e instanceof ForbiddenException + } + } + + @WithMockAdmin + def 'GET /DynamicRegistrations with empty repository as admin'() { + when: + def result = mockMvc.perform(get('/api/DynamicRegistrations')) + + then: + result.andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)).andExpect(content().json('[]')) + } + + @WithMockAdmin + def 'GET /DynamicRegistrations with 1 record in repository as admin'() { + given: + def dynReg = new DynamicRegistrationInfo(resourceId: 'uuid-1', enabled: false, idOfOwner: "admingroup", applicationType: 'apptype', + approved: true, contacts: 'contacts', jwks: 'jwks', logoUri: 'logouri', policyUri: 'policyuri', + redirectUris: 'redirecturis', responseTypes: 'responsetypes', scope: 'scope', subjectType: 'subjecttype', + tokenEndpointAuthMethod: 'token', tosUri: 'tosuri', grantType: GrantType.implicit) + repo.saveAndFlush(dynReg) + + when: + def result = mockMvc.perform(get('/api/DynamicRegistrations')) + + then: + result.andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.[0].resourceId").value("uuid-1")) + .andExpect(jsonPath("\$.[0].enabled").value(false)) + .andExpect(jsonPath("\$.[0].idOfOwner").value("admingroup")) + .andExpect(jsonPath("\$.[0].applicationType").value("apptype")) + .andExpect(jsonPath("\$.[0].contacts").value("contacts")) + .andExpect(jsonPath("\$.[0].jwks").value("jwks")) + .andExpect(jsonPath("\$.[0].logoUri").value("logouri")) + .andExpect(jsonPath("\$.[0].policyUri").value("policyuri")) + .andExpect(jsonPath("\$.[0].redirectUris").value("redirecturis")) + .andExpect(jsonPath("\$.[0].responseTypes").value("responsetypes")) + .andExpect(jsonPath("\$.[0].scope").value("scope")) + .andExpect(jsonPath("\$.[0].subjectType").value("subjecttype")) + .andExpect(jsonPath("\$.[0].tokenEndpointAuthMethod").value("token")) + .andExpect(jsonPath("\$.[0].tosUri").value("tosuri")) + .andExpect(jsonPath("\$.[0].grantType").value("implicit")) + + when: // add another so there should be two results + def dynReg2 = new DynamicRegistrationInfo(resourceId: 'uuid-2', enabled: false, idOfOwner: "admingroup", applicationType: 'apptype2', + approved: true, contacts: 'contacts2', jwks: 'jwks2', logoUri: 'logouri2', policyUri: 'policyuri2', + redirectUris: 'redirecturis2', responseTypes: 'responsetypes2', scope: 'scope2', subjectType: 'subjecttype2', + tokenEndpointAuthMethod: 'token2', tosUri: 'tosuri2', grantType: GrantType.implicit) + repo.saveAndFlush(dynReg2) + def result2 = mockMvc.perform(get('/api/DynamicRegistrations')) + + then: + result2.andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.[0].resourceId").value("uuid-1")) + .andExpect(jsonPath("\$.[0].enabled").value(false)) + .andExpect(jsonPath("\$.[0].idOfOwner").value("admingroup")) + .andExpect(jsonPath("\$.[0].applicationType").value("apptype")) + .andExpect(jsonPath("\$.[0].contacts").value("contacts")) + .andExpect(jsonPath("\$.[0].jwks").value("jwks")) + .andExpect(jsonPath("\$.[0].logoUri").value("logouri")) + .andExpect(jsonPath("\$.[0].policyUri").value("policyuri")) + .andExpect(jsonPath("\$.[0].redirectUris").value("redirecturis")) + .andExpect(jsonPath("\$.[0].responseTypes").value("responsetypes")) + .andExpect(jsonPath("\$.[0].scope").value("scope")) + .andExpect(jsonPath("\$.[0].subjectType").value("subjecttype")) + .andExpect(jsonPath("\$.[0].tokenEndpointAuthMethod").value("token")) + .andExpect(jsonPath("\$.[0].tosUri").value("tosuri")) + .andExpect(jsonPath("\$.[0].grantType").value("implicit")) + + .andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.[1].resourceId").value("uuid-2")) + .andExpect(jsonPath("\$.[1].enabled").value(false)) + .andExpect(jsonPath("\$.[1].idOfOwner").value("admingroup")) + .andExpect(jsonPath("\$.[1].applicationType").value("apptype2")) + .andExpect(jsonPath("\$.[1].contacts").value("contacts2")) + .andExpect(jsonPath("\$.[1].jwks").value("jwks2")) + .andExpect(jsonPath("\$.[1].logoUri").value("logouri2")) + .andExpect(jsonPath("\$.[1].policyUri").value("policyuri2")) + .andExpect(jsonPath("\$.[1].redirectUris").value("redirecturis2")) + .andExpect(jsonPath("\$.[1].responseTypes").value("responsetypes2")) + .andExpect(jsonPath("\$.[1].scope").value("scope2")) + .andExpect(jsonPath("\$.[1].subjectType").value("subjecttype2")) + .andExpect(jsonPath("\$.[1].tokenEndpointAuthMethod").value("token2")) + .andExpect(jsonPath("\$.[1].tosUri").value("tosuri2")) + .andExpect(jsonPath("\$.[1].grantType").value("implicit")) + } + + @WithMockUser(value = "someUser", roles = ["USER"]) + def 'GET someuser /DynamicRegistrations return correct responses'() { + given: + def dynReg = new DynamicRegistrationInfo(resourceId: 'uuid-1', enabled: false, idOfOwner: "testingGroupAAA", applicationType: 'apptypea', + approved: true, contacts: 'contactsa', jwks: 'jwksa', logoUri: 'logouria', policyUri: 'policyuria', + redirectUris: 'redirecturisa', responseTypes: 'responsetypesa', scope: 'scopea', subjectType: 'subjecttypea', + tokenEndpointAuthMethod: 'tokena', tosUri: 'tosuria', grantType: GrantType.implicit) + repo.saveAndFlush(dynReg) + + when: + def result1 = mockMvc.perform(get('/api/DynamicRegistrations')) + + then: + result1.andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)).andExpect(jsonPath("\$").isEmpty()) + + when: + def dynReg2 = new DynamicRegistrationInfo(resourceId: 'uuid-2', enabled: false, applicationType: 'apptype', + approved: true, contacts: 'contacts', jwks: 'jwks', logoUri: 'logouri', policyUri: 'policyuri', + redirectUris: 'redirecturis', responseTypes: 'responsetypes', scope: 'scope', subjectType: 'subjecttype', + tokenEndpointAuthMethod: 'token', tosUri: 'tosuri', grantType: GrantType.implicit) + dynamicRegistrationService.createNew(new DynamicRegistrationRepresentation(dynReg2)) + def result = mockMvc.perform(get('/api/DynamicRegistrations')) + + then: + result.andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$", Matchers.hasSize(1))) + .andExpect(jsonPath("\$.[0].resourceId").value("uuid-2")) + .andExpect(jsonPath("\$.[0].enabled").value(false)) + .andExpect(jsonPath("\$.[0].idOfOwner").value("testingGroupBBB")) + .andExpect(jsonPath("\$.[0].applicationType").value("apptype")) + .andExpect(jsonPath("\$.[0].contacts").value("contacts")) + .andExpect(jsonPath("\$.[0].jwks").value("jwks")) + .andExpect(jsonPath("\$.[0].logoUri").value("logouri")) + .andExpect(jsonPath("\$.[0].policyUri").value("policyuri")) + .andExpect(jsonPath("\$.[0].redirectUris").value("redirecturis")) + .andExpect(jsonPath("\$.[0].responseTypes").value("responsetypes")) + .andExpect(jsonPath("\$.[0].scope").value("scope")) + .andExpect(jsonPath("\$.[0].subjectType").value("subjecttype")) + .andExpect(jsonPath("\$.[0].tokenEndpointAuthMethod").value("token")) + .andExpect(jsonPath("\$.[0].tosUri").value("tosuri")) + .andExpect(jsonPath("\$.[0].grantType").value("implicit")) + + try { + mockMvc.perform(get('/api/DynamicRegistration/uuid-1')) + } catch (Exception e) { + e instanceof ForbiddenException + } + + def result2 = mockMvc.perform(get('/api/DynamicRegistration/uuid-2')) + result2.andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.resourceId").value("uuid-2")) + .andExpect(jsonPath("\$.enabled").value(false)) + .andExpect(jsonPath("\$.idOfOwner").value("testingGroupBBB")) + .andExpect(jsonPath("\$.applicationType").value("apptype")) + .andExpect(jsonPath("\$.contacts").value("contacts")) + .andExpect(jsonPath("\$.jwks").value("jwks")) + .andExpect(jsonPath("\$.logoUri").value("logouri")) + .andExpect(jsonPath("\$.policyUri").value("policyuri")) + .andExpect(jsonPath("\$.redirectUris").value("redirecturis")) + .andExpect(jsonPath("\$.responseTypes").value("responsetypes")) + .andExpect(jsonPath("\$.scope").value("scope")) + .andExpect(jsonPath("\$.subjectType").value("subjecttype")) + .andExpect(jsonPath("\$.tokenEndpointAuthMethod").value("token")) + .andExpect(jsonPath("\$.tosUri").value("tosuri")) + .andExpect(jsonPath("\$.grantType").value("implicit")) + } + + @WithMockUser(value = "someUser", roles = ["USER"]) + def 'POST create new '() { + given: + def dynReg = new DynamicRegistrationInfo(resourceId: 'uuid-2', enabled: false, applicationType: 'apptype', + approved: true, contacts: 'contacts', jwks: 'jwks', logoUri: 'logouri', policyUri: 'policyuri', + redirectUris: 'redirecturis', responseTypes: 'responsetypes', scope: 'scope', subjectType: 'subjecttype', + tokenEndpointAuthMethod: 'token', tosUri: 'tosuri', grantType: GrantType.implicit) + def driJson = mapper.writeValueAsString(dynReg) + + when: + def result = mockMvc.perform(post('/api/DynamicRegistration').contentType(APPLICATION_JSON).content(driJson)) + + then: + result.andDo(MockMvcResultHandlers.print()) + .andExpect(status().isCreated()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.resourceId").value("uuid-2")) + .andExpect(jsonPath("\$.enabled").value(false)) + .andExpect(jsonPath("\$.idOfOwner").value("testingGroupBBB")) + .andExpect(jsonPath("\$.applicationType").value("apptype")) + .andExpect(jsonPath("\$.contacts").value("contacts")) + .andExpect(jsonPath("\$.jwks").value("jwks")) + .andExpect(jsonPath("\$.logoUri").value("logouri")) + .andExpect(jsonPath("\$.policyUri").value("policyuri")) + .andExpect(jsonPath("\$.redirectUris").value("redirecturis")) + .andExpect(jsonPath("\$.responseTypes").value("responsetypes")) + .andExpect(jsonPath("\$.scope").value("scope")) + .andExpect(jsonPath("\$.subjectType").value("subjecttype")) + .andExpect(jsonPath("\$.tokenEndpointAuthMethod").value("token")) + .andExpect(jsonPath("\$.tosUri").value("tosuri")) + .andExpect(jsonPath("\$.grantType").value("implicit")) + + } + + @WithMockAdmin + def "PUT /DynamicRegistration updates properly as admin"() { + given: + def dynReg = new DynamicRegistrationInfo(resourceId: 'uuid-1', enabled: false, idOfOwner: "admingroup", applicationType: 'apptype', + approved: true, contacts: 'contacts', jwks: 'jwks', logoUri: 'logouri', policyUri: 'policyuri', + redirectUris: 'redirecturis', responseTypes: 'responsetypes', scope: 'scope', subjectType: 'subjecttype', + tokenEndpointAuthMethod: 'token', tosUri: 'tosuri', grantType: GrantType.implicit) + repo.saveAndFlush(dynReg) + def result = mockMvc.perform(get('/api/DynamicRegistrations')) + + expect: + result.andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.[0].resourceId").value("uuid-1")) + .andExpect(jsonPath("\$.[0].enabled").value(false)) + .andExpect(jsonPath("\$.[0].idOfOwner").value("admingroup")) + .andExpect(jsonPath("\$.[0].applicationType").value("apptype")) + .andExpect(jsonPath("\$.[0].contacts").value("contacts")) + .andExpect(jsonPath("\$.[0].jwks").value("jwks")) + .andExpect(jsonPath("\$.[0].logoUri").value("logouri")) + .andExpect(jsonPath("\$.[0].policyUri").value("policyuri")) + .andExpect(jsonPath("\$.[0].redirectUris").value("redirecturis")) + .andExpect(jsonPath("\$.[0].responseTypes").value("responsetypes")) + .andExpect(jsonPath("\$.[0].scope").value("scope")) + .andExpect(jsonPath("\$.[0].subjectType").value("subjecttype")) + .andExpect(jsonPath("\$.[0].tokenEndpointAuthMethod").value("token")) + .andExpect(jsonPath("\$.[0].tosUri").value("tosuri")) + .andExpect(jsonPath("\$.[0].grantType").value("implicit")) + + when: + def dri = repo.findByResourceId("uuid-1") + DynamicRegistrationRepresentation drr = new DynamicRegistrationRepresentation(dri) + drr.setApplicationType("apptype2") + drr.setJwks("jwks2") + drr.setContacts("contacts2") + drr.setEnabled(true) + def drrJson = mapper.writeValueAsString(drr) + def update = mockMvc.perform(put("/api/DynamicRegistration/uuid-1").contentType(APPLICATION_JSON).content(drrJson)) + + then: + update.andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.resourceId").value("uuid-1")) + .andExpect(jsonPath("\$.enabled").value(true)) + .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) + .andExpect(jsonPath("\$.applicationType").value("apptype2")) + .andExpect(jsonPath("\$.contacts").value("contacts2")) + .andExpect(jsonPath("\$.jwks").value("jwks2")) + .andExpect(jsonPath("\$.logoUri").value("logouri")) + .andExpect(jsonPath("\$.policyUri").value("policyuri")) + .andExpect(jsonPath("\$.redirectUris").value("redirecturis")) + .andExpect(jsonPath("\$.responseTypes").value("responsetypes")) + .andExpect(jsonPath("\$.scope").value("scope")) + .andExpect(jsonPath("\$.subjectType").value("subjecttype")) + .andExpect(jsonPath("\$.tokenEndpointAuthMethod").value("token")) + .andExpect(jsonPath("\$.tosUri").value("tosuri")) + .andExpect(jsonPath("\$.grantType").value("implicit")) + } + + @WithMockUser(value = "someUser", roles = ["USER"]) + def "PUT /DynamicRegistration disallows non-admin user from enabling"() { + given: + def dynReg = new DynamicRegistrationInfo(resourceId: 'uuid-1', enabled: false, idOfOwner: "testingGroupBBB", applicationType: 'apptype', + approved: true, contacts: 'contacts', jwks: 'jwks', logoUri: 'logouri', policyUri: 'policyuri', + redirectUris: 'redirecturis', responseTypes: 'responsetypes', scope: 'scope', subjectType: 'subjecttype', + tokenEndpointAuthMethod: 'token', tosUri: 'tosuri', grantType: GrantType.implicit) + repo.saveAndFlush(dynReg) + def result = mockMvc.perform(get('/api/DynamicRegistrations')) + + expect: + result.andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.[0].resourceId").value("uuid-1")) + .andExpect(jsonPath("\$.[0].enabled").value(false)) + .andExpect(jsonPath("\$.[0].idOfOwner").value("testingGroupBBB")) + .andExpect(jsonPath("\$.[0].applicationType").value("apptype")) + .andExpect(jsonPath("\$.[0].contacts").value("contacts")) + .andExpect(jsonPath("\$.[0].jwks").value("jwks")) + .andExpect(jsonPath("\$.[0].logoUri").value("logouri")) + .andExpect(jsonPath("\$.[0].policyUri").value("policyuri")) + .andExpect(jsonPath("\$.[0].redirectUris").value("redirecturis")) + .andExpect(jsonPath("\$.[0].responseTypes").value("responsetypes")) + .andExpect(jsonPath("\$.[0].scope").value("scope")) + .andExpect(jsonPath("\$.[0].subjectType").value("subjecttype")) + .andExpect(jsonPath("\$.[0].tokenEndpointAuthMethod").value("token")) + .andExpect(jsonPath("\$.[0].tosUri").value("tosuri")) + .andExpect(jsonPath("\$.[0].grantType").value("implicit")) + + when: + def dri = repo.findByResourceId("uuid-1") + DynamicRegistrationRepresentation drr = new DynamicRegistrationRepresentation(dri) + drr.setApplicationType("apptype2") + drr.setJwks("jwks2") + drr.setContacts("contacts2") + drr.setEnabled(true) + def drrJson = mapper.writeValueAsString(drr) + + then: + try { + mockMvc.perform(put("/api/DynamicRegistration/uuid-1").contentType(APPLICATION_JSON).content(drrJson)) + } catch (Exception e) { + e instanceof ForbiddenException + } + } + + @WithMockUser(value = "someUser", roles = ["USER"]) + def "PUT /DynamicRegistration denies the request if the PUTing user is not an ADMIN and not in the owner group"() { + when: + def dynReg = new DynamicRegistrationInfo(resourceId: 'uuid-1', enabled: false, idOfOwner: "testingGroupAAA", applicationType: 'apptype', + approved: true, contacts: 'contacts', jwks: 'jwks', logoUri: 'logouri', policyUri: 'policyuri', + redirectUris: 'redirecturis', responseTypes: 'responsetypes', scope: 'scope', subjectType: 'subjecttype', + tokenEndpointAuthMethod: 'token', tosUri: 'tosuri', grantType: GrantType.implicit) + def dri = repo.saveAndFlush(dynReg) + + DynamicRegistrationRepresentation drr = new DynamicRegistrationRepresentation(dri) + drr.setApplicationType("apptype2") + def drrJson = mapper.writeValueAsString(drr) + + then: + try { + mockMvc.perform(put("/api/DynamicRegistration/uuid-1").contentType(APPLICATION_JSON).content(drrJson)) + } catch (Exception e) { + e instanceof ForbiddenException + } + } + + @WithMockAdmin + def "PUT /DynamicRegistration update group as admin"() { + given: + def dynReg = new DynamicRegistrationInfo(resourceId: 'uuid-1', enabled: false, idOfOwner: "AAA", applicationType: 'apptype', + approved: true, contacts: 'contacts', jwks: 'jwks', logoUri: 'logouri', policyUri: 'policyuri', + redirectUris: 'redirecturis', responseTypes: 'responsetypes', scope: 'scope', subjectType: 'subjecttype', + tokenEndpointAuthMethod: 'token', tosUri: 'tosuri', grantType: GrantType.implicit) + repo.saveAndFlush(dynReg) + def result = mockMvc.perform(get('/api/DynamicRegistrations')) + + expect: + result.andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.[0].resourceId").value("uuid-1")) + .andExpect(jsonPath("\$.[0].enabled").value(false)) + .andExpect(jsonPath("\$.[0].idOfOwner").value("AAA")) + .andExpect(jsonPath("\$.[0].applicationType").value("apptype")) + .andExpect(jsonPath("\$.[0].contacts").value("contacts")) + .andExpect(jsonPath("\$.[0].jwks").value("jwks")) + .andExpect(jsonPath("\$.[0].logoUri").value("logouri")) + .andExpect(jsonPath("\$.[0].policyUri").value("policyuri")) + .andExpect(jsonPath("\$.[0].redirectUris").value("redirecturis")) + .andExpect(jsonPath("\$.[0].responseTypes").value("responsetypes")) + .andExpect(jsonPath("\$.[0].scope").value("scope")) + .andExpect(jsonPath("\$.[0].subjectType").value("subjecttype")) + .andExpect(jsonPath("\$.[0].tokenEndpointAuthMethod").value("token")) + .andExpect(jsonPath("\$.[0].tosUri").value("tosuri")) + .andExpect(jsonPath("\$.[0].grantType").value("implicit")) + + when: + def update = mockMvc.perform(put("/api/DynamicRegistration/uuid-1/changeGroup/testingGroupBBB")) + + then: + update.andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.resourceId").value("uuid-1")) + .andExpect(jsonPath("\$.enabled").value(false)) + .andExpect(jsonPath("\$.idOfOwner").value("testingGroupBBB")) + .andExpect(jsonPath("\$.applicationType").value("apptype")) + .andExpect(jsonPath("\$.contacts").value("contacts")) + .andExpect(jsonPath("\$.jwks").value("jwks")) + .andExpect(jsonPath("\$.logoUri").value("logouri")) + .andExpect(jsonPath("\$.policyUri").value("policyuri")) + .andExpect(jsonPath("\$.redirectUris").value("redirecturis")) + .andExpect(jsonPath("\$.responseTypes").value("responsetypes")) + .andExpect(jsonPath("\$.scope").value("scope")) + .andExpect(jsonPath("\$.subjectType").value("subjecttype")) + .andExpect(jsonPath("\$.tokenEndpointAuthMethod").value("token")) + .andExpect(jsonPath("\$.tosUri").value("tosuri")) + .andExpect(jsonPath("\$.grantType").value("implicit")) + } + + @WithMockUser(value = "someUser", roles = ["USER"]) + def "PUT /DynamicRegistration update group as user shouldn't work "() { + given: + def dynReg = new DynamicRegistrationInfo(resourceId: 'uuid-1', enabled: false, idOfOwner: "testingGroupBBB", applicationType: 'apptype', + approved: true, contacts: 'contacts', jwks: 'jwks', logoUri: 'logouri', policyUri: 'policyuri', + redirectUris: 'redirecturis', responseTypes: 'responsetypes', scope: 'scope', subjectType: 'subjecttype', + tokenEndpointAuthMethod: 'token', tosUri: 'tosuri', grantType: GrantType.implicit) + repo.saveAndFlush(dynReg) + def result = mockMvc.perform(get('/api/DynamicRegistrations')) + + expect: + result.andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()).andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.[0].resourceId").value("uuid-1")) + .andExpect(jsonPath("\$.[0].enabled").value(false)) + .andExpect(jsonPath("\$.[0].idOfOwner").value("testingGroupBBB")) + .andExpect(jsonPath("\$.[0].applicationType").value("apptype")) + .andExpect(jsonPath("\$.[0].contacts").value("contacts")) + .andExpect(jsonPath("\$.[0].jwks").value("jwks")) + .andExpect(jsonPath("\$.[0].logoUri").value("logouri")) + .andExpect(jsonPath("\$.[0].policyUri").value("policyuri")) + .andExpect(jsonPath("\$.[0].redirectUris").value("redirecturis")) + .andExpect(jsonPath("\$.[0].responseTypes").value("responsetypes")) + .andExpect(jsonPath("\$.[0].scope").value("scope")) + .andExpect(jsonPath("\$.[0].subjectType").value("subjecttype")) + .andExpect(jsonPath("\$.[0].tokenEndpointAuthMethod").value("token")) + .andExpect(jsonPath("\$.[0].tosUri").value("tosuri")) + .andExpect(jsonPath("\$.[0].grantType").value("implicit")) + + try { + def update = mockMvc.perform(put("/api/DynamicRegistration/uuid-1/changeGroup/testingGroupAAA")) + } 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/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..411066916 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 @@ -9,7 +9,6 @@ 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.envers.EnversVersionServiceSupport import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects -import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository import edu.internet2.tier.shibboleth.admin.ui.security.model.Group import edu.internet2.tier.shibboleth.admin.ui.security.model.Role import edu.internet2.tier.shibboleth.admin.ui.security.model.User @@ -43,9 +42,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 +162,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..fcfa882c4 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,12 @@ 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.util.WithMockAdmin import groovy.json.JsonOutput import org.springframework.beans.factory.annotation.Autowired @@ -17,14 +19,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 +43,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 +58,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 +134,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..99ca11bd5 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/permission/ShibUiPermissionDelegateTests.groovy @@ -0,0 +1,205 @@ +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.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.security.repository.DynamicRegistrationInfoRepository +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 + DynamicRegistrationInfoRepository dynamicRegistrationInfoRepository + + @Autowired + JPAEntityDescriptorServiceImpl jpaEntityDescriptorService + + def entityDescriptor + def entityDescriptor2 + def entityDescriptor3 + + @Transactional + def setup() { + delegate = new ShibUiPermissionDelegate(dynamicRegistrationInfoRepository, 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/gradle.properties b/gradle.properties index 395c835fc..7af90e6ae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ name=shibui group=edu.internet2.tier.shibboleth.admin.ui -version=1.14.0-SNAPSHOT +version=1.15.0-SNAPSHOT ### library versions ### commonsCollections4Version=4.4 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/ConfigurationForm.js b/ui/src/app/admin/component/ConfigurationForm.js index bceac7a42..3c5b2a84e 100644 --- a/ui/src/app/admin/component/ConfigurationForm.js +++ b/ui/src/app/admin/component/ConfigurationForm.js @@ -17,7 +17,7 @@ import { includes, uniqBy } from 'lodash'; export function ConfigurationForm({ configurations, configuration = {}, loading, onSave, onCancel }) { const [names, setNames] = React.useState([]); - + const { control, register, getValues, watch, formState: { errors, isValid }, handleSubmit } = useForm({ defaultValues: { ...configuration @@ -25,7 +25,7 @@ export function ConfigurationForm({ configurations, configuration = {}, loading, reValidateMode: 'onChange', mode: 'onChange', }); - + const { fields, append, remove } = useFieldArray({ control, name: "properties", @@ -35,10 +35,11 @@ export function ConfigurationForm({ configurations, configuration = {}, loading, }); const properties = useProperties(); - const selected = watch('properties'); const addProperties = (props) => { + const selected = getValues('properties'); + const parsed = props.reduce((coll, prop, idx) => { if (prop.isCategory) { return [...coll, ...properties.filter(p => p.category === prop.category)]; @@ -147,7 +148,7 @@ export function ConfigurationForm({ configurations, configuration = {}, loading, {fields.map((p, idx) => ( - + { p.propertyName } { p.category } { p.displayType === 'number' ? 'integer' : p.displayType } 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,6 +54,7 @@ export function GroupsList({ groups, onDelete }) { {group.name} {group.description || ''} + {group.approversList?.length > 0 ? group.approversList[0].approverGroupIds.join(', ') : '-'} diff --git a/ui/src/app/admin/container/MetadataActions.js b/ui/src/app/admin/container/MetadataActions.js index 4ed567d23..7fdcd752b 100644 --- a/ui/src/app/admin/container/MetadataActions.js +++ b/ui/src/app/admin/container/MetadataActions.js @@ -1,6 +1,6 @@ import React from 'react'; import { DeleteConfirmation } from '../../core/components/DeleteConfirmation'; -import { useMetadataActivator, useMetadataEntity } from '../../metadata/hooks/api'; +import { useMetadataActivator, useMetadataApprover, useMetadataEntity } from '../../metadata/hooks/api'; import { NotificationContext, createNotificationAction, NotificationTypes } from '../../notifications/hoc/Notifications'; @@ -46,10 +46,32 @@ export function MetadataActions ({type, children}) { } } + const approver = useMetadataApprover('source'); + + async function approveEntity(entity, enabled, cb = () => {}) { + await approver.patch(`${type === 'source' ? entity.id : entity.resourceId}/${enabled ? 'approve' : 'unapprove'}`); + if (approver?.response.ok) { + dispatch(createNotificationAction( + `Metadata ${type} has been ${enabled ? 'approved' : 'unapproved'}.` + )); + cb(); + } else { + const { errorCode, errorMessage, cause } = approver?.response?.data; + dispatch(createNotificationAction( + `${errorCode}: ${errorMessage} ${cause ? `-${cause}` : ''}`, + NotificationTypes.ERROR + )); + } + } + return ( {(block) => - <>{children(enableEntity, (id, cb) => block(() => deleteEntity(id, cb)))} + <>{children({ + enable: enableEntity, + remove: (id, cb) => block(() => deleteEntity(id, cb)), + approve: approveEntity + })} } ); diff --git a/ui/src/app/admin/container/NewGroup.js b/ui/src/app/admin/container/NewGroup.js index 433990acf..c24f4f516 100644 --- a/ui/src/app/admin/container/NewGroup.js +++ b/ui/src/app/admin/container/NewGroup.js @@ -11,7 +11,7 @@ import { createNotificationAction, NotificationTypes, useNotificationDispatcher import { useTranslator } from '../../i18n/hooks'; import { BASE_PATH } from '../../App.constant'; -export function NewGroup() { +export function NewGroup({ groups }) { const history = useHistory(); const notifier = useNotificationDispatcher(); const translator = useTranslator(); @@ -66,12 +66,13 @@ export function NewGroup() { {(data, errors) => <> save(data)} - onCancel={() => cancel()} /> + context={ { groups } } + group={data} + errors={errors} + schema={schema} + loading={loading} + onSave={(data) => save(data)} + onCancel={() => cancel()} /> } } diff --git a/ui/src/app/admin/hooks.js b/ui/src/app/admin/hooks.js index 54d9d3117..102b58220 100644 --- a/ui/src/app/admin/hooks.js +++ b/ui/src/app/admin/hooks.js @@ -3,6 +3,8 @@ import isNil from 'lodash/isNil'; import {isValidRegex} from '../core/utility/is_valid_regex'; import API_BASE_PATH from '../App.constant'; +import set from 'lodash/set'; + export function useGroups (opts = { cachePolicy: 'no-cache' }) { return useFetch(`${API_BASE_PATH}/admin/groups`, opts); } @@ -23,14 +25,51 @@ export function useRole(id) { }); } +export function useGroupSchema (schema, groups, invalid = []) { + const filtered = groups.filter(g => !(invalid.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; +} + export function useGroupUiSchema () { return { description: { 'ui:widget': 'textarea' + }, + approversList: { + 'ui:options': { + 'widget': 'MultiSelectWidget', + } } }; } +export function useGroupFormatter () { + return (group) => ({ + ...group, + approversList: [ + ...(group?.approversList?.length ? group.approversList[0].approverGroupIds : [] ) + ] + }); +} + +export function useGroupParser () { + return (group = {}) => ({ + ...group, + approversList: [ + { + approverGroupIds: [ + ...group?.approversList + ] + } + ] + }); +} + export function useGroupUiValidator() { return (formData, errors) => { if (!isNil(formData?.validationRegex) && formData?.validationRegex !== '') { diff --git a/ui/src/app/core/components/VersionInfo.js b/ui/src/app/core/components/VersionInfo.js index 1c88ad271..a833cec08 100644 --- a/ui/src/app/core/components/VersionInfo.js +++ b/ui/src/app/core/components/VersionInfo.js @@ -16,7 +16,7 @@ export function VersionInfo () { 'mode':'no-cors' } } - const { data = {} } = useFetch(`${ACTUATOR_PATH}actuator/info`, opts, []); + const { data = {} } = useFetch(`${ACTUATOR_PATH}/info`, opts, []); const [ versionData, setVersionData ] = React.useState(''); diff --git a/ui/src/app/core/user/UserContext.js b/ui/src/app/core/user/UserContext.js index ddd1897ef..1dff57b24 100644 --- a/ui/src/app/core/user/UserContext.js +++ b/ui/src/app/core/user/UserContext.js @@ -72,7 +72,12 @@ function useIsAdminOrInGroup() { function useCanEnable() { const isAdmin = useIsAdmin(); const isEnabler = useIsEnabler(); - return isAdmin || isEnabler; + return (approved) => isAdmin ? true : (isEnabler && approved); +} + +function useIsApprover() { + const user = useCurrentUser(); + return user.canApprove; } function useUserGroup() { @@ -103,6 +108,7 @@ export { useCurrentUser, useIsAdmin, useIsAdminOrInGroup, + useIsApprover, useCanEnable, useCurrentUserLoading, useCurrentUserLoader, diff --git a/ui/src/app/dashboard/component/Scroller.js b/ui/src/app/dashboard/component/Scroller.js index 10baf42cf..ee3626bae 100644 --- a/ui/src/app/dashboard/component/Scroller.js +++ b/ui/src/app/dashboard/component/Scroller.js @@ -13,7 +13,7 @@ export function Scroller ({ entities, children }) { React.useEffect(() => { let maxIndex = (page * PAGE_LIMIT) - 1, minIndex = 0; - const l = entities.filter((resolver, index) => (maxIndex >= index && index >= minIndex)); + const l = entities?.filter((resolver, index) => (maxIndex >= index && index >= minIndex)); setLimited(l); }, [entities, page]) @@ -23,9 +23,9 @@ export function Scroller ({ entities, children }) { return ( loadNext()} - hasMore={entities.length > limited.length} + hasMore={entities?.length > limited?.length} > { children(limited) } diff --git a/ui/src/app/dashboard/view/ActionsTab.js b/ui/src/app/dashboard/view/ActionsTab.js index eda40b751..dcc6a9122 100644 --- a/ui/src/app/dashboard/view/ActionsTab.js +++ b/ui/src/app/dashboard/view/ActionsTab.js @@ -5,8 +5,15 @@ import Spinner from '../../core/components/Spinner'; import Translate from '../../i18n/components/translate'; import SourceList from '../../metadata/domain/source/component/SourceList'; +import { ProtectRoute } from '../../core/components/ProtectRoute'; +import Nav from 'react-bootstrap/Nav'; +import { Switch, Route, useRouteMatch, Redirect } from 'react-router-dom'; +import { NavLink } from 'react-router-dom'; +import Badge from 'react-bootstrap/Badge'; -export function ActionsTab({ sources, users, reloadSources, reloadUsers, loadingSources, loadingUsers }) { +export function ActionsTab({ sources, users, approvals, reloadSources, reloadUsers, reloadApprovals, loadingSources, loadingUsers, loadingApprovals }) { + + const { path, url } = useRouteMatch(); return ( <> @@ -15,33 +22,77 @@ export function ActionsTab({ sources, users, reloadSources, reloadUsers, loading
- Enable Metadata Sources + Actions Required
-
- - {(enable) => - enable(s, e, reloadSources)}> - {loadingSources &&
} -
+
+ +
-
- -
-
-
-
-
- User Access Request -
-
+
+ + + + + + + {({approve, remove}) => + remove(id, reloadApprovals)} + onApprove={(s, e) => approve(s, e, reloadApprovals)}> + {loadingApprovals &&
} +
+ } +
+ } /> + + + + {({enable, remove}) => + remove(id, reloadSources)} + onEnable={(s, e) => enable(s, e, reloadSources)}> + {loadingSources &&
} +
+ } +
+
+
+ + + + {loadingUsers &&
} +
+
+ } /> +
- - {loadingUsers &&
} -
diff --git a/ui/src/app/dashboard/view/Dashboard.js b/ui/src/app/dashboard/view/Dashboard.js index 7644b6def..496ad2da9 100644 --- a/ui/src/app/dashboard/view/Dashboard.js +++ b/ui/src/app/dashboard/view/Dashboard.js @@ -11,10 +11,10 @@ import { SourcesTab } from './SourcesTab'; import { ProvidersTab } from './ProvidersTab'; import { AdminTab } from './AdminTab'; import { ActionsTab } from './ActionsTab'; -import { useCurrentUserLoading, useIsAdmin } from '../../core/user/UserContext'; +import { useCurrentUserLoading, useIsAdmin, useIsApprover } from '../../core/user/UserContext'; import useFetch from 'use-http'; import API_BASE_PATH from '../../App.constant'; -import { useNonAdminSources } from '../../metadata/hooks/api'; +import { useNonAdminSources, useUnapprovedSources} from '../../metadata/hooks/api'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import Badge from 'react-bootstrap/Badge'; @@ -25,18 +25,21 @@ export function Dashboard () { const location = useLocation(); const isAdmin = useIsAdmin(); + const isApprover = useIsApprover(); const loadingUser = useCurrentUserLoading(); const [actions, setActions] = React.useState(0); - const [users, setUsers] = React.useState([]); - const [sources, setSources] = React.useState([]); + const [users, setUsers] = React.useState(null); + const [sources, setSources] = React.useState(null); + const [approvals, setApprovals] = React.useState([]); const { get, response, loading } = useFetch(`${API_BASE_PATH}`, { cachePolicy: 'no-cache' }); const sourceLoader = useNonAdminSources(); + const approvalLoader = useUnapprovedSources(); async function loadUsers() { const users = await get('/admin/users') @@ -47,20 +50,30 @@ export function Dashboard () { async function loadSources() { const s = await sourceLoader.get(); - if (response.ok) { + if (sourceLoader.response.ok) { setSources(s); } } + async function loadApprovals() { + const a = await approvalLoader.get(); + if (approvalLoader.response.ok) { + setApprovals(a); + } + } + /*eslint-disable react-hooks/exhaustive-deps*/ React.useEffect(() => { - loadSources(); - loadUsers(); - }, [location]); + if (isAdmin) { + loadSources(); + loadUsers(); + } + loadApprovals(); + }, [location, isAdmin]); React.useEffect(() => { - setActions(users.length + sources.length); - }, [users, sources]); + setActions((users?.length || 0) + (sources?.length || 0) + approvals.length); + }, [users, sources, approvals]); return (
@@ -87,14 +100,16 @@ export function Dashboard () { Admin - - - Action Required - {actions} - - } + {isApprover && + + + Action Required + {actions} + + + } @@ -102,22 +117,23 @@ export function Dashboard () { + + + } /> } /> - - - - - } /> diff --git a/ui/src/app/dashboard/view/ProvidersTab.js b/ui/src/app/dashboard/view/ProvidersTab.js index d986f3f68..a8627ab5d 100644 --- a/ui/src/app/dashboard/view/ProvidersTab.js +++ b/ui/src/app/dashboard/view/ProvidersTab.js @@ -47,7 +47,7 @@ export function ProvidersTab () { {(searched) => - {(enable) => + {({enable}) => {(searched) => - {(enable, remove) => + {({enable, remove}) => remove(id, loadSources)} diff --git a/ui/src/app/form/component/fields/FilterTargetField.js b/ui/src/app/form/component/fields/FilterTargetField.js index d42738059..9fabd0661 100644 --- a/ui/src/app/form/component/fields/FilterTargetField.js +++ b/ui/src/app/form/component/fields/FilterTargetField.js @@ -18,6 +18,7 @@ import Editor from 'react-simple-code-editor'; // import 'prismjs/components/prism-javascript'; import { FilterTargetPreview } from '../../../metadata/hoc/FilterTargetPreview'; +import { remove } from 'lodash'; const ToggleButton = ({ isOpen, onClick, disabled }) => ( diff --git a/ui/src/app/form/component/index.js b/ui/src/app/form/component/index.js index b9e190811..e72109970 100644 --- a/ui/src/app/form/component/index.js +++ b/ui/src/app/form/component/index.js @@ -6,6 +6,7 @@ import AttributeReleaseWidget from './widgets/AttributeReleaseWidget'; import RadioWidget from './widgets/RadioWidget'; import OptionWidget from './widgets/OptionWidget'; import UpDownWidget from './widgets/UpDownWidget'; +import MultiSelectWidget from './widgets/MultiSelectWidget'; import FieldTemplate from './templates/FieldTemplate'; import ArrayFieldTemplate from './templates/ArrayFieldTemplate'; @@ -40,5 +41,6 @@ export const widgets = { CheckboxWidget, RadioWidget, UpDownWidget, - AttributeReleaseWidget + AttributeReleaseWidget, + MultiSelectWidget, }; \ No newline at end of file diff --git a/ui/src/app/form/component/templates/FieldTemplate.js b/ui/src/app/form/component/templates/FieldTemplate.js index e2b2dbe7b..f98078ef9 100644 --- a/ui/src/app/form/component/templates/FieldTemplate.js +++ b/ui/src/app/form/component/templates/FieldTemplate.js @@ -19,7 +19,7 @@ export function FieldTemplate ({ return ( <>{!props.hidden ? - +
{children}
diff --git a/ui/src/app/form/component/widgets/MultiSelectWidget.js b/ui/src/app/form/component/widgets/MultiSelectWidget.js new file mode 100644 index 000000000..042f2deec --- /dev/null +++ b/ui/src/app/form/component/widgets/MultiSelectWidget.js @@ -0,0 +1,87 @@ +import React, { useRef } from "react"; + +import ListGroup from "react-bootstrap/ListGroup"; +import Form from "react-bootstrap/Form"; + + +import Translate from "../../../i18n/components/translate"; +import { InfoIcon } from "../InfoIcon"; + +import { Typeahead } from 'react-bootstrap-typeahead'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faAsterisk } from "@fortawesome/free-solid-svg-icons"; + +const MultiSelectWidget = ({ + id, + placeholder, + required, + readonly, + disabled, + type, + label, + value, + onChange, + onBlur, + onFocus, + autofocus, + schema, + rawErrors = [], + formContext, + ...props +}) => { + // const inputType = (type || schema.type) === 'string' ? 'text' : `${type || schema.type}`; + const typeahead = useRef(); + + const [enums, setEnums] = React.useState(schema.items.enum); + const [enumNames, setEnumNames] = React.useState(schema.items.enumNames); + + React.useEffect(() => { + const { items } = schema; + setEnums(items.enum); + setEnumNames(items.enumNames); + }, [schema]); + + const [touched] = React.useState(false); + + React.useEffect(() => { + + }, [schema]); + + return ( + + 0) ? "text-danger" : ""}`} htmlFor={`option-selector-${id}`}> + + + {(label || schema.title) && required ? : Item {id + 1}} + + {schema.description && } + + enumNames[enums.indexOf(option)] } + onChange={ onChange } + options={enums} + multiple + placeholder="Choose approval groups..." + selected={value} + /> + {rawErrors?.length > 0 && touched && ( + + {rawErrors.map((error, i) => { + return ( + 0 ? 'sr-only' : ''}`}> + + {error} + + + ); + })} + + )} + + ); +}; + +export default MultiSelectWidget; \ No newline at end of file diff --git a/ui/src/app/form/component/widgets/TextWidget.js b/ui/src/app/form/component/widgets/TextWidget.js index 2f8c183b0..2e2db043f 100644 --- a/ui/src/app/form/component/widgets/TextWidget.js +++ b/ui/src/app/form/component/widgets/TextWidget.js @@ -41,8 +41,10 @@ const TextWidget = ({ }; React.useEffect(() => { - onChange(fieldValue); - }, [fieldValue, onChange]); + if (fieldValue || touched) { + onChange(fieldValue); + } + }, [fieldValue, onChange, touched]); const translator = useTranslator(); diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js index c749a180f..dc8ec7581 100644 --- a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js @@ -53,7 +53,7 @@ export function MetadataFilterConfigurationListItem ({ filter, isLast, isFirst, { filter['@type'] } } checked={filter.filterEnabled} disabled={loading} diff --git a/ui/src/app/metadata/domain/provider/component/ProviderList.js b/ui/src/app/metadata/domain/provider/component/ProviderList.js index 9a9ac90d4..08188dba0 100644 --- a/ui/src/app/metadata/domain/provider/component/ProviderList.js +++ b/ui/src/app/metadata/domain/provider/component/ProviderList.js @@ -75,7 +75,7 @@ export function ProviderList({ children, entities, reorder = true, first, last, onEnable(provider, checked)} checked={provider.enabled} diff --git a/ui/src/app/metadata/domain/source/component/SourceList.js b/ui/src/app/metadata/domain/source/component/SourceList.js index 923f41273..e43791a67 100644 --- a/ui/src/app/metadata/domain/source/component/SourceList.js +++ b/ui/src/app/metadata/domain/source/component/SourceList.js @@ -6,8 +6,10 @@ import Button from 'react-bootstrap/Button'; import Form from 'react-bootstrap/Form'; import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; + import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTrash } from '@fortawesome/free-solid-svg-icons'; +import { faSquareCheck, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { faSquare } from '@fortawesome/free-regular-svg-icons'; import FormattedDate from '../../../../core/components/FormattedDate'; import Translate from '../../../../i18n/components/translate'; @@ -16,7 +18,7 @@ import { useTranslator } from '../../../../i18n/hooks'; import { useCanEnable, useIsAdmin } from '../../../../core/user/UserContext'; import { GroupsProvider } from '../../../../admin/hoc/GroupsProvider'; -export default function SourceList({ entities, onDelete, onEnable, onChangeGroup, children }) { +export default function SourceList({ entities, onDelete, onEnable, onApprove, onChangeGroup, children }) { const translator = useTranslator(); const isAdmin = useIsAdmin(); @@ -24,7 +26,7 @@ export default function SourceList({ entities, onDelete, onEnable, onChangeGroup return ( - + {(limited) =>
@@ -35,6 +37,7 @@ export default function SourceList({ entities, onDelete, onEnable, onChangeGroup + {isAdmin && onChangeGroup && } {onDelete && isAdmin && @@ -65,7 +68,26 @@ export default function SourceList({ entities, onDelete, onEnable, onChangeGroup + + + {isAdmin && onChangeGroup &&
Protocol Author Created DateApproval EnabledGroup - {onEnable && canEnable ? + {onApprove ? + + : + + + + } + + + + {onEnable && canEnable(source.approved) ? : - + } diff --git a/ui/src/app/metadata/hoc/FilterTargetPreview.js b/ui/src/app/metadata/hoc/FilterTargetPreview.js index 2fd81e4c3..9982910d5 100644 --- a/ui/src/app/metadata/hoc/FilterTargetPreview.js +++ b/ui/src/app/metadata/hoc/FilterTargetPreview.js @@ -35,7 +35,7 @@ export function FilterTargetPreview ({ entityId, children }) { return ( - {children(preview, loading, data)} + {children(data ? preview : null, loading, data)} Preview XML diff --git a/ui/src/app/metadata/hooks/api.js b/ui/src/app/metadata/hooks/api.js index 24f96b1e6..f38e8a8fc 100644 --- a/ui/src/app/metadata/hooks/api.js +++ b/ui/src/app/metadata/hooks/api.js @@ -24,7 +24,13 @@ export function getMetadataPath(type) { } export function useNonAdminSources() { - return useFetch(`${API_BASE_PATH}${getMetadataPath('source')}/disabledNonAdmin`, { + return useFetch(`${API_BASE_PATH}${getMetadataPath('source')}/disabledSources`, { + cachePolicy: 'no-cache' + }); +} + +export function useUnapprovedSources() { + return useFetch(`${API_BASE_PATH}${getMetadataListPath('source')}/needsApproval`, { cachePolicy: 'no-cache' }); } @@ -147,6 +153,12 @@ export function useMetadataActivator(type, opts = { return useFetch(`${API_BASE_PATH}/activate/${type === 'source' ? 'entityDescriptor' : 'MetadataResolvers'}/`, opts); } +export function useMetadataApprover(type, opts = { + cachePolicy: 'no-cache' +}) { + return useFetch(`${API_BASE_PATH}/approve/${type === 'source' ? 'entityDescriptor' : 'MetadataResolvers'}/`, opts); +} + export function useFilterActivator(providerId, opts = { cachePolicy: 'no-cache' }) { @@ -161,4 +173,4 @@ export function useMetadataAttributes (opts = {}, onMount) { export function useMetadataAttribute(opts = {}, onMount) { // return useFetch(`${API_BASE_PATH}/custom/entity/attribute`, opts, onMount); -} \ No newline at end of file +} diff --git a/ui/src/app/metadata/hooks/api.test.js b/ui/src/app/metadata/hooks/api.test.js index 4f2133677..40c19e33b 100644 --- a/ui/src/app/metadata/hooks/api.test.js +++ b/ui/src/app/metadata/hooks/api.test.js @@ -82,7 +82,7 @@ describe('api hooks', () => { describe('useNonAdminSources', () => { it('should call useFetch', () => { const sources = useNonAdminSources(); - expect(useFetch).toHaveBeenCalledWith(`${API_BASE_PATH}${getMetadataPath('source')}/disabledNonAdmin`, { "cachePolicy": "no-cache" }) + expect(useFetch).toHaveBeenCalledWith(`${API_BASE_PATH}${getMetadataPath('source')}/disabledSources`, { "cachePolicy": "no-cache" }) }) }); @@ -92,7 +92,7 @@ describe('api hooks', () => { const opts = {}; const onMount = []; const sources = useMetadataEntities(type, opts, onMount); - + expect(useFetch).toHaveBeenCalledWith(`${API_BASE_PATH}${getMetadataListPath(type)}`, opts, onMount) }); @@ -283,4 +283,4 @@ describe('api hooks', () => { expect(mockPut).toHaveBeenCalled(); }); }); -}); \ No newline at end of file +}); diff --git a/ui/src/app/metadata/view/MetadataOptions.js b/ui/src/app/metadata/view/MetadataOptions.js index 60a075f1f..35a0977ac 100644 --- a/ui/src/app/metadata/view/MetadataOptions.js +++ b/ui/src/app/metadata/view/MetadataOptions.js @@ -52,12 +52,12 @@ export function MetadataOptions ({reload}) { const enabled = type === 'source' ? metadata.serviceEnabled : metadata.enabled; - const canEnable = useCanEnable(); + const canEnable = useCanEnable()(metadata.approved); const isAdmin = useIsAdmin(); return ( - {(enable, remove) => + {({enable, remove, approve}) => <>