diff --git a/Jenkinsfile b/Jenkinsfile index eb56795ef..936f73c2c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -68,5 +68,8 @@ pipeline { success { emailext body: '''${SCRIPT, template="groovy-text.template"}''', recipientProviders: [[$class: 'DevelopersRecipientProvider'], [$class: 'RequesterRecipientProvider']], subject: '[SHIBUI] Build Success' } + always { + cleanWs() + } } -} \ No newline at end of file +} diff --git a/backend/build.gradle b/backend/build.gradle index 40a87ada6..9180c519a 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -3,7 +3,6 @@ plugins { id 'war' id 'org.springframework.boot' version '2.0.0.RELEASE' id 'com.gorylenko.gradle-git-properties' version '1.4.21' - id 'net.researchgate.release' version '2.6.0' id 'io.franzbecker.gradle-lombok' version '1.13' id 'com.palantir.docker' version '0.20.1' id 'com.palantir.docker-run' version '0.20.1' @@ -46,6 +45,36 @@ configurations { } } +def generatedSrcDir = new File(buildDir, 'generated/src/main/java') + +sourceSets { + main { + groovy { + srcDirs = ['src/main/groovy', 'src/main/java', generatedSrcDir] + } + java { + srcDirs = [] + } + } + integrationTest { + groovy { + srcDirs = ['src/integration/groovy'] + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + } + resources { + srcDir 'src/integration/resources' + } + } + + enversTest { + groovy { + srcDir 'src/enversTest/groovy' + } + resources.srcDir 'src/enversTest/resources' + } +} + processResources.dependsOn(':ui:npm_run_buildProd') jar { @@ -174,9 +203,15 @@ dependencies { // Envers for persistent entities versioning compile 'org.hibernate:hibernate-envers' -} -def generatedSrcDir = new File(buildDir, 'generated/src/main/java') + enversTestCompile sourceSets.main.output + enversTestCompile sourceSets.test.output + enversTestCompile configurations.compile + enversTestCompile configurations.testCompile + + enversTestRuntime configurations.runtime + enversTestRuntime configurations.testRuntime +} sourceSets { main { @@ -218,6 +253,19 @@ task integrationTest(type: Test) { systemProperties['user.dir'] = workingDir } +task enversTest(type: Test) { + group = 'verification' + description = 'Run tests pertaing to envers versioning engine' + testClassesDirs = sourceSets.enversTest.output.classesDirs + classpath = sourceSets.enversTest.runtimeClasspath + systemProperties = System.properties + systemProperties['user.dir'] = workingDir +} + +check { + dependsOn enversTest +} + task generateSources { inputs.dir('src/main/templates') inputs.files fileTree('src/main/resources') { diff --git a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerVersionEndpointsIntegrationTests.groovy b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerVersionEndpointsIntegrationTests.groovy new file mode 100644 index 000000000..705f50021 --- /dev/null +++ b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerVersionEndpointsIntegrationTests.groovy @@ -0,0 +1,118 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +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.repository.EntityDescriptorRepository +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 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles(['no-auth', 'dev']) +class EntityDescriptorControllerVersionEndpointsIntegrationTests extends Specification { + + @Autowired + private TestRestTemplate restTemplate + + @Autowired + EntityDescriptorRepository entityDescriptorRepository + + static BASE_URI = '/api/EntityDescriptor' + + static ALL_VERSIONS_URI = "$BASE_URI/%s/Versions" + + static SPECIFIC_VERSION_URI = "$BASE_URI/%s/Versions/%s" + + def "GET /api/EntityDescriptor/{resourceId}/Versions with non-existent entity descriptor"() { + when: + def result = getAllEntityDescriptorVersions('non-existent-ed-id', String) + + then: + result.statusCodeValue == 404 + } + + def "GET /api/EntityDescriptor{resourceId}/Versions with 1 entity descriptor version"() { + given: + EntityDescriptor ed = new EntityDescriptor(entityID: 'http://test/controller', createdBy: 'anonymousUser') + entityDescriptorRepository.save(ed) + + when: + def result = getAllEntityDescriptorVersions(ed.resourceId, List) + + then: + result.statusCodeValue == 200 + result.body.size == 1 + result.body[0].id && result.body[0].creator && result.body[0].date + } + + def "GET /api/EntityDescriptor{resourceId}/Versions with 2 entity descriptor versions"() { + given: + EntityDescriptor ed = new EntityDescriptor(entityID: 'http://test/controller', createdBy: 'anonymousUser') + ed = entityDescriptorRepository.save(ed) + //Will created a second version for UPDATE revision + ed.serviceEnabled = true + entityDescriptorRepository.save(ed) + + when: + def result = getAllEntityDescriptorVersions(ed.resourceId, List) + + then: + result.statusCodeValue == 200 + result.body.size == 2 + result.body[0].id < result.body[1].id + result.body[0].date < result.body[1].date + } + + def "GET /api/EntityDescriptor{resourceId}/Versions/{version} for non existent version"() { + given: + EntityDescriptor ed = new EntityDescriptor(entityID: 'http://test/controller', createdBy: 'anonymousUser') + ed = entityDescriptorRepository.save(ed) + + when: + def result = getEntityDescriptorForVersion(ed.resourceId, '1000', EntityDescriptorRepresentation) + + then: + result.statusCodeValue == 404 + } + + def "GET /api/EntityDescriptor{resourceId}/Versions/{version} with 2 entity descriptor versions returns correct ED for specific versions"() { + given: + EntityDescriptor ed = new EntityDescriptor(entityID: 'http://test/controller', createdBy: 'anonymousUser', serviceProviderName: 'SP1') + ed = entityDescriptorRepository.save(ed) + //Will created a second version for UPDATE revision + ed.serviceProviderName = 'SP2' + entityDescriptorRepository.save(ed) + + when: + def allVersions = getAllEntityDescriptorVersions(ed.resourceId, List) + def edv1 = getEntityDescriptorForVersion(ed.resourceId, allVersions.body[0].id, EntityDescriptorRepresentation) + def edv2 = getEntityDescriptorForVersion(ed.resourceId, allVersions.body[1].id, EntityDescriptorRepresentation) + + then: + edv1.statusCodeValue == 200 + edv1.body.serviceProviderName == 'SP1' + edv2.statusCodeValue == 200 + edv2.body.serviceProviderName == 'SP2' + } + + private getAllEntityDescriptorVersions(String resourceId, responseType) { + this.restTemplate.getForEntity(resourceUriFor(ALL_VERSIONS_URI, resourceId), responseType) + } + + private getEntityDescriptorForVersion(String resourceId, String version, responseType) { + this.restTemplate.getForEntity(resourceUriFor(SPECIFIC_VERSION_URI, resourceId, version), responseType) + } + + private static resourceUriFor(String uriTemplate, String resourceId, String version) { + String.format(uriTemplate, resourceId, version) + } + + private static resourceUriFor(String uriTemplate, String resourceId) { + String.format(uriTemplate, resourceId) + } +} 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 new file mode 100644 index 000000000..f23ea333d --- /dev/null +++ b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolverControllerVersionEndpointsIntegrationTests.groovy @@ -0,0 +1,121 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FilesystemMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +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 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles(['no-auth', 'dev']) +class MetadataResolverControllerVersionEndpointsIntegrationTests extends Specification { + + @Autowired + private TestRestTemplate restTemplate + + @Autowired + MetadataResolverRepository repository + + static BASE_URI = '/api/MetadataResolvers' + + static ALL_VERSIONS_URI = "$BASE_URI/%s/Versions" + + static SPECIFIC_VERSION_URI = "$BASE_URI/%s/Versions/%s" + + def "GET /api/MetadataResolvers/{resourceId}/Versions with non-existent resolver"() { + when: + def result = getAllMetadataResolverVersions('non-existent-resolver-id', String) + + then: + result.statusCodeValue == 404 + } + + def "GET /api/MetadataResolvers/{resourceId}/Versions with 1 resolver version"() { + given: + MetadataResolver mr = new LocalDynamicMetadataResolver(name: 'resolver') + repository.save(mr) + + when: + def result = getAllMetadataResolverVersions(mr.resourceId, List) + + then: + result.statusCodeValue == 200 + result.body.size == 1 + result.body[0].id && result.body[0].creator && result.body[0].date + } + + def "GET /api/MetadataResolvers/{resourceId}/Versions with 2 resolver versions"() { + given: + MetadataResolver mr = new FileBackedHttpMetadataResolver(name: 'resolver') + mr = repository.save(mr) + //Will create a second version for UPDATE revision + mr.name = 'resolverUPDATED' + repository.save(mr) + + when: + def result = getAllMetadataResolverVersions(mr.resourceId, List) + + then: + result.statusCodeValue == 200 + result.body.size == 2 + result.body[0].id < result.body[1].id + result.body[0].date < result.body[1].date + } + + def "GET /api/MetadataResolvers/{resourceId}/Versions/{version} for non existent version"() { + given: + MetadataResolver mr = new DynamicHttpMetadataResolver(name: 'resolver') + mr = repository.save(mr) + + when: + def result = getMetadataResolverForVersion(mr.resourceId, '1000', MetadataResolver) + + then: + result.statusCodeValue == 404 + } + + def "GET /api/MetadataResolvers/{resourceId}/Versions/{version} with 2 resolver versions returns correct resolver for specific version"() { + given: + MetadataResolver mr = new FilesystemMetadataResolver(name: 'resolver') + mr = repository.save(mr) + //Will create a second version for UPDATE revision + mr.name = 'resolverUPDATED' + repository.save(mr) + + when: + def allVersions = getAllMetadataResolverVersions(mr.resourceId, List) + def mrv1 = getMetadataResolverForVersion(mr.resourceId, allVersions.body[0].id, MetadataResolver) + def mrv2 = getMetadataResolverForVersion(mr.resourceId, allVersions.body[1].id, MetadataResolver) + + then: + mrv1.statusCodeValue == 200 + mrv1.body.name == 'resolver' + mrv2.statusCodeValue == 200 + mrv2.body.name == 'resolverUPDATED' + } + + private getAllMetadataResolverVersions(String resourceId, responseType) { + this.restTemplate.getForEntity(resourceUriFor(ALL_VERSIONS_URI, resourceId), responseType) + } + + private getMetadataResolverForVersion(String resourceId, String version, responseType) { + this.restTemplate.getForEntity(resourceUriFor(SPECIFIC_VERSION_URI, resourceId, version), responseType) + } + + private static resourceUriFor(String uriTemplate, String resourceId, String version) { + String.format(uriTemplate, resourceId, version) + } + + private static resourceUriFor(String uriTemplate, String resourceId) { + String.format(uriTemplate, resourceId) + } +} diff --git a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/EntityDescriptorEnversVersioningTests.groovy b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/EntityDescriptorEnversVersioningTests.groovy new file mode 100644 index 000000000..b7f31270a --- /dev/null +++ b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/EntityDescriptorEnversVersioningTests.groovy @@ -0,0 +1,651 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository.envers + +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.AssertionConsumerService +import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute +import edu.internet2.tier.shibboleth.admin.ui.domain.ContactPerson +import edu.internet2.tier.shibboleth.admin.ui.domain.Description +import edu.internet2.tier.shibboleth.admin.ui.domain.DisplayName +import edu.internet2.tier.shibboleth.admin.ui.domain.EmailAddress +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributes +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.KeyInfo +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.Organization +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.PrivacyStatementURL +import edu.internet2.tier.shibboleth.admin.ui.domain.SPSSODescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.SingleLogoutService +import edu.internet2.tier.shibboleth.admin.ui.domain.UIInfo +import edu.internet2.tier.shibboleth.admin.ui.domain.X509Certificate +import edu.internet2.tier.shibboleth.admin.ui.domain.X509Data +import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean +import edu.internet2.tier.shibboleth.admin.ui.domain.XSString +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ContactRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.LogoutEndpointRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.MduiRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSsoDescriptorRepresentation +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService +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.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.Specification + +import javax.persistence.EntityManager + +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.updateAndGetRevisionHistoryOfEntityDescriptor + +/** + * Testing entity descriptor envers versioning + */ +@DataJpaTest +@ContextConfiguration(classes = [CoreShibUiConfiguration, InternationalizationConfiguration, SearchConfiguration, TestConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class EntityDescriptorEnversVersioningTests extends Specification { + + @Autowired + EntityDescriptorRepository entityDescriptorRepository + + @Autowired + EntityDescriptorService entityDescriptorService + + @Autowired + EntityManager entityManager + + @Autowired + PlatformTransactionManager txMgr + + @Autowired + OpenSamlObjects openSamlObjects + + def "test versioning with contact persons"() { + setup: + def expectedModifiedPersistentEntities = [EntityDescriptor.name, ContactPerson.name, GivenName.name, EmailAddress.name] + + when: + def ed = new EntityDescriptor() + def representation = new EntityDescriptorRepresentation().with { + it.contacts = [new ContactRepresentation(type: 'administrative', name: 'name', emailAddress: 'test@test')] + it + } + def entityDescriptorHistory = updateAndGetRevisionHistoryOfEntityDescriptor(ed, representation, entityDescriptorService, + entityDescriptorRepository, + txMgr, + entityManager) + + then: + entityDescriptorHistory.size() == 1 + getTargetEntityForRevisionIndex(entityDescriptorHistory, 0).contactPersons[0].givenName.name == 'name' + getTargetEntityForRevisionIndex(entityDescriptorHistory, 0).contactPersons[0].type == org.opensaml.saml.saml2.metadata.ContactPersonTypeEnumeration.ADMINISTRATIVE + getTargetEntityForRevisionIndex(entityDescriptorHistory, 0).contactPersons[0].emailAddresses[0].address == 'test@test' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 0).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 0).timestamp > 0L + getModifiedEntityNames(entityDescriptorHistory, 0).sort() == expectedModifiedPersistentEntities.sort() + + when: + representation = new EntityDescriptorRepresentation().with { + it.contacts = [new ContactRepresentation(type: 'administrative', name: 'nameUPDATED', emailAddress: 'test@test')] + it + } + entityDescriptorHistory = updateAndGetRevisionHistoryOfEntityDescriptor(ed, representation, entityDescriptorService, + entityDescriptorRepository, + txMgr, + entityManager) + then: + entityDescriptorHistory.size() == 2 + getTargetEntityForRevisionIndex(entityDescriptorHistory, 1).contactPersons[0].givenName.name == 'nameUPDATED' + getTargetEntityForRevisionIndex(entityDescriptorHistory, 1).contactPersons[0].type == org.opensaml.saml.saml2.metadata.ContactPersonTypeEnumeration.ADMINISTRATIVE + getTargetEntityForRevisionIndex(entityDescriptorHistory, 1).contactPersons[0].emailAddresses[0].address == 'test@test' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 1).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 1).timestamp > 0L + getModifiedEntityNames(entityDescriptorHistory, 1).sort() == expectedModifiedPersistentEntities.sort() + + when: + representation = new EntityDescriptorRepresentation().with { + it.contacts = [new ContactRepresentation(type: 'other', name: 'nameUPDATED2', emailAddress: 'test@test.com')] + it + } + entityDescriptorHistory = updateAndGetRevisionHistoryOfEntityDescriptor(ed, representation, + entityDescriptorService, + entityDescriptorRepository, + txMgr, + entityManager) + + then: + entityDescriptorHistory.size() == 3 + getTargetEntityForRevisionIndex(entityDescriptorHistory, 2).contactPersons[0].givenName.name == 'nameUPDATED2' + getTargetEntityForRevisionIndex(entityDescriptorHistory, 2).contactPersons[0].type == org.opensaml.saml.saml2.metadata.ContactPersonTypeEnumeration.OTHER + getTargetEntityForRevisionIndex(entityDescriptorHistory, 2).contactPersons[0].emailAddresses[0].address == 'test@test.com' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 2).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 2).timestamp > 0L + getModifiedEntityNames(entityDescriptorHistory, 2).sort() == expectedModifiedPersistentEntities.sort() + + //Also make sure we have our original revision + getTargetEntityForRevisionIndex(entityDescriptorHistory, 1).contactPersons[0].givenName.name == 'nameUPDATED' + getTargetEntityForRevisionIndex(entityDescriptorHistory, 1).contactPersons[0].type == org.opensaml.saml.saml2.metadata.ContactPersonTypeEnumeration.ADMINISTRATIVE + getTargetEntityForRevisionIndex(entityDescriptorHistory, 1).contactPersons[0].emailAddresses[0].address == 'test@test' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 1).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 1).timestamp > 0L + + } + + def "test versioning with organization"() { + setup: + def expectedModifiedPersistentEntities = [EntityDescriptor.name, + Organization.name, + OrganizationDisplayName.name, + OrganizationName.name, + OrganizationURL.name] + + when: + EntityDescriptor ed = new EntityDescriptor() + def representation = new EntityDescriptorRepresentation().with { + it.organization = new OrganizationRepresentation(name: 'org', displayName: 'display org', url: 'http://org.edu') + it + } + def entityDescriptorHistory = updateAndGetRevisionHistoryOfEntityDescriptor(ed, representation, entityDescriptorService, + entityDescriptorRepository, + txMgr, + entityManager) + then: + entityDescriptorHistory.size() == 1 + getTargetEntityForRevisionIndex(entityDescriptorHistory, 0).organization.organizationNames[0].value == 'org' + getTargetEntityForRevisionIndex(entityDescriptorHistory, 0).organization.displayNames[0].value == 'display org' + getTargetEntityForRevisionIndex(entityDescriptorHistory, 0).organization.URLs[0].value == 'http://org.edu' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 0).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 0).timestamp > 0L + getModifiedEntityNames(entityDescriptorHistory, 0).sort() == expectedModifiedPersistentEntities.sort() + + + + + + when: + representation = new EntityDescriptorRepresentation().with { + it.organization = new OrganizationRepresentation(name: 'orgUpdated', displayName: 'display org Updated', url: 'http://org2.edu') + it + } + entityDescriptorHistory = updateAndGetRevisionHistoryOfEntityDescriptor(ed, representation, entityDescriptorService, + entityDescriptorRepository, + txMgr, + entityManager) + then: + entityDescriptorHistory.size() == 2 + getTargetEntityForRevisionIndex(entityDescriptorHistory, 1).organization.organizationNames[0].value == 'orgUpdated' + getTargetEntityForRevisionIndex(entityDescriptorHistory, 1).organization.displayNames[0].value == 'display org Updated' + getTargetEntityForRevisionIndex(entityDescriptorHistory, 1).organization.URLs[0].value == 'http://org2.edu' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 0).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 0).timestamp > 0L + getModifiedEntityNames(entityDescriptorHistory, 1).sort() == expectedModifiedPersistentEntities.sort() + + //Check the original revision is intact + getTargetEntityForRevisionIndex(entityDescriptorHistory, 0).organization.organizationNames[0].value == 'org' + getTargetEntityForRevisionIndex(entityDescriptorHistory, 0).organization.displayNames[0].value == 'display org' + getTargetEntityForRevisionIndex(entityDescriptorHistory, 0).organization.URLs[0].value == 'http://org.edu' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 1).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 1).timestamp > 0L + } + + def "test versioning with sp sso descriptor"() { + setup: + def expectedModifiedPersistentEntities = [EntityDescriptor.name, + NameIDFormat.name, + SPSSODescriptor.name] + when: + EntityDescriptor ed = new EntityDescriptor() + def representation = new EntityDescriptorRepresentation().with { + it.serviceProviderSsoDescriptor = new ServiceProviderSsoDescriptorRepresentation().with { + it.protocolSupportEnum = 'SAML 1.1' + it.nameIdFormats = ['format'] + it + } + it + } + def entityDescriptorHistory = updateAndGetRevisionHistoryOfEntityDescriptor(ed, representation, entityDescriptorService, + entityDescriptorRepository, + txMgr, + entityManager) + + then: + entityDescriptorHistory.size() == 1 + getTargetEntityForRevisionIndex(entityDescriptorHistory, 0).roleDescriptors[0].nameIDFormats[0].format == 'format' + getTargetEntityForRevisionIndex(entityDescriptorHistory, 0).roleDescriptors[0].supportedProtocols[0] == 'urn:oasis:names:tc:SAML:1.1:protocol' + getTargetEntityForRevisionIndex(entityDescriptorHistory, 0).roleDescriptors[0].supportedProtocols[1] == null + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 0).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 0).timestamp > 0L + getModifiedEntityNames(entityDescriptorHistory, 0).sort() == expectedModifiedPersistentEntities.sort() + + when: + representation = new EntityDescriptorRepresentation().with { + it.serviceProviderSsoDescriptor = new ServiceProviderSsoDescriptorRepresentation().with { + it.protocolSupportEnum = 'SAML 1.1, SAML 2' + it.nameIdFormats = ['formatUPDATED'] + it + } + it + } + + entityDescriptorHistory = updateAndGetRevisionHistoryOfEntityDescriptor(ed, representation, entityDescriptorService, + entityDescriptorRepository, + txMgr, + entityManager) + + then: + entityDescriptorHistory.size() == 2 + getTargetEntityForRevisionIndex(entityDescriptorHistory, 1).roleDescriptors[0].nameIDFormats[0].format == 'formatUPDATED' + getTargetEntityForRevisionIndex(entityDescriptorHistory, 1).roleDescriptors[0].supportedProtocols[0] == 'urn:oasis:names:tc:SAML:1.1:protocol' + getTargetEntityForRevisionIndex(entityDescriptorHistory, 1).roleDescriptors[0].supportedProtocols[1] == 'urn:oasis:names:tc:SAML:2.0:protocol' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 1).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 1).timestamp > 0L + getModifiedEntityNames(entityDescriptorHistory, 1).sort() == expectedModifiedPersistentEntities.sort() + + //Check the original revision is intact + getTargetEntityForRevisionIndex(entityDescriptorHistory, 0).roleDescriptors[0].nameIDFormats[0].format == 'format' + getTargetEntityForRevisionIndex(entityDescriptorHistory, 0).roleDescriptors[0].supportedProtocols[0] == 'urn:oasis:names:tc:SAML:1.1:protocol' + getTargetEntityForRevisionIndex(entityDescriptorHistory, 0).roleDescriptors[0].supportedProtocols[1] == null + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 0).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(entityDescriptorHistory, 0).timestamp > 0L + } + + def "test versioning with uiInfo"() { + setup: + def expectedModifiedPersistentEntities = [EntityDescriptor.name, + Description.name, + DisplayName.name, + SPSSODescriptor.name, + Extensions.name, + InformationURL.name, + Logo.name, + PrivacyStatementURL.name, + UIInfo.name] + + when: + EntityDescriptor ed = new EntityDescriptor() + def representation = new EntityDescriptorRepresentation().with { + it.mdui = new MduiRepresentation().with { + it.displayName = 'Initial display name' + it.informationUrl = 'http://info' + it.privacyStatementUrl = 'http://privacy' + it.description = 'Initial desc' + it.logoUrl = 'http://logo' + it.logoHeight = 20 + it.logoWidth = 30 + it + } + it + } + def entityDescriptorHistory = updateAndGetRevisionHistoryOfEntityDescriptor(ed, representation, entityDescriptorService, + entityDescriptorRepository, + txMgr, + entityManager) + + //Groovy FTW - able to call any private methods on ANY object. Get first revision + UIInfo uiinfo = entityDescriptorService.getUIInfo(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + + then: + entityDescriptorHistory.size() == 1 + uiinfo.displayNames[0].value == 'Initial display name' + uiinfo.informationURLs[0].value == 'http://info' + uiinfo.privacyStatementURLs[0].value == 'http://privacy' + uiinfo.descriptions[0].value == 'Initial desc' + uiinfo.logos[0].URL == 'http://logo' + uiinfo.logos[0].height == 20 + uiinfo.logos[0].width == 30 + getModifiedEntityNames(entityDescriptorHistory, 0).sort() == expectedModifiedPersistentEntities.sort() + + when: + representation = new EntityDescriptorRepresentation().with { + it.mdui = new MduiRepresentation().with { + it.displayName = 'Display name UPDATED' + it.informationUrl = 'http://info.updated' + it.privacyStatementUrl = 'http://privacy.updated' + it.description = 'Desc UPDATED' + it.logoUrl = 'http://logo.updated' + it.logoHeight = 30 + it.logoWidth = 40 + it + } + it + } + entityDescriptorHistory = updateAndGetRevisionHistoryOfEntityDescriptor(ed, representation, entityDescriptorService, + entityDescriptorRepository, + txMgr, + entityManager) + + //Get second revision + uiinfo = entityDescriptorService.getUIInfo(getTargetEntityForRevisionIndex(entityDescriptorHistory, 1)) + //And initial revision + def uiinfoInitialRevision = entityDescriptorService.getUIInfo(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + + then: + entityDescriptorHistory.size() == 2 + uiinfo.displayNames[0].value == 'Display name UPDATED' + uiinfo.informationURLs[0].value == 'http://info.updated' + uiinfo.privacyStatementURLs[0].value == 'http://privacy.updated' + uiinfo.descriptions[0].value == 'Desc UPDATED' + uiinfo.logos[0].URL == 'http://logo.updated' + uiinfo.logos[0].height == 30 + uiinfo.logos[0].width == 40 + getModifiedEntityNames(entityDescriptorHistory, 1).sort() == expectedModifiedPersistentEntities.sort() + + //Check the initial revision is still intact + uiinfoInitialRevision.displayNames[0].value == 'Initial display name' + uiinfoInitialRevision.informationURLs[0].value == 'http://info' + uiinfoInitialRevision.privacyStatementURLs[0].value == 'http://privacy' + uiinfoInitialRevision.descriptions[0].value == 'Initial desc' + uiinfoInitialRevision.logos[0].URL == 'http://logo' + uiinfoInitialRevision.logos[0].height == 20 + uiinfoInitialRevision.logos[0].width == 30 + } + + def "test versioning with security"() { + setup: + def expectedModifiedPersistentEntities = [EntityDescriptor.name, + KeyDescriptor.name, + KeyInfo.name, + SPSSODescriptor.name, + X509Certificate.name, + X509Data.name] + + when: + EntityDescriptor ed = new EntityDescriptor() + def representation = new EntityDescriptorRepresentation().with { + it.securityInfo = new SecurityInfoRepresentation().with { + it.authenticationRequestsSigned = true + it.x509CertificateAvailable = true + it.x509Certificates = [new SecurityInfoRepresentation.X509CertificateRepresentation(name: 'sign', type: 'signing', value: 'signingValue')] + it + } + it + } + + def entityDescriptorHistory = updateAndGetRevisionHistoryOfEntityDescriptor(ed, representation, entityDescriptorService, + entityDescriptorRepository, + txMgr, + entityManager) + + //Get initial revision + SPSSODescriptor spssoDescriptor = + entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) + + KeyDescriptor keyDescriptor = spssoDescriptor.keyDescriptors[0] + X509Certificate x509cert = keyDescriptor.keyInfo.x509Datas[0].x509Certificates[0] + + then: + entityDescriptorHistory.size() == 1 + spssoDescriptor.isAuthnRequestsSigned() + keyDescriptor.name == 'sign' + keyDescriptor.usageType == 'signing' + x509cert.value == 'signingValue' + getModifiedEntityNames(entityDescriptorHistory, 0).sort() == expectedModifiedPersistentEntities.sort() + + when: + representation = new EntityDescriptorRepresentation().with { + it.securityInfo = new SecurityInfoRepresentation().with { + it.authenticationRequestsSigned = false + it.x509CertificateAvailable = true + it.x509Certificates = [new SecurityInfoRepresentation.X509CertificateRepresentation(name: 'sign', type: 'signing', value: 'signingValue'), + new SecurityInfoRepresentation.X509CertificateRepresentation(name: 'encrypt', type: 'encryption', value: 'encryptionValue')] + it + } + it + } + + entityDescriptorHistory = updateAndGetRevisionHistoryOfEntityDescriptor(ed, representation, entityDescriptorService, + entityDescriptorRepository, + txMgr, + entityManager) + + + //Get second revision + SPSSODescriptor spssoDescriptor_second = entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,1)) + + KeyDescriptor keyDescriptor_second1 = spssoDescriptor_second.keyDescriptors[0] + X509Certificate x509cert_second1 = keyDescriptor_second1.keyInfo.x509Datas[0].x509Certificates[0] + KeyDescriptor keyDescriptor_second2 = spssoDescriptor_second.keyDescriptors[1] + X509Certificate x509cert_second2 = keyDescriptor_second2.keyInfo.x509Datas[0].x509Certificates[0] + + + //Get initial revision + spssoDescriptor = + entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) + + keyDescriptor = spssoDescriptor.keyDescriptors[0] + x509cert = keyDescriptor.keyInfo.x509Datas[0].x509Certificates[0] + + then: + entityDescriptorHistory.size() == 2 + !spssoDescriptor_second.isAuthnRequestsSigned() + keyDescriptor_second1.name == 'sign' + keyDescriptor_second1.usageType == 'signing' + keyDescriptor_second2.name == 'encrypt' + keyDescriptor_second2.usageType == 'encryption' + x509cert_second1.value == 'signingValue' + x509cert_second2.value == 'encryptionValue' + getModifiedEntityNames(entityDescriptorHistory, 1).sort() == expectedModifiedPersistentEntities.sort() + + //Check the initial version is intact + spssoDescriptor.keyDescriptors.size() == 1 + spssoDescriptor.isAuthnRequestsSigned() + keyDescriptor.name == 'sign' + keyDescriptor.usageType == 'signing' + x509cert.value == 'signingValue' + } + + def "test versioning ACS"() { + setup: + def expectedModifiedPersistentEntities = [EntityDescriptor.name, + SPSSODescriptor.name, + AssertionConsumerService.name] + + when: + EntityDescriptor ed = new EntityDescriptor() + def representation = new EntityDescriptorRepresentation().with { + it.assertionConsumerServices = [ + new AssertionConsumerServiceRepresentation(locationUrl: 'http://acs', binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST')] + it + } + + def entityDescriptorHistory = updateAndGetRevisionHistoryOfEntityDescriptor(ed, representation, entityDescriptorService, + entityDescriptorRepository, + txMgr, + entityManager) + + SPSSODescriptor spssoDescriptor = + entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) + AssertionConsumerService acs = spssoDescriptor.assertionConsumerServices[0] + + then: + entityDescriptorHistory.size() == 1 + !acs.isDefault() + acs.location == 'http://acs' + acs.binding == 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' + getModifiedEntityNames(entityDescriptorHistory, 0).sort() == expectedModifiedPersistentEntities.sort() + + + when: + representation = new EntityDescriptorRepresentation().with { + it.assertionConsumerServices = [ + new AssertionConsumerServiceRepresentation(locationUrl: 'http://acs.updated', binding: 'urn:oasis:names:tc:SAML:2.0:bindings:PAOS', makeDefault: true), + new AssertionConsumerServiceRepresentation(locationUrl: 'http://acs2', binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact')] + it + } + + entityDescriptorHistory = updateAndGetRevisionHistoryOfEntityDescriptor(ed, representation, entityDescriptorService, + entityDescriptorRepository, + txMgr, + entityManager) + + SPSSODescriptor spssoDescriptor2 = + entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,1)) + def (acs1, acs2) = [spssoDescriptor2.assertionConsumerServices[0], spssoDescriptor2.assertionConsumerServices[1]] + + //Initial revision + spssoDescriptor = + entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) + acs = spssoDescriptor.assertionConsumerServices[0] + + then: + entityDescriptorHistory.size() == 2 + acs1.isDefault() + !acs2.isDefault() + acs1.location == 'http://acs.updated' + acs1.binding == 'urn:oasis:names:tc:SAML:2.0:bindings:PAOS' + acs2.location == 'http://acs2' + acs2.binding == 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact' + getModifiedEntityNames(entityDescriptorHistory, 1).sort() == expectedModifiedPersistentEntities.sort() + + //Check the initial revision is intact + !acs.isDefault() + acs.location == 'http://acs' + acs.binding == 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' + } + + def "test versioning logout"() { + setup: + def expectedModifiedPersistentEntities = [EntityDescriptor.name, + SPSSODescriptor.name, + SingleLogoutService.name] + + when: + EntityDescriptor ed = new EntityDescriptor() + def representation = new EntityDescriptorRepresentation().with { + it.logoutEndpoints = [new LogoutEndpointRepresentation(url: 'http://logout', bindingType: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST')] + it + } + + def entityDescriptorHistory = updateAndGetRevisionHistoryOfEntityDescriptor(ed, representation, entityDescriptorService, + entityDescriptorRepository, + txMgr, + entityManager) + + SPSSODescriptor spssoDescriptor = + entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + SingleLogoutService slo = spssoDescriptor.singleLogoutServices[0] + + then: + entityDescriptorHistory.size() == 1 + slo.location == 'http://logout' + slo.binding == 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' + getModifiedEntityNames(entityDescriptorHistory, 0).sort() == expectedModifiedPersistentEntities.sort() + + when: + representation = new EntityDescriptorRepresentation().with { + it.logoutEndpoints = [new LogoutEndpointRepresentation(url: 'http://logout.updated', bindingType: 'urn:oasis:names:tc:SAML:2.0:bindings:PAOS'), + new LogoutEndpointRepresentation(url: 'http://logout2', bindingType: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact')] + it + } + + entityDescriptorHistory = updateAndGetRevisionHistoryOfEntityDescriptor(ed, representation, entityDescriptorService, + entityDescriptorRepository, + txMgr, + entityManager) + + SPSSODescriptor spssoDescriptor2 = + entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory, 1)) + def (slo1, slo2) = [spssoDescriptor2.singleLogoutServices[0], spssoDescriptor2.singleLogoutServices[1]] + + //Initial revision + spssoDescriptor = + entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + slo = spssoDescriptor.singleLogoutServices[0] + + then: + entityDescriptorHistory.size() == 2 + slo1.location == 'http://logout.updated' + slo1.binding == 'urn:oasis:names:tc:SAML:2.0:bindings:PAOS' + slo2.location == 'http://logout2' + slo2.binding == 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact' + getModifiedEntityNames(entityDescriptorHistory, 1).sort() == expectedModifiedPersistentEntities.sort() + + //Check the initial version is intact + slo.location == 'http://logout' + slo.binding == 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' + } + + def "test versioning relying party overrides"() { + setup: + def expectedModifiedPersistentEntities = [EntityDescriptor.name, + EntityAttributes.name, + Extensions.name, + Attribute.name, + XSBoolean.name, + XSString.name] + + when: + EntityDescriptor ed = new EntityDescriptor() + def representation = new EntityDescriptorRepresentation().with { + it.relyingPartyOverrides = [signAssertion: true] + it.attributeRelease = ['attr1'] + it + } + + def entityDescriptorHistory = updateAndGetRevisionHistoryOfEntityDescriptor(ed, representation, entityDescriptorService, + entityDescriptorRepository, + txMgr, + entityManager) + + EntityAttributes attrs = entityDescriptorService.getEntityAttributes(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + + then: + entityDescriptorHistory.size() == 1 + attrs.attributes[0].attributeValues[0].storedValue == 'true' + attrs.attributes[1].attributeValues[0].xsStringvalue == 'attr1' + getModifiedEntityNames(entityDescriptorHistory, 0).sort() == expectedModifiedPersistentEntities.sort() + + when: + representation = new EntityDescriptorRepresentation().with { + it.relyingPartyOverrides = [signAssertion: false] + it.attributeRelease = ['attr1', 'attr2'] + it + } + + entityDescriptorHistory = updateAndGetRevisionHistoryOfEntityDescriptor(ed, representation, entityDescriptorService, + entityDescriptorRepository, + txMgr, + entityManager) + + EntityAttributes attrs2 = entityDescriptorService.getEntityAttributes(getTargetEntityForRevisionIndex(entityDescriptorHistory, 1)) + + //Initial revision + attrs = entityDescriptorService.getEntityAttributes(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + + expectedModifiedPersistentEntities = [EntityDescriptor.name, + EntityAttributes.name, + Attribute.name, + XSString.name] + then: + entityDescriptorHistory.size() == 2 + attrs2.attributes[0].attributeValues[0].xsStringvalue == 'attr1' + attrs2.attributes[0].attributeValues[1].xsStringvalue == 'attr2' + getModifiedEntityNames(entityDescriptorHistory, 1).sort() == expectedModifiedPersistentEntities.sort() + + //Check the initial revision is intact + attrs.attributes[0].attributeValues[0].storedValue == 'true' + attrs.attributes[1].attributeValues[0].xsStringvalue == 'attr1' + attrs.attributes[1].attributeValues[1] == null + } +} diff --git a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/MetadataFilterEnversVersioningTests.groovy b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/MetadataFilterEnversVersioningTests.groovy new file mode 100644 index 000000000..fe1e5eee4 --- /dev/null +++ b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/MetadataFilterEnversVersioningTests.groovy @@ -0,0 +1,315 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository.envers + +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.EntitiesVersioningConfiguration +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.filters.EntityAttributesFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityRoleWhiteListFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.SignatureValidationFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FilesystemMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverVersionService +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.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.Specification + + +/** + * Testing metadata resolver envers versioning with metadata filters + */ +@DataJpaTest +@ContextConfiguration(classes = [CoreShibUiConfiguration, InternationalizationConfiguration, SearchConfiguration, TestConfiguration, EntitiesVersioningConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class MetadataFilterEnversVersioningTests extends Specification { + + @Autowired + MetadataResolverRepository metadataResolverRepository + + @Autowired + FilterRepository filterRepository + + @Autowired + MetadataResolverVersionService metadataResolverVersionService + + @Autowired + PlatformTransactionManager txMgr + + + def "test versioning of MetadataResolver with EntityRoleWhiteListFilter"() { + when: 'Add initial filter' + def mr = new LocalDynamicMetadataResolver(name: 'resolver') + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + EntityRoleWhiteListFilter filter = new EntityRoleWhiteListFilter().with { + it.retainedRoles = ['role1'] + it + } + mr.metadataFilters.add(filter) + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + def versions = metadataResolverVersionService.findVersionsForMetadataResolver(mr.resourceId) + def mrv1 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[0].id) + def mrv2 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[1].id) + + then: + versions.size() == 2 + mrv1.metadataFilters.size() == 0 + mrv2.metadataFilters.size() == 1 + + when: 'Update filter' + filter = mr.metadataFilters[0] + filter.retainedRoles = ['role1', 'role2'] + filter.removeEmptyEntitiesDescriptors = false + filter = EnversTestsSupport.doInExplicitTransaction(txMgr) { + filterRepository.save(filter) + } + mr.markAsModified() + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + versions = metadataResolverVersionService.findVersionsForMetadataResolver(mr.resourceId) + mrv1 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[0].id) + mrv2 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[1].id) + def mrv3 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[2].id) + + then: + versions.size() == 3 + mrv1.metadataFilters.size() == 0 + mrv2.metadataFilters[0].retainedRoles.size() == 1 + mrv2.metadataFilters[0].retainedRoles == ['role1'] + mrv3.metadataFilters[0].retainedRoles.size() == 2 + mrv3.metadataFilters[0].retainedRoles == ['role1','role2'] + mrv3.metadataFilters[0].removeEmptyEntitiesDescriptors == false + } + + def "test versioning of MetadataResolver with EntityAttributesFilter"() { + when: 'Add initial filter' + def mr = new FileBackedHttpMetadataResolver(name: 'resolver') + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + EntityAttributesFilter filter = new EntityAttributesFilter().with { + it.attributeRelease = ['attr1'] + it + } + mr.metadataFilters.add(filter) + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + def versions = metadataResolverVersionService.findVersionsForMetadataResolver(mr.resourceId) + def mrv1 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[0].id) + def mrv2 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[1].id) + + then: + versions.size() == 2 + mrv1.metadataFilters.size() == 0 + mrv2.metadataFilters.size() == 1 + + when: 'Update filter' + filter = mr.metadataFilters[0] + filter.attributeRelease = ['attr1, attr2'] + filter = EnversTestsSupport.doInExplicitTransaction(txMgr) { + filterRepository.save(filter) + } + mr.markAsModified() + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + versions = metadataResolverVersionService.findVersionsForMetadataResolver(mr.resourceId) + mrv1 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[0].id) + mrv2 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[1].id) + def mrv3 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[2].id) + + then: + versions.size() == 3 + mrv1.metadataFilters.size() == 0 + mrv2.metadataFilters[0].attributes[0].attributeValues[0].xsStringvalue == 'attr1' + mrv3.metadataFilters[0].attributes[0].attributeValues[0].xsStringvalue == 'attr1, attr2' + } + + def "test versioning of MetadataResolver with SignatureValidationFilter"() { + when: 'Add initial filter' + def mr = new DynamicHttpMetadataResolver(name: 'resolver') + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + SignatureValidationFilter filter = new SignatureValidationFilter().with { + it.certificateFile = 'cert1.file' + it + } + mr.metadataFilters.add(filter) + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + def versions = metadataResolverVersionService.findVersionsForMetadataResolver(mr.resourceId) + def mrv1 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[0].id) + def mrv2 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[1].id) + + then: + versions.size() == 2 + mrv1.metadataFilters.size() == 0 + mrv2.metadataFilters.size() == 1 + mrv2.metadataFilters[0].certificateFile == 'cert1.file' + + when: 'Update filter' + filter = mr.metadataFilters[0] + filter.certificateFile = 'cert2.file' + filter = EnversTestsSupport.doInExplicitTransaction(txMgr) { + filterRepository.save(filter) + } + mr.markAsModified() + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + versions = metadataResolverVersionService.findVersionsForMetadataResolver(mr.resourceId) + mrv1 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[0].id) + mrv2 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[1].id) + def mrv3 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[2].id) + + then: + versions.size() == 3 + mrv1.metadataFilters.size() == 0 + mrv2.metadataFilters[0].certificateFile == 'cert1.file' + mrv3.metadataFilters[0].certificateFile == 'cert2.file' + } + + def "test versioning of MetadataResolver with RequiredValidUntilFilter"() { + when: 'Add initial filter' + def mr = new FilesystemMetadataResolver(name: 'resolver') + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + RequiredValidUntilFilter filter = new RequiredValidUntilFilter().with { + it.maxValidityInterval = "PT1S" + it + } + mr.metadataFilters.add(filter) + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + def versions = metadataResolverVersionService.findVersionsForMetadataResolver(mr.resourceId) + def mrv1 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[0].id) + def mrv2 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[1].id) + + then: + versions.size() == 2 + mrv1.metadataFilters.size() == 0 + mrv2.metadataFilters.size() == 1 + mrv2.metadataFilters[0].maxValidityInterval == 'PT1S' + + when: 'Update filter' + filter = mr.metadataFilters[0] + filter.maxValidityInterval = 'PT30S' + filter = EnversTestsSupport.doInExplicitTransaction(txMgr) { + filterRepository.save(filter) + } + mr.markAsModified() + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + versions = metadataResolverVersionService.findVersionsForMetadataResolver(mr.resourceId) + mrv1 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[0].id) + mrv2 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[1].id) + def mrv3 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[2].id) + + then: + versions.size() == 3 + mrv1.metadataFilters.size() == 0 + mrv2.metadataFilters[0].maxValidityInterval == 'PT1S' + mrv3.metadataFilters[0].maxValidityInterval == 'PT30S' + } + + def "test versioning of MetadataResolver with NameIdFormatFilter"() { + when: 'Add initial filter' + def mr = new ResourceBackedMetadataResolver(name: 'resolver') + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + NameIdFormatFilter filter = new NameIdFormatFilter().with { + it.formats = ['format1'] + it + } + mr.metadataFilters.add(filter) + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + def versions = metadataResolverVersionService.findVersionsForMetadataResolver(mr.resourceId) + def mrv1 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[0].id) + def mrv2 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[1].id) + + then: + versions.size() == 2 + mrv1.metadataFilters.size() == 0 + mrv2.metadataFilters.size() == 1 + mrv2.metadataFilters[0].formats == ['format1'] + + when: 'Update filter' + filter = mr.metadataFilters[0] + filter.formats = ['format1', 'format2'] + filter.removeExistingFormats = true + filter = EnversTestsSupport.doInExplicitTransaction(txMgr) { + filterRepository.save(filter) + } + mr.markAsModified() + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + versions = metadataResolverVersionService.findVersionsForMetadataResolver(mr.resourceId) + mrv1 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[0].id) + mrv2 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[1].id) + def mrv3 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[2].id) + + then: + versions.size() == 3 + mrv1.metadataFilters.size() == 0 + mrv2.metadataFilters[0].formats == ['format1'] + mrv3.metadataFilters[0].formats == ['format1', 'format2'] + mrv3.metadataFilters[0].removeExistingFormats == true + } + + def "test versioning of deleting a filter"() { + when: 'Add initial filter' + def mr = new MetadataResolver(name: 'resolver') + def filter = new EntityRoleWhiteListFilter() + + mr.metadataFilters.add(filter) + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + //And now remove filter + filter = filterRepository.findByResourceId(filter.resourceId) + mr.metadataFilters = [] + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + EnversTestsSupport.doInExplicitTransaction(txMgr) { + filterRepository.delete(filter) + } + + def versions = metadataResolverVersionService.findVersionsForMetadataResolver(mr.resourceId) + def mrv1 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[0].id) + def mrv2 = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[1].id) + + then: + versions.size() == 2 + mrv1.metadataFilters.size() == 1 + mrv2.metadataFilters.size() == 0 + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/MetadataResolverEntityBasicEnversVersioningTests.groovy b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/MetadataResolverEntityBasicEnversVersioningTests.groovy similarity index 95% rename from backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/MetadataResolverEntityBasicEnversVersioningTests.groovy rename to backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/MetadataResolverEntityBasicEnversVersioningTests.groovy index c4f70dbcd..f8ac3b431 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/MetadataResolverEntityBasicEnversVersioningTests.groovy +++ b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/MetadataResolverEntityBasicEnversVersioningTests.groovy @@ -21,8 +21,6 @@ import spock.lang.Specification import javax.persistence.EntityManager -import static org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW - /** * Testing metadata resolvers basic versioning by envers is functioning. */ @@ -55,7 +53,7 @@ class MetadataResolverEntityBasicEnversVersioningTests extends Specification { def rev = metadataResolverHistory[0] then: - rev[1].principalUserName == 'anonymous' + rev[1].principalUserName == 'anonymousUser' when: mdr.name = 'Updated' @@ -98,7 +96,7 @@ class MetadataResolverEntityBasicEnversVersioningTests extends Specification { //boundary of the test method which commits tx only after an execution of the test method. This let's us explicitly //start/commit transaction making envers data written out and verifiable private doInExplicitTransaction(Closure uow) { - def txStatus = txMgr.getTransaction(new DefaultTransactionDefinition(PROPAGATION_REQUIRES_NEW)) + def txStatus = txMgr.getTransaction(new DefaultTransactionDefinition(org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW)) def entity = uow() txMgr.commit(txStatus) entity 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 new file mode 100644 index 000000000..2ba81bcdf --- /dev/null +++ b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/MetadataResolverEnversVersioningTests.groovy @@ -0,0 +1,318 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository.envers + +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.resolvers.ClasspathMetadataResource +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicMetadataResolverAttributes +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FilesystemMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.HttpMetadataResolverAttributes +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ReloadableMetadataResolverAttributes +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.TemplateScheme +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +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.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.Specification + +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.* + +/** + * Testing metadata resolver envers versioning + */ +@DataJpaTest +@ContextConfiguration(classes = [CoreShibUiConfiguration, InternationalizationConfiguration, SearchConfiguration, TestConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class MetadataResolverEnversVersioningTests extends Specification { + + @Autowired + MetadataResolverRepository metadataResolverRepository + + @Autowired + EntityManager entityManager + + @Autowired + PlatformTransactionManager txMgr + + def "test versioning of LocalDynamicMetadataResolver"() { + setup: + def expectedModifiedPersistentEntities = [LocalDynamicMetadataResolver.name] + + when: + LocalDynamicMetadataResolver resolver = new LocalDynamicMetadataResolver(name: 'ldmr').with { + it.dynamicMetadataResolverAttributes = new DynamicMetadataResolverAttributes() + it + } + def resolverHistory = updateAndGetRevisionHistoryOfMetadataResolver(resolver, + metadataResolverRepository, + LocalDynamicMetadataResolver, + txMgr, + entityManager) + + then: + resolverHistory.size() == 1 + getTargetEntityForRevisionIndex(resolverHistory, 0).name == 'ldmr' + getTargetEntityForRevisionIndex(resolverHistory, 0).dynamicMetadataResolverAttributes.refreshDelayFactor == 0.75 + getRevisionEntityForRevisionIndex(resolverHistory, 0).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(resolverHistory, 0).timestamp > 0L + getModifiedEntityNames(resolverHistory, 0).sort() == expectedModifiedPersistentEntities.sort() + + when: + resolver.name = 'ldmr_UPDATED' + resolver.dynamicMetadataResolverAttributes.refreshDelayFactor = 1.00 + resolverHistory = updateAndGetRevisionHistoryOfMetadataResolver(resolver, + metadataResolverRepository, + LocalDynamicMetadataResolver, + txMgr, + entityManager) + + then: + resolverHistory.size() == 2 + getTargetEntityForRevisionIndex(resolverHistory, 1).name == 'ldmr_UPDATED' + getTargetEntityForRevisionIndex(resolverHistory, 1).dynamicMetadataResolverAttributes.refreshDelayFactor == 1.00 + getRevisionEntityForRevisionIndex(resolverHistory, 1).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(resolverHistory, 1).timestamp > 0L + + //Check the original revision is intact + getTargetEntityForRevisionIndex(resolverHistory, 0).name == 'ldmr' + getTargetEntityForRevisionIndex(resolverHistory, 0).dynamicMetadataResolverAttributes.refreshDelayFactor == 0.75 + getRevisionEntityForRevisionIndex(resolverHistory, 0).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(resolverHistory, 0).timestamp > 0L + } + + def "test versioning of FileBackedHttpMetadataResolver"() { + setup: + def expectedModifiedPersistentEntities = [FileBackedHttpMetadataResolver.name] + + when: + FileBackedHttpMetadataResolver resolver = new FileBackedHttpMetadataResolver(name: 'fbmr').with { + it.httpMetadataResolverAttributes = new HttpMetadataResolverAttributes(proxyUser: 'proxyUser', + proxyPassword: 'proxyPass', + httpCaching: none) + it.reloadableMetadataResolverAttributes = new ReloadableMetadataResolverAttributes(indexesRef: 'indexRef') + it + } + def resolverHistory = updateAndGetRevisionHistoryOfMetadataResolver(resolver, + metadataResolverRepository, + FileBackedHttpMetadataResolver, + txMgr, + entityManager) + + then: + resolverHistory.size() == 1 + getTargetEntityForRevisionIndex(resolverHistory, 0).name == 'fbmr' + getTargetEntityForRevisionIndex(resolverHistory, 0).httpMetadataResolverAttributes.proxyUser == 'proxyUser' + getTargetEntityForRevisionIndex(resolverHistory, 0).httpMetadataResolverAttributes.proxyPassword == 'proxyPass' + getTargetEntityForRevisionIndex(resolverHistory, 0).httpMetadataResolverAttributes.httpCaching == none + getTargetEntityForRevisionIndex(resolverHistory, 0).reloadableMetadataResolverAttributes.indexesRef == 'indexRef' + getRevisionEntityForRevisionIndex(resolverHistory, 0).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(resolverHistory, 0).timestamp > 0L + getModifiedEntityNames(resolverHistory, 0).sort() == expectedModifiedPersistentEntities.sort() + + when: + resolver.name = 'fbmrUPDATED' + resolver.httpMetadataResolverAttributes.proxyUser = 'proxyUserUPDATED' + resolver.httpMetadataResolverAttributes.proxyPassword = 'proxyPassUPDATED' + resolver.httpMetadataResolverAttributes.httpCaching = file + resolver.reloadableMetadataResolverAttributes.indexesRef = 'indexRefUPDATED' + + resolverHistory = updateAndGetRevisionHistoryOfMetadataResolver(resolver, + metadataResolverRepository, + FileBackedHttpMetadataResolver, + txMgr, + entityManager) + + then: + resolverHistory.size() == 2 + getTargetEntityForRevisionIndex(resolverHistory, 1).name == 'fbmrUPDATED' + getTargetEntityForRevisionIndex(resolverHistory, 1).httpMetadataResolverAttributes.proxyUser == 'proxyUserUPDATED' + getTargetEntityForRevisionIndex(resolverHistory, 1).httpMetadataResolverAttributes.proxyPassword == 'proxyPassUPDATED' + getTargetEntityForRevisionIndex(resolverHistory, 1).httpMetadataResolverAttributes.httpCaching == file + getTargetEntityForRevisionIndex(resolverHistory, 1).reloadableMetadataResolverAttributes.indexesRef == 'indexRefUPDATED' + getRevisionEntityForRevisionIndex(resolverHistory, 1).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(resolverHistory, 1).timestamp > 0L + getModifiedEntityNames(resolverHistory, 1).sort() == expectedModifiedPersistentEntities.sort() + + //Check the original revision is intact + getTargetEntityForRevisionIndex(resolverHistory, 0).name == 'fbmr' + getTargetEntityForRevisionIndex(resolverHistory, 0).httpMetadataResolverAttributes.proxyUser == 'proxyUser' + getTargetEntityForRevisionIndex(resolverHistory, 0).httpMetadataResolverAttributes.proxyPassword == 'proxyPass' + getTargetEntityForRevisionIndex(resolverHistory, 0).httpMetadataResolverAttributes.httpCaching == none + getTargetEntityForRevisionIndex(resolverHistory, 0).reloadableMetadataResolverAttributes.indexesRef == 'indexRef' + getRevisionEntityForRevisionIndex(resolverHistory, 0).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(resolverHistory, 0).timestamp > 0L + } + + def "test versioning of DynamicHttpMetadataResolver"() { + setup: + def expectedModifiedPersistentEntities = [DynamicHttpMetadataResolver.name, + TemplateScheme.name] + + when: + DynamicHttpMetadataResolver resolver = new DynamicHttpMetadataResolver(name: 'dhmr').with { + it.metadataRequestURLConstructionScheme = new TemplateScheme().with { + it.content = 'content' + it + } + it + } + + def resolverHistory = updateAndGetRevisionHistoryOfMetadataResolver(resolver, + metadataResolverRepository, + DynamicHttpMetadataResolver, + txMgr, + entityManager) + + then: + resolverHistory.size() == 1 + getTargetEntityForRevisionIndex(resolverHistory, 0).name == 'dhmr' + getRevisionEntityForRevisionIndex(resolverHistory, 0).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(resolverHistory, 0).timestamp > 0L + getModifiedEntityNames(resolverHistory, 0).sort() == expectedModifiedPersistentEntities.sort() + + when: + resolver.name = 'dhmrUPDATED' + resolver.metadataRequestURLConstructionScheme.content = 'Updated content' + + resolverHistory = updateAndGetRevisionHistoryOfMetadataResolver(resolver, + metadataResolverRepository, + DynamicHttpMetadataResolver, + txMgr, + entityManager) + + then: + resolverHistory.size() == 2 + getTargetEntityForRevisionIndex(resolverHistory, 1).name == 'dhmrUPDATED' + getTargetEntityForRevisionIndex(resolverHistory, 1).metadataRequestURLConstructionScheme.content == 'Updated content' + getRevisionEntityForRevisionIndex(resolverHistory, 1).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(resolverHistory, 1).timestamp > 0L + getModifiedEntityNames(resolverHistory, 1).sort() == expectedModifiedPersistentEntities.sort() + + //Check the original revision is intact + getTargetEntityForRevisionIndex(resolverHistory, 0).name == 'dhmr' + getRevisionEntityForRevisionIndex(resolverHistory, 0).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(resolverHistory, 0).timestamp > 0L + getModifiedEntityNames(resolverHistory, 0).sort() == expectedModifiedPersistentEntities.sort() + } + + def "test versioning of FilesystemMetadataResolver"() { + setup: + def expectedModifiedPersistentEntities = [FilesystemMetadataResolver.name] + + when: + FilesystemMetadataResolver resolver = new FilesystemMetadataResolver(name: 'fmr', metadataFile: 'metadata.xml').with { + it.reloadableMetadataResolverAttributes = new ReloadableMetadataResolverAttributes(indexesRef: 'indexesRef') + it + } + + def resolverHistory = updateAndGetRevisionHistoryOfMetadataResolver(resolver, + metadataResolverRepository, + FilesystemMetadataResolver, + txMgr, + entityManager) + + then: + resolverHistory.size() == 1 + getTargetEntityForRevisionIndex(resolverHistory, 0).name == 'fmr' + getTargetEntityForRevisionIndex(resolverHistory, 0).metadataFile == 'metadata.xml' + getTargetEntityForRevisionIndex(resolverHistory, 0).reloadableMetadataResolverAttributes.indexesRef == 'indexesRef' + getRevisionEntityForRevisionIndex(resolverHistory, 0).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(resolverHistory, 0).timestamp > 0L + getModifiedEntityNames(resolverHistory, 0).sort() == expectedModifiedPersistentEntities.sort() + + when: + resolver.name = 'fmrUPDATED' + resolver.metadataFile = 'metadataUPDATED.xml' + resolver.reloadableMetadataResolverAttributes.indexesRef = 'indexesRefUPDATED' + + resolverHistory = updateAndGetRevisionHistoryOfMetadataResolver(resolver, + metadataResolverRepository, + FilesystemMetadataResolver, + txMgr, + entityManager) + + then: + resolverHistory.size() == 2 + getTargetEntityForRevisionIndex(resolverHistory, 1).name == 'fmrUPDATED' + getTargetEntityForRevisionIndex(resolverHistory, 1).metadataFile == 'metadataUPDATED.xml' + getTargetEntityForRevisionIndex(resolverHistory, 1).reloadableMetadataResolverAttributes.indexesRef == 'indexesRefUPDATED' + getRevisionEntityForRevisionIndex(resolverHistory, 1).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(resolverHistory, 1).timestamp > 0L + getModifiedEntityNames(resolverHistory, 1).sort() == expectedModifiedPersistentEntities.sort() + + //Check the original revision is intact + getTargetEntityForRevisionIndex(resolverHistory, 0).name == 'fmr' + getTargetEntityForRevisionIndex(resolverHistory, 0).metadataFile == 'metadata.xml' + getTargetEntityForRevisionIndex(resolverHistory, 0).reloadableMetadataResolverAttributes.indexesRef == 'indexesRef' + getRevisionEntityForRevisionIndex(resolverHistory, 0).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(resolverHistory, 0).timestamp > 0L + } + + def "test versioning of ResourceBackedMetadataResolver"() { + setup: + def expectedModifiedPersistentEntities = [ResourceBackedMetadataResolver.name] + + when: + ResourceBackedMetadataResolver resolver = new ResourceBackedMetadataResolver(name: 'rbmr').with { + it.reloadableMetadataResolverAttributes = new ReloadableMetadataResolverAttributes(taskTimerRef: 'taskTimerRef') + it.classpathMetadataResource = new ClasspathMetadataResource(file: 'metadata.xml') + it + } + + def resolverHistory = updateAndGetRevisionHistoryOfMetadataResolver(resolver, + metadataResolverRepository, + ResourceBackedMetadataResolver, + txMgr, + entityManager) + + then: + resolverHistory.size() == 1 + getTargetEntityForRevisionIndex(resolverHistory, 0).name == 'rbmr' + getTargetEntityForRevisionIndex(resolverHistory, 0).reloadableMetadataResolverAttributes.taskTimerRef == 'taskTimerRef' + getTargetEntityForRevisionIndex(resolverHistory, 0).classpathMetadataResource.file == 'metadata.xml' + getRevisionEntityForRevisionIndex(resolverHistory, 0).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(resolverHistory, 0).timestamp > 0L + getModifiedEntityNames(resolverHistory, 0).sort() == expectedModifiedPersistentEntities.sort() + + when: + resolver.name = 'rbmrUPDATED' + resolver.reloadableMetadataResolverAttributes.taskTimerRef = 'taskTimerRefUPDATED' + resolver.classpathMetadataResource.file = 'metadataUPDATED.xml' + + resolverHistory = updateAndGetRevisionHistoryOfMetadataResolver(resolver, + metadataResolverRepository, + ResourceBackedMetadataResolver, + txMgr, + entityManager) + + then: + resolverHistory.size() == 2 + getTargetEntityForRevisionIndex(resolverHistory, 1).name == 'rbmrUPDATED' + getTargetEntityForRevisionIndex(resolverHistory, 1).reloadableMetadataResolverAttributes.taskTimerRef == 'taskTimerRefUPDATED' + getTargetEntityForRevisionIndex(resolverHistory, 1).classpathMetadataResource.file == 'metadataUPDATED.xml' + getRevisionEntityForRevisionIndex(resolverHistory, 1).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(resolverHistory, 1).timestamp > 0L + getModifiedEntityNames(resolverHistory, 1).sort() == expectedModifiedPersistentEntities.sort() + + //Check the original revision is intact + getTargetEntityForRevisionIndex(resolverHistory, 0).name == 'rbmr' + getTargetEntityForRevisionIndex(resolverHistory, 0).reloadableMetadataResolverAttributes.taskTimerRef == 'taskTimerRef' + getTargetEntityForRevisionIndex(resolverHistory, 0).classpathMetadataResource.file == 'metadata.xml' + getRevisionEntityForRevisionIndex(resolverHistory, 0).principalUserName == 'anonymousUser' + getRevisionEntityForRevisionIndex(resolverHistory, 0).timestamp > 0L + } +} diff --git a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/service/envers/EnversEntityDescriptorVersionServiceTests.groovy b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/service/envers/EnversEntityDescriptorVersionServiceTests.groovy new file mode 100644 index 000000000..ac5a4d129 --- /dev/null +++ b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/service/envers/EnversEntityDescriptorVersionServiceTests.groovy @@ -0,0 +1,121 @@ +package edu.internet2.tier.shibboleth.admin.ui.service.envers + +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.EntitiesVersioningConfiguration +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.repository.EntityDescriptorRepository +import edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionService +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.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.Specification + +import java.time.LocalDateTime +import java.time.ZonedDateTime + + +@DataJpaTest +@ContextConfiguration(classes = [CoreShibUiConfiguration, InternationalizationConfiguration, TestConfiguration, SearchConfiguration, EntitiesVersioningConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class EnversEntityDescriptorVersionServiceTests extends Specification { + + @Autowired + EntityDescriptorVersionService entityDescriptorVersionService + + @Autowired + EntityDescriptorRepository entityDescriptorRepository + + @Autowired + EntityDescriptorService entityDescriptorService + + @Autowired + PlatformTransactionManager txMgr + + def "versioning service returns correct number of versions sorted by modified date in natural order"() { + when: 'Initial version' + EntityDescriptor ed = new EntityDescriptor(entityID: 'ed', serviceProviderName: 'SP1') + ed = EnversTestsSupport.doInExplicitTransaction(txMgr) { + entityDescriptorRepository.save(ed) + } + def versions = entityDescriptorVersionService.findVersionsForEntityDescriptor(ed.resourceId) + + then: + versions.size() == 1 + versions[0].id + versions[0].creator + versions[0].date < ZonedDateTime.now() + + when: 'Second version' + ed.serviceProviderName = 'SP2' + ed = edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.doInExplicitTransaction(txMgr) { + entityDescriptorRepository.save(ed) + } + versions = entityDescriptorVersionService.findVersionsForEntityDescriptor(ed.resourceId) + + then: + versions.size() == 2 + versions[0].id && versions[1].id + versions[0].creator && versions[1].creator + versions[0].date < versions[1].date + + when: 'Third version' + ed.serviceProviderName = 'SP3' + ed = edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.doInExplicitTransaction(txMgr) { + entityDescriptorRepository.save(ed) + } + versions = entityDescriptorVersionService.findVersionsForEntityDescriptor(ed.resourceId) + + then: + versions.size() == 3 + versions[0].id && versions[1].id && versions[2].id + versions[0].creator && versions[1].creator && versions[2].creator + (versions[0].date < versions[1].date) && (versions[1].date < versions[2].date) + } + + def "versioning service returns correct entity descriptor for version number"() { + when: 'Initial version' + EntityDescriptor ed = new EntityDescriptor(entityID: 'ed', serviceProviderName: 'SP1', createdBy: 'anonymousUser') + ed = edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.doInExplicitTransaction(txMgr) { + entityDescriptorRepository.save(ed) + } + def versions = entityDescriptorVersionService.findVersionsForEntityDescriptor(ed.resourceId) + def v1EdRepresentation = entityDescriptorVersionService.findSpecificVersionOfEntityDescriptor(ed.resourceId, versions[0].id) + + then: + v1EdRepresentation.serviceProviderName == 'SP1' + v1EdRepresentation.id == ed.resourceId + + when: 'Update the original' + ed.serviceProviderName = 'SP2' + ed = edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.doInExplicitTransaction(txMgr) { + entityDescriptorRepository.save(ed) + } + versions = entityDescriptorVersionService.findVersionsForEntityDescriptor(ed.resourceId) + def v2EdRepresentation = entityDescriptorVersionService.findSpecificVersionOfEntityDescriptor(ed.resourceId, versions[1].id) + + then: + v2EdRepresentation.serviceProviderName == 'SP2' + v2EdRepresentation.id == ed.resourceId + } + + def "versioning service returns null for non existent version number"() { + when: 'Initial version' + EntityDescriptor ed = new EntityDescriptor(entityID: 'ed', serviceProviderName: 'SP1', createdBy: 'anonymousUser') + ed = edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport.doInExplicitTransaction(txMgr) { + entityDescriptorRepository.save(ed) + } + def edRepresentation = entityDescriptorVersionService.findSpecificVersionOfEntityDescriptor(ed.resourceId, '1000') + + then: + !edRepresentation + } +} diff --git a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/service/envers/EnversMetadataResolverVersionServiceTests.groovy b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/service/envers/EnversMetadataResolverVersionServiceTests.groovy new file mode 100644 index 000000000..8f68922ed --- /dev/null +++ b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/service/envers/EnversMetadataResolverVersionServiceTests.groovy @@ -0,0 +1,137 @@ +package edu.internet2.tier.shibboleth.admin.ui.service.envers + + +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.EntitiesVersioningConfiguration +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.resolvers.DynamicHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FilesystemMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.repository.envers.EnversTestsSupport +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverVersionService +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.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.PlatformTransactionManager +import spock.lang.Specification + +import java.time.LocalDateTime +import java.time.ZonedDateTime + + +@DataJpaTest +@ContextConfiguration(classes = [CoreShibUiConfiguration, InternationalizationConfiguration, TestConfiguration, SearchConfiguration, EntitiesVersioningConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class EnversMetadataResolverVersionServiceTests extends Specification { + + @Autowired + MetadataResolverVersionService metadataResolverVersionService + + @Autowired + MetadataResolverRepository metadataResolverRepository + + @Autowired + PlatformTransactionManager txMgr + + def "versioning service returns correct number of versions sorted by modified date in natural order"() { + when: 'Initial version' + MetadataResolver mr = new LocalDynamicMetadataResolver(name: 'ldmr') + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + def versions = metadataResolverVersionService.findVersionsForMetadataResolver(mr.resourceId) + + then: + versions.size() == 1 + versions[0].id + versions[0].creator + versions[0].date < ZonedDateTime.now() + + when: 'Second version' + mr.name = 'ldmr2' + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + versions = metadataResolverVersionService.findVersionsForMetadataResolver(mr.resourceId) + + + then: + versions.size() == 2 + versions[0].id && versions[1].id + versions[0].creator && versions[1].creator + versions[0].date < versions[1].date + + when: 'Third version' + mr.name = 'ldmr3' + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + versions = metadataResolverVersionService.findVersionsForMetadataResolver(mr.resourceId) + + then: + versions.size() == 3 + versions[0].id && versions[1].id && versions[2].id + versions[0].creator && versions[1].creator && versions[2].creator + (versions[0].date < versions[1].date) && (versions[1].date < versions[2].date) + } + + def "versioning service returns correct metadata resolver for version number"() { + when: 'Initial version' + MetadataResolver mr = new FilesystemMetadataResolver(name: 'fsmr') + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + def versions = metadataResolverVersionService.findVersionsForMetadataResolver(mr.resourceId) + def v1Mr = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[0].id) + + then: + v1Mr.name == 'fsmr' + v1Mr.resourceId == mr.resourceId + + when: 'Update the original' + mr.name = 'fsmr2' + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + versions = metadataResolverVersionService.findVersionsForMetadataResolver(mr.resourceId) + def v2Mr = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, versions[1].id) + + then: + v2Mr.name == 'fsmr2' + v2Mr.resourceId == mr.resourceId + } + + def "versioning service returns null for non existent version number"() { + when: 'Initial version' + MetadataResolver mr = new ResourceBackedMetadataResolver(name: 'rbmr') + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + def nonexitentMrVersion = metadataResolverVersionService.findSpecificVersionOfMetadataResolver(mr.resourceId, '1000') + + then: + !nonexitentMrVersion + } + + def "versioning service returns null for non existent metadata resolver number"() { + when: 'Initial version' + MetadataResolver mr = new DynamicHttpMetadataResolver(name: 'dhmr') + mr = EnversTestsSupport.doInExplicitTransaction(txMgr) { + metadataResolverRepository.save(mr) + } + def versions = metadataResolverVersionService.findVersionsForMetadataResolver(mr.resourceId) + def nonexitentMr = metadataResolverVersionService.findSpecificVersionOfMetadataResolver('non-existent', versions[0].id) + + then: + !nonexitentMr + } +} 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 92a7ce6b9..f28f15214 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 @@ -9,6 +9,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.HttpMetadataResol import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataQueryProtocolScheme import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ReloadableMetadataResolverAttributes +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.Role @@ -16,6 +17,7 @@ import edu.internet2.tier.shibboleth.admin.ui.security.model.User import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions + import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Profile import org.springframework.stereotype.Component @@ -32,11 +34,19 @@ class DevConfig { private final MetadataResolverRepository metadataResolverRepository private final EntityDescriptorRepository entityDescriptorRepository - DevConfig(UserRepository adminUserRepository, MetadataResolverRepository metadataResolverRepository, RoleRepository roleRepository, EntityDescriptorRepository entityDescriptorRepository) { + private final OpenSamlObjects openSamlObjects + + DevConfig(UserRepository adminUserRepository, + MetadataResolverRepository metadataResolverRepository, + RoleRepository roleRepository, + EntityDescriptorRepository entityDescriptorRepository, + OpenSamlObjects openSamlObjects) { + this.adminUserRepository = adminUserRepository this.metadataResolverRepository = metadataResolverRepository this.roleRepository = roleRepository this.entityDescriptorRepository = entityDescriptorRepository + this.openSamlObjects = openSamlObjects } @Transactional diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/RelyingPartyOverridesJsonSchemaValidatingControllerAdvice.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/RelyingPartyOverridesJsonSchemaValidatingControllerAdvice.groovy index 9151e4ef4..620d2252c 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/RelyingPartyOverridesJsonSchemaValidatingControllerAdvice.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/RelyingPartyOverridesJsonSchemaValidatingControllerAdvice.groovy @@ -1,16 +1,13 @@ package edu.internet2.tier.shibboleth.admin.ui.jsonschema -import edu.internet2.tier.shibboleth.admin.ui.controller.ErrorResponse + import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation import mjson.Json import org.springframework.beans.factory.annotation.Autowired import org.springframework.core.MethodParameter import org.springframework.http.HttpInputMessage -import org.springframework.http.HttpStatus -import org.springframework.http.ResponseEntity import org.springframework.http.converter.HttpMessageConverter import org.springframework.web.bind.annotation.ControllerAdvice -import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter import javax.annotation.PostConstruct diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java index 2bd90bcc3..42e7901e7 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/ShibbolethUiApplication.java @@ -23,7 +23,7 @@ @SpringBootApplication @ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = "edu.internet2.tier.shibboleth.admin.ui.configuration.auto.*")) -@EntityScan(basePackages = {"edu.internet2.tier.shibboleth.admin.ui.domain", "edu.internet2.tier.shibboleth.admin.ui.security.model"}) +@EntityScan(basePackages = {"edu.internet2.tier.shibboleth.admin.ui.domain", "edu.internet2.tier.shibboleth.admin.ui.envers", "edu.internet2.tier.shibboleth.admin.ui.security.model"}) @EnableJpaAuditing @EnableScheduling @EnableWebSecurity diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/EntitiesVersioningConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/EntitiesVersioningConfiguration.java new file mode 100644 index 000000000..ec21d42c4 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/EntitiesVersioningConfiguration.java @@ -0,0 +1,35 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration; + +import edu.internet2.tier.shibboleth.admin.ui.envers.EnversVersionServiceSupport; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionService; +import edu.internet2.tier.shibboleth.admin.ui.service.EnversEntityDescriptorVersionService; +import edu.internet2.tier.shibboleth.admin.ui.service.EnversMetadataResolverVersionService; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverVersionService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +@Configuration +public class EntitiesVersioningConfiguration { + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public EntityDescriptorVersionService entityDescriptorVersionService(EntityDescriptorService entityDescriptorService) { + return new EnversEntityDescriptorVersionService(entityManager, entityDescriptorService); + } + + @Bean + public MetadataResolverVersionService metadataResolverVersionService() { + return new EnversMetadataResolverVersionService(enversVersionServiceSupport()); + } + + @Bean + public EnversVersionServiceSupport enversVersionServiceSupport() { + return new EnversVersionServiceSupport(entityManager); + } +} 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 80213f5cd..8bb0da84f 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 @@ -2,13 +2,13 @@ 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.domain.versioning.Version; 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.User; -import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository; -import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionService; import org.opensaml.core.xml.io.MarshallingException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,8 +32,8 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import javax.annotation.PostConstruct; -import javax.xml.ws.Response; import java.net.URI; +import java.util.List; import java.util.stream.Collectors; @RestController @@ -52,20 +52,17 @@ public class EntityDescriptorController { @Autowired RestTemplateBuilder restTemplateBuilder; - private UserRepository userRepository; - - private RoleRepository roleRepository; - private UserService userService; private RestTemplate restTemplate; + private EntityDescriptorVersionService versionService; + private static Logger LOGGER = LoggerFactory.getLogger(EntityDescriptorController.class); - public EntityDescriptorController(UserRepository userRepository, RoleRepository roleRepository, UserService userService) { - this.userRepository = userRepository; - this.roleRepository = roleRepository; + public EntityDescriptorController(UserService userService, EntityDescriptorVersionService versionService) { this.userService = userService; + this.versionService = versionService; } @PostConstruct @@ -220,6 +217,40 @@ public ResponseEntity deleteOne(@PathVariable String resourceId) { } } + //Versioning endpoints + + @GetMapping("/EntityDescriptor/{resourceId}/Versions") + public ResponseEntity getAllVersions(@PathVariable String resourceId) { + EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); + if (ed == null) { + return ResponseEntity.notFound().build(); + } + List versions = versionService.findVersionsForEntityDescriptor(resourceId); + if (versions.isEmpty()) { + return ResponseEntity.notFound().build(); + } + if(isAuthorizedFor(ed.getCreatedBy())) { + return ResponseEntity.ok(versions); + } + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + + @GetMapping("/EntityDescriptor/{resourceId}/Versions/{versionId}") + public ResponseEntity getSpecificVersion(@PathVariable String resourceId, @PathVariable String versionId) { + EntityDescriptorRepresentation edRepresentation = + versionService.findSpecificVersionOfEntityDescriptor(resourceId, versionId); + + if (edRepresentation == null) { + return ResponseEntity.notFound().build(); + } + if(isAuthorizedFor(edRepresentation.getCreatedBy())) { + return ResponseEntity.ok(edRepresentation); + } + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + + //Private methods + private static URI getResourceUriFor(EntityDescriptor ed) { return ServletUriComponentsBuilder .fromCurrentServletMapping().path("/api/EntityDescriptor") @@ -267,4 +298,11 @@ private ResponseEntity handleUploadingEntityDescriptorXml(byte[] rawXmlBytes, .body(entityDescriptorService.createRepresentationFromDescriptor(persistedEd)); } + private boolean isAuthorizedFor(String username) { + User u = userService.getCurrentUser(); + return (u != null) && + (u.getRole().equals("ROLE_ADMIN") + || (u.getUsername().equals(username))); + } + } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java index 57d734f77..cbf5051ba 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java @@ -7,16 +7,10 @@ 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.resolvers.MetadataResolver; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlFunctionDrivenDynamicHTTPMetadataResolver; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlLocalDynamicMetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; -import net.shibboleth.utilities.java.support.component.ComponentInitializationException; -import net.shibboleth.utilities.java.support.resolver.ResolverException; -import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver; -import org.opensaml.saml.metadata.resolver.RefreshableMetadataResolver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -36,6 +30,7 @@ import java.net.URI; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Stream; @@ -102,19 +97,19 @@ public ResponseEntity create(@PathVariable String metadataResolverId, @Reques public ResponseEntity update(@PathVariable String metadataResolverId, @PathVariable String resourceId, @RequestBody MetadataFilter updatedFilter) { - MetadataFilter filterTobeUpdated = filterRepository.findByResourceId(resourceId); - if (filterTobeUpdated == null) { - return ResponseEntity.notFound().build(); - } MetadataResolver metadataResolver = findResolverOrThrowHttp404(metadataResolverId); - // check to make sure that the relationship exists - if (!metadataResolver.getMetadataFilters().contains(filterTobeUpdated)) { - // TODO: find a better response - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + //Now we operate directly on the filter attached to MetadataResolver, + //Instead of fetching filter separately, to accommodate correct envers versioning with uni-directional one-to-many + Optional filterTobeUpdatedOptional = metadataResolver.getMetadataFilters() + .stream() + .filter(it -> it.getResourceId().equals(resourceId)) + .findFirst(); + if(!filterTobeUpdatedOptional.isPresent()) { + return ResponseEntity.notFound().build(); } - + MetadataFilter filterTobeUpdated = filterTobeUpdatedOptional.get(); if (!resourceId.equals(updatedFilter.getResourceId())) { return new ResponseEntity(HttpStatus.CONFLICT); } @@ -130,6 +125,10 @@ public ResponseEntity update(@PathVariable String metadataResolverId, MetadataFilter persistedFilter = filterRepository.save(filterTobeUpdated); + //To support envers versioning from MetadataResolver side + metadataResolver.markAsModified(); + repository.save(metadataResolver); + // TODO: this is wrong metadataResolverService.reloadFilters(metadataResolver.getResourceId()); @@ -244,4 +243,4 @@ private static URI getResourceUriFor(MetadataResolver mr, String filterResourceI .build() .toUri(); } -} \ No newline at end of file +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java index 952c118c2..dbdc0ddd1 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java @@ -5,10 +5,12 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidationService; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.versioning.Version; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; import edu.internet2.tier.shibboleth.admin.ui.service.IndexWriterService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverConverterService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverVersionService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolversPositionOrderContainerService; import edu.internet2.tier.shibboleth.admin.util.OpenSamlChainingMetadataResolverUtil; import lombok.extern.slf4j.Slf4j; @@ -69,6 +71,9 @@ public class MetadataResolversController { @Autowired MetadataResolverConverterService metadataResolverConverterService; + @Autowired + MetadataResolverVersionService versionService; + @ExceptionHandler({InvalidTypeIdException.class, IOException.class, HttpMessageNotReadableException.class}) public ResponseEntity unableToParseJson(Exception ex) { return ResponseEntity.badRequest().body(new ErrorResponse(HttpStatus.BAD_REQUEST.toString(), ex.getMessage(), ex.getCause().getMessage())); @@ -149,6 +154,30 @@ public ResponseEntity update(@PathVariable String resourceId, @RequestBody Me return ResponseEntity.ok(persistedResolver); } + //Versioning endpoints + + @GetMapping("/MetadataResolvers/{resourceId}/Versions") + public ResponseEntity getAllVersions(@PathVariable String resourceId) { + MetadataResolver resolver = resolverRepository.findByResourceId(resourceId); + if (resolver == null) { + return ResponseEntity.notFound().build(); + } + List versions = versionService.findVersionsForMetadataResolver(resourceId); + if (versions.isEmpty()) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(versions); + } + + @GetMapping("/MetadataResolvers/{resourceId}/Versions/{versionId}") + public ResponseEntity getSpecificVersion(@PathVariable String resourceId, @PathVariable String versionId) { + MetadataResolver resolver = versionService.findSpecificVersionOfMetadataResolver(resourceId, versionId); + if (resolver == null) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(resolver); + } + @SuppressWarnings("Unchecked") private ResponseEntity validate(MetadataResolver metadataResolver) { ValidationResult validationResult = metadataResolverValidationService.validateIfNecessary(metadataResolver); @@ -159,6 +188,8 @@ private ResponseEntity validate(MetadataResolver metadataResolver) { return null; } + //Private methods + private static URI getResourceUriFor(MetadataResolver resolver) { return ServletUriComponentsBuilder .fromCurrentServletMapping().path("/api/MetadataResolvers/") diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAttributeExtensibleXMLObject.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAttributeExtensibleXMLObject.java index 075280330..2b4208865 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAttributeExtensibleXMLObject.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAttributeExtensibleXMLObject.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.AttributeExtensibleXMLObject; import org.opensaml.core.xml.util.AttributeMap; @@ -10,6 +11,7 @@ @MappedSuperclass @EqualsAndHashCode(callSuper = true, exclude={"unknownAttributes"}) +@Audited public abstract class AbstractAttributeExtensibleXMLObject extends AbstractXMLObject implements AttributeExtensibleXMLObject { private transient final AttributeMap unknownAttributes; @@ -24,4 +26,4 @@ public abstract class AbstractAttributeExtensibleXMLObject extends AbstractXMLOb public AttributeMap getUnknownAttributes() { return this.unknownAttributes; } -} \ No newline at end of file +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAuditable.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAuditable.java index 1d23d5113..567a6637e 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAuditable.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractAuditable.java @@ -3,6 +3,7 @@ import lombok.EqualsAndHashCode; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.envers.Audited; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; @@ -22,6 +23,7 @@ @MappedSuperclass @EntityListeners(AuditingEntityListener.class) @EqualsAndHashCode +@Audited public abstract class AbstractAuditable implements Auditable { @Id diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractDescriptor.java index 1f6075f24..449cda701 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractDescriptor.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractDescriptor.java @@ -2,6 +2,7 @@ import lombok.EqualsAndHashCode; import org.hibernate.annotations.Type; +import org.hibernate.envers.Audited; import org.joda.time.DateTime; import org.opensaml.core.xml.XMLObject; import org.opensaml.saml.saml2.common.CacheableSAMLObject; @@ -18,6 +19,7 @@ @MappedSuperclass @EqualsAndHashCode(callSuper = true) +@Audited public abstract class AbstractDescriptor extends AbstractAttributeExtensibleXMLObject implements CacheableSAMLObject, TimeBoundSAMLObject, SignableXMLObject { private Long cacheDuration; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractElementExtensibleXMLObject.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractElementExtensibleXMLObject.java index d406e8256..03a91e284 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractElementExtensibleXMLObject.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractElementExtensibleXMLObject.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.ElementExtensibleXMLObject; import org.opensaml.core.xml.XMLObject; @@ -20,6 +21,7 @@ @Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) @EqualsAndHashCode(callSuper = true) +@Audited public abstract class AbstractElementExtensibleXMLObject extends AbstractXMLObject implements ElementExtensibleXMLObject { @OneToMany(cascade = CascadeType.ALL) @OrderColumn diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractLangBearingURL.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractLangBearingURL.java index 7f66a06c6..96fb81a69 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractLangBearingURL.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractLangBearingURL.java @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; +import org.hibernate.envers.Audited; import org.opensaml.saml.saml2.metadata.LocalizedURI; import javax.annotation.Nullable; @@ -10,6 +11,7 @@ @Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) +@Audited abstract class AbstractLangBearingURL extends XSURI implements LocalizedURI { @Column(name = "informationUrlXmlLang") private String xmlLang; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractXMLObject.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractXMLObject.java index 1e8f4c2a0..3b4ac0d92 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractXMLObject.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractXMLObject.java @@ -3,6 +3,8 @@ import lombok.EqualsAndHashCode; import net.shibboleth.utilities.java.support.collection.LockableClassToInstanceMultiMap; import net.shibboleth.utilities.java.support.xml.QNameSupport; +import org.hibernate.envers.AuditOverride; +import org.hibernate.envers.Audited; import org.opensaml.core.config.ConfigurationService; import org.opensaml.core.xml.Namespace; import org.opensaml.core.xml.NamespaceManager; @@ -30,6 +32,8 @@ @Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) @EqualsAndHashCode(callSuper = true) +@Audited +@AuditOverride(forClass = AbstractAuditable.class) public abstract class AbstractXMLObject extends AbstractAuditable implements XMLObject { private String namespaceURI; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ArtifactResolutionService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ArtifactResolutionService.java index 0d82f8a0d..1231299a7 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ArtifactResolutionService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ArtifactResolutionService.java @@ -1,11 +1,13 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class ArtifactResolutionService extends IndexedEndpoint implements org.opensaml.saml.saml2.metadata.ArtifactResolutionService { } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AssertionConsumerService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AssertionConsumerService.java index a89045075..f63a76fcd 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AssertionConsumerService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AssertionConsumerService.java @@ -1,11 +1,13 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class AssertionConsumerService extends IndexedEndpoint implements org.opensaml.saml.saml2.metadata.AssertionConsumerService { } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Attribute.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Attribute.java index 45e1500e9..1a9a28323 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Attribute.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Attribute.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.XMLObject; import javax.annotation.Nullable; @@ -15,6 +16,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class Attribute extends AbstractAttributeExtensibleXMLObject implements org.opensaml.saml.saml2.core.Attribute { private String name; @@ -25,6 +27,7 @@ public class Attribute extends AbstractAttributeExtensibleXMLObject implements o @OneToMany(cascade = CascadeType.ALL) @OrderColumn + @Audited private List attributeValues = new ArrayList<>(); @Override diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeConsumingService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeConsumingService.java index 5080c1fc0..a1dba47fa 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeConsumingService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeConsumingService.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.schema.XSBooleanValue; import javax.persistence.CascadeType; @@ -14,6 +15,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class AttributeConsumingService extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.AttributeConsumingService { private int acsIndex; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ContactPerson.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ContactPerson.java index 104dc076f..ca0400f32 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ContactPerson.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ContactPerson.java @@ -1,6 +1,8 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; +import org.hibernate.envers.NotAudited; import org.opensaml.core.xml.XMLObject; import org.opensaml.saml.saml2.metadata.ContactPersonTypeEnumeration; @@ -12,20 +14,24 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class ContactPerson extends AbstractAttributeExtensibleXMLObject implements org.opensaml.saml.saml2.metadata.ContactPerson { private String contactPersonType; @OneToOne(cascade = CascadeType.ALL) + @NotAudited private Extensions extensions; @OneToOne(cascade = CascadeType.ALL) + @NotAudited private Company company; @OneToOne(cascade = CascadeType.ALL) private GivenName givenName; @OneToOne(cascade = CascadeType.ALL) + @NotAudited private SurName surName; @OneToMany(cascade = CascadeType.ALL) @@ -36,6 +42,7 @@ public class ContactPerson extends AbstractAttributeExtensibleXMLObject implemen @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "contactpersn_telenmbr_id") @OrderColumn + @NotAudited private List telephoneNumbers = new ArrayList<>(); @Override diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Description.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Description.java index 81b66542d..db994740a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Description.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Description.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.annotation.Nullable; import javax.persistence.Column; @@ -8,6 +9,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class Description extends AbstractXMLObject implements org.opensaml.saml.ext.saml2mdui.Description { @Column(name = "descriptionXMLLang") diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/DisplayName.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/DisplayName.java index 564b0b0f6..5ee4815d9 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/DisplayName.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/DisplayName.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.annotation.Nullable; import javax.persistence.Column; @@ -8,6 +9,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class DisplayName extends AbstractXMLObject implements org.opensaml.saml.ext.saml2mdui.DisplayName { @Column(name = "displayNameXMLLan") diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EmailAddress.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EmailAddress.java index 313ebd65e..2cf346579 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EmailAddress.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EmailAddress.java @@ -1,11 +1,14 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.AuditOverride; +import org.hibernate.envers.Audited; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class EmailAddress extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.EmailAddress { private String address; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EncryptionMethod.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EncryptionMethod.java index 688dea31d..06a47da63 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EncryptionMethod.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EncryptionMethod.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.xmlsec.encryption.KeySize; import org.opensaml.xmlsec.encryption.OAEPparams; @@ -10,6 +11,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class EncryptionMethod extends AbstractElementExtensibleXMLObject implements org.opensaml.saml.saml2.metadata.EncryptionMethod { private String algorithm; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Endpoint.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Endpoint.java index 97094187a..2699895a0 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Endpoint.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Endpoint.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.XMLObject; import javax.annotation.Nonnull; @@ -14,6 +15,7 @@ */ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class Endpoint extends AbstractAttributeExtensibleXMLObject implements org.opensaml.saml.saml2.metadata.Endpoint { private String binding; @@ -72,4 +74,4 @@ public List getUnknownXMLObjects() { public List getUnknownXMLObjects(@Nonnull QName qName) { return null; //TODO } -} \ No newline at end of file +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributes.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributes.java index ed97305fc..d48a3ea6f 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributes.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributes.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.XMLObject; import org.opensaml.saml.saml2.core.Assertion; @@ -16,6 +17,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class EntityAttributes extends AbstractElementExtensibleXMLObject implements org.opensaml.saml.ext.saml2mdattr.EntityAttributes { @OneToMany(cascade = CascadeType.ALL) 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 2f53e5cc8..dc036fa7f 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 @@ -2,27 +2,21 @@ import com.google.common.base.MoreObjects; import com.google.common.collect.Lists; - import lombok.EqualsAndHashCode; -import org.opensaml.core.config.ConfigurationService; +import org.hibernate.envers.Audited; +import org.hibernate.envers.NotAudited; import org.opensaml.core.xml.XMLObject; -import org.opensaml.core.xml.config.XMLObjectProviderRegistry; -import org.opensaml.core.xml.io.MarshallingException; import org.springframework.util.StringUtils; -import org.w3c.dom.Element; import javax.annotation.Nullable; - -import javax.persistence.JoinColumn; import javax.persistence.CascadeType; import javax.persistence.Entity; +import javax.persistence.JoinColumn; import javax.persistence.OneToMany; import javax.persistence.OneToOne; import javax.persistence.OrderColumn; import javax.persistence.Transient; - import javax.xml.namespace.QName; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -32,7 +26,8 @@ @Entity -@EqualsAndHashCode(callSuper = true) +@EqualsAndHashCode(callSuper = true, exclude={"versionModifiedTimestamp"}) +@Audited public class EntityDescriptor extends AbstractDescriptor implements org.opensaml.saml.saml2.metadata.EntityDescriptor { private String localId; @@ -44,6 +39,8 @@ public class EntityDescriptor extends AbstractDescriptor implements org.opensaml private String resourceId; + private Long versionModifiedTimestamp; + @OneToOne(cascade = CascadeType.ALL) private Organization organization; @@ -58,18 +55,23 @@ public class EntityDescriptor extends AbstractDescriptor implements org.opensaml @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "entitydesc_addlmetdatlocations_id") @OrderColumn + @NotAudited private List additionalMetadataLocations = new ArrayList<>(); @OneToOne(cascade = CascadeType.ALL) + @NotAudited private AuthnAuthorityDescriptor authnAuthorityDescriptor; @OneToOne(cascade = CascadeType.ALL) + @NotAudited private AttributeAuthorityDescriptor attributeAuthorityDescriptor; @OneToOne(cascade = CascadeType.ALL) + @NotAudited private PDPDescriptor pdpDescriptor; @OneToOne(cascade = CascadeType.ALL) + @NotAudited private AffiliationDescriptor affiliationDescriptor; public EntityDescriptor() { @@ -77,6 +79,10 @@ public EntityDescriptor() { this.resourceId = UUID.randomUUID().toString(); } + public void setVersionModifiedTimestamp(Long versionModifiedTimestamp) { + this.versionModifiedTimestamp = versionModifiedTimestamp; + } + //getters and setters @Override public String getID() { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Extensions.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Extensions.java index c1538f463..01dcf4aa8 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Extensions.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Extensions.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.XMLObject; import javax.annotation.Nullable; @@ -13,6 +14,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class Extensions extends AbstractElementExtensibleXMLObject implements org.opensaml.saml.saml2.metadata.Extensions { @Nullable @Override diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/GivenName.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/GivenName.java index 785b60804..612733fc4 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/GivenName.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/GivenName.java @@ -1,11 +1,14 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.AuditOverride; +import org.hibernate.envers.Audited; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class GivenName extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.GivenName { private String name; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IndexedEndpoint.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IndexedEndpoint.java index e8d8f1551..cc0fdd6d5 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IndexedEndpoint.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/IndexedEndpoint.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.schema.XSBooleanValue; import javax.persistence.Entity; @@ -8,6 +9,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class IndexedEndpoint extends Endpoint implements org.opensaml.saml.saml2.metadata.IndexedEndpoint { private Integer endpointIndex; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/InformationURL.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/InformationURL.java index 45d5477d1..e960e8463 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/InformationURL.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/InformationURL.java @@ -1,10 +1,12 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class InformationURL extends AbstractLangBearingURL implements org.opensaml.saml.ext.saml2mdui.InformationURL { } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/KeyDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/KeyDescriptor.java index 74ddbaa2c..2b07eea4c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/KeyDescriptor.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/KeyDescriptor.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.XMLObject; import org.opensaml.security.credential.UsageType; import org.opensaml.xmlsec.signature.KeyInfo; @@ -12,6 +13,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class KeyDescriptor extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.KeyDescriptor { @Column(name = "keyDescriptorName") diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/KeyInfo.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/KeyInfo.java index 7ea604d17..7497e5837 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/KeyInfo.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/KeyInfo.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.XMLObject; import org.opensaml.xmlsec.encryption.AgreementMethod; import org.opensaml.xmlsec.encryption.EncryptedKey; @@ -29,6 +30,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class KeyInfo extends AbstractXMLObject implements org.opensaml.xmlsec.signature.KeyInfo { @OneToMany(cascade = CascadeType.ALL) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/LocalizedName.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/LocalizedName.java index 2aa493293..e0a54d494 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/LocalizedName.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/LocalizedName.java @@ -1,9 +1,12 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; +import org.hibernate.envers.Audited; + import javax.annotation.Nullable; import javax.persistence.MappedSuperclass; @MappedSuperclass +@Audited public class LocalizedName extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.LocalizedName { private String xMLLang; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Logo.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Logo.java index b9173bb22..13453438a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Logo.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Logo.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.annotation.Nullable; import javax.persistence.Column; @@ -8,6 +9,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class Logo extends AbstractXMLObject implements org.opensaml.saml.ext.saml2mdui.Logo { @Column(name = "logUrl") private String url; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ManageNameIDService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ManageNameIDService.java index ec43780d3..70924db96 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ManageNameIDService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ManageNameIDService.java @@ -1,11 +1,13 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class ManageNameIDService extends Endpoint implements org.opensaml.saml.saml2.metadata.ManageNameIDService { } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/NameIDFormat.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/NameIDFormat.java index 4e63ff301..148ec91e7 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/NameIDFormat.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/NameIDFormat.java @@ -1,11 +1,13 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class NameIDFormat extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.NameIDFormat { private String format; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Organization.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Organization.java index 1e11b499e..7ea76b1f0 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Organization.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Organization.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.XMLObject; import javax.annotation.Nullable; @@ -11,6 +12,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class Organization extends AbstractAttributeExtensibleXMLObject implements org.opensaml.saml.saml2.metadata.Organization { @OneToOne(cascade = CascadeType.ALL) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationDisplayName.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationDisplayName.java index 8d473aa82..971217fd8 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationDisplayName.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationDisplayName.java @@ -1,11 +1,13 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class OrganizationDisplayName extends LocalizedName implements org.opensaml.saml.saml2.metadata.OrganizationDisplayName { } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationName.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationName.java index edc9317c1..5af2408b9 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationName.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationName.java @@ -1,11 +1,13 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class OrganizationName extends LocalizedName implements org.opensaml.saml.saml2.metadata.OrganizationName { } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationURL.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationURL.java index 6af0d7318..1cf739ece 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationURL.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/OrganizationURL.java @@ -1,12 +1,14 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.annotation.Nullable; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class OrganizationURL extends AbstractXMLObject implements org.opensaml.saml.saml2.metadata.OrganizationURL { private String xMLLang; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/PrivacyStatementURL.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/PrivacyStatementURL.java index a6d792d80..a98f5a7ad 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/PrivacyStatementURL.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/PrivacyStatementURL.java @@ -1,10 +1,12 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class PrivacyStatementURL extends AbstractLangBearingURL implements org.opensaml.saml.ext.saml2mdui.PrivacyStatementURL { } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RequestedAttribute.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RequestedAttribute.java index a7860e931..17c932ed2 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RequestedAttribute.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RequestedAttribute.java @@ -1,12 +1,14 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.schema.XSBooleanValue; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class RequestedAttribute extends Attribute implements org.opensaml.saml.saml2.metadata.RequestedAttribute { private boolean isRequired; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptor.java index fc235c110..b5bc78bc6 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptor.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/RoleDescriptor.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.util.AttributeMap; @@ -25,6 +26,7 @@ @Entity @EqualsAndHashCode(callSuper = true, exclude={"unknownAttributes"}) +@Audited public class RoleDescriptor extends AbstractDescriptor implements org.opensaml.saml.saml2.metadata.RoleDescriptor { @ElementCollection diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SPSSODescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SPSSODescriptor.java index 379c2c928..e90542c5a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SPSSODescriptor.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SPSSODescriptor.java @@ -2,6 +2,7 @@ import com.google.common.collect.Lists; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.schema.XSBooleanValue; @@ -14,6 +15,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class SPSSODescriptor extends SSODescriptor implements org.opensaml.saml.saml2.metadata.SPSSODescriptor { private Boolean isAuthnRequestsSigned; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SSODescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SSODescriptor.java index 1f34fc931..4e885581d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SSODescriptor.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SSODescriptor.java @@ -2,6 +2,7 @@ import com.google.common.collect.Lists; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.XMLObject; import javax.annotation.Nullable; @@ -17,6 +18,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class SSODescriptor extends RoleDescriptor implements org.opensaml.saml.saml2.metadata.SSODescriptor { @OneToMany(cascade = CascadeType.ALL) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ServiceDescription.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ServiceDescription.java index 4c3a54511..9fda942ae 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ServiceDescription.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ServiceDescription.java @@ -1,11 +1,13 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class ServiceDescription extends LocalizedName implements org.opensaml.saml.saml2.metadata.ServiceDescription { } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ServiceName.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ServiceName.java index b4047f0c8..7e23c2b67 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ServiceName.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/ServiceName.java @@ -1,11 +1,13 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class ServiceName extends LocalizedName implements org.opensaml.saml.saml2.metadata.ServiceName { } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SingleLogoutService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SingleLogoutService.java index 67967676e..ca9aa095c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SingleLogoutService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/SingleLogoutService.java @@ -1,11 +1,13 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class SingleLogoutService extends Endpoint implements org.opensaml.saml.saml2.metadata.SingleLogoutService { } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/UIInfo.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/UIInfo.java index f7912aa65..241eebfac 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/UIInfo.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/UIInfo.java @@ -3,6 +3,7 @@ import lombok.EqualsAndHashCode; import org.hibernate.annotations.Cascade; import org.hibernate.annotations.CascadeType; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.XMLObject; import org.opensaml.saml.ext.saml2mdui.Description; import org.opensaml.saml.ext.saml2mdui.DisplayName; @@ -22,6 +23,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class UIInfo extends AbstractXMLObject implements org.opensaml.saml.ext.saml2mdui.UIInfo { @OneToMany @Cascade(CascadeType.ALL) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Certificate.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Certificate.java index 2fe5ea571..b1db21b72 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Certificate.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Certificate.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.annotation.Nullable; import javax.persistence.Column; @@ -9,6 +10,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class X509Certificate extends AbstractXMLObject implements org.opensaml.xmlsec.signature.X509Certificate { @Column(name = "x509CertificateValue") @Lob diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Data.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Data.java index 46c58324a..e875932cd 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Data.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/X509Data.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.XMLObject; import org.opensaml.xmlsec.signature.X509CRL; import org.opensaml.xmlsec.signature.X509Certificate; @@ -23,6 +24,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class X509Data extends AbstractXMLObject implements org.opensaml.xmlsec.signature.X509Data { @OneToMany(cascade = CascadeType.ALL) @OrderColumn diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSAny.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSAny.java index 1c1d32f4b..de81fcdf6 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSAny.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSAny.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.util.AttributeMap; import javax.annotation.Nonnull; @@ -10,6 +11,7 @@ @Entity @EqualsAndHashCode(callSuper = true, exclude = {"unknownAttributes"}) +@Audited public class XSAny extends AbstractElementExtensibleXMLObject implements org.opensaml.core.xml.schema.XSAny { private String textContext; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBase64Binary.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBase64Binary.java index 0d886d52a..edfd8eec8 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBase64Binary.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBase64Binary.java @@ -1,12 +1,14 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.annotation.Nullable; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class XSBase64Binary extends AbstractXMLObject implements org.opensaml.core.xml.schema.XSBase64Binary { private String b64value; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBoolean.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBoolean.java index 72399bf06..8b0f258d1 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBoolean.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBoolean.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.opensaml.core.xml.schema.XSBooleanValue; import javax.annotation.Nullable; @@ -9,6 +10,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class XSBoolean extends AbstractXMLObject implements org.opensaml.core.xml.schema.XSBoolean { private String storedValue; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSDateTime.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSDateTime.java index 0ea3116f8..1b13a3cd4 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSDateTime.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSDateTime.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import org.joda.time.DateTime; import org.joda.time.chrono.ISOChronology; import org.joda.time.format.DateTimeFormatter; @@ -13,6 +14,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class XSDateTime extends AbstractXMLObject implements org.opensaml.core.xml.schema.XSDateTime { private DateTime dateTime; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSInteger.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSInteger.java index 294d0a575..cd6c205da 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSInteger.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSInteger.java @@ -1,12 +1,14 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.annotation.Nullable; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class XSInteger extends AbstractXMLObject implements org.opensaml.core.xml.schema.XSInteger { private int intValue; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSQName.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSQName.java index 259e3df36..b210ea7a4 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSQName.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSQName.java @@ -2,6 +2,7 @@ import lombok.EqualsAndHashCode; import net.shibboleth.utilities.java.support.xml.QNameSupport; +import org.hibernate.envers.Audited; import javax.annotation.Nullable; import javax.persistence.Entity; @@ -10,6 +11,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class XSQName extends AbstractXMLObject implements org.opensaml.core.xml.schema.XSQName { @Nullable @Override diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSString.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSString.java index f35e8e287..dbc0d4c38 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSString.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSString.java @@ -1,12 +1,14 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.annotation.Nullable; import javax.persistence.Entity; @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class XSString extends AbstractXMLObject implements org.opensaml.core.xml.schema.XSString { private String xsStringvalue; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSURI.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSURI.java index 2780c5472..8663bf818 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSURI.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSURI.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; import lombok.EqualsAndHashCode; +import org.hibernate.envers.Audited; import javax.annotation.Nullable; import javax.persistence.Column; @@ -8,6 +9,7 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited public class XSURI extends AbstractXMLObject implements org.opensaml.core.xml.schema.XSURI { @Column(name = "xsuriValue") private String value; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java index 9d68c39b3..5d6d3132a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java @@ -6,6 +6,7 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.hibernate.envers.Audited; import javax.persistence.CascadeType; import javax.persistence.Entity; @@ -28,6 +29,7 @@ @Getter @Setter @ToString +@Audited public class EntityAttributesFilter extends MetadataFilter { public EntityAttributesFilter() { @@ -70,4 +72,4 @@ public void intoTransientRepresentation() { this.attributeRelease = getAttributeReleaseListFromAttributeList(this.attributes); this.relyingPartyOverrides = getRelyingPartyOverridesRepresentationFromAttributeList(this.attributes); } -} \ No newline at end of file +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTarget.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTarget.java index 5effdae3d..0abbbd11c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTarget.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTarget.java @@ -1,8 +1,9 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.filters; -import com.fasterxml.jackson.annotation.JsonIgnore; import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; import lombok.EqualsAndHashCode; +import org.hibernate.envers.AuditOverride; +import org.hibernate.envers.Audited; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,6 +15,8 @@ @Entity @EqualsAndHashCode(callSuper = true) +@Audited +@AuditOverride(forClass = AbstractAuditable.class) public class EntityAttributesFilterTarget extends AbstractAuditable { public enum EntityAttributesFilterTargetType { ENTITY, CONDITION_SCRIPT, CONDITION_REF, REGEX diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityRoleWhiteListFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityRoleWhiteListFilter.java index 111b75c2a..26c5e9976 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityRoleWhiteListFilter.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityRoleWhiteListFilter.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.hibernate.envers.Audited; import javax.persistence.CollectionTable; import javax.persistence.Column; @@ -19,6 +20,7 @@ @Getter @Setter @ToString +@Audited public class EntityRoleWhiteListFilter extends MetadataFilter { public EntityRoleWhiteListFilter() { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java index 5ee500437..b15058cd0 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java @@ -10,6 +10,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; +import org.hibernate.envers.AuditOverride; import org.hibernate.envers.Audited; import javax.persistence.Column; @@ -36,6 +37,7 @@ @JsonSubTypes.Type(value=RequiredValidUntilFilter.class, name="RequiredValidUntil"), @JsonSubTypes.Type(value=NameIdFormatFilter.class, name="NameIDFormat")}) @Audited +@AuditOverride(forClass = AbstractAuditable.class) public class MetadataFilter extends AbstractAuditable { @JsonProperty("@type") @@ -59,4 +61,4 @@ public int getVersion() { } return this.hashCode(); } -} \ No newline at end of file +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilter.java index 608561407..157e5f766 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilter.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilter.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.hibernate.envers.Audited; import javax.persistence.CascadeType; import javax.persistence.ElementCollection; @@ -17,6 +18,7 @@ @Getter @Setter @ToString +@Audited public class NameIdFormatFilter extends MetadataFilter { public NameIdFormatFilter() { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilterTarget.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilterTarget.java index a346d983f..c8fd10c90 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilterTarget.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilterTarget.java @@ -3,6 +3,8 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; import lombok.EqualsAndHashCode; import lombok.ToString; +import org.hibernate.envers.AuditOverride; +import org.hibernate.envers.Audited; import javax.persistence.ElementCollection; import javax.persistence.Entity; @@ -13,6 +15,8 @@ @Entity @EqualsAndHashCode(callSuper = true) @ToString +@Audited +@AuditOverride(forClass = AbstractAuditable.class) public class NameIdFormatFilterTarget extends AbstractAuditable { public enum NameIdFormatFilterTargetType { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/RequiredValidUntilFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/RequiredValidUntilFilter.java index 1aca84d4b..cda431408 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/RequiredValidUntilFilter.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/RequiredValidUntilFilter.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.hibernate.envers.Audited; import javax.persistence.Entity; @@ -12,6 +13,7 @@ @Getter @Setter @ToString +@Audited public class RequiredValidUntilFilter extends MetadataFilter { public RequiredValidUntilFilter() { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/SignatureValidationFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/SignatureValidationFilter.java index 7e83ae6d4..5cf71225e 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/SignatureValidationFilter.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/SignatureValidationFilter.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.hibernate.envers.Audited; import javax.persistence.Entity; @@ -12,6 +13,7 @@ @Getter @Setter @ToString +@Audited public class SignatureValidationFilter extends MetadataFilter { public SignatureValidationFilter() { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicHttpMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicHttpMetadataResolver.java index e8deb0e3e..732078747 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicHttpMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicHttpMetadataResolver.java @@ -22,6 +22,7 @@ @Getter @Setter @ToString +@Audited public class DynamicHttpMetadataResolver extends MetadataResolver { public static final String DEFAULT_TIMEOUT = "PT5S"; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FileBackedHttpMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FileBackedHttpMetadataResolver.java index 4ffadae52..17fe28ab9 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FileBackedHttpMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FileBackedHttpMetadataResolver.java @@ -1,11 +1,10 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; -import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; -import org.hibernate.envers.AuditOverride; + import org.hibernate.envers.Audited; import javax.persistence.Embedded; @@ -17,7 +16,6 @@ @Setter @ToString @Audited -@AuditOverride(forClass = AbstractAuditable.class) public class FileBackedHttpMetadataResolver extends MetadataResolver { public FileBackedHttpMetadataResolver() { type = "FileBackedHttpMetadataResolver"; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FilesystemMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FilesystemMetadataResolver.java index b96a74c14..ce3319d02 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FilesystemMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FilesystemMetadataResolver.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.hibernate.envers.Audited; import javax.persistence.Embedded; import javax.persistence.Entity; @@ -16,6 +17,7 @@ @Getter @Setter @ToString +@Audited public class FilesystemMetadataResolver extends MetadataResolver { public FilesystemMetadataResolver() { type = "FilesystemMetadataResolver"; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/LocalDynamicMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/LocalDynamicMetadataResolver.java index 6b722803a..49cc09642 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/LocalDynamicMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/LocalDynamicMetadataResolver.java @@ -8,11 +8,14 @@ import javax.persistence.Embedded; import javax.persistence.Entity; +import org.hibernate.envers.Audited; + @Entity @EqualsAndHashCode(callSuper = true) @Getter @Setter @ToString +@Audited public class LocalDynamicMetadataResolver extends MetadataResolver { public LocalDynamicMetadataResolver() { type = "LocalDynamicMetadataResolver"; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataQueryProtocolScheme.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataQueryProtocolScheme.java index cc6dffb98..77b7cc733 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataQueryProtocolScheme.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataQueryProtocolScheme.java @@ -3,6 +3,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import org.hibernate.envers.Audited; import javax.persistence.Entity; @@ -13,6 +14,7 @@ @Getter @Setter @EqualsAndHashCode(callSuper = true) +@Audited public class MetadataQueryProtocolScheme extends MetadataRequestURLConstructionScheme { public MetadataQueryProtocolScheme() { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataRequestURLConstructionScheme.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataRequestURLConstructionScheme.java index adddcc5aa..af3b0eab5 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataRequestURLConstructionScheme.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataRequestURLConstructionScheme.java @@ -7,6 +7,8 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import org.hibernate.envers.AuditOverride; +import org.hibernate.envers.Audited; import javax.persistence.Entity; import javax.persistence.Transient; @@ -24,6 +26,8 @@ @JsonSubTypes({@JsonSubTypes.Type(value=MetadataQueryProtocolScheme.class, name="MetadataQueryProtocol"), @JsonSubTypes.Type(value=TemplateScheme.class, name="Template"), @JsonSubTypes.Type(value=RegexScheme.class, name="Regex")}) +@Audited +@AuditOverride(forClass = AbstractAuditable.class) public abstract class MetadataRequestURLConstructionScheme extends AbstractAuditable { public enum SchemeType { METADATA_QUERY_PROTOCOL("MetadataQueryProtocol"), diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java index ec639ba38..dc30d40d8 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -11,6 +12,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; +import org.hibernate.envers.AuditOverride; import org.hibernate.envers.Audited; import javax.persistence.CascadeType; @@ -27,7 +29,7 @@ @Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) -@EqualsAndHashCode(callSuper = true, exclude = {"version"}) +@EqualsAndHashCode(callSuper = true, exclude = {"version", "versionModifiedTimestamp"}) @NoArgsConstructor @Getter @Setter @@ -39,6 +41,7 @@ @JsonSubTypes.Type(value = FilesystemMetadataResolver.class, name = "FilesystemMetadataResolver"), @JsonSubTypes.Type(value = ResourceBackedMetadataResolver.class, name = "ResourceBackedMetadataResolver")}) @Audited +@AuditOverride(forClass = AbstractAuditable.class) public class MetadataResolver extends AbstractAuditable { @JsonProperty("@type") @@ -70,6 +73,9 @@ public class MetadataResolver extends AbstractAuditable { private Boolean doInitialization = true; + @JsonIgnore + private Long versionModifiedTimestamp; + @OneToMany(cascade = CascadeType.ALL) @OrderColumn private List metadataFilters = new ArrayList<>(); @@ -84,4 +90,8 @@ public int getVersion() { } return this.hashCode(); } + + public void markAsModified() { + this.versionModifiedTimestamp = System.currentTimeMillis(); + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/RegexScheme.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/RegexScheme.java index a8d4fffaa..ce0202b6c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/RegexScheme.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/RegexScheme.java @@ -3,6 +3,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import org.hibernate.envers.Audited; import javax.persistence.Column; import javax.persistence.Entity; @@ -15,6 +16,7 @@ @Getter @Setter @EqualsAndHashCode(callSuper = true) +@Audited public class RegexScheme extends MetadataRequestURLConstructionScheme { public RegexScheme() { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataResolver.java index 55c833f2d..2b480641f 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataResolver.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.hibernate.envers.Audited; import javax.persistence.Embedded; import javax.persistence.Entity; @@ -16,6 +17,7 @@ @Getter @Setter @ToString +@Audited public class ResourceBackedMetadataResolver extends MetadataResolver { public ResourceBackedMetadataResolver() { type = "ResourceBackedMetadataResolver"; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/TemplateScheme.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/TemplateScheme.java index 6cb0c8d90..a176bacd7 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/TemplateScheme.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/TemplateScheme.java @@ -3,6 +3,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import org.hibernate.envers.Audited; import javax.persistence.Entity; @@ -13,6 +14,7 @@ @Getter @Setter @EqualsAndHashCode(callSuper = true) +@Audited public class TemplateScheme extends MetadataRequestURLConstructionScheme { public TemplateScheme () { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/versioning/Version.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/versioning/Version.java new file mode 100644 index 000000000..100b0fdbd --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/versioning/Version.java @@ -0,0 +1,34 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.versioning; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; +import java.time.ZonedDateTime; + +/** + * Represents version information of any versioned entity in the system. + */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +@EqualsAndHashCode +public class Version implements Serializable { + + private String id; + + private String creator; + + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + private ZonedDateTime date; + + private static final long serialVersionUID = 3429591830989243421L; + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/envers/EnversVersionServiceSupport.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/envers/EnversVersionServiceSupport.java new file mode 100644 index 000000000..964ede86a --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/envers/EnversVersionServiceSupport.java @@ -0,0 +1,61 @@ +package edu.internet2.tier.shibboleth.admin.ui.envers; + +import edu.internet2.tier.shibboleth.admin.ui.domain.versioning.Version; +import org.hibernate.envers.AuditReaderFactory; +import org.hibernate.envers.query.AuditEntity; + +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import java.time.ZoneOffset; +import java.util.List; + +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; + +/** + * Encapsulates common functionality interfacing with Envers AuditReader low level API + * to query for revisions of various persistent entities. + */ +public class EnversVersionServiceSupport { + + private EntityManager entityManager; + + public EnversVersionServiceSupport(EntityManager entityManager) { + this.entityManager = entityManager; + } + + public List findVersionsForPersistentEntity(String resourceId, Class entityClass) { + List revs = AuditReaderFactory.get(entityManager).createQuery() + .forRevisionsOfEntity(entityClass, false, false) + .add(AuditEntity.property("resourceId").eq(resourceId)) + .getResultList(); + + Object listOfVersions = revs.stream() + .map(it -> ((Object[]) it)[1]) + .map(it -> { + return new Version(((PrincipalAwareRevisionEntity) it).idAsString(), + ((PrincipalAwareRevisionEntity) it).getPrincipalUserName(), + ((PrincipalAwareRevisionEntity) it).getRevisionDate() + .toInstant() + .atOffset(ZoneOffset.UTC) + .toZonedDateTime()); + }) + .sorted(comparing(Version::getDate)) + .collect(toList()); + + return (List) listOfVersions; + } + + public Object findSpecificVersionOfPersistentEntity(String resourceId, String versionId, Class entityClass) { + try { + return AuditReaderFactory.get(entityManager).createQuery() + .forEntitiesAtRevision(entityClass, Integer.valueOf(versionId)) + .add(AuditEntity.property("resourceId").eq(resourceId)) + .add(AuditEntity.revisionNumber().eq(Integer.valueOf(versionId))) + .getSingleResult(); + } + catch (NoResultException e) { + return null; + } + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/envers/PrincipalAwareRevisionEntity.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/envers/PrincipalAwareRevisionEntity.java index 8ee27218f..6a2847b38 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/envers/PrincipalAwareRevisionEntity.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/envers/PrincipalAwareRevisionEntity.java @@ -3,6 +3,7 @@ import lombok.Getter; import lombok.Setter; import org.hibernate.envers.DefaultRevisionEntity; +import org.hibernate.envers.DefaultTrackingModifiedEntitiesRevisionEntity; import org.hibernate.envers.RevisionEntity; import javax.persistence.Entity; @@ -14,7 +15,11 @@ @RevisionEntity(PrincipalEnhancingRevisionListener.class) @Getter @Setter -public class PrincipalAwareRevisionEntity extends DefaultRevisionEntity { +public class PrincipalAwareRevisionEntity extends DefaultTrackingModifiedEntitiesRevisionEntity { private String principalUserName; + + public String idAsString() { + return String.valueOf(getId()); + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/envers/PrincipalEnhancingRevisionListener.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/envers/PrincipalEnhancingRevisionListener.java index 12af196ed..c1895b052 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/envers/PrincipalEnhancingRevisionListener.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/envers/PrincipalEnhancingRevisionListener.java @@ -9,7 +9,7 @@ */ public class PrincipalEnhancingRevisionListener implements RevisionListener { - private static final String ANONYMOUS = "anonymous"; + private static final String ANONYMOUS = "anonymousUser"; @Override public void newRevision(Object revisionEntity) { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorVersionService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorVersionService.java new file mode 100644 index 000000000..5e1542ea2 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorVersionService.java @@ -0,0 +1,17 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +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.domain.versioning.Version; + +import java.util.List; + +/** + * API containing operations pertaining to {@link EntityDescriptor} versioning. + */ +public interface EntityDescriptorVersionService { + + List findVersionsForEntityDescriptor(String resourceId); + + EntityDescriptorRepresentation findSpecificVersionOfEntityDescriptor(String resourceId, String versionId); +} 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 new file mode 100644 index 000000000..f21d9d227 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EnversEntityDescriptorVersionService.java @@ -0,0 +1,79 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +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.domain.versioning.Version; +import edu.internet2.tier.shibboleth.admin.ui.envers.PrincipalAwareRevisionEntity; +import org.hibernate.envers.AuditReader; +import org.hibernate.envers.AuditReaderFactory; +import org.hibernate.envers.RevisionEntity; +import org.hibernate.envers.query.AuditEntity; +import org.springframework.data.jpa.repository.JpaContext; + +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import javax.persistence.PersistenceContext; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; + +/** + * Hibernate Envers based implementation of {@link EntityDescriptorVersionService}. + * TODO: refactor this implementation using EnversVersionServiceSupport class + */ +public class EnversEntityDescriptorVersionService implements EntityDescriptorVersionService { + + private EntityManager entityManager; + + private EntityDescriptorService entityDescriptorService; + + public EnversEntityDescriptorVersionService(EntityManager entityManager, EntityDescriptorService entityDescriptorService) { + this.entityManager = entityManager; + this.entityDescriptorService = entityDescriptorService; + } + + @Override + public List findVersionsForEntityDescriptor(String resourceId) { + List revs = AuditReaderFactory.get(entityManager).createQuery() + .forRevisionsOfEntity(EntityDescriptor.class, false, false) + .add(AuditEntity.property("resourceId").eq(resourceId)) + .getResultList(); + + Object listOfVersions = revs.stream() + .map(it -> ((Object[])it)[1]) + .map(it -> { + return new Version(((PrincipalAwareRevisionEntity) it).idAsString(), + ((PrincipalAwareRevisionEntity) it).getPrincipalUserName(), + ((PrincipalAwareRevisionEntity) it).getRevisionDate() + .toInstant() + .atOffset(ZoneOffset.UTC) + .toZonedDateTime()); + }) + .sorted(comparing(Version::getDate)) + .collect(toList()); + + return (List)listOfVersions; + } + + @Override + public EntityDescriptorRepresentation findSpecificVersionOfEntityDescriptor(String resourceId, String versionId) { + try { + Object revision = AuditReaderFactory.get(entityManager).createQuery() + .forEntitiesAtRevision(EntityDescriptor.class, Integer.valueOf(versionId)) + .add(AuditEntity.property("resourceId").eq(resourceId)) + .add(AuditEntity.revisionNumber().eq(Integer.valueOf(versionId))) + .getSingleResult(); + return entityDescriptorService.createRepresentationFromDescriptor((EntityDescriptor) revision); + } + catch (NoResultException e) { + return null; + } + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EnversMetadataResolverVersionService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EnversMetadataResolverVersionService.java new file mode 100644 index 000000000..c98203eca --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EnversMetadataResolverVersionService.java @@ -0,0 +1,32 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.versioning.Version; +import edu.internet2.tier.shibboleth.admin.ui.envers.EnversVersionServiceSupport; + +import java.util.List; + + +/** + * Hibernate Envers based implementation of {@link MetadataResolverVersionService}. + */ +public class EnversMetadataResolverVersionService implements MetadataResolverVersionService { + + + private EnversVersionServiceSupport enversVersionServiceSupport; + + public EnversMetadataResolverVersionService(EnversVersionServiceSupport enversVersionServiceSupport) { + this.enversVersionServiceSupport = enversVersionServiceSupport; + } + + @Override + public List findVersionsForMetadataResolver(String resourceId) { + return enversVersionServiceSupport.findVersionsForPersistentEntity(resourceId, MetadataResolver.class); + } + + @Override + public MetadataResolver findSpecificVersionOfMetadataResolver(String resourceId, String versionId) { + Object mrObject = enversVersionServiceSupport.findSpecificVersionOfPersistentEntity(resourceId, versionId, MetadataResolver.class); + return mrObject == null ? null : (MetadataResolver) mrObject; + } +} 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 9bbf5ec2a..ca7a8659e 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 @@ -56,6 +56,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -120,6 +121,10 @@ private EntityDescriptor buildDescriptorFromRepresentation(final EntityDescripto setupLogout(ed, representation); setupRelyingPartyOverrides(ed, representation); + //Let envers recognize update revision type for EntityDescriptor type + //when modifying Attributes and SPSSODescriptor inside RoleDescriptors collection + ed.setVersionModifiedTimestamp(System.currentTimeMillis()); + return ed; } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverVersionService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverVersionService.java new file mode 100644 index 000000000..16f3dd1a3 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverVersionService.java @@ -0,0 +1,16 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.versioning.Version; + +import java.util.List; + +/** + * API containing operations pertaining to {@link MetadataResolver} versioning. + */ +public interface MetadataResolverVersionService { + + List findVersionsForMetadataResolver(String resourceId); + + MetadataResolver findSpecificVersionOfMetadataResolver(String resourceId, String versionId); +} diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 7cadee855..389162b05 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -48,9 +48,13 @@ action.move-down=Move Down action.edit=Edit action.add-filter=Add Filter action.manage-filters=Manage Filters +action.version-history=Version History +action.options=Options +action.xml=XML value.enabled=Enabled value.disabled=Disabled +value.current=Current value.none=None value.file=File value.memory=Memory @@ -236,6 +240,7 @@ label.filter-name=Filter Name label.filter-enabled=Filter Enabled label.filter-target=FilterTarget label.filter-type=Filter Type +label.option=Option label.value=Value label.binding-type=Binding Type label.sign-assertion=Sign Assertions @@ -384,6 +389,11 @@ label.email=Email label.role=Role label.delete=Delete? +label.title=Title +label.enabled=Enabled +label.author=Author +label.creation-date=Creation Date + label.metadata-resolver-history=Metadata resolver history label.metadata-version-history=Metadata Version History label.select-version=Select Version @@ -396,6 +406,9 @@ label.current=Current label.restore=Restore label.compare-selected=Compare Selected +label.saved=Saved +label.by=by + message.delete-user-title=Delete User? message.delete-user-body=You are requesting to delete a user. If you complete this process the user will be removed. This cannot be undone. Do you wish to continue? diff --git a/backend/src/main/resources/metadata-sources-ui-schema.json b/backend/src/main/resources/metadata-sources-ui-schema.json index 3f002253a..9d64e75b4 100644 --- a/backend/src/main/resources/metadata-sources-ui-schema.json +++ b/backend/src/main/resources/metadata-sources-ui-schema.json @@ -310,6 +310,7 @@ }, "attributeRelease": { "type": "array", + "title": "label.attribute-release", "description": "Attribute release table - select the attributes you want to release (default unchecked)", "widget": { "id": "checklist", 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 928cd18ec..89cb89d54 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 @@ -11,6 +11,7 @@ import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorReposit import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator @@ -69,6 +70,7 @@ class EntityDescriptorControllerTests extends Specification { RoleRepository roleRepository = Mock() UserService userService + EntityDescriptorVersionService versionService = Mock() def setup() { generator = new TestObjectGenerator() @@ -78,7 +80,7 @@ class EntityDescriptorControllerTests extends Specification { userService = new UserService(roleRepository, userRepository) service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects), userService) - controller = new EntityDescriptorController(userRepository, roleRepository, userService) + controller = new EntityDescriptorController(userService, versionService) controller.entityDescriptorRepository = entityDescriptorRepository controller.openSamlObjects = openSamlObjects controller.entityDescriptorService = service 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 802afdb81..16ab5f6e0 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 @@ -201,7 +201,6 @@ class MetadataFiltersControllerTests extends Specification { updatedMetadataResolver.getMetadataFilters().add(updatedFilter) 1 * metadataResolverRepository.findByResourceId(_) >> originalMetadataResolver - 1 * metadataFilterRepository.findByResourceId(_) >> originalFilter 1 * metadataFilterRepository.save(_) >> updatedFilter def filterUUID = updatedFilter.getResourceId() @@ -240,7 +239,6 @@ class MetadataFiltersControllerTests extends Specification { originalMetadataResolver.getMetadataFilters().add(randomFilter) 1 * metadataResolverRepository.findByResourceId(_) >> originalMetadataResolver - 1 * metadataFilterRepository.findByResourceId(_) >> randomFilter def filterUUID = randomFilter.getResourceId() @@ -253,4 +251,4 @@ class MetadataFiltersControllerTests extends Specification { then: result.andExpect(status().is(409)) } -} \ No newline at end of file +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/versioning/VersionJsonSerializationBasicTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/versioning/VersionJsonSerializationBasicTests.groovy new file mode 100644 index 000000000..011fe7033 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/versioning/VersionJsonSerializationBasicTests.groovy @@ -0,0 +1,41 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.versioning + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import groovy.json.JsonSlurper +import spock.lang.Specification + +import java.time.ZonedDateTime + +class VersionJsonSerializationBasicTests extends Specification { + + ObjectMapper mapper + + JsonSlurper jsonSlurper + + def setup() { + mapper = new ObjectMapper() + mapper.registerModule(new JavaTimeModule()) + jsonSlurper = new JsonSlurper() + } + + def "Verify basic Version JSON serialization"() { + given: + def staticDate = ZonedDateTime.parse("2019-05-20T15:00:00.574Z") + def version = new Version('2', 'kramer', staticDate) + def expectedJson = """ + { + "id": "2", + "creator": "kramer", + "date": "2019-05-20T15:00:00.574Z" + } + """ + def expectedJsonMap = jsonSlurper.parseText(expectedJson) + + when: + def deSerializedJsonMap = jsonSlurper.parseText(mapper.writeValueAsString(version)) + + then: + deSerializedJsonMap.date == expectedJsonMap.date + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/EnversTestsSupport.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/EnversTestsSupport.groovy new file mode 100644 index 000000000..2052cf9d1 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/EnversTestsSupport.groovy @@ -0,0 +1,76 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository.envers + +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.domain.resolvers.MetadataResolver +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.service.EntityDescriptorService +import org.hibernate.envers.AuditReaderFactory +import org.hibernate.envers.query.AuditEntity +import org.hibernate.envers.query.AuditQuery +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.support.DefaultTransactionDefinition + +import javax.persistence.EntityManager + +import static org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW + +class EnversTestsSupport { + + //This explicit low level transaction dance is required in order to verify history/version data that envers + //writes out only after the explicit transaction is committed, therefore making it impossible to verify within the main tx + //boundary of the test method which commits tx only after an execution of the test method. This let's us explicitly + //start/commit transaction making envers data written out and verifiable + static doInExplicitTransaction(PlatformTransactionManager txMgr, Closure uow) { + def txStatus = txMgr.getTransaction(new DefaultTransactionDefinition(PROPAGATION_REQUIRES_NEW)) + def entity = uow() + txMgr.commit(txStatus) + entity + } + + static updateAndGetRevisionHistoryOfEntityDescriptor(EntityDescriptor ed, EntityDescriptorRepresentation representation, + EntityDescriptorService eds, + EntityDescriptorRepository edr, + PlatformTransactionManager txMgr, + EntityManager em) { + eds.updateDescriptorFromRepresentation(ed, representation) + doInExplicitTransaction(txMgr) { + edr.save(ed) + } + getRevisionHistoryForEntityType(em, EntityDescriptor, ed.resourceId) + } + + static updateAndGetRevisionHistoryOfMetadataResolver(MetadataResolver mr, + MetadataResolverRepository mrr, + Class < ? > type, + PlatformTransactionManager + txMgr, EntityManager em) { + + doInExplicitTransaction(txMgr) { + mrr.save(mr) + } + getRevisionHistoryForEntityType(em, type, mr.resourceId) + } + + static getRevisionHistoryForEntityType(EntityManager em, Class entityType, String resourceId) { + def auditReader = AuditReaderFactory.get(em) + AuditQuery auditQuery = auditReader + .createQuery() + .forRevisionsOfEntity(entityType, false, false) + .add(AuditEntity.property("resourceId").eq(resourceId)) + auditQuery.resultList + } + + static getTargetEntityForRevisionIndex(List revHistory, int revIndex) { + revHistory[revIndex][0] + } + + static getRevisionEntityForRevisionIndex(List revHistory, int revIndex) { + revHistory[revIndex][1] + } + + static getModifiedEntityNames(List revHistory, int revIndex) { + getRevisionEntityForRevisionIndex(revHistory, revIndex).modifiedEntityNames + } +} 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 b0626d431..1c1778c60 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 @@ -5,7 +5,6 @@ import edu.internet2.tier.shibboleth.admin.ui.ShibbolethUiApplication import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor -import edu.internet2.tier.shibboleth.admin.ui.domain.SPSSODescriptor import edu.internet2.tier.shibboleth.admin.ui.domain.XSAny import edu.internet2.tier.shibboleth.admin.ui.domain.XSAnyBuilder import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean @@ -26,7 +25,6 @@ import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService 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.AttributeUtility -import groovy.json.JsonOutput import org.skyscreamer.jsonassert.JSONAssert import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest @@ -37,7 +35,6 @@ 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 spock.lang.Specification @ContextConfiguration(classes=[CoreShibUiConfiguration, CustomPropertiesConfiguration]) diff --git a/build.gradle b/build.gradle index b1dad6f5c..ff3c80edc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,22 @@ +plugins { + id 'base' + id 'net.researchgate.release' version '2.6.0' + id 'com.github.breadmoirai.github-release' version '2.2.9' +} + +tasks.findByName('release').dependsOn project.getTasksByName('test', true) + +githubRelease { + token project.'i2.github.token' + owner project.'i2.github.owner' + repo project.'i2.github.repo' + releaseAssets project('backend').getTasksByName('bootWar', false).outputs, project('backend').getTasksByName('bootJar', false).outputs + overwrite true + apiEndpoint project.'i2.github.apiEndpoint' +} + +afterReleaseBuild.dependsOn project.getTasksByName('githubRelease', false) + task wrapper(type: Wrapper) { gradleVersion = '4.8.1' } diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 0022b801c..8b3566fc6 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -390,71 +390,6 @@ paths: '409': description: Already Exists deprecated: false - '/MetadataResolvers/{metadataResolverId}/Filters/{resourceId}/Versions': - get: - tags: - - metadata-filters-controller - description: Get list of Versions for an entity descriptor - produces: - - application/json - parameters: - - name: metadataResolverId - in: path - description: metadataResolverId - required: true - type: string - - name: resourceId - in: path - description: resourceId - required: true - type: string - responses: - '200': - description: OK. Items returned should be ordered by `date`, ascending (oldest version first). - schema: - type: array - items: - $ref: '#/definitions/Version' - '401': - description: Unauthorized - '403': - description: Forbidden - '404': - description: Not Found - '/MetadataResolvers/{metadataResolverId}/Filters/{resourceId}/Versions/{versionId}': - get: - tags: - - metadata-filters-controller - description: Get a Version of an entity descriptor - produces: - - application/json - parameters: - - name: metadataResolverId - in: path - description: metadataResolverId - required: true - type: string - - name: resourceId - in: path - description: resourceId - required: true - type: string - - name: versionId - in: path - description: versionId of a version of the EntityDescriptor - required: true - type: string - responses: - '200': - description: OK - schema: - $ref: '#/definitions/MetadataFilter' - '401': - description: Unauthorized - '403': - description: Forbidden - '404': - description: Not Found '/MetadataResolvers/{resourceId}': get: tags: diff --git a/gradle.properties b/gradle.properties index 01ee65808..335e4cb7b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,3 +12,9 @@ hibernate.version=5.2.11.Final lucene.version=7.2.1 org.gradle.jvmargs=-Xmx4g -XX:-UseGCOverheadLimit + +# set token in personal global +i2.github.token= +i2.github.owner=TIER +i2.github.repo=shib-idp-ui +i2.github.apiEndpoint=https://github.internet2.edu/api/v3 diff --git a/settings.gradle b/settings.gradle index 8fae26617..b3ab3e757 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include 'backend', 'ui', 'pac4j-module' \ No newline at end of file +include 'backend', 'ui', 'pac4j-module' diff --git a/ui/build.gradle b/ui/build.gradle index 7735a3116..64fe2e561 100644 --- a/ui/build.gradle +++ b/ui/build.gradle @@ -9,6 +9,8 @@ node { download = true } +npmInstall.setNpmCommand('ci') + npm_run_build { inputs.dir 'src' outputs.dir 'dist' diff --git a/ui/package-lock.json b/ui/package-lock.json index fc76f65cc..5b8942a76 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -10232,6 +10232,14 @@ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, + "ngx-infinite-scroll": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-7.2.0.tgz", + "integrity": "sha512-EcqjKpU1ukRV3YXOW8cTVtbzPpa9UPaRtYBCg0ZQH3ceCDm+xzLbd4pXy6oKAIN4zN1r/pyGuf5XOJkA8vr6yg==", + "requires": { + "opencollective-postinstall": "^2.0.2" + } + }, "ngx-schema-form": { "version": "2.2.0-beta.1", "resolved": "https://registry.npmjs.org/ngx-schema-form/-/ngx-schema-form-2.2.0-beta.1.tgz", @@ -10748,6 +10756,11 @@ "is-wsl": "^1.1.0" } }, + "opencollective-postinstall": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz", + "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==" + }, "opener": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz", diff --git a/ui/package.json b/ui/package.json index 6cfe807cd..9881e1fb7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -6,7 +6,7 @@ "ng": "ng", "start": "ng serve --proxy-config proxy.conf.json", "build": "ng build", - "test": "ng test --code-coverage --source-map=false", + "test": "ng test --code-coverage --source-map=true", "lint": "ng lint", "e2e": "ng e2e", "build:static": "node-sass src/static.scss ./dist/unsecured/static.css", @@ -43,6 +43,7 @@ "deepmerge": "^2.2.1", "file-saver": "^1.3.3", "font-awesome": "^4.7.0", + "ngx-infinite-scroll": "^7.2.0", "ngx-schema-form": "^2.2.0-beta.1", "rxjs": "^6.5.1", "rxjs-compat": "^6.5.1", diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index fb19ae16a..6af7be5ab 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -58,7 +58,7 @@ -
+
diff --git a/ui/src/app/app.component.scss b/ui/src/app/app.component.scss index dbe420760..fcee7de97 100644 --- a/ui/src/app/app.component.scss +++ b/ui/src/app/app.component.scss @@ -13,3 +13,15 @@ nav.navbar { max-height: 60px; } } + +@media only screen and (min-width: 1024px) { + .pad-content { + padding: 1rem 3rem; + } +} + +@media only screen and (min-width: 1200px) { + .pad-content { + padding: 2rem 5rem; + } +} diff --git a/ui/src/app/metadata/configuration/action/configuration.action.ts b/ui/src/app/metadata/configuration/action/configuration.action.ts new file mode 100644 index 000000000..b5b86ade1 --- /dev/null +++ b/ui/src/app/metadata/configuration/action/configuration.action.ts @@ -0,0 +1,130 @@ +import { Action } from '@ngrx/store'; +import { Metadata } from '../../domain/domain.type'; +import { Schema } from '../model/schema'; +import { Wizard } from '../../../wizard/model'; + +export enum ConfigurationActionTypes { + LOAD_METADATA_REQUEST = '[Metadata Configuration] Load Metadata Request', + LOAD_METADATA_SUCCESS = '[Metadata Configuration] Load Metadata Success', + LOAD_METADATA_ERROR = '[Metadata Configuration] Load Metadata Error', + + LOAD_SCHEMA_REQUEST = '[Metadata Configuration] Load Schema Request', + LOAD_SCHEMA_SUCCESS = '[Metadata Configuration] Load Schema Success', + LOAD_SCHEMA_ERROR = '[Metadata Configuration] Load Schema Error', + + LOAD_XML_REQUEST = '[Metadata Configuration] Load XML Request', + LOAD_XML_SUCCESS = '[Metadata Configuration] Load XML Success', + LOAD_XML_ERROR = '[Metadata Configuration] Load XML Error', + + SET_METADATA = '[Metadata Configuration] Set Metadata Model', + SET_DEFINITION = '[Metadata Configuration] Set Metadata Definition', + SET_SCHEMA = '[Metadata Configuration] Set Metadata Schema', + SET_XML = '[Metadata Configuration] Set Metadata Xml', + + DOWNLOAD_XML = '[Metadata Configuration] Download Metadata Xml', + + CLEAR = '[Metadata Configuration] Clear' +} + +export class LoadMetadataRequest implements Action { + readonly type = ConfigurationActionTypes.LOAD_METADATA_REQUEST; + + constructor(public payload: { id: string, type: string }) { } +} + +export class LoadMetadataSuccess implements Action { + readonly type = ConfigurationActionTypes.LOAD_METADATA_SUCCESS; + + constructor(public payload: Metadata) { } +} + +export class LoadMetadataError implements Action { + readonly type = ConfigurationActionTypes.LOAD_METADATA_ERROR; + + constructor(public payload: any) { } +} + +export class LoadSchemaRequest implements Action { + readonly type = ConfigurationActionTypes.LOAD_SCHEMA_REQUEST; + + constructor(public payload: string) { } +} + +export class LoadSchemaSuccess implements Action { + readonly type = ConfigurationActionTypes.LOAD_SCHEMA_SUCCESS; + + constructor(public payload: Schema) { } +} + +export class LoadSchemaError implements Action { + readonly type = ConfigurationActionTypes.LOAD_SCHEMA_ERROR; + + constructor(public payload: any) { } +} + +export class LoadXmlRequest implements Action { + readonly type = ConfigurationActionTypes.LOAD_XML_REQUEST; + + constructor(public payload: { id: string, type: string }) { } +} + +export class LoadXmlSuccess implements Action { + readonly type = ConfigurationActionTypes.LOAD_XML_SUCCESS; + + constructor(public payload: string) { } +} + +export class LoadXmlError implements Action { + readonly type = ConfigurationActionTypes.LOAD_XML_ERROR; + + constructor(public payload: any) { } +} + +export class SetMetadata implements Action { + readonly type = ConfigurationActionTypes.SET_METADATA; + + constructor(public payload: Metadata) { } +} + +export class SetDefinition implements Action { + readonly type = ConfigurationActionTypes.SET_DEFINITION; + + constructor(public payload: Wizard) { } +} + +export class SetSchema implements Action { + readonly type = ConfigurationActionTypes.SET_SCHEMA; + + constructor(public payload: Schema) { } +} + +export class SetXml implements Action { + readonly type = ConfigurationActionTypes.SET_XML; + + constructor(public payload: string) { } +} + +export class DownloadXml implements Action { + readonly type = ConfigurationActionTypes.DOWNLOAD_XML; +} + +export class ClearConfiguration implements Action { + readonly type = ConfigurationActionTypes.CLEAR; +} + +export type ConfigurationActionsUnion = + | LoadMetadataRequest + | LoadMetadataSuccess + | LoadMetadataError + | LoadSchemaRequest + | LoadSchemaSuccess + | LoadSchemaError + | LoadXmlRequest + | LoadXmlSuccess + | LoadXmlError + | SetMetadata + | SetDefinition + | SetSchema + | SetXml + | DownloadXml + | ClearConfiguration; diff --git a/ui/src/app/metadata/configuration/action/history.action.ts b/ui/src/app/metadata/configuration/action/history.action.ts new file mode 100644 index 000000000..5ee05ea65 --- /dev/null +++ b/ui/src/app/metadata/configuration/action/history.action.ts @@ -0,0 +1,52 @@ +import { Action } from '@ngrx/store'; +import { MetadataHistory } from '../model/history'; + +export enum HistoryActionTypes { + LOAD_HISTORY_REQUEST = '[Configuration History] Load History Request', + LOAD_HISTORY_SUCCESS = '[Configuration History] Load History Success', + LOAD_HISTORY_ERROR = '[Configuration History] Load History Error', + SET_HISTORY = '[Configuration History] Set History', + CLEAR_HISTORY = '[Configuration History] Clear History', + SELECT_VERSION = '[Configuration History] Select Version' +} + +export class LoadHistoryRequest implements Action { + readonly type = HistoryActionTypes.LOAD_HISTORY_REQUEST; + + constructor(public payload: { id: string, type: string }) { } +} + +export class LoadHistorySuccess implements Action { + readonly type = HistoryActionTypes.LOAD_HISTORY_SUCCESS; + + constructor(public payload: MetadataHistory) { } +} + +export class LoadHistoryError implements Action { + readonly type = HistoryActionTypes.LOAD_HISTORY_ERROR; + + constructor(public payload: any) { } +} + +export class SelectVersion implements Action { + readonly type = HistoryActionTypes.SELECT_VERSION; + constructor(public payload: string | null) { } +} + +export class SetHistory implements Action { + readonly type = HistoryActionTypes.SET_HISTORY; + + constructor(public payload: MetadataHistory) {} +} + +export class ClearHistory implements Action { + readonly type = HistoryActionTypes.CLEAR_HISTORY; +} + +export type HistoryActionsUnion = + | LoadHistoryRequest + | LoadHistorySuccess + | LoadHistoryError + | SetHistory + | ClearHistory + | SelectVersion; diff --git a/ui/src/app/metadata/configuration/component/array-property.component.html b/ui/src/app/metadata/configuration/component/array-property.component.html new file mode 100644 index 000000000..ee177445e --- /dev/null +++ b/ui/src/app/metadata/configuration/component/array-property.component.html @@ -0,0 +1,51 @@ +
+ +
{{ property.name }}
+
+
+
+
+ {{ i + 1 }}.  + {{ property.items.properties[prop].title }} +
+
+ {{ value[prop] }} +
+
+
+
+
+ + + + + + + + +
+ {{ attr.label }} +
+ + true + + + false + +
+
+
+
+
+ +
+ {{ property.name }} +

+
    +
  • + {{ item }} +
  • +
+
+
\ No newline at end of file diff --git a/ui/src/app/metadata/configuration/component/array-property.component.spec.ts b/ui/src/app/metadata/configuration/component/array-property.component.spec.ts new file mode 100644 index 000000000..453d9d484 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/array-property.component.spec.ts @@ -0,0 +1,115 @@ +import { Component, ViewChild, Input } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbDropdownModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; +import { Property } from '../../domain/model/property'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; +import { SCHEMA } from '../../../../testing/form-schema.stub'; +import { getStepProperty } from '../../domain/utility/configuration'; +import { ArrayPropertyComponent } from './array-property.component'; +import { AttributesService } from '../../domain/service/attributes.service'; +import { MockAttributeService } from '../../../../testing/attributes.stub'; +import { of } from 'rxjs'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(ArrayPropertyComponent) + public componentUnderTest: ArrayPropertyComponent; + + property: Property = getStepProperty(SCHEMA.properties.list, { + name: 'foo', + type: 'baz', + description: 'foo bar baz', + list: [] + }, SCHEMA.definitions); + + setProperty(property: Property): void { + this.property = property; + } +} + +describe('Array Property Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: ArrayPropertyComponent; + let service: AttributesService; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbPopoverModule, + MockI18nModule, + RouterTestingModule + ], + declarations: [ + ArrayPropertyComponent, + TestHostComponent + ], + providers: [ + { provide: AttributesService, useClass: MockAttributeService } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + service = TestBed.get(AttributesService); + fixture.detectChanges(); + })); + + it('should accept a property input', async(() => { + expect(app).toBeTruthy(); + })); + + describe('attributeList$ getter', () => { + it('should return an empty list when no data or dataUrl is set', () => { + app.attributeList$.subscribe((list) => { + expect(list).toEqual([]); + }); + }); + it('should return a list of data items from the schema', () => { + const datalist = [ + { key: 'foo', label: 'foo' }, + { key: 'bar', label: 'bar' }, + { key: 'baz', label: 'baz' }, + ]; + instance.setProperty({ + ...instance.property, + widget: { + id: 'datalist', + data: datalist + } + }); + fixture.detectChanges(); + app.attributeList$.subscribe(list => { + expect(list).toEqual(datalist); + }); + }); + + it('should call the attribute service with a provided dataUrl', () => { + const datalist = [ + { key: 'foo', label: 'foo' }, + { key: 'bar', label: 'bar' }, + { key: 'baz', label: 'baz' }, + ]; + spyOn(service, 'query').and.returnValue(of(datalist)); + instance.setProperty({ + ...instance.property, + widget: { + id: 'datalist', + dataUrl: '/foo' + } + }); + fixture.detectChanges(); + app.attributeList$.subscribe(list => { + expect(list).toEqual(datalist); + }); + }); + }); +}); diff --git a/ui/src/app/metadata/configuration/component/array-property.component.ts b/ui/src/app/metadata/configuration/component/array-property.component.ts new file mode 100644 index 000000000..ff9cd0ac4 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/array-property.component.ts @@ -0,0 +1,32 @@ +import { Component, Input } from '@angular/core'; +import { Property } from '../../domain/model/property'; +import { Observable, of } from 'rxjs'; +import { AttributesService } from '../../domain/service/attributes.service'; +import { ConfigurationPropertyComponent } from './configuration-property.component'; + +@Component({ + selector: 'array-property', + templateUrl: './array-property.component.html', + styleUrls: [] +}) + +export class ArrayPropertyComponent extends ConfigurationPropertyComponent { + @Input() property: Property; + + constructor( + private attrService: AttributesService + ) { + super(); + } + + get attributeList$(): Observable<{ key: string, label: string }[]> { + if (this.property.widget && this.property.widget.hasOwnProperty('data')) { + return of(this.property.widget.data); + } + if (this.property.widget && this.property.widget.hasOwnProperty('dataUrl')) { + return this.attrService.query(this.property.widget.dataUrl); + } + return of([]); + } +} + diff --git a/ui/src/app/metadata/configuration/component/configuration-property.component.html b/ui/src/app/metadata/configuration/component/configuration-property.component.html new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/metadata/configuration/component/configuration-property.component.spec.ts b/ui/src/app/metadata/configuration/component/configuration-property.component.spec.ts new file mode 100644 index 000000000..5ef0452e0 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/configuration-property.component.spec.ts @@ -0,0 +1,69 @@ +import { Component, ViewChild, Input } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbDropdownModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; +import { Property } from '../../domain/model/property'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; +import { SCHEMA } from '../../../../testing/form-schema.stub'; +import { getStepProperties, getStepProperty } from '../../domain/utility/configuration'; +import { ConfigurationPropertyComponent } from './configuration-property.component'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(ConfigurationPropertyComponent) + public componentUnderTest: ConfigurationPropertyComponent; + + property: Property = getStepProperty(SCHEMA.properties.name, { + name: 'foo', + type: 'baz', + description: 'foo bar baz' + }, SCHEMA.definitions); +} + +describe('Configuration Property Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: ConfigurationPropertyComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbPopoverModule, + MockI18nModule, + RouterTestingModule + ], + declarations: [ + ConfigurationPropertyComponent, + TestHostComponent + ] + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should accept a property input', async(() => { + expect(app).toBeTruthy(); + })); + + describe('getKeys method', () => { + it('should return the property`s child keys', () => { + expect(app.getKeys({ properties: { foo: 'bar', baz: 'bar' } })).toEqual(['foo', 'baz']); + }); + }); + + describe('getItemType method', () => { + it('should return the item`s type', () => { + expect(app.getItemType({ widget: { id: 'string' } } as Property)).toBe('string'); + expect(app.getItemType({} as Property)).toBe('default'); + }); + }); +}); diff --git a/ui/src/app/metadata/configuration/component/configuration-property.component.ts b/ui/src/app/metadata/configuration/component/configuration-property.component.ts new file mode 100644 index 000000000..bcdd45711 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/configuration-property.component.ts @@ -0,0 +1,23 @@ +import { Component, Input } from '@angular/core'; +import { Property } from '../../domain/model/property'; + +@Component({ + selector: 'configuration-property', + template: `{{ property | json }}`, + styleUrls: [] +}) + +export class ConfigurationPropertyComponent { + @Input() property: Property; + + constructor() { } + + getKeys(schema): string[] { + return Object.keys(schema.properties); + } + + getItemType(items: Property): string { + return items.widget ? items.widget.id : 'default'; + } +} + diff --git a/ui/src/app/metadata/version/component/history-list.component.html b/ui/src/app/metadata/configuration/component/history-list.component.html similarity index 74% rename from ui/src/app/metadata/version/component/history-list.component.html rename to ui/src/app/metadata/configuration/component/history-list.component.html index 6fa941e6e..2983c151c 100644 --- a/ui/src/app/metadata/version/component/history-list.component.html +++ b/ui/src/app/metadata/configuration/component/history-list.component.html @@ -12,7 +12,7 @@ - +
@@ -21,19 +21,19 @@
- Current (v{{ version.versionNumber }}) - v{{ version.versionNumber }} - {{ version.saveDate | date }} - {{ version.changedBy }} + Current (v{{ i + 1 }}) + v{{ i + 1 }} + {{ version.date | date }} + {{ version.creator }} - - \ No newline at end of file diff --git a/ui/src/app/metadata/version/component/history-list.component.spec.ts b/ui/src/app/metadata/configuration/component/history-list.component.spec.ts similarity index 78% rename from ui/src/app/metadata/version/component/history-list.component.spec.ts rename to ui/src/app/metadata/configuration/component/history-list.component.spec.ts index 4cb6770eb..43d49a29f 100644 --- a/ui/src/app/metadata/version/component/history-list.component.spec.ts +++ b/ui/src/app/metadata/configuration/component/history-list.component.spec.ts @@ -8,16 +8,15 @@ import { MetadataVersion } from '../model/version'; export const TestData = { versions: [ { - versionNumber: 1, - saveDate: new Date(), - changedBy: 'admin', - actions: [] + id: 'foo', + date: new Date().toDateString(), + creator: 'admin' } ] }; @Component({ - template: `` + template: `` }) class TestHostComponent { @ViewChild(HistoryListComponent) @@ -32,7 +31,7 @@ class TestHostComponent { describe('Metadata History List Component', () => { let fixture: ComponentFixture; let instance: TestHostComponent; - let table: HistoryListComponent; + let list: HistoryListComponent; beforeEach(() => { TestBed.configureTestingModule({ @@ -48,32 +47,34 @@ describe('Metadata History List Component', () => { fixture = TestBed.createComponent(TestHostComponent); instance = fixture.componentInstance; - table = instance.componentUnderTest; + list = instance.componentUnderTest; fixture.detectChanges(); }); it('should compile', () => { - expect(table).toBeDefined(); + expect(list).toBeDefined(); }); describe('compare selected', () => { it('should allow the user to toggle selected versions for comparison', () => { - table.toggleVersionSelected(TestData.versions[0]); - expect(table.selected.length).toBe(1); + list.toggleVersionSelected(TestData.versions[0]); + expect(list.selected.length).toBe(1); }); it('should emit an event with the selected values when the Compare Selected button is clicked', () => { spyOn(instance, 'compare'); const selected = TestData.versions; - table.compareSelected(selected); + list.compareSelected(selected); fixture.detectChanges(); expect(instance.compare).toHaveBeenCalledWith(selected); }); + }); + describe('restore', () => { it('should emit an event with the selected version when the Restore button is clicked', () => { spyOn(instance, 'restore'); const selected = TestData.versions[0]; - table.restoreVersion(selected); + list.restoreVersion(selected); fixture.detectChanges(); expect(instance.restore).toHaveBeenCalledWith(selected); }); diff --git a/ui/src/app/metadata/version/component/history-list.component.ts b/ui/src/app/metadata/configuration/component/history-list.component.ts similarity index 96% rename from ui/src/app/metadata/version/component/history-list.component.ts rename to ui/src/app/metadata/configuration/component/history-list.component.ts index 34696a98e..7e03468e2 100644 --- a/ui/src/app/metadata/version/component/history-list.component.ts +++ b/ui/src/app/metadata/configuration/component/history-list.component.ts @@ -9,7 +9,7 @@ import { MetadataVersion } from '../model/version'; styleUrls: [] }) export class HistoryListComponent { - @Input() history: MetadataHistory; + @Input() history: MetadataVersion[]; @Output() compare: EventEmitter = new EventEmitter(); @Output() restore: EventEmitter = new EventEmitter(); diff --git a/ui/src/app/metadata/configuration/component/metadata-configuration.component.html b/ui/src/app/metadata/configuration/component/metadata-configuration.component.html new file mode 100644 index 000000000..4f58d07f1 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/metadata-configuration.component.html @@ -0,0 +1,23 @@ +
+
+
+
+

+ 0{{ i + 1 }} + {{ section.label | translate }} +

+
+ +
+
+
+ Option + Value +
+ +
+
+
diff --git a/ui/src/app/metadata/configuration/component/metadata-configuration.component.spec.ts b/ui/src/app/metadata/configuration/component/metadata-configuration.component.spec.ts new file mode 100644 index 000000000..f26c8d65c --- /dev/null +++ b/ui/src/app/metadata/configuration/component/metadata-configuration.component.spec.ts @@ -0,0 +1,60 @@ +import { Component, ViewChild, Input } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { MetadataConfigurationComponent } from './metadata-configuration.component'; + +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { MetadataConfiguration } from '../model/metadata-configuration'; +import { Property } from '../../domain/model/property'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; + +@Component({ + selector: 'object-property', + template: `` +}) +class ObjectPropertyComponent { + @Input() property: Property; +} + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(MetadataConfigurationComponent) + public componentUnderTest: MetadataConfigurationComponent; + + configuration: MetadataConfiguration = {sections: []}; +} + +describe('Metadata Configuration Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: MetadataConfigurationComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule, + MockI18nModule, + RouterTestingModule + ], + declarations: [ + MetadataConfigurationComponent, + ObjectPropertyComponent, + TestHostComponent + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should accept a configuration input', async(() => { + expect(app).toBeTruthy(); + })); +}); diff --git a/ui/src/app/metadata/configuration/component/metadata-configuration.component.ts b/ui/src/app/metadata/configuration/component/metadata-configuration.component.ts new file mode 100644 index 000000000..9e94bcdbd --- /dev/null +++ b/ui/src/app/metadata/configuration/component/metadata-configuration.component.ts @@ -0,0 +1,22 @@ +import { Component, ChangeDetectionStrategy, Input } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; +import { MetadataConfiguration } from '../model/metadata-configuration'; + +@Component({ + selector: 'metadata-configuration', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './metadata-configuration.component.html', + styleUrls: [] +}) +export class MetadataConfigurationComponent { + @Input() configuration: MetadataConfiguration; + + constructor( + private router: Router, + private activatedRoute: ActivatedRoute + ) { } + + edit(id: string): void { + this.router.navigate(['../', 'edit', id], { relativeTo: this.activatedRoute.parent }); + } +} diff --git a/ui/src/app/metadata/configuration/component/metadata-header.component.html b/ui/src/app/metadata/configuration/component/metadata-header.component.html new file mode 100644 index 000000000..19f183ba8 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/metadata-header.component.html @@ -0,0 +1,19 @@ +
+
+

+ Version {{ versionNumber }} +

+

+ Saved  + {{ version.date | date }}, + by  + {{ version.creator }} +

+

+ Enabled +   + Disabled + Current +

+
+
\ No newline at end of file diff --git a/ui/src/app/metadata/configuration/component/metadata-header.component.spec.ts b/ui/src/app/metadata/configuration/component/metadata-header.component.spec.ts new file mode 100644 index 000000000..e8db42c36 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/metadata-header.component.spec.ts @@ -0,0 +1,58 @@ +import { Component, ViewChild } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; +import { MetadataVersion } from '../model/version'; +import { MetadataHeaderComponent } from './metadata-header.component'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(MetadataHeaderComponent) + public componentUnderTest: MetadataHeaderComponent; + + isEnabled = true; + + version: MetadataVersion = { + id: 'foo', + creator: 'foobar', + date: new Date().toDateString() + }; + versionNumber = 1; + isCurrent = false; +} + +describe('Metadata Header Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: MetadataHeaderComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + MockI18nModule + ], + declarations: [ + MetadataHeaderComponent, + TestHostComponent + ] + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should accept a property input', async(() => { + expect(app).toBeTruthy(); + })); +}); diff --git a/ui/src/app/metadata/configuration/component/metadata-header.component.ts b/ui/src/app/metadata/configuration/component/metadata-header.component.ts new file mode 100644 index 000000000..8d0dcc3db --- /dev/null +++ b/ui/src/app/metadata/configuration/component/metadata-header.component.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; +import { Metadata, MetadataTypes } from '../../domain/domain.type'; +import { MetadataVersion } from '../model/version'; + +@Component({ + selector: 'metadata-header', + templateUrl: './metadata-header.component.html', + styleUrls: [] +}) + +export class MetadataHeaderComponent { + @Input() isEnabled: boolean; + @Input() version: MetadataVersion; + @Input() versionNumber: number; + @Input() isCurrent: boolean; + + constructor() {} +} + diff --git a/ui/src/app/metadata/configuration/component/object-property.component.html b/ui/src/app/metadata/configuration/component/object-property.component.html new file mode 100644 index 000000000..b0aa7b967 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/object-property.component.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ui/src/app/metadata/configuration/component/object-property.component.spec.ts b/ui/src/app/metadata/configuration/component/object-property.component.spec.ts new file mode 100644 index 000000000..6299b20d5 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/object-property.component.spec.ts @@ -0,0 +1,60 @@ +import { Component, ViewChild, Input } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbDropdownModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; +import { Property } from '../../domain/model/property'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; +import { ObjectPropertyComponent } from './object-property.component'; +import { SCHEMA } from '../../../../testing/form-schema.stub'; +import { getStepProperties, getStepProperty } from '../../domain/utility/configuration'; +import { PrimitivePropertyComponent } from './primitive-property.component'; +import { ArrayPropertyComponent } from './array-property.component'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(ObjectPropertyComponent) + public componentUnderTest: ObjectPropertyComponent; + + property: Property = getStepProperty(SCHEMA.properties.name, { + name: 'foo', + type: 'baz', + description: 'foo bar baz' + }, SCHEMA.definitions); +} + +describe('Object Property Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: ObjectPropertyComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbPopoverModule, + MockI18nModule, + RouterTestingModule + ], + declarations: [ + ObjectPropertyComponent, + PrimitivePropertyComponent, + ArrayPropertyComponent, + TestHostComponent + ] + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should accept a property input', async(() => { + expect(app).toBeTruthy(); + })); +}); diff --git a/ui/src/app/metadata/configuration/component/object-property.component.ts b/ui/src/app/metadata/configuration/component/object-property.component.ts new file mode 100644 index 000000000..f7892877e --- /dev/null +++ b/ui/src/app/metadata/configuration/component/object-property.component.ts @@ -0,0 +1,18 @@ +import { Component, Input } from '@angular/core'; +import { Property } from '../../domain/model/property'; +import { ConfigurationPropertyComponent } from './configuration-property.component'; + +@Component({ + selector: 'object-property', + templateUrl: './object-property.component.html', + styleUrls: [] +}) + +export class ObjectPropertyComponent extends ConfigurationPropertyComponent { + @Input() property: Property; + + constructor() { + super(); + } +} + diff --git a/ui/src/app/metadata/configuration/component/primitive-property.component.html b/ui/src/app/metadata/configuration/component/primitive-property.component.html new file mode 100644 index 000000000..9eef2181f --- /dev/null +++ b/ui/src/app/metadata/configuration/component/primitive-property.component.html @@ -0,0 +1,5 @@ +
+ {{ property.name }} + {{ property.value || property.value === false ? property.value : '-' }} +
\ No newline at end of file diff --git a/ui/src/app/metadata/configuration/component/primitive-property.component.spec.ts b/ui/src/app/metadata/configuration/component/primitive-property.component.spec.ts new file mode 100644 index 000000000..9ce81bfb5 --- /dev/null +++ b/ui/src/app/metadata/configuration/component/primitive-property.component.spec.ts @@ -0,0 +1,60 @@ +import { Component, ViewChild, Input } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { Property } from '../../domain/model/property'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; +import { PrimitivePropertyComponent } from './primitive-property.component'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(PrimitivePropertyComponent) + public componentUnderTest: PrimitivePropertyComponent; + + property: Property = { + title: 'foo', + type: 'string', + name: 'foo', + value: ['bar'], + items: null, + properties: null, + widget: { + id: 'string' + } + }; +} + +describe('Primitive Property Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: PrimitivePropertyComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule, + MockI18nModule, + RouterTestingModule + ], + declarations: [ + PrimitivePropertyComponent, + TestHostComponent + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should accept a property input', async(() => { + expect(app).toBeTruthy(); + })); +}); diff --git a/ui/src/app/metadata/configuration/component/primitive-property.component.ts b/ui/src/app/metadata/configuration/component/primitive-property.component.ts new file mode 100644 index 000000000..951d2c72a --- /dev/null +++ b/ui/src/app/metadata/configuration/component/primitive-property.component.ts @@ -0,0 +1,15 @@ +import { Component, Input } from '@angular/core'; +import { ConfigurationPropertyComponent } from './configuration-property.component'; + +@Component({ + selector: 'primitive-property', + templateUrl: './primitive-property.component.html', + styleUrls: [] +}) + +export class PrimitivePropertyComponent extends ConfigurationPropertyComponent { + constructor() { + super(); + } +} + diff --git a/ui/src/app/metadata/configuration/configuration.module.ts b/ui/src/app/metadata/configuration/configuration.module.ts new file mode 100644 index 000000000..9aa2cf880 --- /dev/null +++ b/ui/src/app/metadata/configuration/configuration.module.ts @@ -0,0 +1,71 @@ +import { NgModule, ModuleWithProviders } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; + +import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; + +import { I18nModule } from '../../i18n/i18n.module'; +import { MetadataConfigurationComponent } from './component/metadata-configuration.component'; +import { ConfigurationComponent } from './container/configuration.component'; +import { MetadataConfigurationService } from './service/configuration.service'; +import * as fromConfig from './reducer'; +import { MetadataConfigurationEffects } from './effect/configuration.effect'; +import { ConfigurationPropertyComponent } from './component/configuration-property.component'; +import { PrimitivePropertyComponent } from './component/primitive-property.component'; +import { ObjectPropertyComponent } from './component/object-property.component'; +import { ArrayPropertyComponent } from './component/array-property.component'; +import { RouterModule } from '@angular/router'; +import { MetadataOptionsComponent } from './container/metadata-options.component'; +import { MetadataXmlComponent } from './container/metadata-xml.component'; +import { MetadataHeaderComponent } from './component/metadata-header.component'; +import { MetadataHistoryEffects } from './effect/history.effect'; +import { MetadataHistoryService } from './service/history.service'; +import { MetadataHistoryComponent } from './container/metadata-history.component'; +import { HistoryListComponent } from './component/history-list.component'; + +@NgModule({ + declarations: [ + MetadataConfigurationComponent, + MetadataOptionsComponent, + MetadataXmlComponent, + ConfigurationPropertyComponent, + PrimitivePropertyComponent, + ObjectPropertyComponent, + ArrayPropertyComponent, + ConfigurationComponent, + MetadataHeaderComponent, + MetadataHistoryComponent, + HistoryListComponent + ], + entryComponents: [], + imports: [ + CommonModule, + I18nModule, + NgbPopoverModule, + RouterModule + ], + exports: [], + providers: [] +}) +export class MetadataConfigurationModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: RootMetadataConfigurationModule, + providers: [ + MetadataConfigurationService, + MetadataHistoryService + ] + }; + } +} + +@NgModule({ + imports: [ + MetadataConfigurationModule, + StoreModule.forFeature('metadata-configuration', fromConfig.reducers), + EffectsModule.forFeature([MetadataConfigurationEffects, MetadataHistoryEffects]) + ], + providers: [] +}) +export class RootMetadataConfigurationModule { } diff --git a/ui/src/app/metadata/configuration/configuration.routing.ts b/ui/src/app/metadata/configuration/configuration.routing.ts new file mode 100644 index 000000000..d27b29872 --- /dev/null +++ b/ui/src/app/metadata/configuration/configuration.routing.ts @@ -0,0 +1,30 @@ +import { Routes } from '@angular/router'; +import { ConfigurationComponent } from './container/configuration.component'; +import { MetadataOptionsComponent } from './container/metadata-options.component'; +import { MetadataXmlComponent } from './container/metadata-xml.component'; +import { MetadataHistoryComponent } from './container/metadata-history.component'; + +export const ConfigurationRoutes: Routes = [ + { + path: ':type/:id/configuration', + component: ConfigurationComponent, + children: [ + { + path: '', + redirectTo: 'options' + }, + { + path: 'options', + component: MetadataOptionsComponent + }, + { + path: 'xml', + component: MetadataXmlComponent + }, + { + path: 'history', + component: MetadataHistoryComponent + } + ] + } +]; diff --git a/ui/src/app/metadata/configuration/configuration.values.ts b/ui/src/app/metadata/configuration/configuration.values.ts new file mode 100644 index 000000000..c7f696eae --- /dev/null +++ b/ui/src/app/metadata/configuration/configuration.values.ts @@ -0,0 +1,10 @@ +import { MetadataSourceEditor } from '../domain/model/wizards/metadata-source-editor'; + +export enum PATHS { + resolver = 'EntityDescriptor', + provider = 'MetadataResolvers' +} + +export const DEFINITIONS = { + resolver: MetadataSourceEditor +}; diff --git a/ui/src/app/metadata/version/container/version-history.component.html b/ui/src/app/metadata/configuration/container/configuration.component.html similarity index 85% rename from ui/src/app/metadata/version/container/version-history.component.html rename to ui/src/app/metadata/configuration/container/configuration.component.html index 099841308..36626f240 100644 --- a/ui/src/app/metadata/version/container/version-history.component.html +++ b/ui/src/app/metadata/configuration/container/configuration.component.html @@ -1,6 +1,5 @@
-
+
@@ -12,7 +11,7 @@
- +
-
+ \ No newline at end of file diff --git a/ui/src/app/metadata/configuration/container/configuration.component.spec.ts b/ui/src/app/metadata/configuration/container/configuration.component.spec.ts new file mode 100644 index 000000000..c399fbacf --- /dev/null +++ b/ui/src/app/metadata/configuration/container/configuration.component.spec.ts @@ -0,0 +1,55 @@ +import { Component, ViewChild, Input } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { StoreModule, combineReducers } from '@ngrx/store'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; + +import { MetadataConfiguration } from '../model/metadata-configuration'; +import { ConfigurationComponent } from './configuration.component'; +import * as fromConfiguration from '../reducer'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(ConfigurationComponent) + public componentUnderTest: ConfigurationComponent; + + configuration: MetadataConfiguration = { sections: [] }; +} + +describe('Metadata Configuration Page Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: ConfigurationComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule, + StoreModule.forRoot({ + 'metadata-configuration': combineReducers(fromConfiguration.reducers), + }), + MockI18nModule, + RouterTestingModule + ], + declarations: [ + ConfigurationComponent, + TestHostComponent + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should load metadata objects', async(() => { + expect(app).toBeTruthy(); + })); +}); diff --git a/ui/src/app/metadata/configuration/container/configuration.component.ts b/ui/src/app/metadata/configuration/container/configuration.component.ts new file mode 100644 index 000000000..2c49c0961 --- /dev/null +++ b/ui/src/app/metadata/configuration/container/configuration.component.ts @@ -0,0 +1,60 @@ +import { Store } from '@ngrx/store'; +import { Component, ChangeDetectionStrategy, OnDestroy } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; +import { ActivatedRoute, Params } from '@angular/router'; + +import * as fromConfiguration from '../reducer'; +import { MetadataConfiguration } from '../model/metadata-configuration'; +import { takeUntil, map, withLatestFrom, filter } from 'rxjs/operators'; +import { LoadMetadataRequest, ClearConfiguration, LoadXmlRequest } from '../action/configuration.action'; +import { LoadHistoryRequest, ClearHistory, SelectVersion } from '../action/history.action'; +import * as fromReducer from '../reducer'; + +@Component({ + selector: 'configuration-page', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './configuration.component.html', + styleUrls: [] +}) +export class ConfigurationComponent implements OnDestroy { + private ngUnsubscribe: Subject = new Subject(); + + constructor( + private store: Store, + private routerState: ActivatedRoute + ) { + this.routerState.params.pipe( + takeUntil(this.ngUnsubscribe), + map(params => new LoadMetadataRequest({id: params.id, type: params.type})) + ).subscribe(store); + + this.routerState.params.pipe( + takeUntil(this.ngUnsubscribe), + map(params => new LoadHistoryRequest({ id: params.id, type: params.type })) + ).subscribe(store); + + this.store.select(fromReducer.getVersionCollection).pipe( + takeUntil(this.ngUnsubscribe), + withLatestFrom( + this.routerState.queryParams + ), + map(([collection, params]) => { + if (collection && collection.length) { + return params.version || collection[0].id; + } + return null; + }) + ).subscribe(version => { + if (version) { + this.store.dispatch(new SelectVersion(version)); + } + }); + } + + ngOnDestroy() { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + this.store.dispatch(new ClearConfiguration()); + this.store.dispatch(new ClearHistory()); + } +} diff --git a/ui/src/app/metadata/configuration/container/metadata-history.component.html b/ui/src/app/metadata/configuration/container/metadata-history.component.html new file mode 100644 index 000000000..225912756 --- /dev/null +++ b/ui/src/app/metadata/configuration/container/metadata-history.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/ui/src/app/metadata/version/container/version-history.component.spec.ts b/ui/src/app/metadata/configuration/container/metadata-history.component.spec.ts similarity index 65% rename from ui/src/app/metadata/version/container/version-history.component.spec.ts rename to ui/src/app/metadata/configuration/container/metadata-history.component.spec.ts index 4398b5128..084d79ef0 100644 --- a/ui/src/app/metadata/version/container/version-history.component.spec.ts +++ b/ui/src/app/metadata/configuration/container/metadata-history.component.spec.ts @@ -1,11 +1,13 @@ import { Component, ViewChild, Input, EventEmitter, Output } from '@angular/core'; import { TestBed, ComponentFixture } from '@angular/core/testing'; import { MockI18nModule } from '../../../../testing/i18n.stub'; -import { VersionHistoryComponent } from './version-history.component'; +import { MetadataHistoryComponent } from './metadata-history.component'; import { MetadataHistory } from '../model/history'; import { MetadataVersion } from '../model/version'; -import { HistoryService } from '../service/history.service'; +import { MetadataHistoryService } from '../service/history.service'; import { of } from 'rxjs'; +import { StoreModule, combineReducers } from '@ngrx/store'; +import * as fromConfiguration from '../reducer'; export const TestData = { versions: [ @@ -33,26 +35,29 @@ const MockHistoryService = { }; describe('Metadata Version History Component', () => { - let fixture: ComponentFixture; - let instance: VersionHistoryComponent; + let fixture: ComponentFixture; + let instance: MetadataHistoryComponent; beforeEach(() => { TestBed.configureTestingModule({ providers: [ { - provide: HistoryService, useValue: MockHistoryService + provide: MetadataHistoryService, useValue: MockHistoryService } ], imports: [ - MockI18nModule + MockI18nModule, + StoreModule.forRoot({ + 'metadata-configuration': combineReducers(fromConfiguration.reducers), + }), ], declarations: [ MockHistoryListComponent, - VersionHistoryComponent + MetadataHistoryComponent ], }); - fixture = TestBed.createComponent(VersionHistoryComponent); + fixture = TestBed.createComponent(MetadataHistoryComponent); instance = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/ui/src/app/metadata/configuration/container/metadata-history.component.ts b/ui/src/app/metadata/configuration/container/metadata-history.component.ts new file mode 100644 index 000000000..6fb60bdb9 --- /dev/null +++ b/ui/src/app/metadata/configuration/container/metadata-history.component.ts @@ -0,0 +1,22 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Store } from '@ngrx/store'; +import { ConfigurationState, getVersionCollection } from '../reducer'; +import { MetadataVersion } from '../model/version'; + +@Component({ + selector: 'metadata-history', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './metadata-history.component.html', + styleUrls: [] +}) +export class MetadataHistoryComponent { + + history$: Observable; + + constructor( + private store: Store + ) { + this.history$ = this.store.select(getVersionCollection); + } +} diff --git a/ui/src/app/metadata/configuration/container/metadata-options.component.html b/ui/src/app/metadata/configuration/container/metadata-options.component.html new file mode 100644 index 000000000..9c4dbb489 --- /dev/null +++ b/ui/src/app/metadata/configuration/container/metadata-options.component.html @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/ui/src/app/metadata/configuration/container/metadata-options.component.spec.ts b/ui/src/app/metadata/configuration/container/metadata-options.component.spec.ts new file mode 100644 index 000000000..0cb1bd5b0 --- /dev/null +++ b/ui/src/app/metadata/configuration/container/metadata-options.component.spec.ts @@ -0,0 +1,86 @@ +import { Component, ViewChild, Input } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { StoreModule, combineReducers, Store } from '@ngrx/store'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; + +import { MetadataConfiguration } from '../model/metadata-configuration'; +import * as fromConfiguration from '../reducer'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; +import { MetadataOptionsComponent } from './metadata-options.component'; +import { Metadata } from '../../domain/domain.type'; +import { MetadataVersion } from '../model/version'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'metadata-configuration', + template: `` +}) +class MetadataConfigurationComponent { + @Input() configuration: MetadataConfiguration; +} + +@Component({ + selector: 'metadata-header', + template: `` +}) +class MetadataHeaderComponent { + @Input() isEnabled: boolean; + @Input() version: MetadataVersion; + @Input() versionNumber: number; + @Input() isCurrent: boolean; +} + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(MetadataOptionsComponent) + public componentUnderTest: MetadataOptionsComponent; + + configuration: MetadataConfiguration = { sections: [] }; +} + +describe('Metadata Options Page Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: MetadataOptionsComponent; + let store: Store; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule, + StoreModule.forRoot({ + 'metadata-configuration': combineReducers(fromConfiguration.reducers), + }), + MockI18nModule, + RouterTestingModule, + CommonModule + ], + declarations: [ + MetadataOptionsComponent, + MetadataConfigurationComponent, + MetadataHeaderComponent, + TestHostComponent + ], + }).compileComponents(); + + store = TestBed.get(Store); + spyOn(store, 'dispatch'); + spyOn(store, 'select').and.callThrough(); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should load metadata objects', async(() => { + expect(app).toBeTruthy(); + expect(store.select).toHaveBeenCalled(); + })); +}); diff --git a/ui/src/app/metadata/configuration/container/metadata-options.component.ts b/ui/src/app/metadata/configuration/container/metadata-options.component.ts new file mode 100644 index 000000000..461252f1d --- /dev/null +++ b/ui/src/app/metadata/configuration/container/metadata-options.component.ts @@ -0,0 +1,42 @@ +import { Store } from '@ngrx/store'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { + ConfigurationState, + getConfigurationSections, + getConfigurationModel, + getSelectedVersion, + getSelectedVersionNumber, + getSelectedIsCurrent +} from '../reducer'; +import { MetadataConfiguration } from '../model/metadata-configuration'; +import { MetadataVersion } from '../model/version'; +import { map } from 'rxjs/operators'; + +@Component({ + selector: 'metadata-options-page', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './metadata-options.component.html', + styleUrls: [] +}) +export class MetadataOptionsComponent { + + configuration$: Observable; + isEnabled$: Observable; + version$: Observable; + versionNumber$: Observable; + isCurrent$: Observable; + + constructor( + private store: Store + ) { + this.configuration$ = this.store.select(getConfigurationSections); + this.isEnabled$ = this.store.select(getConfigurationModel).pipe( + map(config => config ? ('serviceEnabled' in config) ? config.serviceEnabled : config.enabled : false) + ); + this.version$ = this.store.select(getSelectedVersion); + this.versionNumber$ = this.store.select(getSelectedVersionNumber); + this.isCurrent$ = this.store.select(getSelectedIsCurrent); + } +} diff --git a/ui/src/app/metadata/configuration/container/metadata-xml.component.html b/ui/src/app/metadata/configuration/container/metadata-xml.component.html new file mode 100644 index 000000000..613dc7699 --- /dev/null +++ b/ui/src/app/metadata/configuration/container/metadata-xml.component.html @@ -0,0 +1,15 @@ +
+
+
+ Options + XML +
+
+
+
{{ xml$ | async }}
+ +
+
\ No newline at end of file diff --git a/ui/src/app/metadata/configuration/container/metadata-xml.component.spec.ts b/ui/src/app/metadata/configuration/container/metadata-xml.component.spec.ts new file mode 100644 index 000000000..81fbd836f --- /dev/null +++ b/ui/src/app/metadata/configuration/container/metadata-xml.component.spec.ts @@ -0,0 +1,61 @@ +import { Component, ViewChild, Input } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { StoreModule, combineReducers, Store } from '@ngrx/store'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; + +import { MetadataConfiguration } from '../model/metadata-configuration'; +import * as fromConfiguration from '../reducer'; +import { MockI18nModule } from '../../../../testing/i18n.stub'; +import { MetadataXmlComponent } from './metadata-xml.component'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(MetadataXmlComponent) + public componentUnderTest: MetadataXmlComponent; + + configuration: MetadataConfiguration = { sections: [] }; +} + +describe('Metadata Xml Page Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: MetadataXmlComponent; + let store: Store; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule, + StoreModule.forRoot({ + 'metadata-configuration': combineReducers(fromConfiguration.reducers), + }), + MockI18nModule, + RouterTestingModule + ], + declarations: [ + MetadataXmlComponent, + TestHostComponent + ], + }).compileComponents(); + + store = TestBed.get(Store); + spyOn(store, 'dispatch'); + spyOn(store, 'select'); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should load metadata objects', async(() => { + expect(app).toBeTruthy(); + expect(store.select).toHaveBeenCalledTimes(2); + })); +}); diff --git a/ui/src/app/metadata/configuration/container/metadata-xml.component.ts b/ui/src/app/metadata/configuration/container/metadata-xml.component.ts new file mode 100644 index 000000000..1a56906a0 --- /dev/null +++ b/ui/src/app/metadata/configuration/container/metadata-xml.component.ts @@ -0,0 +1,31 @@ +import { Store } from '@ngrx/store'; +import { Component } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; + +import * as fromConfiguration from '../reducer'; +import { Metadata } from '../../domain/domain.type'; +import { DownloadXml } from '../action/configuration.action'; + +@Component({ + selector: 'metadata-xml-page', + templateUrl: './metadata-xml.component.html', + styleUrls: [] +}) +export class MetadataXmlComponent { + + entity: Metadata; + entity$: Observable; + xml: string; + xml$: Observable; + + constructor( + private store: Store + ) { + this.xml$ = this.store.select(fromConfiguration.getConfigurationXml); + this.entity$ = this.store.select(fromConfiguration.getConfigurationModel); + } + + preview(): void { + this.store.dispatch(new DownloadXml()); + } +} diff --git a/ui/src/app/metadata/configuration/effect/configuration.effect.ts b/ui/src/app/metadata/configuration/effect/configuration.effect.ts new file mode 100644 index 000000000..ff76c8125 --- /dev/null +++ b/ui/src/app/metadata/configuration/effect/configuration.effect.ts @@ -0,0 +1,132 @@ +import { Injectable } from '@angular/core'; +import { Effect, Actions, ofType } from '@ngrx/effects'; +import { switchMap, catchError, map, tap, withLatestFrom } from 'rxjs/operators'; +import { of, Observable } from 'rxjs'; +import * as FileSaver from 'file-saver'; +import { Store } from '@ngrx/store'; + +import { MetadataConfigurationService } from '../service/configuration.service'; +import { + LoadMetadataRequest, + LoadMetadataSuccess, + LoadMetadataError, + ConfigurationActionTypes, + SetMetadata, + SetDefinition, + LoadSchemaRequest, + LoadSchemaSuccess, + SetSchema, + LoadSchemaError, + LoadXmlSuccess, + LoadXmlError, + SetXml, + DownloadXml +} from '../action/configuration.action'; +import { ResolverService } from '../../domain/service/resolver.service'; +import { EntityIdService } from '../../domain/service/entity-id.service'; +import { State } from '../reducer/configuration.reducer'; +import { getConfigurationModel, getConfigurationXml } from '../reducer'; +import { MetadataResolver } from '../../domain/model'; + +@Injectable() +export class MetadataConfigurationEffects { + + @Effect() + loadMetadata$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.LOAD_METADATA_REQUEST), + switchMap(action => + this.configService + .find(action.payload.id, action.payload.type) + .pipe( + map(md => new LoadMetadataSuccess(md)), + catchError(error => of(new LoadMetadataError(error))) + ) + ) + ); + + @Effect() + loadMetadataXml$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.LOAD_METADATA_REQUEST), + switchMap(action => { + let loader: Observable; + switch (action.payload.type) { + case 'filter': + loader = this.entityService.preview(action.payload.id); + break; + default: + loader = this.providerService.preview(action.payload.id); + break; + } + + return loader.pipe( + map(xml => new LoadXmlSuccess(xml)), + catchError(error => of(new LoadXmlError(error))) + ); + }) + ); + + @Effect() + setXmlOnLoad$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.LOAD_XML_SUCCESS), + map(action => new SetXml(action.payload)) + ); + + @Effect() + setMetadataOnLoad$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.LOAD_METADATA_SUCCESS), + map(action => new SetMetadata(action.payload)) + ); + + @Effect() + setDefinition$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.SET_METADATA), + map(action => new SetDefinition(this.configService.getDefinition(action.payload['@type']))) + ); + + @Effect() + loadSchemaOnDefinitionSet$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.SET_DEFINITION), + map(action => new LoadSchemaRequest(action.payload.schema)) + ); + + @Effect() + loadSchemaData$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.LOAD_SCHEMA_REQUEST), + switchMap(action => + this.configService + .loadSchema(action.payload) + .pipe( + map(schema => new LoadSchemaSuccess(schema)), + catchError(error => of(new LoadSchemaError(error))) + ) + ) + ); + + @Effect() + setSchema$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.LOAD_SCHEMA_SUCCESS), + map(action => new SetSchema(action.payload)) + ); + + @Effect({dispatch: false}) + downloadXml$ = this.actions$.pipe( + ofType(ConfigurationActionTypes.DOWNLOAD_XML), + withLatestFrom( + this.store.select(getConfigurationModel), + this.store.select(getConfigurationXml) + ), + tap(([action, entity, xml]) => { + const name = entity.name ? entity.name : (entity as MetadataResolver).serviceProviderName; + const blob = new Blob([xml], { type: 'text/xml;charset=utf-8' }); + FileSaver.saveAs(blob, `${name}.xml`); + }) + ); + + constructor( + private configService: MetadataConfigurationService, + private actions$: Actions, + private providerService: ResolverService, + private entityService: EntityIdService, + private store: Store + ) { } +} diff --git a/ui/src/app/metadata/configuration/effect/history.effect.ts b/ui/src/app/metadata/configuration/effect/history.effect.ts new file mode 100644 index 000000000..0bda812ec --- /dev/null +++ b/ui/src/app/metadata/configuration/effect/history.effect.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { Effect, Actions, ofType } from '@ngrx/effects'; +import { switchMap, catchError, map } from 'rxjs/operators'; +import { of } from 'rxjs'; +import { LoadHistoryRequest, HistoryActionTypes, LoadHistorySuccess, LoadHistoryError, SetHistory } from '../action/history.action'; +import { MetadataHistoryService } from '../service/history.service'; + +@Injectable() +export class MetadataHistoryEffects { + + @Effect() + loadHistory$ = this.actions$.pipe( + ofType(HistoryActionTypes.LOAD_HISTORY_REQUEST), + switchMap(action => + this.historyService + .query(action.payload.id, action.payload.type) + .pipe( + map(history => new LoadHistorySuccess(history)), + catchError(error => of(new LoadHistoryError(error))) + ) + ) + ); + + @Effect() + loadHistorySuccess$ = this.actions$.pipe( + ofType(HistoryActionTypes.LOAD_HISTORY_SUCCESS), + map(action => new SetHistory(action.payload)) + ); + + constructor( + private historyService: MetadataHistoryService, + private actions$: Actions + ) { } +} diff --git a/ui/src/app/metadata/version/model/history.ts b/ui/src/app/metadata/configuration/model/history.ts similarity index 100% rename from ui/src/app/metadata/version/model/history.ts rename to ui/src/app/metadata/configuration/model/history.ts diff --git a/ui/src/app/metadata/configuration/model/metadata-configuration.ts b/ui/src/app/metadata/configuration/model/metadata-configuration.ts new file mode 100644 index 000000000..b8a37b85e --- /dev/null +++ b/ui/src/app/metadata/configuration/model/metadata-configuration.ts @@ -0,0 +1,5 @@ +import Section from './section'; + +export interface MetadataConfiguration { + sections: Section[]; +} diff --git a/ui/src/app/metadata/configuration/model/schema.ts b/ui/src/app/metadata/configuration/model/schema.ts new file mode 100644 index 000000000..469650111 --- /dev/null +++ b/ui/src/app/metadata/configuration/model/schema.ts @@ -0,0 +1,3 @@ +export interface Schema { + [prop: string]: any; +} diff --git a/ui/src/app/metadata/configuration/model/section.ts b/ui/src/app/metadata/configuration/model/section.ts new file mode 100644 index 000000000..089a1953a --- /dev/null +++ b/ui/src/app/metadata/configuration/model/section.ts @@ -0,0 +1,11 @@ +import { Property } from '../../domain/model/property'; + +export interface Section { + id: string; + index: number; + label: string; + pageNumber: number; + properties: Property[]; +} + +export default Section; diff --git a/ui/src/app/metadata/configuration/model/version.ts b/ui/src/app/metadata/configuration/model/version.ts new file mode 100644 index 000000000..0eb7cb815 --- /dev/null +++ b/ui/src/app/metadata/configuration/model/version.ts @@ -0,0 +1,5 @@ +export interface MetadataVersion { + id: string; + date: string; + creator: string; +} diff --git a/ui/src/app/metadata/configuration/reducer/configuration.reducer.spec.ts b/ui/src/app/metadata/configuration/reducer/configuration.reducer.spec.ts new file mode 100644 index 000000000..4be8661e0 --- /dev/null +++ b/ui/src/app/metadata/configuration/reducer/configuration.reducer.spec.ts @@ -0,0 +1,106 @@ +import { reducer } from './configuration.reducer'; +import * as fromConfig from './configuration.reducer'; +import * as actions from '../action/configuration.action'; +import { MetadataSourceEditor } from '../../domain/model/wizards/metadata-source-editor'; +import { SCHEMA as schema } from '../../../../testing/form-schema.stub'; +import { MetadataResolver } from '../../domain/model'; + +describe('Configuration Reducer', () => { + const initialState: fromConfig.State = { ...fromConfig.initialState }; + + const definition = new MetadataSourceEditor(); + const model: MetadataResolver = { + id: 'foo', + serviceProviderName: 'foo', + '@type': 'MetadataResolver', + createdBy: 'admin' + }; + const xml = ``; + + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(undefined, {} as any); + + expect(result).toEqual(initialState); + }); + }); + + describe('SET_DEFINITION action', () => { + it('should set the state definition', () => { + const action = new actions.SetDefinition(definition); + const result = reducer(initialState, action); + + expect(result).toEqual({ ...initialState, definition }); + }); + }); + + describe('SET_SCHEMA action', () => { + it('should set the state schema', () => { + const action = new actions.SetSchema(schema); + const result = reducer(initialState, action); + + expect(result).toEqual({ ...initialState, schema }); + }); + }); + + describe('SET_METADATA action', () => { + it('should set the state metadata model', () => { + const action = new actions.SetMetadata(model as MetadataResolver); + const result = reducer(initialState, action); + + expect(result).toEqual({ ...initialState, model }); + }); + }); + + describe('SET_XML action', () => { + it('should set the state metadata model', () => { + const action = new actions.SetXml(xml); + const result = reducer(initialState, action); + + expect(result).toEqual({ ...initialState, xml }); + }); + }); + + describe('CLEAR action', () => { + it('should clear the state and reset to initial state', () => { + const action = new actions.ClearConfiguration(); + const result = reducer({ + ...initialState, + model, + definition, + schema + }, action); + + expect(result).toEqual(initialState); + }); + }); + + describe('selector functions', () => { + /* + export const getModel = (state: State) => state.model; + export const getDefinition = (state: State) => state.definition; + export const getSchema = (state: State) => state.schema; + export const getXml = (state: State) => state.xml; + */ + describe('getModel', () => { + it('should retrieve the model from state', () => { + expect(fromConfig.getModel({...initialState, model})).toBe(model); + }); + }); + describe('getDefinition', () => { + it('should retrieve the definition from state', () => { + expect(fromConfig.getDefinition({ ...initialState, definition })).toBe(definition); + }); + }); + describe('getSchema', () => { + it('should retrieve the schema from state', () => { + expect(fromConfig.getSchema({ ...initialState, schema })).toBe(schema); + }); + }); + describe('getXml', () => { + it('should retrieve the schema from state', () => { + expect(fromConfig.getXml({ ...initialState, xml })).toBe(xml); + }); + }); + }); +}); diff --git a/ui/src/app/metadata/configuration/reducer/configuration.reducer.ts b/ui/src/app/metadata/configuration/reducer/configuration.reducer.ts new file mode 100644 index 000000000..a35e20dd7 --- /dev/null +++ b/ui/src/app/metadata/configuration/reducer/configuration.reducer.ts @@ -0,0 +1,55 @@ +import { ConfigurationActionTypes, ConfigurationActionsUnion } from '../action/configuration.action'; +import { Metadata } from '../../domain/domain.type'; +import { Wizard } from '../../../wizard/model'; +import { Schema } from '../model/schema'; + +export interface State { + model: Metadata; + schema: Schema; + definition: Wizard; + xml: string; +} + +export const initialState: State = { + model: null, + schema: null, + definition: null, + xml: '' +}; + +export function reducer(state = initialState, action: ConfigurationActionsUnion): State { + switch (action.type) { + case ConfigurationActionTypes.SET_SCHEMA: + return { + ...state, + schema: action.payload + }; + case ConfigurationActionTypes.SET_DEFINITION: + return { + ...state, + definition: action.payload + }; + case ConfigurationActionTypes.SET_METADATA: + return { + ...state, + model: action.payload + }; + case ConfigurationActionTypes.SET_XML: + return { + ...state, + xml: action.payload + }; + case ConfigurationActionTypes.CLEAR: + return { + ...initialState + }; + default: { + return state; + } + } +} + +export const getModel = (state: State) => state.model; +export const getDefinition = (state: State) => state.definition; +export const getSchema = (state: State) => state.schema; +export const getXml = (state: State) => state.xml; diff --git a/ui/src/app/metadata/configuration/reducer/history.reducer.spec.ts b/ui/src/app/metadata/configuration/reducer/history.reducer.spec.ts new file mode 100644 index 000000000..a3bc830e5 --- /dev/null +++ b/ui/src/app/metadata/configuration/reducer/history.reducer.spec.ts @@ -0,0 +1,70 @@ +import { reducer } from './history.reducer'; +import * as fromHistory from './history.reducer'; +import * as actions from '../action/history.action'; +import { MetadataHistory } from '../model/history'; + +describe('History Reducer', () => { + + const baseState = fromHistory.initialState; + + const history: MetadataHistory = { + versions: [ + { + id: '1', + date: new Date().toLocaleDateString(), + creator: 'foo' + }, + { + id: '2', + date: new Date().toDateString(), + creator: 'foo' + } + ] + }; + + describe('undefined action', () => { + it('should return the default state', () => { + const result = reducer(undefined, {} as any); + + expect(result).toEqual(fromHistory.initialState); + }); + }); + + describe('SET_HISTORY action', () => { + it('should set the state metadata model', () => { + const action = new actions.SetHistory(history); + const result = reducer(fromHistory.initialState, action); + + expect(Object.keys(result.entities)).toEqual(['1', '2']); + }); + }); + + describe('SELECT_VERSION action', () => { + it('should set the state metadata model', () => { + const action = new actions.SelectVersion('1'); + const result = reducer(baseState, action); + + expect(result).toEqual({ ...baseState, selectedVersionId: '1' }); + }); + }); + + describe('CLEAR action', () => { + it('should clear the state and reset to initial state', () => { + const action = new actions.ClearHistory(); + const result = reducer({ + ...baseState, + ...history + }, action); + + expect(result).toEqual(baseState); + }); + }); + + describe('selector functions', () => { + describe('getSelectedId', () => { + it('should return the selected version id', () => { + expect(fromHistory.getSelectedVersionId({ ...baseState, selectedVersionId: '1' })).toBe('1'); + }); + }); + }); +}); diff --git a/ui/src/app/metadata/configuration/reducer/history.reducer.ts b/ui/src/app/metadata/configuration/reducer/history.reducer.ts new file mode 100644 index 000000000..8a2566f59 --- /dev/null +++ b/ui/src/app/metadata/configuration/reducer/history.reducer.ts @@ -0,0 +1,50 @@ +import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; +import { HistoryActionTypes, HistoryActionsUnion } from '../action/history.action'; +import { MetadataVersion } from '../model/version'; + +export interface HistoryState extends EntityState { + selectedVersionId: string; +} + +export function sortByDate(a: MetadataVersion, b: MetadataVersion): number { + return a.date.localeCompare(b.date); +} + +export const adapter: EntityAdapter = createEntityAdapter({ + sortComparer: sortByDate, + selectId: (model: MetadataVersion) => model.id +}); + +export const initialState: HistoryState = adapter.getInitialState({ + selectedVersionId: null +}); + +export function reducer(state = initialState, action: HistoryActionsUnion): HistoryState { + switch (action.type) { + case HistoryActionTypes.SET_HISTORY: + return adapter.addAll(action.payload.versions, { + ...state, + selectedVersionId: state.selectedVersionId + }); + case HistoryActionTypes.SELECT_VERSION: + return { + ...state, + selectedVersionId: action.payload + }; + case HistoryActionTypes.CLEAR_HISTORY: + return adapter.removeAll({ + ...initialState + }); + default: { + return state; + } + } +} + +export const getSelectedVersionId = (state: HistoryState) => state.selectedVersionId; +export const { + selectIds: selectVersionIds, + selectEntities: selectVersionEntities, + selectAll: selectAllVersions, + selectTotal: selectVersionTotal +} = adapter.getSelectors(); diff --git a/ui/src/app/metadata/configuration/reducer/index.spec.ts b/ui/src/app/metadata/configuration/reducer/index.spec.ts new file mode 100644 index 000000000..5a675621c --- /dev/null +++ b/ui/src/app/metadata/configuration/reducer/index.spec.ts @@ -0,0 +1,19 @@ +import { getConfigurationSectionsFn } from './index'; +import { SCHEMA as schema } from '../../../../testing/form-schema.stub'; +import { MetadataSourceEditor } from '../../domain/model/wizards/metadata-source-editor'; + +describe('Configuration Reducer', () => { + const model = { + name: 'foo', + '@type': 'MetadataResolver' + }; + + const definition = new MetadataSourceEditor(); + + describe('getConfigurationSectionsFn', () => { + it('should parse the schema, definition, and model into a MetadataConfiguration', () => { + const config = getConfigurationSectionsFn(model, definition, schema); + expect(config.sections).toBeDefined(); + }); + }); +}); diff --git a/ui/src/app/metadata/configuration/reducer/index.ts b/ui/src/app/metadata/configuration/reducer/index.ts new file mode 100644 index 000000000..702e731f4 --- /dev/null +++ b/ui/src/app/metadata/configuration/reducer/index.ts @@ -0,0 +1,85 @@ +import { createSelector, createFeatureSelector } from '@ngrx/store'; + +import * as fromRoot from '../../../app.reducer'; +import * as fromConfiguration from './configuration.reducer'; +import * as fromHistory from './history.reducer'; +import { WizardStep } from '../../../wizard/model'; + +import * as utils from '../../domain/utility/configuration'; +import { getSplitSchema } from '../../../wizard/reducer'; +import { getInCollectionFn } from '../../domain/domain.util'; + +export interface ConfigurationState { + configuration: fromConfiguration.State; + history: fromHistory.HistoryState; +} + +export const reducers = { + configuration: fromConfiguration.reducer, + history: fromHistory.reducer +}; + +export interface State extends fromRoot.State { + 'metadata-configuration': ConfigurationState; +} + +export const getState = createFeatureSelector('metadata-configuration'); + +export const getConfigurationStateFn = (state: ConfigurationState) => state.configuration; +export const getHistoryStateFn = (state: ConfigurationState) => state.history; + +export const getConfigurationState = createSelector(getState, getConfigurationStateFn); +export const getConfigurationModel = createSelector(getConfigurationState, fromConfiguration.getModel); +export const getConfigurationDefinition = createSelector(getConfigurationState, fromConfiguration.getDefinition); +export const getConfigurationSchema = createSelector(getConfigurationState, fromConfiguration.getSchema); +export const getConfigurationXml = createSelector(getConfigurationState, fromConfiguration.getXml); + +export const getConfigurationSectionsFn = (model, definition, schema) => !definition || !schema ? null : + ({ + sections: definition.steps + .filter(step => step.id !== 'summary') + .map( + (step: WizardStep, num: number) => { + return ({ + id: step.id, + pageNumber: num + 1, + index: step.index, + label: step.label, + properties: utils.getStepProperties( + getSplitSchema(schema, step), + definition.formatter(model), + schema.definitions || {} + ) + }); + } + ) + }); +export const getConfigurationSections = createSelector( + getConfigurationModel, + getConfigurationDefinition, + getConfigurationSchema, + getConfigurationSectionsFn +); + +// Version History + +export const getHistoryState = createSelector(getState, getHistoryStateFn); + +export const getVersionEntities = createSelector(getHistoryState, fromHistory.selectVersionEntities); +export const getSelectedVersionId = createSelector(getHistoryState, fromHistory.getSelectedVersionId); +export const getVersionIds = createSelector(getHistoryState, fromHistory.selectVersionIds); +export const getVersionCollection = createSelector(getHistoryState, getVersionIds, fromHistory.selectAllVersions); +export const getSelectedVersion = createSelector(getVersionEntities, getSelectedVersionId, getInCollectionFn); +export const getSelectedVersionNumber = createSelector( + getVersionCollection, + getSelectedVersionId, + (versions, selectedId) => versions.indexOf(versions.find(v => v.id === selectedId)) + 1 +); + +export const getSelectedIsCurrent = createSelector( + getSelectedVersion, + getVersionCollection, + (selected, collection) => { + return selected ? collection[0].id === selected.id : null; + } +); diff --git a/ui/src/app/metadata/configuration/service/configuration.service.spec.ts b/ui/src/app/metadata/configuration/service/configuration.service.spec.ts new file mode 100644 index 000000000..3d44cae6a --- /dev/null +++ b/ui/src/app/metadata/configuration/service/configuration.service.spec.ts @@ -0,0 +1,64 @@ +import { TestBed, async, inject } from '@angular/core/testing'; +import { HttpClientModule, HttpRequest } from '@angular/common/http'; +import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing'; +import { MetadataConfigurationService } from './configuration.service'; +import { FileBackedHttpMetadataProviderEditor } from '../../provider/model'; +import { MetadataSourceEditor } from '../../domain/model/wizards/metadata-source-editor'; +import { PATHS } from '../configuration.values'; + +describe(`Attributes Service`, () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientModule, + HttpClientTestingModule + ], + providers: [ + MetadataConfigurationService + ] + }); + }); + + describe('find method', () => { + it(`should send an expected GET request`, async(inject([MetadataConfigurationService, HttpTestingController], + (service: MetadataConfigurationService, backend: HttpTestingController) => { + const type = 'resolver'; + const id = 'foo'; + service.find(id, type).subscribe(); + backend.expectOne((req: HttpRequest) => { + return req.url === `${service.base}/${PATHS[type]}/${id}` + && req.method === 'GET'; + }, `GET metadata by id and type`); + } + ))); + }); + + describe('loadSchema method', () => { + it(`should send an expected GET request`, async(inject([MetadataConfigurationService, HttpTestingController], + (service: MetadataConfigurationService, backend: HttpTestingController) => { + const path = '/foo.json'; + service.loadSchema(path).subscribe(); + backend.expectOne((req: HttpRequest) => { + return req.url === `${path}` + && req.method === 'GET'; + }, `GET schema by path`); + } + ))); + }); + + describe('getDefinition method', () => { + it(`should retrieve the editor definition by model type`, async(inject([MetadataConfigurationService, HttpTestingController], + (service: MetadataConfigurationService, backend: HttpTestingController) => { + const def = service.getDefinition('FileBackedHttpMetadataResolver'); + expect(def).toBe(FileBackedHttpMetadataProviderEditor); + } + ))); + + it(`should instantiate an editor for resolvers`, async(inject([MetadataConfigurationService], + (service: MetadataConfigurationService) => { + const def = service.getDefinition('foo'); + expect(def instanceof MetadataSourceEditor).toBe(true); + } + ))); + }); +}); diff --git a/ui/src/app/metadata/configuration/service/configuration.service.ts b/ui/src/app/metadata/configuration/service/configuration.service.ts new file mode 100644 index 000000000..ee0093b36 --- /dev/null +++ b/ui/src/app/metadata/configuration/service/configuration.service.ts @@ -0,0 +1,32 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { Metadata } from '../../domain/domain.type'; +import { Wizard } from '../../../wizard/model'; +import { MetadataSourceEditor } from '../../domain/model/wizards/metadata-source-editor'; +import { MetadataProviderEditorTypes } from '../../provider/model'; +import { Schema } from '../model/schema'; +import { PATHS } from '../configuration.values'; + +@Injectable() +export class MetadataConfigurationService { + + readonly base = '/api'; + + constructor( + private http: HttpClient + ) {} + + find(id: string, type: string): Observable { + return this.http.get(`${this.base}/${PATHS[type]}/${id}`); + } + + getDefinition(type: string): Wizard { + return MetadataProviderEditorTypes.find(def => def.type === type) || new MetadataSourceEditor(); + } + + loadSchema(path: string): Observable { + return this.http.get(path); + } +} + diff --git a/ui/src/app/metadata/version/service/history.service.spec.ts b/ui/src/app/metadata/configuration/service/history.service.spec.ts similarity index 65% rename from ui/src/app/metadata/version/service/history.service.spec.ts rename to ui/src/app/metadata/configuration/service/history.service.spec.ts index 71e6ec960..0e8bc77a4 100644 --- a/ui/src/app/metadata/version/service/history.service.spec.ts +++ b/ui/src/app/metadata/configuration/service/history.service.spec.ts @@ -1,7 +1,7 @@ import { TestBed, async, inject } from '@angular/core/testing'; import { HttpClientModule } from '@angular/common/http'; import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing'; -import { HistoryService } from './history.service'; +import { MetadataHistoryService } from './history.service'; describe(`Attributes Service`, () => { beforeEach(() => { @@ -11,15 +11,15 @@ describe(`Attributes Service`, () => { HttpClientTestingModule ], providers: [ - HistoryService + MetadataHistoryService ] }); }); describe('query method', () => { - it(`should return a MetadataHistory`, async(inject([HistoryService, HttpTestingController], - (service: HistoryService) => { - service.query().subscribe(history => { + it(`should return a MetadataHistory`, async(inject([MetadataHistoryService, HttpTestingController], + (service: MetadataHistoryService) => { + service.query('foo', 'resolver').subscribe(history => { expect(history).toBeDefined(); }); } diff --git a/ui/src/app/metadata/configuration/service/history.service.ts b/ui/src/app/metadata/configuration/service/history.service.ts new file mode 100644 index 000000000..3125c2091 --- /dev/null +++ b/ui/src/app/metadata/configuration/service/history.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, of } from 'rxjs'; +import { MetadataHistory } from '../model/history'; + +import { PATHS } from '../../configuration/configuration.values'; +import { MetadataVersion } from '../model/version'; +import { map } from 'rxjs/operators'; + +@Injectable() +export class MetadataHistoryService { + + readonly base = `api`; + readonly path = `Versions`; + + constructor( + private http: HttpClient + ) { } + + query(resourceId: string, type: string): Observable { + return this.http.get(`/${this.base}/${PATHS[type]}/${resourceId}/${this.path}`).pipe( + map(resp => ({ + versions: resp + })) + ); + } +} diff --git a/ui/src/app/metadata/domain/component/editor-nav.component.spec.ts b/ui/src/app/metadata/domain/component/editor-nav.component.spec.ts index 3a93f2f8c..3d6627a4e 100644 --- a/ui/src/app/metadata/domain/component/editor-nav.component.spec.ts +++ b/ui/src/app/metadata/domain/component/editor-nav.component.spec.ts @@ -35,8 +35,7 @@ describe('Editor Nav Component', () => { id: 'common', label: 'Common Attributes', index: 2, - initialValues: [], - schema: 'assets/schema/provider/filebacked-http-common.schema.json' + initialValues: [] }; beforeEach(async(() => { diff --git a/ui/src/app/metadata/domain/component/wizard-summary.component.spec.ts b/ui/src/app/metadata/domain/component/wizard-summary.component.spec.ts index 335cd634f..107ef7241 100644 --- a/ui/src/app/metadata/domain/component/wizard-summary.component.spec.ts +++ b/ui/src/app/metadata/domain/component/wizard-summary.component.spec.ts @@ -4,7 +4,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { NgbDropdownModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; -import { getStepProperties, WizardSummaryComponent } from './wizard-summary.component'; +import { WizardSummaryComponent } from './wizard-summary.component'; import { SchemaFormModule, WidgetRegistry, DefaultWidgetRegistry } from 'ngx-schema-form'; import { Wizard } from '../../../wizard/model'; import { MetadataProvider } from '../../domain/model'; @@ -68,17 +68,6 @@ describe('Provider Wizard Summary Component', () => { expect(app).toBeTruthy(); })); - describe('getStepProperties function', () => { - it('should return an empty array of schema or schema.properties is not defined', () => { - expect(getStepProperties(null, {})).toEqual([]); - expect(getStepProperties({}, {})).toEqual([]); - }); - - it('should return a formatted list of properties', () => { - expect(getStepProperties(SCHEMA, {}).length).toBe(2); - }); - }); - describe('gotoPage function', () => { it('should emit an empty string if page is null', () => { spyOn(app.onPageSelect, 'emit'); diff --git a/ui/src/app/metadata/domain/component/wizard-summary.component.ts b/ui/src/app/metadata/domain/component/wizard-summary.component.ts index fa570a1c0..e8b8deb34 100644 --- a/ui/src/app/metadata/domain/component/wizard-summary.component.ts +++ b/ui/src/app/metadata/domain/component/wizard-summary.component.ts @@ -1,10 +1,11 @@ import { Component, Input, SimpleChanges, OnChanges, Output, EventEmitter } from '@angular/core'; +import merge from 'deepmerge'; import { Wizard, WizardStep } from '../../../wizard/model'; import { MetadataProvider, MetadataResolver } from '../../domain/model'; import { Property } from '../model/property'; import { getSplitSchema } from '../../../wizard/reducer'; -import merge from 'deepmerge'; +import { getStepProperties } from '../utility/configuration'; interface Section { id: string; @@ -14,47 +15,6 @@ interface Section { properties: Property[]; } -export function getDefinition(path: string, definitions: any): any { - let def = path.split('/').pop(); - return definitions[def]; -} - -export function getPropertyItemSchema(items: any, definitions: any): any { - if (!items) { return null; } - return items.$ref ? getDefinition(items.$ref, definitions) : items; -} - -export function getStepProperty(property, model, definitions): Property { - if (!property) { return null; } - property = property.$ref ? { ...property, ...getDefinition(property.$ref, definitions) } : property; - return { - name: property.title, - value: model, - type: property.type, - items: getPropertyItemSchema(property.items, definitions), - properties: getStepProperties( - property, - model, - definitions - ), - widget: property.widget instanceof String ? { id: property.widget } : { ...property.widget } - }; -} - - -export function getStepProperties(schema: any, model: any, definitions: any = {}): Property[] { - if (!schema || !schema.properties) { return []; } - return Object - .keys(schema.properties) - .map(property => { - return getStepProperty( - schema.properties[property], - model && model.hasOwnProperty(property) ? model[property] : null, - definitions - ); - }); -} - @Component({ selector: 'wizard-summary', templateUrl: './wizard-summary.component.html', diff --git a/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.ts b/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.ts index 304e6055e..839324879 100644 --- a/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.ts +++ b/ui/src/app/metadata/domain/entity/filter/entity-attributes-filter.ts @@ -7,6 +7,7 @@ export class EntityAttributesFilterEntity implements MetadataFilter, MetadataEnt modifiedDate?: string; version: string; resourceId: string; + createdBy: string; name = ''; filterEnabled = false; diff --git a/ui/src/app/metadata/domain/entity/filter/nameid-format-filter.ts b/ui/src/app/metadata/domain/entity/filter/nameid-format-filter.ts index d57b1e345..3d745f725 100644 --- a/ui/src/app/metadata/domain/entity/filter/nameid-format-filter.ts +++ b/ui/src/app/metadata/domain/entity/filter/nameid-format-filter.ts @@ -7,6 +7,7 @@ export class NameIDFormatFilterEntity implements MetadataFilter, MetadataEntity modifiedDate?: string; version: string; resourceId: string; + createdBy: string; name = ''; filterEnabled = false; diff --git a/ui/src/app/metadata/domain/entity/resolver/file-backed-http-metadata-resolver.ts b/ui/src/app/metadata/domain/entity/resolver/file-backed-http-metadata-resolver.ts index 8a59cb691..81a2a2ca7 100644 --- a/ui/src/app/metadata/domain/entity/resolver/file-backed-http-metadata-resolver.ts +++ b/ui/src/app/metadata/domain/entity/resolver/file-backed-http-metadata-resolver.ts @@ -18,6 +18,7 @@ export class FileBackedHttpMetadataResolver implements MetadataResolver, Metadat createdDate?: string; modifiedDate?: string; version: string; + createdBy: string; entityId = ''; serviceProviderName = ''; @@ -48,6 +49,8 @@ export class FileBackedHttpMetadataResolver implements MetadataResolver, Metadat attributeRelease = [] as string[]; + [property: string]: unknown; + constructor(descriptor?: Partial) { Object.assign(this, descriptor); } @@ -65,7 +68,7 @@ export class FileBackedHttpMetadataResolver implements MetadataResolver, Metadat } getCreationDate(): Date { - return new Date(this.createdDate); + return this.createdDate ? new Date(this.createdDate) : null; } get name(): string { diff --git a/ui/src/app/metadata/domain/model/metadata-base.ts b/ui/src/app/metadata/domain/model/metadata-base.ts index 2de4e32bf..e8847a6ee 100644 --- a/ui/src/app/metadata/domain/model/metadata-base.ts +++ b/ui/src/app/metadata/domain/model/metadata-base.ts @@ -1,6 +1,7 @@ export interface MetadataBase { id?: string; createdDate?: string; + createdBy: string; modifiedDate?: string; version?: string; } diff --git a/ui/src/app/metadata/domain/model/metadata-entity.ts b/ui/src/app/metadata/domain/model/metadata-entity.ts index 49d34a78a..699502c8f 100644 --- a/ui/src/app/metadata/domain/model/metadata-entity.ts +++ b/ui/src/app/metadata/domain/model/metadata-entity.ts @@ -3,6 +3,8 @@ export interface MetadataEntity { enabled: boolean; kind: string; + createdBy: string; + getId(): string; getDisplayId(): string; isDraft(): boolean; diff --git a/ui/src/app/metadata/domain/model/metadata-resolver.ts b/ui/src/app/metadata/domain/model/metadata-resolver.ts index 01505cff5..e97c08d07 100644 --- a/ui/src/app/metadata/domain/model/metadata-resolver.ts +++ b/ui/src/app/metadata/domain/model/metadata-resolver.ts @@ -25,4 +25,6 @@ export interface MetadataResolver extends MetadataBase { serviceEnabled?: boolean; relyingPartyOverrides?: RelyingPartyOverrides; attributeRelease?: string[]; + + [property: string]: unknown; } diff --git a/ui/src/app/metadata/domain/model/property.ts b/ui/src/app/metadata/domain/model/property.ts index f54829916..a792514d8 100644 --- a/ui/src/app/metadata/domain/model/property.ts +++ b/ui/src/app/metadata/domain/model/property.ts @@ -1,4 +1,5 @@ export interface Property { + title?: string; type: string; name: string; value: string[]; diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts index 673f176c1..aab844d5e 100644 --- a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts @@ -9,6 +9,7 @@ export class MetadataSourceBase implements Wizard { label = 'Metadata Source'; type = '@MetadataProvider'; steps: WizardStep[] = []; + schema = ''; bindings = { '/securityInfo/x509CertificateAvailable': [ diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-editor.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-editor.ts index cbe98d5de..13136ddfc 100644 --- a/ui/src/app/metadata/domain/model/wizards/metadata-source-editor.ts +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-editor.ts @@ -2,13 +2,13 @@ import { Wizard, WizardStep } from '../../../../wizard/model'; import { MetadataResolver } from '../metadata-resolver'; import { MetadataSourceBase } from './metadata-source-base'; -export class MetadataSourceEditor extends MetadataSourceBase implements Wizard { +export class MetadataSourceEditor extends MetadataSourceBase implements Wizard { + schema = '/api/ui/MetadataSources'; steps: WizardStep[] = [ { index: 1, id: 'common', label: 'label.sp-org-info', - schema: '/api/ui/MetadataSources', fields: [ 'serviceProviderName', 'entityId', @@ -38,7 +38,6 @@ export class MetadataSourceEditor extends MetadataSourceBase implements Wizard< index: 3, id: 'metadata-ui', label: 'label.metadata-ui', - schema: '/api/ui/MetadataSources', fields: [ 'mdui' ] @@ -47,7 +46,6 @@ export class MetadataSourceEditor extends MetadataSourceBase implements Wizard< index: 4, id: 'descriptor-info', label: 'label.descriptor-info', - schema: '/api/ui/MetadataSources', fields: [ 'serviceProviderSsoDescriptor' ] @@ -56,7 +54,6 @@ export class MetadataSourceEditor extends MetadataSourceBase implements Wizard< index: 5, id: 'logout-endpoints', label: 'label.logout-endpoints', - schema: '/api/ui/MetadataSources', fields: [ 'logoutEndpoints' ], @@ -73,7 +70,6 @@ export class MetadataSourceEditor extends MetadataSourceBase implements Wizard< index: 6, id: 'key-info', label: 'label.key-info', - schema: '/api/ui/MetadataSources', fields: [ 'securityInfo' ] @@ -82,7 +78,6 @@ export class MetadataSourceEditor extends MetadataSourceBase implements Wizard< index: 7, id: 'assertion', label: 'label.assertion', - schema: '/api/ui/MetadataSources', fields: [ 'assertionConsumerServices' ], @@ -99,7 +94,6 @@ export class MetadataSourceEditor extends MetadataSourceBase implements Wizard< index: 8, id: 'relying-party', label: 'label.relying-party', - schema: '/api/ui/MetadataSources', fields: [ 'relyingPartyOverrides' ], @@ -116,7 +110,6 @@ export class MetadataSourceEditor extends MetadataSourceBase implements Wizard< index: 9, id: 'attribute', label: 'label.attribute-release', - schema: '/api/ui/MetadataSources', fields: [ 'attributeRelease' ], diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts index bd7ad9fd9..10b7c4e79 100644 --- a/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-wizard.ts @@ -3,12 +3,12 @@ import { MetadataResolver } from '../metadata-resolver'; import { MetadataSourceBase } from './metadata-source-base'; export class MetadataSourceWizard extends MetadataSourceBase implements Wizard { + schema = '/api/ui/MetadataSources'; steps: WizardStep[] = [ { index: 1, id: 'common', label: 'label.name-and-entity-id', - schema: '/api/ui/MetadataSources', fields: [ 'serviceProviderName', 'entityId' @@ -28,7 +28,6 @@ export class MetadataSourceWizard extends MetadataSourceBase implements Wizard { + describe('getStepProperties function', () => { + it('should return an empty array of schema or schema.properties is not defined', () => { + expect(getStepProperties(null, {})).toEqual([]); + expect(getStepProperties({}, {})).toEqual([]); + }); + + it('should return a formatted list of properties', () => { + expect(getStepProperties(SCHEMA, {}).length).toBe(3); + }); + }); + + describe('getDefinitions method', () => { + it('should retrieve the definitions from the json schema', () => { + const definition = { + id: 'foo', + title: 'bar', + description: 'baz', + type: 'string' + }; + expect(getDefinition('/foo/bar', {bar: definition})).toBe(definition); + }); + }); + + describe('getPropertyItemSchema method', () => { + it('should return null if no items are provided', () => { + expect(getPropertyItemSchema(null, SCHEMA.definitions)).toBeNull(); + }); + it('should retrieve the definitions from the items schema', () => { + expect(getPropertyItemSchema({$ref: 'description'}, SCHEMA.definitions)).toBe(SCHEMA.definitions.description); + }); + it('should return the item itself if no $ref', () => { + let item = {}; + expect(getPropertyItemSchema(item, SCHEMA.definitions)).toBe(item); + }); + }); + + describe('getStepProperty method', () => { + const model = { + name: 'foo', + type: 'bar', + description: 'baz' + }; + it('should return null if no items are provided', () => { + expect(getStepProperty(null, null, SCHEMA.definitions)).toBeNull(); + }); + + it('should retrieve the property $ref definition if available', () => { + const property = getStepProperty( + { $ref: 'description' }, + model, + SCHEMA.definitions + ); + expect(property.type).toBe('string'); + }); + }); +}); + diff --git a/ui/src/app/metadata/domain/utility/configuration.ts b/ui/src/app/metadata/domain/utility/configuration.ts new file mode 100644 index 000000000..dc641c4dd --- /dev/null +++ b/ui/src/app/metadata/domain/utility/configuration.ts @@ -0,0 +1,42 @@ +import { Property } from '../model/property'; + +export function getDefinition(path: string, definitions: any): any { + let def = path.split('/').pop(); + return definitions[def]; +} + +export function getPropertyItemSchema(items: any, definitions: any): any { + if (!items) { return null; } + return items.$ref ? getDefinition(items.$ref, definitions) : items; +} + +export function getStepProperty(property, model, definitions): Property { + if (!property) { return null; } + property = property.$ref ? { ...property, ...getDefinition(property.$ref, definitions) } : property; + return { + name: property.title, + value: model, + type: property.type, + items: getPropertyItemSchema(property.items, definitions), + properties: getStepProperties( + property, + model, + definitions + ), + widget: property.widget instanceof String ? { id: property.widget } : { ...property.widget } + }; +} + + +export function getStepProperties(schema: any, model: any, definitions: any = {}): Property[] { + if (!schema || !schema.properties) { return []; } + return Object + .keys(schema.properties) + .map(property => { + return getStepProperty( + schema.properties[property], + model && model.hasOwnProperty(property) ? model[property] : null, + definitions + ); + }); +} diff --git a/ui/src/app/metadata/manager/component/provider-search.component.html b/ui/src/app/metadata/manager/component/provider-search.component.html index 29479de5d..381aa8568 100644 --- a/ui/src/app/metadata/manager/component/provider-search.component.html +++ b/ui/src/app/metadata/manager/component/provider-search.component.html @@ -1,5 +1,5 @@
-
+
-   diff --git a/ui/src/app/metadata/resolver/container/resolver-edit.component.ts b/ui/src/app/metadata/resolver/container/resolver-edit.component.ts index 6161919f9..775c956d3 100644 --- a/ui/src/app/metadata/resolver/container/resolver-edit.component.ts +++ b/ui/src/app/metadata/resolver/container/resolver-edit.component.ts @@ -56,8 +56,6 @@ export class ResolverEditComponent implements OnDestroy, CanComponentDeactivate let startIndex$ = this.route.firstChild.params.pipe(map(p => p.form)); startIndex$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(index => this.store.dispatch(new SetIndex(index))); - this.index$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(index => index && this.go(index)); - this.store .select(fromWizard.getCurrentWizardSchema) .pipe(filter(s => !!s)) @@ -67,10 +65,6 @@ export class ResolverEditComponent implements OnDestroy, CanComponentDeactivate this.store.select(fromResolver.getEntityChanges).subscribe(changes => this.latest = changes); } - go(index: string): void { - this.router.navigate(['./', index], { relativeTo: this.route }); - } - ngOnDestroy() { this.clear(); this.ngUnsubscribe.next(); @@ -87,7 +81,7 @@ export class ResolverEditComponent implements OnDestroy, CanComponentDeactivate cancel(): void { this.clear(); - this.router.navigate(['dashboard', 'metadata', 'manager', 'resolvers']); + this.router.navigate(['metadata', 'resolver', this.resolver.id, 'configuration']); } canDeactivate( diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.spec.ts b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.spec.ts index 8864220c7..56c4784a7 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.spec.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.spec.ts @@ -56,7 +56,8 @@ describe('Resolver Wizard Step Component', () => { ...initialState, changes: { id: 'foo', - serviceProviderName: 'bar' + serviceProviderName: 'bar', + createdBy: 'admin' } } }), diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard.component.spec.ts b/ui/src/app/metadata/resolver/container/resolver-wizard.component.spec.ts index c7837309f..984a746a2 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard.component.spec.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.spec.ts @@ -62,7 +62,8 @@ describe('Resolver Wizard Component', () => { ...initialState, changes: { id: 'foo', - serviceProviderName: 'bar' + serviceProviderName: 'bar', + createdBy: 'admin' } } }), @@ -126,7 +127,7 @@ describe('Resolver Wizard Component', () => { })); it('should open a modal', () => { - app.changes = {id: 'bar', serviceProviderName: 'foo'}; + app.changes = {id: 'bar', serviceProviderName: 'foo', createdBy: 'admin'}; spyOn(modal, 'open').and.callThrough(); app.canDeactivate(null, null, { url: 'foo' diff --git a/ui/src/app/metadata/resolver/effect/collection.effects.ts b/ui/src/app/metadata/resolver/effect/collection.effects.ts index f3b4a4ac8..e5f1edd94 100644 --- a/ui/src/app/metadata/resolver/effect/collection.effects.ts +++ b/ui/src/app/metadata/resolver/effect/collection.effects.ts @@ -84,7 +84,7 @@ export class ResolverCollectionEffects { updateResolverSuccessRedirect$ = this.actions$.pipe( ofType(ResolverCollectionActionTypes.UPDATE_RESOLVER_SUCCESS), map(action => action.payload), - tap(provider => this.router.navigate(['dashboard'])) + tap(provider => this.router.navigate(['metadata', 'resolver', provider.id, 'configuration'])) ); @Effect() diff --git a/ui/src/app/metadata/version/container/version-history.component.ts b/ui/src/app/metadata/version/container/version-history.component.ts deleted file mode 100644 index dc26aa1fa..000000000 --- a/ui/src/app/metadata/version/container/version-history.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Component, ChangeDetectionStrategy } from '@angular/core'; -import { MetadataHistory } from '../model/history'; -import { HistoryService } from '../service/history.service'; -import { Observable } from 'rxjs'; - -@Component({ - selector: 'version-history', - changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: './version-history.component.html', - styleUrls: [] -}) -export class VersionHistoryComponent { - - history$: Observable; - - constructor( - private historyService: HistoryService - ) { - this.history$ = this.historyService.query(); - } -} diff --git a/ui/src/app/metadata/version/model/version.ts b/ui/src/app/metadata/version/model/version.ts deleted file mode 100644 index 85d1c9c7d..000000000 --- a/ui/src/app/metadata/version/model/version.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface MetadataVersion { - versionNumber: Number; - saveDate: Date; - changedBy: String; - actions: string[]; -} diff --git a/ui/src/app/metadata/version/service/history.service.ts b/ui/src/app/metadata/version/service/history.service.ts deleted file mode 100644 index acf22b2ff..000000000 --- a/ui/src/app/metadata/version/service/history.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable, of } from 'rxjs'; -import { MetadataHistory } from '../model/history'; - - -@Injectable() -export class HistoryService { - - readonly base = '/api'; - - constructor( - private http: HttpClient - ) { } - - query(): Observable { - return of({ - versions: [ - { - versionNumber: 1, - saveDate: new Date(), - changedBy: 'admin', - actions: [] - }, - { - versionNumber: 2, - saveDate: new Date(), - changedBy: 'admin', - actions: ['restore'] - } - ] - }); - } -} diff --git a/ui/src/app/metadata/version/version.module.ts b/ui/src/app/metadata/version/version.module.ts deleted file mode 100644 index 2e3d36fb5..000000000 --- a/ui/src/app/metadata/version/version.module.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { NgModule, ModuleWithProviders } from '@angular/core'; -import { StoreModule } from '@ngrx/store'; -import { EffectsModule } from '@ngrx/effects'; -import { HistoryListComponent } from './component/history-list.component'; -import { CommonModule } from '@angular/common'; -import { VersionHistoryComponent } from './container/version-history.component'; -import { HistoryService } from './service/history.service'; -import { I18nModule } from '../../i18n/i18n.module'; - -@NgModule({ - declarations: [ - HistoryListComponent, - VersionHistoryComponent - ], - entryComponents: [], - imports: [ - CommonModule, - I18nModule - ], - exports: [], - providers: [] -}) -export class MetadataVersionModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: RootMetadataVersionModule, - providers: [] - }; - } -} - -@NgModule({ - imports: [ - MetadataVersionModule, - // StoreModule.forFeature('resolver', fromResolver.reducers), - // EffectsModule.forFeature([]) - ], - providers: [ - HistoryService - ] -}) -export class RootMetadataVersionModule { } diff --git a/ui/src/app/metadata/version/version.routing.ts b/ui/src/app/metadata/version/version.routing.ts deleted file mode 100644 index dfb76468d..000000000 --- a/ui/src/app/metadata/version/version.routing.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Routes } from '@angular/router'; -import { VersionHistoryComponent } from './container/version-history.component'; - -export const VersionRoutes: Routes = [ - { - path: ':type/:id/versions', - component: VersionHistoryComponent - } -]; diff --git a/ui/src/app/wizard/model/wizard.ts b/ui/src/app/wizard/model/wizard.ts index 3115ed041..5729c4f24 100644 --- a/ui/src/app/wizard/model/wizard.ts +++ b/ui/src/app/wizard/model/wizard.ts @@ -2,13 +2,13 @@ import { FormDefinition } from './form-definition'; export interface Wizard extends FormDefinition { steps: WizardStep[]; + schema: string; } export interface WizardStep { id: string; label: string; initialValues?: WizardValue[]; - schema?: string; index: number; locked?: boolean; fields?: string[]; diff --git a/ui/src/app/wizard/reducer/index.spec.ts b/ui/src/app/wizard/reducer/index.spec.ts index dd407ba01..5f72dd71f 100644 --- a/ui/src/app/wizard/reducer/index.spec.ts +++ b/ui/src/app/wizard/reducer/index.spec.ts @@ -5,12 +5,12 @@ describe('wizard index selectors', () => { describe('getSchema method', () => { it('should return the schema by index name', () => { expect( - selectors.getSchemaPath('common', FileBackedHttpMetadataProviderWizard) - ).toBe(FileBackedHttpMetadataProviderWizard.steps[0].schema); + selectors.getSchemaPath(FileBackedHttpMetadataProviderWizard) + ).toBe(FileBackedHttpMetadataProviderWizard.schema); }); it('should return nothing if no schema is found', () => { expect( - selectors.getSchemaPath('common', null) + selectors.getSchemaPath(null) ).toBeFalsy(); }); }); diff --git a/ui/src/app/wizard/reducer/index.ts b/ui/src/app/wizard/reducer/index.ts index 3446f3713..4c3321c62 100644 --- a/ui/src/app/wizard/reducer/index.ts +++ b/ui/src/app/wizard/reducer/index.ts @@ -48,11 +48,7 @@ export const getWizardIsDisabled = createSelector(getState, fromWizard.getDisabl export const getWizardDefinition = createSelector(getState, fromWizard.getDefinition); export const getSchemaCollection = createSelector(getState, fromWizard.getCollection); -export const getSchemaPath = (index: string, wizard: Wizard) => { - if (!wizard) { return null; } - const step = wizard.steps.find(s => s.id === index); - return step ? step.schema : null; -}; +export const getSchemaPath = (wizard: Wizard) => wizard ? wizard.schema : null; export const getSplitSchema = (schema: any, step: WizardStep) => { if (!schema || !step.fields || !step.fields.length || !schema.properties) { @@ -93,7 +89,7 @@ export const getSplitSchema = (schema: any, step: WizardStep) => { return s; }; -export const getCurrentWizardSchema = createSelector(getWizardIndex, getWizardDefinition, getSchemaPath); +export const getCurrentWizardSchema = createSelector(getWizardDefinition, getSchemaPath); export const getPreviousFn = (index: string, wizard: Wizard) => { if (!wizard) { return null; } diff --git a/ui/src/assets/schema/provider/filebacked-http-advanced.schema.json b/ui/src/assets/schema/provider/filebacked-http-advanced.schema.json deleted file mode 100644 index 2e19da247..000000000 --- a/ui/src/assets/schema/provider/filebacked-http-advanced.schema.json +++ /dev/null @@ -1,242 +0,0 @@ -{ - "type": "object", - "title": "", - "properties": { - "httpMetadataResolverAttributes": { - "order": [], - "type": "object", - "fieldsets": [ - { - "title": "label.http-connection-attributes", - "type": "section", - "fields": [ - "connectionRequestTimeout", - "connectionTimeout", - "socketTimeout" - ] - }, - { - "title": "label.http-security-attributes", - "type": "section", - "class": "col-12", - "fields": [ - "disregardTLSCertificate" - ] - }, - { - "title": "label.http-proxy-attributes", - "type": "section", - "class": "col-12", - "fields": [ - "proxyHost", - "proxyPort", - "proxyUser", - "proxyPassword" - ] - }, - { - "title": "label.http-caching-attributes", - "type": "section", - "class": "col-12", - "fields": [ - "httpCaching", - "httpCacheDirectory", - "httpMaxCacheEntries", - "httpMaxCacheEntrySize" - ] - }, - { - "title": "", - "type": "hidden", - "class": "col-12", - "fields": [ - "tlsTrustEngineRef", - "httpClientSecurityParametersRef", - "httpClientRef" - ] - } - ], - "properties": { - "httpClientRef": { - "type": "string", - "title": "", - "description": "", - "placeholder": "", - "widget": "hidden" - }, - "connectionRequestTimeout": { - "type": "string", - "title": "label.connection-request-timeout", - "description": "tooltip.connection-request-timeout", - "placeholder": "label.duration", - "widget": { - "id": "datalist", - "data": [ - "PT0S", - "PT30S", - "PT1M", - "PT10M", - "PT30M", - "PT1H", - "PT4H", - "PT12H", - "PT24H" - ] - }, - "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" - }, - "connectionTimeout": { - "type": "string", - "title": "label.connection-timeout", - "description": "tooltip.connection-timeout", - "placeholder": "label.duration", - "widget": { - "id": "datalist", - "data": [ - "PT0S", - "PT30S", - "PT1M", - "PT10M", - "PT30M", - "PT1H", - "PT4H", - "PT12H", - "PT24H" - ] - }, - "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" - }, - "socketTimeout": { - "type": "string", - "title": "label.socket-timeout", - "description": "tooltip.socket-timeout", - "placeholder": "label.duration", - "widget": { - "id": "datalist", - "data": [ - "PT0S", - "PT30S", - "PT1M", - "PT10M", - "PT30M", - "PT1H", - "PT4H", - "PT12H", - "PT24H" - ] - }, - "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" - }, - "disregardTLSCertificate": { - "type": "boolean", - "title": "label.disregard-tls-cert", - "description": "tooltip.disregard-tls-cert", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "True" - }, - { - "enum": [ - false - ], - "description": "False" - } - ] - }, - "tlsTrustEngineRef": { - "type": "string", - "title": "", - "description": "", - "placeholder": "", - "widget": "hidden" - }, - "httpClientSecurityParametersRef": { - "type": "string", - "title": "", - "description": "", - "placeholder": "", - "widget": "hidden" - }, - "proxyHost": { - "type": "string", - "title": "label.proxy-host", - "description": "tooltip.proxy-host", - "placeholder": "" - }, - "proxyPort": { - "type": "string", - "title": "label.proxy-port", - "description": "tooltip.proxy-port", - "placeholder": "" - }, - "proxyUser": { - "type": "string", - "title": "label.proxy-user", - "description": "tooltip.proxy-user", - "placeholder": "" - }, - "proxyPassword": { - "type": "string", - "title": "label.proxy-password", - "description": "tooltip.proxy-password", - "placeholder": "" - }, - "httpCaching": { - "type": "string", - "title": "label.http-caching", - "description": "tooltip.http-caching", - "placeholder": "label.select-caching-type", - "widget": { - "id": "select" - }, - "oneOf": [ - { - "enum": [ - "none" - ], - "description": "value.none" - }, - { - "enum": [ - "file" - ], - "description": "value.file" - }, - { - "enum": [ - "memory" - ], - "description": "value.memory" - } - ] - }, - "httpCacheDirectory": { - "type": "string", - "title": "label.http-caching-directory", - "description": "tooltip.http-caching-directory", - "placeholder": "" - }, - "httpMaxCacheEntries": { - "type": "integer", - "title": "label.http-max-cache-entries", - "description": "tooltip.http-max-cache-entries", - "placeholder": "", - "minimum": 0 - }, - "httpMaxCacheEntrySize": { - "type": "integer", - "title": "label.max-cache-entry-size", - "description": "tooltip.max-cache-entry-size", - "placeholder": "", - "minimum": 0 - } - } - } - } -} diff --git a/ui/src/assets/schema/provider/filebacked-http-common.editor.schema.json b/ui/src/assets/schema/provider/filebacked-http-common.editor.schema.json deleted file mode 100644 index 9e1d37308..000000000 --- a/ui/src/assets/schema/provider/filebacked-http-common.editor.schema.json +++ /dev/null @@ -1,259 +0,0 @@ -{ - "type": "object", - "order": [ - "name", - "@type", - "xmlId", - "metadataURL", - "initializeFromBackupFile", - "backingFile", - "backupFileInitNextRefreshDelay", - "requireValidMetadata", - "failFastInitialization", - "useDefaultPredicateRegistry", - "satisfyAnyPredicates" - ], - "required": [ - "name", - "xmlId", - "metadataURL", - "backingFile", - "backupFileInitNextRefreshDelay" - ], - "anyOf": [ - { - "properties": { - "initializeFromBackupFile": { - "enum": [ - true - ] - } - } - }, - { - "properties": { - "initializeFromBackupFile": { - "enum": [ - false - ] - } - } - } - ], - "fieldsets": [ - { - "type": "section", - "fields": [ - "name", - "@type", - "enabled" - ] - }, - { - "type": "group-lg", - "fields": [ - "xmlId", - "metadataURL", - "initializeFromBackupFile", - "backingFile", - "backupFileInitNextRefreshDelay", - "requireValidMetadata", - "failFastInitialization", - "useDefaultPredicateRegistry", - "satisfyAnyPredicates" - ] - } - ], - "properties": { - "name": { - "title": "label.metadata-provider-name", - "description": "tooltip.metadata-provider-name", - "type": "string", - "widget": { - "id": "string", - "help": "message.must-be-unique" - } - }, - "@type": { - "title": "label.metadata-provider-type", - "description": "tooltip.metadata-provider-type", - "placeholder": "label.select-metadata-type", - "type": "string", - "readOnly": true, - "widget": { - "id": "select", - "disabled": true - }, - "oneOf": [ - { - "enum": [ - "FileBackedHttpMetadataResolver" - ], - "description": "value.file-backed-http-metadata-provider" - } - ] - }, - "enabled": { - "title": "label.enable-service", - "description": "tooltip.enable-service", - "type": "boolean", - "default": false - }, - "xmlId": { - "title": "label.xml-id", - "description": "tooltip.xml-id", - "type": "string", - "default": "", - "minLength": 1 - }, - "metadataURL": { - "title": "label.metadata-url", - "description": "tooltip.metadata-url", - "type": "string", - "default": "", - "minLength": 1 - }, - "initializeFromBackupFile": { - "title": "label.init-from-backup", - "description": "tooltip.init-from-backup", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": true - }, - "backingFile": { - "title": "label.backing-file", - "description": "tooltip.backing-file", - "type": "string", - "default": "" - }, - "backupFileInitNextRefreshDelay": { - "title": "label.backup-file-init-refresh-delay", - "description": "tooltip.backup-file-init-refresh-delay", - "type": "string", - "widget": { - "id": "datalist", - "data": [ - "PT0S", - "PT30S", - "PT1M", - "PT10M", - "PT30M", - "PT1H", - "PT4H", - "PT12H", - "PT24H" - ] - }, - "default": null, - "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" - }, - "requireValidMetadata": { - "title": "label.require-valid-metadata", - "description": "tooltip.require-valid-metadata", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": true - }, - "failFastInitialization": { - "title": "label.fail-fast-init", - "description": "tooltip.fail-fast-init", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": true - }, - "useDefaultPredicateRegistry": { - "title": "label.use-default-predicate-reg", - "description": "tooltip.use-default-predicate-reg", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": true - }, - "satisfyAnyPredicates": { - "title": "label.satisfy-any-predicates", - "description": "tooltip.satisfy-any-predicates", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": false - } - } -} \ No newline at end of file diff --git a/ui/src/assets/schema/provider/filebacked-http-common.schema.json b/ui/src/assets/schema/provider/filebacked-http-common.schema.json deleted file mode 100644 index 5db366d07..000000000 --- a/ui/src/assets/schema/provider/filebacked-http-common.schema.json +++ /dev/null @@ -1,173 +0,0 @@ -{ - "type": "object", - "order": [ - "xmlId", - "metadataURL", - "initializeFromBackupFile", - "backingFile", - "backupFileInitNextRefreshDelay", - "requireValidMetadata", - "failFastInitialization", - "useDefaultPredicateRegistry", - "satisfyAnyPredicates" - ], - "required": ["xmlId", "metadataURL", "backingFile", "backupFileInitNextRefreshDelay"], - "properties": { - "xmlId": { - "title": "label.xml-id", - "description": "tooltip.xml-id", - "type": "string", - "default": "", - "minLength": 1 - }, - "metadataURL": { - "title": "label.metadata-url", - "description": "tooltip.metadata-url", - "type": "string", - "default": "", - "minLength": 1 - }, - "initializeFromBackupFile": { - "title": "label.init-from-backup", - "description": "tooltip.init-from-backup", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": true - }, - "backingFile": { - "title": "label.backing-file", - "description": "tooltip.backing-file", - "type": "string", - "default": "" - }, - "backupFileInitNextRefreshDelay": { - "title": "label.backup-file-init-refresh-delay", - "description": "tooltip.backup-file-init-refresh-delay", - "type": "string", - "widget": { - "id": "datalist", - "data": [ - "PT0S", - "PT30S", - "PT1M", - "PT10M", - "PT30M", - "PT1H", - "PT4H", - "PT12H", - "PT24H" - ] - }, - "default": null, - "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" - }, - "requireValidMetadata": { - "title": "label.require-valid-metadata", - "description": "tooltip.require-valid-metadata", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": true - }, - "failFastInitialization": { - "title": "label.fail-fast-init", - "description": "tooltip.fail-fast-init", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": true - }, - "useDefaultPredicateRegistry": { - "title": "label.use-default-predicate-reg", - "description": "tooltip.use-default-predicate-reg", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": true - }, - "satisfyAnyPredicates": { - "title": "label.satisfy-any-predicates", - "description": "tooltip.satisfy-any-predicates", - "type": "boolean", - "widget": { - "id": "boolean-radio" - }, - "oneOf": [ - { - "enum": [ - true - ], - "description": "value.true" - }, - { - "enum": [ - false - ], - "description": "value.false" - } - ], - "default": false - } - } -} diff --git a/ui/src/assets/schema/provider/filebacked-http-filters.schema.json b/ui/src/assets/schema/provider/filebacked-http-filters.schema.json deleted file mode 100644 index 820063bfa..000000000 --- a/ui/src/assets/schema/provider/filebacked-http-filters.schema.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "type": "object", - "properties": { - "metadataFilters": { - "title": "", - "description": "", - "type": "object", - "properties": { - "RequiredValidUntil": { - "title": "label.required-valid-until", - "type": "object", - "widget": { - "id": "fieldset" - }, - "properties": { - "maxValidityInterval": { - "title": "label.max-validity-interval", - "description": "tooltip.max-validity-interval", - "type": "string", - "placeholder": "label.duration", - "widget": { - "id": "datalist", - "data": [ - "PT0S", - "P14D", - "P7D", - "P1D", - "PT12H" - ] - }, - "default": null, - "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" - } - } - }, - "SignatureValidation": { - "title": "label.signature-validation-filter", - "type": "object", - "widget": { - "id": "fieldset" - }, - "properties": { - "requireSignedRoot": { - "title": "label.require-signed-root", - "description": "tooltip.require-signed-root", - "type": "boolean", - "default": true - }, - "certificateFile": { - "title": "label.certificate-file", - "description": "tooltip.certificate-file", - "type": "string", - "widget": "textline" - } - }, - "anyOf": [ - { - "properties": { - "requireSignedRoot": { - "enum": [ true ] - }, - "certificateFile": { - "minLength": 1, - "type": "string" - } - }, - "required": [ - "certificateFile" - ] - }, - { - "properties": { - "requireSignedRoot": { - "enum": [ false ] - } - } - } - ] - }, - "EntityRoleWhiteList": { - "title": "label.entity-role-whitelist", - "type": "object", - "widget": { - "id": "fieldset" - }, - "properties": { - "retainedRoles": { - "title": "label.retained-roles", - "description": "tooltip.retained-roles", - "type": "array", - "items": { - "widget": { - "id": "select" - }, - "type": "string", - "oneOf": [ - { - "enum": [ - "md:SPSSODescriptor" - ], - "description": "value.spdescriptor" - }, - { - "enum": [ - "md:AttributeAuthorityDescriptor" - ], - "description": "value.attr-auth-descriptor" - } - ] - } - }, - "removeRolelessEntityDescriptors": { - "title": "label.remove-roleless-entity-descriptors", - "description": "tooltip.remove-roleless-entity-descriptors", - "type": "boolean", - "default": true - }, - "removeEmptyEntitiesDescriptors": { - "title": "label.remove-empty-entities-descriptors", - "description": "tooltip.remove-empty-entities-descriptors", - "type": "boolean", - "default": true - } - } - } - } - } - } -} diff --git a/ui/src/assets/schema/provider/filebacked-http-reloading.schema.json b/ui/src/assets/schema/provider/filebacked-http-reloading.schema.json deleted file mode 100644 index 6723e4cb1..000000000 --- a/ui/src/assets/schema/provider/filebacked-http-reloading.schema.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "type": "object", - "properties": { - "reloadableMetadataResolverAttributes": { - "type": "object", - "properties": { - "minRefreshDelay": { - "title": "label.min-refresh-delay", - "description": "tooltip.min-refresh-delay", - "type": "string", - "placeholder": "label.duration", - "widget": { - "id": "datalist", - "data": [ - "PT0S", - "PT30S", - "PT1M", - "PT10M", - "PT30M", - "PT1H", - "PT4H", - "PT12H", - "PT24H" - ] - }, - "default": null, - "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" - }, - "maxRefreshDelay": { - "title": "label.max-refresh-delay", - "description": "tooltip.max-refresh-delay", - "type": "string", - "placeholder": "label.duration", - "widget": { - "id": "datalist", - "data": [ - "PT0S", - "PT30S", - "PT1M", - "PT10M", - "PT30M", - "PT1H", - "PT4H", - "PT12H", - "PT24H" - ] - }, - "default": null, - "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" - }, - "refreshDelayFactor": { - "title": "label.refresh-delay-factor", - "description": "tooltip.refresh-delay-factor", - "type": "string", - "widget": { - "id": "string", - "help": "message.real-number" - }, - "placeholder": "label.real-number", - "minimum": 0, - "maximum": 1, - "default": "", - "pattern": "^(?:([0]*(\\.[0-9]+)?|[0]*\\.[0-9]*[1-9][0-9]*)|)$" - } - } - } - } -} diff --git a/ui/src/assets/schema/provider/filebacked-http.schema.json b/ui/src/assets/schema/provider/filebacked-http.schema.json new file mode 100644 index 000000000..f543e9e89 --- /dev/null +++ b/ui/src/assets/schema/provider/filebacked-http.schema.json @@ -0,0 +1,686 @@ +{ + "type": "object", + "order": [ + "name", + "@type", + "xmlId", + "metadataURL", + "initializeFromBackupFile", + "backingFile", + "backupFileInitNextRefreshDelay", + "requireValidMetadata", + "failFastInitialization", + "useDefaultPredicateRegistry", + "satisfyAnyPredicates" + ], + "required": [ + "name", + "xmlId", + "metadataURL", + "backingFile", + "backupFileInitNextRefreshDelay" + ], + "anyOf": [ + { + "properties": { + "initializeFromBackupFile": { + "enum": [ + true + ] + } + } + }, + { + "properties": { + "initializeFromBackupFile": { + "enum": [ + false + ] + } + } + } + ], + "fieldsets": [ + { + "type": "section", + "fields": [ + "name", + "@type", + "enabled" + ] + }, + { + "type": "group-lg", + "fields": [ + "xmlId", + "metadataURL", + "initializeFromBackupFile", + "backingFile", + "backupFileInitNextRefreshDelay", + "requireValidMetadata", + "failFastInitialization", + "useDefaultPredicateRegistry", + "satisfyAnyPredicates" + ] + } + ], + "properties": { + "name": { + "title": "label.metadata-provider-name", + "description": "tooltip.metadata-provider-name", + "type": "string", + "widget": { + "id": "string", + "help": "message.must-be-unique" + } + }, + "@type": { + "title": "label.metadata-provider-type", + "description": "tooltip.metadata-provider-type", + "placeholder": "label.select-metadata-type", + "type": "string", + "readOnly": true, + "widget": { + "id": "select", + "disabled": true + }, + "oneOf": [ + { + "enum": [ + "FileBackedHttpMetadataResolver" + ], + "description": "value.file-backed-http-metadata-provider" + } + ] + }, + "enabled": { + "title": "label.enable-service", + "description": "tooltip.enable-service", + "type": "boolean", + "default": false + }, + "xmlId": { + "title": "label.xml-id", + "description": "tooltip.xml-id", + "type": "string", + "default": "", + "minLength": 1 + }, + "metadataURL": { + "title": "label.metadata-url", + "description": "tooltip.metadata-url", + "type": "string", + "default": "", + "minLength": 1 + }, + "initializeFromBackupFile": { + "title": "label.init-from-backup", + "description": "tooltip.init-from-backup", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": true + }, + "backingFile": { + "title": "label.backing-file", + "description": "tooltip.backing-file", + "type": "string", + "default": "" + }, + "backupFileInitNextRefreshDelay": { + "title": "label.backup-file-init-refresh-delay", + "description": "tooltip.backup-file-init-refresh-delay", + "type": "string", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "default": null, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "requireValidMetadata": { + "title": "label.require-valid-metadata", + "description": "tooltip.require-valid-metadata", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": true + }, + "failFastInitialization": { + "title": "label.fail-fast-init", + "description": "tooltip.fail-fast-init", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": true + }, + "useDefaultPredicateRegistry": { + "title": "label.use-default-predicate-reg", + "description": "tooltip.use-default-predicate-reg", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": true + }, + "satisfyAnyPredicates": { + "title": "label.satisfy-any-predicates", + "description": "tooltip.satisfy-any-predicates", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": false + }, + "httpMetadataResolverAttributes": { + "order": [], + "type": "object", + "fieldsets": [ + { + "title": "label.http-connection-attributes", + "type": "section", + "fields": [ + "connectionRequestTimeout", + "connectionTimeout", + "socketTimeout" + ] + }, + { + "title": "label.http-security-attributes", + "type": "section", + "class": "col-12", + "fields": [ + "disregardTLSCertificate" + ] + }, + { + "title": "label.http-proxy-attributes", + "type": "section", + "class": "col-12", + "fields": [ + "proxyHost", + "proxyPort", + "proxyUser", + "proxyPassword" + ] + }, + { + "title": "label.http-caching-attributes", + "type": "section", + "class": "col-12", + "fields": [ + "httpCaching", + "httpCacheDirectory", + "httpMaxCacheEntries", + "httpMaxCacheEntrySize" + ] + }, + { + "title": "", + "type": "hidden", + "class": "col-12", + "fields": [ + "tlsTrustEngineRef", + "httpClientSecurityParametersRef", + "httpClientRef" + ] + } + ], + "properties": { + "httpClientRef": { + "type": "string", + "title": "", + "description": "", + "placeholder": "", + "widget": "hidden" + }, + "connectionRequestTimeout": { + "type": "string", + "title": "label.connection-request-timeout", + "description": "tooltip.connection-request-timeout", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "connectionTimeout": { + "type": "string", + "title": "label.connection-timeout", + "description": "tooltip.connection-timeout", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "socketTimeout": { + "type": "string", + "title": "label.socket-timeout", + "description": "tooltip.socket-timeout", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "disregardTLSCertificate": { + "type": "boolean", + "title": "label.disregard-tls-cert", + "description": "tooltip.disregard-tls-cert", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "True" + }, + { + "enum": [ + false + ], + "description": "False" + } + ] + }, + "tlsTrustEngineRef": { + "type": "string", + "title": "", + "description": "", + "placeholder": "", + "widget": "hidden" + }, + "httpClientSecurityParametersRef": { + "type": "string", + "title": "", + "description": "", + "placeholder": "", + "widget": "hidden" + }, + "proxyHost": { + "type": "string", + "title": "label.proxy-host", + "description": "tooltip.proxy-host", + "placeholder": "" + }, + "proxyPort": { + "type": "string", + "title": "label.proxy-port", + "description": "tooltip.proxy-port", + "placeholder": "" + }, + "proxyUser": { + "type": "string", + "title": "label.proxy-user", + "description": "tooltip.proxy-user", + "placeholder": "" + }, + "proxyPassword": { + "type": "string", + "title": "label.proxy-password", + "description": "tooltip.proxy-password", + "placeholder": "" + }, + "httpCaching": { + "type": "string", + "title": "label.http-caching", + "description": "tooltip.http-caching", + "placeholder": "label.select-caching-type", + "widget": { + "id": "select" + }, + "oneOf": [ + { + "enum": [ + "none" + ], + "description": "value.none" + }, + { + "enum": [ + "file" + ], + "description": "value.file" + }, + { + "enum": [ + "memory" + ], + "description": "value.memory" + } + ] + }, + "httpCacheDirectory": { + "type": "string", + "title": "label.http-caching-directory", + "description": "tooltip.http-caching-directory", + "placeholder": "" + }, + "httpMaxCacheEntries": { + "type": "integer", + "title": "label.http-max-cache-entries", + "description": "tooltip.http-max-cache-entries", + "placeholder": "", + "minimum": 0 + }, + "httpMaxCacheEntrySize": { + "type": "integer", + "title": "label.max-cache-entry-size", + "description": "tooltip.max-cache-entry-size", + "placeholder": "", + "minimum": 0 + } + } + }, + "reloadableMetadataResolverAttributes": { + "type": "object", + "properties": { + "minRefreshDelay": { + "title": "label.min-refresh-delay", + "description": "tooltip.min-refresh-delay", + "type": "string", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "default": null, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "maxRefreshDelay": { + "title": "label.max-refresh-delay", + "description": "tooltip.max-refresh-delay", + "type": "string", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "default": null, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "refreshDelayFactor": { + "title": "label.refresh-delay-factor", + "description": "tooltip.refresh-delay-factor", + "type": "string", + "widget": { + "id": "string", + "help": "message.real-number" + }, + "placeholder": "label.real-number", + "minimum": 0, + "maximum": 1, + "default": "", + "pattern": "^(?:([0]*(\\.[0-9]+)?|[0]*\\.[0-9]*[1-9][0-9]*)|)$" + } + } + }, + "metadataFilters": { + "title": "", + "description": "", + "type": "object", + "properties": { + "RequiredValidUntil": { + "title": "label.required-valid-until", + "type": "object", + "widget": { + "id": "fieldset" + }, + "properties": { + "maxValidityInterval": { + "title": "label.max-validity-interval", + "description": "tooltip.max-validity-interval", + "type": "string", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "P14D", + "P7D", + "P1D", + "PT12H" + ] + }, + "default": null, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + } + } + }, + "SignatureValidation": { + "title": "label.signature-validation-filter", + "type": "object", + "widget": { + "id": "fieldset" + }, + "properties": { + "requireSignedRoot": { + "title": "label.require-signed-root", + "description": "tooltip.require-signed-root", + "type": "boolean", + "default": true + }, + "certificateFile": { + "title": "label.certificate-file", + "description": "tooltip.certificate-file", + "type": "string", + "widget": "textline" + } + }, + "anyOf": [ + { + "properties": { + "requireSignedRoot": { + "enum": [ + true + ] + }, + "certificateFile": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "certificateFile" + ] + }, + { + "properties": { + "requireSignedRoot": { + "enum": [ + false + ] + } + } + } + ] + }, + "EntityRoleWhiteList": { + "title": "label.entity-role-whitelist", + "type": "object", + "widget": { + "id": "fieldset" + }, + "properties": { + "retainedRoles": { + "title": "label.retained-roles", + "description": "tooltip.retained-roles", + "type": "array", + "items": { + "widget": { + "id": "select" + }, + "type": "string", + "oneOf": [ + { + "enum": [ + "md:SPSSODescriptor" + ], + "description": "value.spdescriptor" + }, + { + "enum": [ + "md:AttributeAuthorityDescriptor" + ], + "description": "value.attr-auth-descriptor" + } + ] + } + }, + "removeRolelessEntityDescriptors": { + "title": "label.remove-roleless-entity-descriptors", + "description": "tooltip.remove-roleless-entity-descriptors", + "type": "boolean", + "default": true + }, + "removeEmptyEntitiesDescriptors": { + "title": "label.remove-empty-entities-descriptors", + "description": "tooltip.remove-empty-entities-descriptors", + "type": "boolean", + "default": true + } + } + } + } + } + } +} \ No newline at end of file diff --git a/ui/src/styles.scss b/ui/src/styles.scss index 454ccba2e..a3f814d8a 100644 --- a/ui/src/styles.scss +++ b/ui/src/styles.scss @@ -6,6 +6,7 @@ @import './theme/alert'; @import './theme/typography'; @import './theme/list'; +@import './theme/utility'; body { background-color: theme-color("light"); diff --git a/ui/src/testing/form-schema.stub.ts b/ui/src/testing/form-schema.stub.ts index c91f63bf1..1ef7d42ae 100644 --- a/ui/src/testing/form-schema.stub.ts +++ b/ui/src/testing/form-schema.stub.ts @@ -30,6 +30,31 @@ export const SCHEMA = { 'description': 'FileBackedHttpMetadataProvider' } ] + }, + 'list': { + 'title': 'label.retained-roles', + 'description': 'tooltip.retained-roles', + 'type': 'array', + 'items': { + 'widget': { + 'id': 'select' + }, + 'type': 'string', + 'oneOf': [ + { + 'enum': [ + 'SPSSODescriptor' + ], + 'description': 'value.spdescriptor' + }, + { + 'enum': [ + 'AttributeAuthorityDescriptor' + ], + 'description': 'value.attr-auth-descriptor' + } + ] + } } }, 'required': [ @@ -44,5 +69,13 @@ export const SCHEMA = { '@type' ] } - ] + ], + 'definitions': { + 'description': { + 'title': 'Description', + 'description': 'A description of the object', + 'type': 'string', + 'widget': 'string' + } + } }; diff --git a/ui/src/theme/utility.scss b/ui/src/theme/utility.scss new file mode 100644 index 000000000..358b75b2c --- /dev/null +++ b/ui/src/theme/utility.scss @@ -0,0 +1,15 @@ +.border-2 { + border-width: 2px !important; +} + +.bg-lighter { + background: #FAFAFA !important; +} + +.w-15 { + width: 15%; +} + +.w-40 { + width: 40%; +} \ No newline at end of file