From 6f20166cf353b777da949d64445521ebf56a5cfd Mon Sep 17 00:00:00 2001
From: Michael Simon <simon@kit.edu>
Date: Wed, 25 Sep 2024 11:58:24 +0200
Subject: [PATCH] ISSUE-196 implement first attribute builder and simple
 transcoder

---
 .../attribute/AttributeReleaseEntity.java     |  23 +++
 .../entity/attribute/value/ValueEntity.java   |  12 ++
 .../oidc/OidcOpScopeLoginProcessor.java       |   8 +-
 .../webapp/user/show-attribute-release.xhtml  |   3 +
 .../webapp/user/show-attribute-releases.xhtml |   3 +
 .../proc/IdentityValuesProcessor.java         |   1 +
 .../attribute/release/AttributeBuilder.java   | 173 ++++++++++++++++++
 .../release/AttributeReleaseHandler.java      |  29 +--
 regapp-saml-idp/pom.xml                       |   5 +
 .../saml/idp/AbstractValueTranscoder.java     |  32 ++++
 .../saml/idp/AttributeAuthorityService.java   |  61 ++++--
 .../saml/idp/SamlAttributeTranscoder.java     |  81 ++++++++
 .../saml/idp/SingleStringValueTranscoder.java |  33 ++++
 .../scc/webreg/saml/idp/ValueTranscoder.java  |   9 +
 regapp-saml-sp/pom.xml                        |   5 +
 .../service/impl/AttributeMapHelper.java      |   5 +-
 16 files changed, 439 insertions(+), 44 deletions(-)
 create mode 100644 regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/release/AttributeBuilder.java
 create mode 100644 regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/AbstractValueTranscoder.java
 create mode 100644 regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/SamlAttributeTranscoder.java
 create mode 100644 regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/SingleStringValueTranscoder.java
 create mode 100644 regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/ValueTranscoder.java

diff --git a/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/attribute/AttributeReleaseEntity.java b/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/attribute/AttributeReleaseEntity.java
index 7a92e5ba2..976fde746 100644
--- a/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/attribute/AttributeReleaseEntity.java
+++ b/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/attribute/AttributeReleaseEntity.java
@@ -14,6 +14,7 @@ import jakarta.persistence.Enumerated;
 import jakarta.persistence.ManyToOne;
 import jakarta.persistence.OneToMany;
 import jakarta.persistence.Table;
+import jakarta.persistence.Transient;
 
 @Entity(name = "AttributeReleaseEntity")
 @Table(name = "attribute_release")
