diff --git a/backend/build.gradle b/backend/build.gradle index bf62e58c7..f3204d4f6 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -77,6 +77,9 @@ dependencies { // TODO: figure out what this should really be runtimeOnly 'org.springframework.boot:spring-boot-starter-tomcat' + //Spring Configuration Annotation Processor - makes IntelliJ happy about @ConfigurationProperties + compileOnly "org.springframework.boot:spring-boot-configuration-processor" + // lucene deps ['core', 'analyzers-common', 'queryparser'].each { compile "org.apache.lucene:lucene-${it}:${project.'lucene.version'}" @@ -218,4 +221,4 @@ docker { noCache true files tasks.bootWar.outputs buildArgs(['JAR_FILE': 'shibui.war']) -} \ No newline at end of file +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java index e17c33b0e..105c1ddf6 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java @@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.ResourceBundleMessageSource; @@ -42,6 +43,7 @@ import javax.servlet.http.HttpServletRequest; @Configuration +@EnableConfigurationProperties(CustomAttributesConfiguration.class) public class CoreShibUiConfiguration { private static final Logger logger = LoggerFactory.getLogger(CoreShibUiConfiguration.class); @@ -170,6 +172,11 @@ public LuceneUtility luceneUtility(DirectoryService directoryService) { return new LuceneUtility(directoryService); } + @Bean + public CustomAttributesConfiguration customAttributesConfiguration() { + return new CustomAttributesConfiguration(); + } + @Bean public Module stringTrimModule() { return new StringTrimModule(); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomAttributesConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomAttributesConfiguration.java new file mode 100644 index 000000000..aa12be6b2 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CustomAttributesConfiguration.java @@ -0,0 +1,26 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@Configuration +@ConfigurationProperties(prefix="custom") +public class CustomAttributesConfiguration { + + private List> attributes = new ArrayList<>(); + + public List> getAttributes() { + return attributes; + } + + public void setAttributes(List> attributes) { + this.attributes = attributes; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ConfigurationController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ConfigurationController.java new file mode 100644 index 000000000..4a0388428 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ConfigurationController.java @@ -0,0 +1,24 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomAttributesConfiguration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@Controller +@RequestMapping(value = "/api") +public class ConfigurationController { + + @Autowired + CustomAttributesConfiguration customAttributesConfiguration; + + @GetMapping(value = "/customAttributes") + public ResponseEntity getCustomAttributes() { + return ResponseEntity.ok(customAttributesConfiguration.getAttributes()); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/ModelRepresentationConversions.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/ModelRepresentationConversions.java index 27fd92445..e68225b5a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/ModelRepresentationConversions.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/ModelRepresentationConversions.java @@ -47,14 +47,11 @@ public static List getAttributeReleaseListFromAttributeList(List attribute.getName().equals(MDDCConstants.RELEASE_ATTRIBUTES)) .collect(Collectors.toList()); - if (releaseAttributes.size() != 1) { - // TODO: What do we do if there is more than one? - } - if (releaseAttributes.size() == 0) { - return new ArrayList<>(); - } else { - return getStringListOfAttributeValues(releaseAttributes.get(0).getAttributeValues()); + List attributeValues = new ArrayList<>(); + for (Attribute attribute : releaseAttributes) { + attributeValues.addAll(getStringListOfAttributeValues(attribute.getAttributeValues())); } + return attributeValues; } public static boolean getBooleanValueOfAttribute(Attribute attribute) { diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 000000000..563d01073 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,28 @@ +custom: + attributes: + # Default attributes + - name: eduPersonPrincipalName + displayName: label.attribute-eduPersonPrincipalName + - name: uid + displayName: label.attribute-uid + - name: mail + displayName: label.attribute-mail + - name: surname + displayName: label.attribute-surname + - name: givenName + displayName: label.attribute-givenName + - name: eduPersonAffiliation + displayName: label.attribute-eduPersonAffiliation + - name: eduPersonScopedAffiliation + displayName: label.attribute-eduPersonScopedAffiliation + - name: eduPersonPrimaryAffiliation + displayName: label.attribute-eduPersonPrimaryAffiliation + - name: eduPersonEntitlement + displayName: label.attribute-eduPersonEntitlement + - name: eduPersonAssurance + displayName: label.attribute-eduPersonAssurance + - name: eduPersonUniqueId + displayName: label.attribute-eduPersonUniqueId + - name: employeeNumber + displayName: label.attribute-employeeNumber + # Custom attributes diff --git a/backend/src/main/resources/i18n/messages_en.properties b/backend/src/main/resources/i18n/messages_en.properties index 2c6a755ab..607e2a38b 100644 --- a/backend/src/main/resources/i18n/messages_en.properties +++ b/backend/src/main/resources/i18n/messages_en.properties @@ -8,7 +8,7 @@ action.clear=Clear action.delete=Delete action.remove=Remove action.save=Save -action.toggle=Toggle { label } +action.toggle=Toggle action.add-contact=Add Contact action.add-contacts=Add Contacts action.use-mine=Use My Changes @@ -284,6 +284,19 @@ label.retained-roles=Retained Roles label.remove-roleless-entity-descriptors=Remove Roleless Entity Descriptors? label.remove-empty-entities-descriptors=Remove Empty Entities Descriptors? +label.attribute-eduPersonPrincipalName=eduPersonPrincipalName (EPPN) +label.attribute-uid=uid +label.attribute-mail=mail +label.attribute-surname=surname +label.attribute-givenName=givenName +label.attribute-eduPersonAffiliation=eduPersonAffiliation +label.attribute-eduPersonScopedAffiliation=eduPersonScopedAffiliation +label.attribute-eduPersonPrimaryAffiliation=eduPersonPrimaryAffiliation +label.attribute-eduPersonEntitlement=eduPersonEntitlement +label.attribute-eduPersonAssurance=eduPersonAssurance +label.attribute-eduPersonUniqueId=eduPersonUniqueId +label.attribute-employeeNumber=employeeNumber + message.must-be-unique=Must be unique. message.conflict=Conflict diff --git a/backend/src/main/resources/i18n/messages_es.properties b/backend/src/main/resources/i18n/messages_es.properties index f90cad6a4..9fa3c547e 100644 --- a/backend/src/main/resources/i18n/messages_es.properties +++ b/backend/src/main/resources/i18n/messages_es.properties @@ -284,6 +284,19 @@ label.retained-roles=(es) Retained Roles label.remove-roleless-entity-descriptors=(es) Remove Roleless Entity Descriptors? label.remove-empty-entities-descriptors=(es) Remove Empty Entities Descriptors? +label.attribute-eduPersonPrincipalName=(es) eduPersonPrincipalName (EPPN) +label.attribute-uid=(es) uid +label.attribute-mail=(es) mail +label.attribute-surname=(es) surname +label.attribute-givenName=(es) givenName +label.attribute-eduPersonAffiliation=(es) eduPersonAffiliation +label.attribute-eduPersonScopedAffiliation=(es) eduPersonScopedAffiliation +label.attribute-eduPersonPrimaryAffiliation=(es) eduPersonPrimaryAffiliation +label.attribute-eduPersonEntitlement=(es) eduPersonEntitlement +label.attribute-eduPersonAssurance=(es) eduPersonAssurance +label.attribute-eduPersonUniqueId=(es) eduPersonUniqueId +label.attribute-employeeNumber=(es) employeeNumber + message.must-be-unique=(es) Must be unique. message.conflict=(es) Conflict diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy index 0c7fe9abf..cecaa25e6 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy @@ -475,6 +475,8 @@ class TestObjectGenerator { it.metadataURL = 'https://idp.unicon.net/idp/shibboleth' it.reloadableMetadataResolverAttributes = new ReloadableMetadataResolverAttributes().with { + it.minRefreshDelay = 'PT0M' + it.maxRefreshDelay = 'P1D' it } it diff --git a/backend/src/test/resources/conf/278.2.xml b/backend/src/test/resources/conf/278.2.xml index ad3cb6c44..34bf7ae16 100644 --- a/backend/src/test/resources/conf/278.2.xml +++ b/backend/src/test/resources/conf/278.2.xml @@ -39,8 +39,9 @@ - + metadataURL="https://idp.unicon.net/idp/shibboleth" + minRefreshDelay='PT0M' + maxRefreshDelay='P1D'> diff --git a/backend/src/test/resources/conf/278.xml b/backend/src/test/resources/conf/278.xml index cb5317531..e47fbd838 100644 --- a/backend/src/test/resources/conf/278.xml +++ b/backend/src/test/resources/conf/278.xml @@ -32,7 +32,9 @@ + metadataURL="https://idp.unicon.net/idp/shibboleth" + minRefreshDelay='PT0M' + maxRefreshDelay='P1D'> diff --git a/backend/src/test/resources/conf/532.xml b/backend/src/test/resources/conf/532.xml index b2be43498..3052217a9 100644 --- a/backend/src/test/resources/conf/532.xml +++ b/backend/src/test/resources/conf/532.xml @@ -8,6 +8,7 @@ - + metadataURL="https://idp.unicon.net/idp/shibboleth" + minRefreshDelay='PT0M' + maxRefreshDelay='P1D' /> diff --git a/ui/src/app/metadata/domain/component/forms/attribute-release-form.component.html b/ui/src/app/metadata/domain/component/forms/attribute-release-form.component.html index f6e70f5c2..9ee740517 100644 --- a/ui/src/app/metadata/domain/component/forms/attribute-release-form.component.html +++ b/ui/src/app/metadata/domain/component/forms/attribute-release-form.component.html @@ -10,7 +10,7 @@ - {{ attr.label }} + {{ attr.label }}
@@ -19,8 +19,8 @@ (change)="onCheck($event, attr.key)" [checked]="isChecked(attr.key)" id="input-{{ i }}" - [attr.name]="attr.label" - [attr.aria-label]="attr.label" + [attr.name]="attr.label | translate" + [attr.aria-label]="attr.label | translate" role="checkbox" aria-checked="false"/> diff --git a/ui/src/app/metadata/domain/component/forms/attribute-release-form.component.spec.ts b/ui/src/app/metadata/domain/component/forms/attribute-release-form.component.spec.ts index 2c67eae1c..c05e6bad3 100644 --- a/ui/src/app/metadata/domain/component/forms/attribute-release-form.component.spec.ts +++ b/ui/src/app/metadata/domain/component/forms/attribute-release-form.component.spec.ts @@ -8,6 +8,7 @@ import { AttributeReleaseFormComponent } from './attribute-release-form.componen import { ListValuesService } from '../../../domain/service/list-values.service'; import * as stubs from '../../../../../testing/resolver.stub'; import { MockI18nModule } from '../../../../../testing/i18n.stub'; +import { MockListValueService } from '../../../../../testing/list-values.stub'; describe('Attribute Release Form Component', () => { let fixture: ComponentFixture; @@ -19,7 +20,7 @@ describe('Attribute Release Form Component', () => { ProviderValueEmitter, ProviderStatusEmitter, NgbPopoverConfig, - ListValuesService + { provide: ListValuesService, useClass: MockListValueService } ], imports: [ NoopAnimationsModule, diff --git a/ui/src/app/metadata/domain/component/forms/descriptor-info-form.component.spec.ts b/ui/src/app/metadata/domain/component/forms/descriptor-info-form.component.spec.ts index 7527f5e3c..d902432fd 100644 --- a/ui/src/app/metadata/domain/component/forms/descriptor-info-form.component.spec.ts +++ b/ui/src/app/metadata/domain/component/forms/descriptor-info-form.component.spec.ts @@ -11,6 +11,7 @@ import { DescriptorInfoFormComponent } from './descriptor-info-form.component'; import * as stubs from '../../../../../testing/resolver.stub'; import { SharedModule } from '../../../../shared/shared.module'; import { MockI18nModule } from '../../../../../testing/i18n.stub'; +import { MockListValueService } from '../../../../../testing/list-values.stub'; @Component({ template: `` @@ -48,7 +49,7 @@ describe('Descriptor Info Form Component', () => { ProviderValueEmitter, ProviderStatusEmitter, NgbPopoverConfig, - ListValuesService + { provide: ListValuesService, useClass: MockListValueService } ], imports: [ NoopAnimationsModule, diff --git a/ui/src/app/metadata/domain/component/forms/finish-form.component.spec.ts b/ui/src/app/metadata/domain/component/forms/finish-form.component.spec.ts index 0d3ea08a4..882590b2b 100644 --- a/ui/src/app/metadata/domain/component/forms/finish-form.component.spec.ts +++ b/ui/src/app/metadata/domain/component/forms/finish-form.component.spec.ts @@ -14,6 +14,7 @@ import * as stubs from '../../../../../testing/resolver.stub'; import { FileBackedHttpMetadataResolver } from '../../entity'; import { InputDefaultsDirective } from '../../../../shared/directive/input-defaults.directive'; import { MockI18nModule } from '../../../../../testing/i18n.stub'; +import { MockListValueService } from '../../../../../testing/list-values.stub'; @Component({ template: `` @@ -42,7 +43,7 @@ describe('Finished Form Component', () => { ProviderValueEmitter, ProviderStatusEmitter, NgbPopoverConfig, - ListValuesService, + { provide: ListValuesService, useClass: MockListValueService }, { provide: Router, useClass: RouterStub }, { provide: ActivatedRoute, useClass: ActivatedRouteStub } ], diff --git a/ui/src/app/metadata/domain/component/forms/key-info-form.component.spec.ts b/ui/src/app/metadata/domain/component/forms/key-info-form.component.spec.ts index fd6ccd5db..0b6751cc3 100644 --- a/ui/src/app/metadata/domain/component/forms/key-info-form.component.spec.ts +++ b/ui/src/app/metadata/domain/component/forms/key-info-form.component.spec.ts @@ -11,6 +11,7 @@ import * as stubs from '../../../../../testing/resolver.stub'; import { FileBackedHttpMetadataResolver } from '../../entity'; import { InputDefaultsDirective } from '../../../../shared/directive/input-defaults.directive'; import { MockI18nModule } from '../../../../../testing/i18n.stub'; +import { MockListValueService } from '../../../../../testing/list-values.stub'; @Component({ template: `` @@ -43,7 +44,7 @@ describe('Security (Key) Info Form Component', () => { ProviderValueEmitter, ProviderStatusEmitter, NgbPopoverConfig, - ListValuesService + { provide: ListValuesService, useClass: MockListValueService } ], imports: [ NoopAnimationsModule, diff --git a/ui/src/app/metadata/domain/component/forms/logout-form.component.spec.ts b/ui/src/app/metadata/domain/component/forms/logout-form.component.spec.ts index 83f1fafe4..89d8b9b09 100644 --- a/ui/src/app/metadata/domain/component/forms/logout-form.component.spec.ts +++ b/ui/src/app/metadata/domain/component/forms/logout-form.component.spec.ts @@ -12,6 +12,7 @@ import * as stubs from '../../../../../testing/resolver.stub'; import { FileBackedHttpMetadataResolver } from '../../entity'; import { InputDefaultsDirective } from '../../../../shared/directive/input-defaults.directive'; import { MockI18nModule } from '../../../../../testing/i18n.stub'; +import { MockListValueService } from '../../../../../testing/list-values.stub'; @Component({ template: `` @@ -41,7 +42,7 @@ describe('Logout Endpoints Form Component', () => { ProviderValueEmitter, ProviderStatusEmitter, NgbPopoverConfig, - ListValuesService + { provide: ListValuesService, useClass: MockListValueService } ], imports: [ NoopAnimationsModule, diff --git a/ui/src/app/metadata/domain/component/forms/metadata-ui-form.component.spec.ts b/ui/src/app/metadata/domain/component/forms/metadata-ui-form.component.spec.ts index b74dc94db..3c86d3b13 100644 --- a/ui/src/app/metadata/domain/component/forms/metadata-ui-form.component.spec.ts +++ b/ui/src/app/metadata/domain/component/forms/metadata-ui-form.component.spec.ts @@ -11,6 +11,7 @@ import * as stubs from '../../../../../testing/resolver.stub'; import { FileBackedHttpMetadataResolver } from '../../entity'; import { InputDefaultsDirective } from '../../../../shared/directive/input-defaults.directive'; import { MockI18nModule } from '../../../../../testing/i18n.stub'; +import { MockListValueService } from '../../../../../testing/list-values.stub'; @Component({ template: `` @@ -39,7 +40,7 @@ describe('Metadata UI Form Component', () => { ProviderValueEmitter, ProviderStatusEmitter, NgbPopoverConfig, - ListValuesService + { provide: ListValuesService, useClass: MockListValueService } ], imports: [ NoopAnimationsModule, diff --git a/ui/src/app/metadata/domain/component/forms/organization-info-form.component.spec.ts b/ui/src/app/metadata/domain/component/forms/organization-info-form.component.spec.ts index 57708fbad..e5a4d30e4 100644 --- a/ui/src/app/metadata/domain/component/forms/organization-info-form.component.spec.ts +++ b/ui/src/app/metadata/domain/component/forms/organization-info-form.component.spec.ts @@ -7,6 +7,7 @@ import { ListValuesService } from '../../../domain/service/list-values.service'; import { OrganizationInfoFormComponent } from './organization-info-form.component'; import * as stubs from '../../../../../testing/resolver.stub'; import { MockI18nModule } from '../../../../../testing/i18n.stub'; +import { MockListValueService } from '../../../../../testing/list-values.stub'; describe('Organization Info Form Component', () => { let fixture: ComponentFixture; @@ -18,7 +19,7 @@ describe('Organization Info Form Component', () => { ProviderValueEmitter, ProviderStatusEmitter, NgbPopoverConfig, - ListValuesService + { provide: ListValuesService, useClass: MockListValueService } ], imports: [ NoopAnimationsModule, diff --git a/ui/src/app/metadata/domain/component/forms/relying-party-form.component.spec.ts b/ui/src/app/metadata/domain/component/forms/relying-party-form.component.spec.ts index 0849591c6..274bbc3e0 100644 --- a/ui/src/app/metadata/domain/component/forms/relying-party-form.component.spec.ts +++ b/ui/src/app/metadata/domain/component/forms/relying-party-form.component.spec.ts @@ -10,6 +10,7 @@ import * as stubs from '../../../../../testing/resolver.stub'; import { SharedModule } from '../../../../shared/shared.module'; import { FileBackedHttpMetadataResolver } from '../../entity'; import { MockI18nModule } from '../../../../../testing/i18n.stub'; +import { MockListValueService } from '../../../../../testing/list-values.stub'; @Component({ @@ -48,7 +49,7 @@ describe('Relying Party Form Component', () => { ProviderValueEmitter, ProviderStatusEmitter, NgbPopoverConfig, - ListValuesService + { provide: ListValuesService, useClass: MockListValueService } ], imports: [ NoopAnimationsModule, diff --git a/ui/src/app/metadata/domain/domain.module.ts b/ui/src/app/metadata/domain/domain.module.ts index 5cf2a72b0..a9120f45d 100644 --- a/ui/src/app/metadata/domain/domain.module.ts +++ b/ui/src/app/metadata/domain/domain.module.ts @@ -13,6 +13,7 @@ import { MetadataProviderService } from './service/provider.service'; import { EntityEffects } from './effect/entity.effect'; import { PreviewDialogComponent } from './component/preview-dialog.component'; import { MetadataFilterService } from './service/filter.service'; +import { AttributesService } from './service/attributes.service'; import { I18nModule } from '../../i18n/i18n.module'; export const COMPONENTS = [ @@ -46,7 +47,8 @@ export class DomainModule { ProviderStatusEmitter, ProviderValueEmitter, MetadataProviderService, - MetadataFilterService + MetadataFilterService, + AttributesService ] }; } diff --git a/ui/src/app/metadata/domain/model/properties/release-attribute.ts b/ui/src/app/metadata/domain/model/properties/release-attribute.ts new file mode 100644 index 000000000..7edc37b6a --- /dev/null +++ b/ui/src/app/metadata/domain/model/properties/release-attribute.ts @@ -0,0 +1,4 @@ +export interface ReleaseAttribute { + key: string; + label: string; +} diff --git a/ui/src/app/metadata/domain/service/attributes.service.spec.ts b/ui/src/app/metadata/domain/service/attributes.service.spec.ts new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/metadata/domain/service/attributes.service.ts b/ui/src/app/metadata/domain/service/attributes.service.ts new file mode 100644 index 000000000..c6b671e01 --- /dev/null +++ b/ui/src/app/metadata/domain/service/attributes.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, throwError } from 'rxjs'; +import { catchError, shareReplay, map } from 'rxjs/operators'; +import { ReleaseAttribute } from '../model/properties/release-attribute'; + +const CACHE_SIZE = 1; + +@Injectable() +export class AttributesService { + + readonly endpoint = '/customAttributes'; + readonly base = '/api'; + + private cache$: Observable; + + constructor( + private http: HttpClient + ) { } + + query(path: string = this.endpoint): Observable { + if (!this.cache$) { + this.cache$ = this.requestAttributes(path).pipe( + shareReplay(CACHE_SIZE) + ); + } + + return this.cache$; + } + + requestAttributes(path: string): Observable { + return this.http.get(`${this.base}${path}`, {}) + .pipe( + map(attrs => attrs.map((attr: any) => ({ key: attr.name, label: attr.displayName }))), + catchError(err => throwError([])) + ); + } +} diff --git a/ui/src/app/metadata/domain/service/list-values.service.spec.ts b/ui/src/app/metadata/domain/service/list-values.service.spec.ts index e90c51a76..2c26ba912 100644 --- a/ui/src/app/metadata/domain/service/list-values.service.spec.ts +++ b/ui/src/app/metadata/domain/service/list-values.service.spec.ts @@ -2,6 +2,8 @@ import { TestBed, async, inject } from '@angular/core/testing'; import { EntityValidators } from './entity-validators.service'; import { Observable, of } from 'rxjs'; import { ListValuesService } from './list-values.service'; +import { AttributesService } from './attributes.service'; +import { MockAttributeService } from '../../../../testing/attributes.stub'; describe(`ListValuesService`, () => { let service: ListValuesService; @@ -9,6 +11,7 @@ describe(`ListValuesService`, () => { TestBed.configureTestingModule({ imports: [], providers: [ + { provide: AttributesService, useClass: MockAttributeService }, ListValuesService ] }); diff --git a/ui/src/app/metadata/domain/service/list-values.service.ts b/ui/src/app/metadata/domain/service/list-values.service.ts index 644a2be7b..dff88ea7e 100644 --- a/ui/src/app/metadata/domain/service/list-values.service.ts +++ b/ui/src/app/metadata/domain/service/list-values.service.ts @@ -2,10 +2,14 @@ import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { debounceTime, distinctUntilChanged, combineLatest } from 'rxjs/operators'; +import { AttributesService } from './attributes.service'; +import { ReleaseAttribute } from '../model/properties/release-attribute'; @Injectable() export class ListValuesService { - constructor() {} + constructor( + private attributes: AttributesService + ) {} readonly nameIdFormats: Observable = of([ 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', @@ -20,21 +24,9 @@ export class ListValuesService { 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' ]); - readonly attributesToRelease: Observable<{ key: string, label: string }[]> = of([ - { key: 'eduPersonPrincipalName', label: 'eduPersonPrincipalName (EPPN)' }, - { key: 'uid', label: 'uid' }, - { key: 'mail', label: 'mail' }, - { key: 'surname', label: 'surname' }, - { key: 'givenName', label: 'givenName' }, - { key: 'displayName', label: 'displayName' }, - { key: 'eduPersonAffiliation', label: 'eduPersonAffiliation' }, - { key: 'eduPersonScopedAffiliation', label: 'eduPersonScopedAffiliation' }, - { key: 'eduPersonPrimaryAffiliation', label: 'eduPersonPrimaryAffiliation' }, - { key: 'eduPersonEntitlement', label: 'eduPersonEntitlement' }, - { key: 'eduPersonAssurance', label: 'eduPersonAssurance' }, - { key: 'eduPersonUniqueId', label: 'eduPersonUniqueId' }, - { key: 'employeeNumber', label: 'employeeNumber' } - ]); + get attributesToRelease(): Observable { + return this.attributes.query(); + } searchStringList = (list: Observable): Function => (text$: Observable) => diff --git a/ui/src/app/schema-form/widget/check/checklist.component.html b/ui/src/app/schema-form/widget/check/checklist.component.html index 6eb177468..aae299bf9 100644 --- a/ui/src/app/schema-form/widget/check/checklist.component.html +++ b/ui/src/app/schema-form/widget/check/checklist.component.html @@ -8,8 +8,8 @@ - - {{ attr.label }} + + {{ attr.label | translate }}
@@ -18,14 +18,14 @@ (change)="onCheck(attr.key)" [checked]="checked[attr.key]" id="input-{{ i }}" - [attr.name]="attr.label" - [attr.aria-label]="attr.label" + [attr.name]="attr.label | translate" + [attr.aria-label]="attr.label | translate" role="checkbox" aria-checked="false" />
diff --git a/ui/src/app/schema-form/widget/check/checklist.component.ts b/ui/src/app/schema-form/widget/check/checklist.component.ts index aca5b12b5..16dd3224a 100644 --- a/ui/src/app/schema-form/widget/check/checklist.component.ts +++ b/ui/src/app/schema-form/widget/check/checklist.component.ts @@ -1,6 +1,8 @@ import { Component, AfterViewInit } from '@angular/core'; import { ArrayWidget } from 'ngx-schema-form'; +import { AttributesService } from '../../../metadata/domain/service/attributes.service'; +import { Observable, of } from 'rxjs'; /* istanbul ignore next */ @Component({ @@ -10,6 +12,12 @@ import { ArrayWidget } from 'ngx-schema-form'; export class ChecklistComponent extends ArrayWidget implements AfterViewInit { checked: any = {}; + constructor( + private attributes: AttributesService + ) { + super(); + } + ngAfterViewInit(): void { super.ngAfterViewInit(); this.formProperty.value.forEach(val => this.checked[val] = true); @@ -19,6 +27,10 @@ export class ChecklistComponent extends ArrayWidget implements AfterViewInit { this.formProperty.setValue(Object.keys(this.checked), false); } + get data(): Observable<{ key: string, label: string }[]> { + return this.schema.widget.data ? of(this.schema.widget.data) : this.attributes.query(this.schema.widget.dataUrl); + } + onCheck(value) { if (!this.checked[value]) { this.checked[value] = true; diff --git a/ui/src/assets/schema/filter/entity-attributes.schema.json b/ui/src/assets/schema/filter/entity-attributes.schema.json index 70a5b2e09..d82ff3b16 100644 --- a/ui/src/assets/schema/filter/entity-attributes.schema.json +++ b/ui/src/assets/schema/filter/entity-attributes.schema.json @@ -182,21 +182,7 @@ "description": "Attribute release table - select the attributes you want to release (default unchecked)", "widget": { "id": "checklist", - "data": [ - { "key": "eduPersonPrincipalName", "label": "eduPersonPrincipalName (EPPN)" }, - { "key": "uid", "label": "uid" }, - { "key": "mail", "label": "mail" }, - { "key": "surname", "label": "surname" }, - { "key": "givenName", "label": "givenName" }, - { "key": "displayName", "label": "displayName" }, - { "key": "eduPersonAffiliation", "label": "eduPersonAffiliation" }, - { "key": "eduPersonScopedAffiliation", "label": "eduPersonScopedAffiliation" }, - { "key": "eduPersonPrimaryAffiliation", "label": "eduPersonPrimaryAffiliation" }, - { "key": "eduPersonEntitlement", "label": "eduPersonEntitlement" }, - { "key": "eduPersonAssurance", "label": "eduPersonAssurance" }, - { "key": "eduPersonUniqueId", "label": "eduPersonUniqueId" }, - { "key": "employeeNumber", "label": "employeeNumber" } - ] + "dataUrl": "/customAttributes" }, "items": { "type": "string" diff --git a/ui/src/testing/attributes.stub.ts b/ui/src/testing/attributes.stub.ts new file mode 100644 index 000000000..200bf8ade --- /dev/null +++ b/ui/src/testing/attributes.stub.ts @@ -0,0 +1,16 @@ +import { Observable, of } from 'rxjs'; +import { Injectable } from '@angular/core'; +import { ReleaseAttribute } from '../app/metadata/domain/model/properties/release-attribute'; + +@Injectable() +export class MockAttributeService { + + readonly path = '/customAttributes'; + readonly base = '/api'; + + constructor() { } + + query(path: string = this.path): Observable { + return of([]); + } +} diff --git a/ui/src/testing/list-values.stub.ts b/ui/src/testing/list-values.stub.ts new file mode 100644 index 000000000..3e9ecf152 --- /dev/null +++ b/ui/src/testing/list-values.stub.ts @@ -0,0 +1,47 @@ +import { Observable, of } from 'rxjs'; +import { Injectable } from '@angular/core'; +import { ReleaseAttribute } from '../app/metadata/domain/model/properties/release-attribute'; +import { debounceTime, distinctUntilChanged, combineLatest } from 'rxjs/operators'; + +@Injectable() +export class MockListValueService { + + constructor() { } + + readonly nameIdFormats: Observable = of([ + 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', + 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', + 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' + ]); + + readonly authenticationMethods: Observable = of([ + 'https://refeds.org/profile/mfa', + 'urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken', + 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' + ]); + + get attributesToRelease(): Observable { + return of([]); + } + + searchStringList = (list: Observable): Function => + (text$: Observable) => + text$.pipe( + debounceTime(100), + distinctUntilChanged(), + combineLatest( + list, + (term, formats) => formats.filter( + v => v.toLowerCase().match(term.toLowerCase()) + ) + .slice(0, 4)) + ) + + get searchFormats(): Function { + return this.searchStringList(this.nameIdFormats); + } + get searchAuthenticationMethods(): Function { + return this.searchStringList(this.authenticationMethods); + } +}