@@ -40,6 +41,12 @@ public class AttributeReleaseEntity extends AbstractBaseEntity {
 	@OneToMany(mappedBy = "attributeRelease")
 	private Set<ValueEntity> values = new HashSet<>(); 
 	
+	@Transient
+	private Boolean changed;
+
+	@Transient
+	private Set<ValueEntity> valuesToDelete;
+	
 	public AttributeConsumerEntity getAttributeConsumer() {
 		return attributeConsumer;
 	}
@@ -87,4 +94,20 @@ public class AttributeReleaseEntity extends AbstractBaseEntity {
 	public void setValues(Set<ValueEntity> values) {
 		this.values = values;
 	}
+
+	public Boolean getChanged() {
+		return changed;
+	}
+
+	public void setChanged(Boolean changed) {
+		this.changed = changed;
+	}
+
+	public Set<ValueEntity> getValuesToDelete() {
+		return valuesToDelete;
+	}
+
+	public void setValuesToDelete(Set<ValueEntity> valuesToDelete) {
+		this.valuesToDelete = valuesToDelete;
+	}
 }
diff --git a/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/attribute/value/ValueEntity.java b/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/attribute/value/ValueEntity.java
index 9a212a82f..a37d8e317 100644
--- a/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/attribute/value/ValueEntity.java
+++ b/bwreg-entities/src/main/java/edu/kit/scc/webreg/entity/attribute/value/ValueEntity.java
@@ -16,6 +16,7 @@ import jakarta.persistence.JoinTable;
 import jakarta.persistence.ManyToMany;
 import jakarta.persistence.ManyToOne;
 import jakarta.persistence.Table;
+import jakarta.persistence.Transient;
 
 @Entity(name = "ValueEntity")
 @Table(name = "value")
@@ -42,6 +43,9 @@ public class ValueEntity extends AbstractBaseEntity {
 
 	@ManyToMany(mappedBy = "nextValues")
 	private Set<ValueEntity> prevValues = new HashSet<>();
+	
+	@Transient
+	private Boolean changed;
 
 	public AttributeEntity getAttribute() {
 		return attribute;
@@ -90,4 +94,12 @@ public class ValueEntity extends AbstractBaseEntity {
 	public void setEndValue(Boolean endValue) {
 		this.endValue = endValue;
 	}
+
+	public Boolean getChanged() {
+		return changed;
+	}
+
+	public void setChanged(Boolean changed) {
+		this.changed = changed;
+	}
 }
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/OidcOpScopeLoginProcessor.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/OidcOpScopeLoginProcessor.java
index 1a34aabe6..c61205c83 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/OidcOpScopeLoginProcessor.java
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/oidc/OidcOpScopeLoginProcessor.java
@@ -24,6 +24,7 @@ import edu.kit.scc.webreg.entity.oidc.OidcOpConfigurationEntity;
 import edu.kit.scc.webreg.entity.oidc.ProjectOidcClientConfigurationEntity;
 import edu.kit.scc.webreg.entity.project.ProjectEntity;
 import edu.kit.scc.webreg.entity.project.ProjectMembershipEntity;
+import edu.kit.scc.webreg.service.attribute.release.AttributeBuilder;
 import edu.kit.scc.webreg.service.attribute.release.AttributeReleaseHandler;
 import edu.kit.scc.webreg.service.saml.exc.OidcAuthenticationException;
 import jakarta.enterprise.context.ApplicationScoped;
@@ -42,6 +43,9 @@ public class OidcOpScopeLoginProcessor extends AbstractOidcOpLoginProcessor {
 	@Inject
 	private AttributeReleaseHandler attributeReleaseHandler;
 
+	@Inject
+	private AttributeBuilder attributeBuilder;
+
 	@Inject
 	private ProjectDao projectDao;
 	
@@ -79,7 +83,7 @@ public class OidcOpScopeLoginProcessor extends AbstractOidcOpLoginProcessor {
 			}
 		}
 		
-		AttributeReleaseEntity attributeRelease = attributeReleaseHandler.requestAttributeRelease(clientConfig,
+		AttributeReleaseEntity attributeRelease = attributeBuilder.requestAttributeRelease(clientConfig,
 				identity);
 		flowState.setAttributeRelease(attributeRelease);
 		flowState.setIdentity(identity);
@@ -143,7 +147,7 @@ public class OidcOpScopeLoginProcessor extends AbstractOidcOpLoginProcessor {
 	public JSONObject buildUserInfo(OidcFlowStateEntity flowState, OidcOpConfigurationEntity opConfig,
 			OidcClientConsumerEntity clientConfig, HttpServletResponse response) throws OidcAuthenticationException {
 		IdentityEntity identity = flowState.getIdentity();
-		AttributeReleaseEntity attributeRelease = attributeReleaseHandler.requestAttributeRelease(clientConfig,
+		AttributeReleaseEntity attributeRelease = attributeBuilder.requestAttributeRelease(clientConfig,
 				identity);
 
 		if (! ReleaseStatusType.GOOD.equals(attributeRelease.getReleaseStatus())) {
diff --git a/bwreg-webapp/src/main/webapp/user/show-attribute-release.xhtml b/bwreg-webapp/src/main/webapp/user/show-attribute-release.xhtml
index 4a9aab1cd..27226113d 100644
--- a/bwreg-webapp/src/main/webapp/user/show-attribute-release.xhtml
+++ b/bwreg-webapp/src/main/webapp/user/show-attribute-release.xhtml
@@ -41,6 +41,9 @@
 			<p:outputPanel rendered="#{showAttributeReleaseBean.release.attributeConsumer.class.simpleName == 'ProjectOidcClientConfigurationEntity'}">
 				<b><h:outputText value="#{showAttributeReleaseBean.release.attributeConsumer.displayName}" /></b>
 			</p:outputPanel>
+			<p:outputPanel rendered="#{showAttributeReleaseBean.release.attributeConsumer.class.simpleName == 'SamlSpMetadataEntity'}">
+				<b><h:outputText value="#{showAttributeReleaseBean.release.attributeConsumer.entityId}" /></b>
+			</p:outputPanel>
 
 			<h:outputText value="#{messages['attribute_release.actual_release_status']}: " />
 			<p:outputPanel>
diff --git a/bwreg-webapp/src/main/webapp/user/show-attribute-releases.xhtml b/bwreg-webapp/src/main/webapp/user/show-attribute-releases.xhtml
index 7805c5917..f32a77e6a 100644
--- a/bwreg-webapp/src/main/webapp/user/show-attribute-releases.xhtml
+++ b/bwreg-webapp/src/main/webapp/user/show-attribute-releases.xhtml
@@ -49,6 +49,9 @@
 					<p:outputPanel rendered="#{release.attributeConsumer.class.simpleName == 'ProjectOidcClientConfigurationEntity'}">
 						<h:outputText value="#{release.attributeConsumer.displayName}"/>
 					</p:outputPanel>
+					<p:outputPanel rendered="#{release.attributeConsumer.class.simpleName == 'SamlSpMetadataEntity'}">
+						<h:outputText value="#{release.attributeConsumer.entityId}"/>
+					</p:outputPanel>
 				</p:column>
 				<p:column width="10%">
 					<p:link value="#{messages['details']}" href="./show-attribute-release.xhtml">
diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/IdentityValuesProcessor.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/IdentityValuesProcessor.java
index e95628c88..21b8f57c3 100644
--- a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/IdentityValuesProcessor.java
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/proc/IdentityValuesProcessor.java
@@ -29,6 +29,7 @@ public class IdentityValuesProcessor {
 				new StringListMergeValueProcessor("voperson_external_affiliation", "eduperson_affiliation"),
 				new StringListMergeValueProcessor("eduperson_assurance", "eduperson_assurance"),
 				new StringListMergeAuthorityValueProcessor("eduperson_entitlement", "eduperson_entitlement"),
+				new SingleStringMergeValueProcessor("eduperson_principal_name", "eduperson_principal_name"),
 				new SingleStringMergeValueProcessor("family_name", "family_name"),
 				new SingleStringMergeValueProcessor("given_name", "given_name"));
 	}
diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/release/AttributeBuilder.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/release/AttributeBuilder.java
new file mode 100644
index 000000000..998cf37f2
--- /dev/null
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/release/AttributeBuilder.java
@@ -0,0 +1,173 @@
+package edu.kit.scc.webreg.service.attribute.release;
+
+import static edu.kit.scc.webreg.dao.ops.RqlExpressions.and;
+import static edu.kit.scc.webreg.dao.ops.RqlExpressions.equal;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+import org.slf4j.Logger;
+
+import edu.kit.scc.webreg.dao.jpa.attribute.AttributeReleaseDao;
+import edu.kit.scc.webreg.dao.jpa.attribute.OutgoingAttributeDao;
+import edu.kit.scc.webreg.dao.jpa.attribute.ValueDao;
+import edu.kit.scc.webreg.entity.attribute.AttributeConsumerEntity;
+import edu.kit.scc.webreg.entity.attribute.AttributeEntity;
+import edu.kit.scc.webreg.entity.attribute.AttributeReleaseEntity;
+import edu.kit.scc.webreg.entity.attribute.AttributeReleaseEntity_;
+import edu.kit.scc.webreg.entity.attribute.OutgoingAttributeEntity;
+import edu.kit.scc.webreg.entity.attribute.OutgoingAttributeEntity_;
+import edu.kit.scc.webreg.entity.attribute.ReleaseStatusType;
+import edu.kit.scc.webreg.entity.attribute.value.StringListValueEntity;
+import edu.kit.scc.webreg.entity.attribute.value.StringValueEntity;
+import edu.kit.scc.webreg.entity.attribute.value.ValueEntity;
+import edu.kit.scc.webreg.entity.attribute.value.ValueEntity_;
+import edu.kit.scc.webreg.entity.identity.IdentityEntity;
+import edu.kit.scc.webreg.service.identity.IdentityAttributeResolver;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+@ApplicationScoped
+public class AttributeBuilder {
+
+	@Inject
+	private Logger logger;
+
+	@Inject
+	private AttributeReleaseDao attributeReleaseDao;
+
+	@Inject
+	private OutgoingAttributeDao outgoingAttributeDao;
+
+	@Inject
+	private IdentityAttributeResolver attributeResolver;
+
+	@Inject
+	private ValueDao valueDao;
+
+	public AttributeReleaseEntity requestAttributeRelease(AttributeConsumerEntity attributeConsumer,
+			IdentityEntity identity) {
+		final AttributeReleaseEntity attributeRelease = resolveAttributeRelease(attributeConsumer, identity);
+		return attributeRelease;
+	}
+
+	public void addSingleStringAttribute(AttributeReleaseEntity attributeRelease, String name,
+			IdentityEntity identity) {
+		addSingleStringAttribute(attributeRelease, name, attributeResolver.resolveSingleStringValue(identity, name));
+	}
+
+	public void addSingleStringAttribute(AttributeReleaseEntity attributeRelease, String name, String value) {
+		if (value != null)
+			setSingleStringValue(attributeRelease, name, value);
+	}
+
+	public void addStringListAttribute(AttributeReleaseEntity attributeRelease, String name, IdentityEntity identity) {
+		setStringListValue(attributeRelease, name, attributeResolver.resolveStringListValue(identity, name));
+	}
+
+	public void deleteValue(ValueEntity value) {
+		value.getAttributeRelease().setChanged(true);
+		value.getAttributeRelease().getValues().remove(value);
+		for (ValueEntity v : value.getPrevValues()) {
+			v.getNextValues().remove(value);
+		}
+		valueDao.delete(value);
+	}
+	
+	private void setStringListValue(AttributeReleaseEntity attributeRelease, String name, List<String> valueList) {
+		final OutgoingAttributeEntity attribute = findOrCreateOutgroingAttribute(name);
+		StringListValueEntity value = (StringListValueEntity) resolveValue(attributeRelease, attribute,
+				StringListValueEntity.class);
+		// Null check, because with a new value it will be null
+		// or the value changed
+		if (value.getValueList() == null || !(new HashSet<>(value.getValueList())).equals(new HashSet<>(valueList))) {
+			value.setValueList(new ArrayList<>(valueList));
+			value.setChanged(true);
+			attributeRelease.setChanged(true);
+			attributeRelease.getValuesToDelete().remove(value);
+		}
+		// The value exists, but stays the same
+		else {
+			attributeRelease.getValuesToDelete().remove(value);
+		}
+	}
+
+	private void setSingleStringValue(AttributeReleaseEntity attributeRelease, String name, String valueString) {
+		final OutgoingAttributeEntity attribute = findOrCreateOutgroingAttribute(name);
+		StringValueEntity value = (StringValueEntity) resolveValue(attributeRelease, attribute,
+				StringValueEntity.class);
+		// Null check, because with a new value it will be null
+		// or the value changed
+		if ((value.getValueString() == null && valueString != null)
+				|| !value.getValueString().equals(valueString)) {
+			// The value differs from the old value. Set the new value and set the changed
+			// attribute
+			value.setValueString(valueString);
+			value.setChanged(true);
+			attributeRelease.setChanged(true);
+			attributeRelease.getValuesToDelete().remove(value);
+		}
+		// The value exists, but stays the same
+		else {
+			attributeRelease.getValuesToDelete().remove(value);
+		}
+	}
+
+	private ValueEntity resolveValue(AttributeReleaseEntity attributeRelease, AttributeEntity attribute,
+			Class<? extends ValueEntity> desiredClass) {
+		ValueEntity value = valueDao.find(
+				and(equal(ValueEntity_.attribute, attribute), equal(ValueEntity_.attributeRelease, attributeRelease)));
+		if (value != null && !(desiredClass.isInstance(value))) {
+			logger.info("Value type has change for attribute {}: {} -> {}", attribute.getName(),
+					value.getClass().getSimpleName(), desiredClass.getSimpleName());
+			for (ValueEntity v : value.getPrevValues()) {
+				v.getNextValues().remove(value);
+			}
+			valueDao.delete(value);
+			value = null;
+		}
+
+		if (value == null) {
+			try {
+				value = desiredClass.getConstructor().newInstance();
+				value.setAttribute(attribute);
+				value.setAttributeRelease(attributeRelease);
+				value = valueDao.persist(value);
+				attributeRelease.getValues().add(value);
+			} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException
+					| InvocationTargetException e) {
+				logger.error("Cannot create instance of class {}: {}", desiredClass.getName(), e.getMessage());
+			}
+		}
+		return value;
+	}
+
+	private OutgoingAttributeEntity findOrCreateOutgroingAttribute(String name) {
+		OutgoingAttributeEntity attribute = outgoingAttributeDao.find(equal(OutgoingAttributeEntity_.name, name));
+		if (attribute == null) {
+			attribute = outgoingAttributeDao.createNew();
+			attribute.setName(name);
+			attribute = outgoingAttributeDao.persist(attribute);
+		}
+		return attribute;
+	}
+
+	private AttributeReleaseEntity resolveAttributeRelease(AttributeConsumerEntity attributeConsumer,
+			IdentityEntity identity) {
+		AttributeReleaseEntity attributeRelease = attributeReleaseDao
+				.find(and(equal(AttributeReleaseEntity_.attributeConsumer, attributeConsumer),
+						equal(AttributeReleaseEntity_.identity, identity)));
+
+		if (attributeRelease == null) {
+			attributeRelease = attributeReleaseDao.createNew();
+			attributeRelease.setIdentity(identity);
+			attributeRelease.setAttributeConsumer(attributeConsumer);
+			attributeRelease.setReleaseStatus(ReleaseStatusType.NEW);
+			attributeRelease = attributeReleaseDao.persist(attributeRelease);
+		}
+
+		return attributeRelease;
+	}
+}
diff --git a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/release/AttributeReleaseHandler.java b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/release/AttributeReleaseHandler.java
index b9e1b82bb..15584d8a8 100644
--- a/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/release/AttributeReleaseHandler.java
+++ b/regapp-idty/src/main/java/edu/kit/scc/webreg/service/attribute/release/AttributeReleaseHandler.java
@@ -13,16 +13,12 @@ import java.util.UUID;
 import org.slf4j.Logger;
 
 import edu.kit.scc.webreg.bootstrap.ApplicationConfig;
-import edu.kit.scc.webreg.dao.jpa.attribute.AttributeReleaseDao;
 import edu.kit.scc.webreg.dao.jpa.attribute.OutgoingAttributeDao;
 import edu.kit.scc.webreg.dao.jpa.attribute.ValueDao;
-import edu.kit.scc.webreg.entity.attribute.AttributeConsumerEntity;
 import edu.kit.scc.webreg.entity.attribute.AttributeEntity;
 import edu.kit.scc.webreg.entity.attribute.AttributeReleaseEntity;
-import edu.kit.scc.webreg.entity.attribute.AttributeReleaseEntity_;
 import edu.kit.scc.webreg.entity.attribute.OutgoingAttributeEntity;
 import edu.kit.scc.webreg.entity.attribute.OutgoingAttributeEntity_;
-import edu.kit.scc.webreg.entity.attribute.ReleaseStatusType;
 import edu.kit.scc.webreg.entity.attribute.value.PairwiseIdentifierValueEntity;
 import edu.kit.scc.webreg.entity.attribute.value.StringListValueEntity;
 import edu.kit.scc.webreg.entity.attribute.value.StringValueEntity;
@@ -42,7 +38,7 @@ public class AttributeReleaseHandler {
 	private Logger logger;
 
 	@Inject
-	private AttributeReleaseDao attributeReleaseDao;
+	private AttributeBuilder attributeBuilder;
 
 	@Inject
 	private OutgoingAttributeDao outgoingAttributeDao;
@@ -139,29 +135,6 @@ public class AttributeReleaseHandler {
 		}
 	}
 
-	public AttributeReleaseEntity requestAttributeRelease(AttributeConsumerEntity attributeConsumer,
-			IdentityEntity identity) {
-		final AttributeReleaseEntity attributeRelease = resolveAttributeRelease(attributeConsumer, identity);
-		return attributeRelease;
-	}
-
-	private AttributeReleaseEntity resolveAttributeRelease(AttributeConsumerEntity attributeConsumer,
-			IdentityEntity identity) {
-		AttributeReleaseEntity attributeRelease = attributeReleaseDao
-				.find(and(equal(AttributeReleaseEntity_.attributeConsumer, attributeConsumer),
-						equal(AttributeReleaseEntity_.identity, identity)));
-
-		if (attributeRelease == null) {
-			attributeRelease = attributeReleaseDao.createNew();
-			attributeRelease.setIdentity(identity);
-			attributeRelease.setAttributeConsumer(attributeConsumer);
-			attributeRelease.setReleaseStatus(ReleaseStatusType.NEW);
-			attributeRelease = attributeReleaseDao.persist(attributeRelease);
-		}
-
-		return attributeRelease;
-	}
-
 	private ValueEntity resolveValue(AttributeReleaseEntity attributeRelease, AttributeEntity attribute,
 			Class<? extends ValueEntity> desiredClass) {
 		ValueEntity value = valueDao.find(
diff --git a/regapp-saml-idp/pom.xml b/regapp-saml-idp/pom.xml
index dcef700fa..e7fdbffc7 100644
--- a/regapp-saml-idp/pom.xml
+++ b/regapp-saml-idp/pom.xml
@@ -41,6 +41,11 @@
 			<artifactId>regapp-saml</artifactId>
 			<version>${project.version}</version>
 		</dependency>
+		<dependency>
+			<groupId>edu.kit.scc</groupId>
+			<artifactId>regapp-idty</artifactId>
+			<version>${project.version}</version>
+		</dependency>
 		<dependency>
 			<groupId>edu.kit.scc</groupId>
 			<artifactId>regapp-drools</artifactId>
diff --git a/regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/AbstractValueTranscoder.java b/regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/AbstractValueTranscoder.java
new file mode 100644
index 000000000..1c6b48e1e
--- /dev/null
+++ b/regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/AbstractValueTranscoder.java
@@ -0,0 +1,32 @@
+package edu.kit.scc.webreg.saml.idp;
+
+import org.opensaml.core.xml.schema.XSString;
+import org.opensaml.saml.saml2.core.Attribute;
+import org.opensaml.saml.saml2.core.AttributeValue;
+
+import edu.kit.scc.webreg.service.saml.SamlHelper;
+
+public abstract class AbstractValueTranscoder {
+
+	protected SamlHelper samlHelper;
+
+	public AbstractValueTranscoder(SamlHelper samlHelper) {
+		super();
+		this.samlHelper = samlHelper;
+	}
+
+	protected Attribute buildAttribute(String name, String friendlyName, String nameFormat, String... values) {
+		Attribute attribute = samlHelper.create(Attribute.class, Attribute.DEFAULT_ELEMENT_NAME);
+		attribute.setName(name);
+		attribute.setFriendlyName(friendlyName);
+		attribute.setNameFormat(nameFormat);
+
+		for (String value : values) {
+			XSString xsany = samlHelper.create(XSString.class, XSString.TYPE_NAME, AttributeValue.DEFAULT_ELEMENT_NAME);
+			xsany.setValue(value);
+			attribute.getAttributeValues().add(xsany);
+		}
+
+		return attribute;
+	}
+}
diff --git a/regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/AttributeAuthorityService.java b/regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/AttributeAuthorityService.java
index 448fb979f..813da7f17 100644
--- a/regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/AttributeAuthorityService.java
+++ b/regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/AttributeAuthorityService.java
@@ -1,6 +1,7 @@
 package edu.kit.scc.webreg.saml.idp;
 
 import java.time.Instant;
+import java.util.HashSet;
 
 import javax.script.Invocable;
 import javax.script.ScriptEngine;
@@ -32,7 +33,10 @@ import edu.kit.scc.webreg.entity.SamlAAConfigurationEntity;
 import edu.kit.scc.webreg.entity.SamlSpMetadataEntity;
 import edu.kit.scc.webreg.entity.ScriptEntity;
 import edu.kit.scc.webreg.entity.UserEntity;
+import edu.kit.scc.webreg.entity.attribute.AttributeReleaseEntity;
 import edu.kit.scc.webreg.script.ScriptingEnv;
+import edu.kit.scc.webreg.service.attribute.release.AttributeBuilder;
+import edu.kit.scc.webreg.service.identity.IdentityAttributeResolver;
 import edu.kit.scc.webreg.service.saml.Saml2ResponseValidationService;
 import edu.kit.scc.webreg.service.saml.SamlHelper;
 import edu.kit.scc.webreg.service.saml.SsoHelper;
@@ -54,16 +58,25 @@ public class AttributeAuthorityService {
 
 	@Inject
 	private ScriptDao scriptDao;
-	
+
 	@Inject
 	private ScriptingEnv scriptingEnv;
-	
+
 	@Inject
 	private SamlHelper samlHelper;
 
 	@Inject
 	private SsoHelper ssoHelper;
 
+	@Inject
+	private AttributeBuilder attributeBuilder;
+
+	@Inject
+	private IdentityAttributeResolver attributeResolver;
+
+	@Inject
+	private SamlAttributeTranscoder attributeTranscoder;
+
 	public Envelope processAttributeQuery(SamlAAConfigurationEntity aaConfig, AttributeQuery query)
 			throws SamlAuthenticationException {
 
@@ -101,9 +114,16 @@ public class AttributeAuthorityService {
 				throw new SamlAuthenticationException(
 						"NameId Resolver not configured, cannot resolve account. This is a server side error, contact the server administrator");
 			String resolveUserScript = spEntity.getGenericStore().get("aq_resolve_user_scipt");
+
+			if (!spEntity.getGenericStore().containsKey("aq_resolve_attribute_scipt"))
+				throw new SamlAuthenticationException(
+						"Attribute Resolver not configured, cannot resolve attributes. This is a server side error, contact the server administrator");
+			String resolveAttributeScript = spEntity.getGenericStore().get("aq_resolve_attribute_scipt");
+
 			ScriptEntity script = scriptDao.findByName(resolveUserScript);
 			if (script == null)
-				throw new SamlAuthenticationException("NameId Resolver not configured correctly. Script not found: " + resolveUserScript);
+				throw new SamlAuthenticationException(
+						"NameId Resolver not configured correctly. Script not found: " + resolveUserScript);
 
 			try {
 				ScriptEngine engine = (new ScriptEngineManager()).getEngineByName(script.getScriptEngine());
@@ -113,23 +133,38 @@ public class AttributeAuthorityService {
 				String nameIdValue = query.getSubject().getNameID().getValue();
 				String nameIdFormat = query.getSubject().getNameID().getFormat();
 
-				UserEntity user = (UserEntity) invocable.invokeFunction("resolveUser", scriptingEnv, nameIdFormat, nameIdValue, logger,
-						spEntity, aaConfig);
+				UserEntity user = (UserEntity) invocable.invokeFunction("resolveUser", scriptingEnv, nameIdFormat,
+						nameIdValue, logger, spEntity, aaConfig);
 				if (user != null) {
-					Assertion assertion = samlHelper.create(Assertion.class, Assertion.DEFAULT_ELEMENT_NAME);
-					assertion.setIssueInstant(Instant.now());
-					assertion.setIssuer(ssoHelper.buildIssuser(aaConfig.getEntityId()));
-					assertion.setSubject(
-							ssoHelper.buildAQSubject(aaConfig, spEntity, nameIdValue, NameID.UNSPECIFIED, query.getID()));
-					assertion.getAttributeStatements().add(buildAttributeStatement(user));
+
+					AttributeReleaseEntity attributeRelease = attributeBuilder.requestAttributeRelease(spEntity,
+							user.getIdentity());
+
+					script = scriptDao.findByName(resolveAttributeScript);
+					if (script == null)
+						throw new SamlAuthenticationException(
+								"Attribute Resolver not configured correctly. Script not found: " + resolveUserScript);
+					engine = (new ScriptEngineManager()).getEngineByName(script.getScriptEngine());
+					engine.eval(script.getScript());
+					invocable = (Invocable) engine;
+
+					attributeRelease.setValuesToDelete(new HashSet<>(attributeRelease.getValues()));
+					invocable.invokeFunction("resolveAttributes", scriptingEnv, attributeBuilder, attributeResolver,
+							attributeRelease, user.getIdentity(), logger, spEntity, aaConfig);
+					attributeRelease.getValuesToDelete().stream().forEach(v -> attributeBuilder.deleteValue(v));
+
+					Assertion assertion = attributeTranscoder.convertAttributes(attributeRelease, aaConfig, spEntity,
+							query);
 					samlResponse.getAssertions().add(assertion);
 				}
 			} catch (NoSuchMethodException e) {
 				logger.warn("Method is missing in script: {}", e.getMessage());
-				throw new SamlAuthenticationException("NameId Resolver not configured correctly. Method resolveUser not found in " + resolveUserScript);
+				throw new SamlAuthenticationException(
+						"NameId Resolver not configured correctly. Method resolveUser not found in "
+								+ resolveUserScript);
 			} catch (ScriptException e) {
 				logger.warn("Script contains errors: {}", e.getMessage());
-				throw new SamlAuthenticationException("NameId Resolver "+ resolveUserScript + " contains errors");
+				throw new SamlAuthenticationException("NameId Resolver " + resolveUserScript + " contains errors");
 			}
 		} else {
 			throw new SamlAuthenticationException("Subject or Subject Name ID is missing in request");
diff --git a/regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/SamlAttributeTranscoder.java b/regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/SamlAttributeTranscoder.java
new file mode 100644
index 000000000..5a0267b7a
--- /dev/null
+++ b/regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/SamlAttributeTranscoder.java
@@ -0,0 +1,81 @@
+package edu.kit.scc.webreg.saml.idp;
+
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Attribute;
+import org.opensaml.saml.saml2.core.AttributeQuery;
+import org.opensaml.saml.saml2.core.AttributeStatement;
+import org.opensaml.saml.saml2.core.NameID;
+import org.slf4j.Logger;
+
+import edu.kit.scc.webreg.entity.SamlAAConfigurationEntity;
+import edu.kit.scc.webreg.entity.SamlSpMetadataEntity;
+import edu.kit.scc.webreg.entity.attribute.AttributeReleaseEntity;
+import edu.kit.scc.webreg.entity.attribute.value.StringValueEntity;
+import edu.kit.scc.webreg.entity.attribute.value.ValueEntity;
+import edu.kit.scc.webreg.service.saml.SamlHelper;
+import edu.kit.scc.webreg.service.saml.SsoHelper;
+import jakarta.annotation.PostConstruct;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+@ApplicationScoped
+public class SamlAttributeTranscoder {
+
+	@Inject
+	private Logger logger;
+
+	@Inject
+	private SamlHelper samlHelper;
+
+	@Inject
+	private SsoHelper ssoHelper;
+
+	private Map<String, ValueTranscoder> transcoderMap;
+
+	@PostConstruct
+	public void init() {
+		transcoderMap = new HashMap<>();
+		transcoderMap.put("family_name",
+				new SingleStringValueTranscoder(samlHelper, "urn:oid:2.5.4.4", "sn", Attribute.BASIC));
+		transcoderMap.put("given_name",
+				new SingleStringValueTranscoder(samlHelper, "urn:oid:2.5.4.42", "givenName", Attribute.BASIC));
+		transcoderMap.put("eduperson_principal_name", new SingleStringValueTranscoder(samlHelper,
+				"urn:oid:1.3.6.1.4.1.5923.1.1.1.6", "eduPersonPrincipalName", Attribute.URI_REFERENCE));
+		transcoderMap.put("eduperson_entitlement", new SingleStringValueTranscoder(samlHelper,
+				"urn:oid:1.3.6.1.4.1.5923.1.1.1.7", "eduPersonEntitlement", Attribute.BASIC));
+	}
+
+	public Assertion convertAttributes(AttributeReleaseEntity attributeRelease, SamlAAConfigurationEntity aaConfig,
+			SamlSpMetadataEntity spEntity, AttributeQuery query) {
+		Assertion assertion = samlHelper.create(Assertion.class, Assertion.DEFAULT_ELEMENT_NAME);
+		assertion.setIssueInstant(Instant.now());
+		assertion.setIssuer(ssoHelper.buildIssuser(aaConfig.getEntityId()));
+
+		AttributeStatement attributeStatement = buildAttributeStatement();
+		assertion.getAttributeStatements().add(attributeStatement);
+
+		for (ValueEntity value : attributeRelease.getValues()) {
+			if (value.getAttribute().getName().equals("sub")) {
+				assertion.setSubject(ssoHelper.buildAQSubject(aaConfig, spEntity,
+						((StringValueEntity) value).getValueString(), NameID.UNSPECIFIED, query.getID()));
+			} else if (transcoderMap.containsKey(value.getAttribute().getName())) {
+				attributeStatement.getAttributes()
+						.add(transcoderMap.get(value.getAttribute().getName()).transcode(value));
+			} else {
+				logger.debug("No SAML Transcoder for attribute {}", value.getAttribute().getName());
+			}
+		}
+
+		return assertion;
+	}
+
+	private AttributeStatement buildAttributeStatement() {
+		AttributeStatement attributeStatement = samlHelper.create(AttributeStatement.class,
+				AttributeStatement.DEFAULT_ELEMENT_NAME);
+		return attributeStatement;
+	}
+}
diff --git a/regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/SingleStringValueTranscoder.java b/regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/SingleStringValueTranscoder.java
new file mode 100644
index 000000000..7cdd76993
--- /dev/null
+++ b/regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/SingleStringValueTranscoder.java
@@ -0,0 +1,33 @@
+package edu.kit.scc.webreg.saml.idp;
+
+import org.opensaml.saml.saml2.core.Attribute;
+
+import edu.kit.scc.webreg.entity.attribute.value.StringListValueEntity;
+import edu.kit.scc.webreg.entity.attribute.value.StringValueEntity;
+import edu.kit.scc.webreg.entity.attribute.value.ValueEntity;
+import edu.kit.scc.webreg.service.saml.SamlHelper;
+
+public class SingleStringValueTranscoder extends AbstractValueTranscoder implements ValueTranscoder {
+
+	private String outgoingName;
+	private String outgoingFriendlyName;
+	private String nameFormat;
+
+	public SingleStringValueTranscoder(SamlHelper samlHelper, String outgoingName, String outgoingFriendlyName, String nameFormat) {
+		super(samlHelper);
+		this.outgoingName = outgoingName;
+		this.outgoingFriendlyName = outgoingFriendlyName;
+		this.nameFormat = nameFormat;
+	}
+	
+	public Attribute transcode(ValueEntity value) {
+		if (value instanceof StringValueEntity)
+			return buildAttribute(outgoingName, outgoingFriendlyName, nameFormat,
+				((StringValueEntity) value).getValueString());
+		else if (value instanceof StringListValueEntity) 
+			return buildAttribute(outgoingName, outgoingFriendlyName, nameFormat,
+					((StringListValueEntity) value).getValueList().toArray(new String[] {}));
+		else 
+			throw new IllegalArgumentException("Cannot transcaode value of type " + value.getClass().getSimpleName());
+	}	
+}
diff --git a/regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/ValueTranscoder.java b/regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/ValueTranscoder.java
new file mode 100644
index 000000000..6bb37b659
--- /dev/null
+++ b/regapp-saml-idp/src/main/java/edu/kit/scc/webreg/saml/idp/ValueTranscoder.java
@@ -0,0 +1,9 @@
+package edu.kit.scc.webreg.saml.idp;
+
+import org.opensaml.saml.saml2.core.Attribute;
+
+import edu.kit.scc.webreg.entity.attribute.value.ValueEntity;
+
+public interface ValueTranscoder {
+	Attribute transcode(ValueEntity value);
+}
diff --git a/regapp-saml-sp/pom.xml b/regapp-saml-sp/pom.xml
index 8b3aa3ca4..f86d32d02 100644
--- a/regapp-saml-sp/pom.xml
+++ b/regapp-saml-sp/pom.xml
@@ -46,6 +46,11 @@
 			<artifactId>regapp-saml</artifactId>
 			<version>${project.version}</version>
 		</dependency>
+		<dependency>
+			<groupId>edu.kit.scc</groupId>
+			<artifactId>regapp-idty</artifactId>
+			<version>${project.version}</version>
+		</dependency>
 		<dependency>
 			<groupId>edu.kit.scc</groupId>
 			<artifactId>regapp-session</artifactId>
diff --git a/regapp-tools/src/main/java/edu/kit/scc/webreg/service/impl/AttributeMapHelper.java b/regapp-tools/src/main/java/edu/kit/scc/webreg/service/impl/AttributeMapHelper.java
index 64e786fb9..e5fbd508b 100644
--- a/regapp-tools/src/main/java/edu/kit/scc/webreg/service/impl/AttributeMapHelper.java
+++ b/regapp-tools/src/main/java/edu/kit/scc/webreg/service/impl/AttributeMapHelper.java
@@ -36,7 +36,10 @@ public class AttributeMapHelper implements Serializable {
 		if (o instanceof String) {
 			return (String) o;
 		} else {
-			return o.toString();
+			if (o == null)
+				return null;
+			else
+				return o.toString();
 		}
 	}
 
-- 
GitLab