From b670c3ed558d3e2b25199e1ab2a4713f7c70cf9d Mon Sep 17 00:00:00 2001
From: Michael Simon <simon@kit.edu>
Date: Mon, 23 Sep 2024 12:42:21 +0200
Subject: [PATCH] ISSUE-196 make account resolver scriptable for resolving an
 account

for a nameid.
---
 .../saml/SamlAttributeQueryService.java       | 66 +--------------
 .../saml/SamlAttributeQueryServiceImpl.java   | 75 +++++++++++++++++
 .../saml/idp/AttributeAuthorityService.java   | 81 ++++++++++++++-----
 .../sp/as/AttributeQueryAttributeSource.java  |  2 -
 .../service/saml/AttributeQueryHelper.java    |  1 -
 .../service/saml/Saml2AssertionService.java   |  6 +-
 .../saml/Saml2ResponseValidationService.java  |  5 +-
 7 files changed, 144 insertions(+), 92 deletions(-)
 create mode 100644 bwreg-service/src/main/java/edu/kit/scc/webreg/service/saml/SamlAttributeQueryServiceImpl.java

diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/saml/SamlAttributeQueryService.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/saml/SamlAttributeQueryService.java
index 12fb79aa0..01bf78d90 100644
--- a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/saml/SamlAttributeQueryService.java
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/saml/SamlAttributeQueryService.java
@@ -3,73 +3,13 @@ package edu.kit.scc.webreg.service.saml;
 import java.io.IOException;
 import java.io.Serializable;
 
-import org.opensaml.messaging.decoder.MessageDecodingException;
-import org.opensaml.saml.saml2.core.AttributeQuery;
-import org.opensaml.saml.saml2.core.StatusCode;
-import org.opensaml.soap.soap11.Envelope;
-import org.slf4j.Logger;
-
-import edu.kit.scc.webreg.annotations.RetryTransaction;
 import edu.kit.scc.webreg.entity.SamlAAConfigurationEntity;
-import edu.kit.scc.webreg.saml.idp.AttributeAuthorityService;
-import edu.kit.scc.webreg.service.saml.exc.SamlAuthenticationException;
-import jakarta.ejb.Stateless;
-import jakarta.ejb.TransactionManagement;
-import jakarta.ejb.TransactionManagementType;
-import jakarta.inject.Inject;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
-import net.shibboleth.shared.component.ComponentInitializationException;
-
-@Stateless
-@TransactionManagement(TransactionManagementType.BEAN)
-public class SamlAttributeQueryService implements Serializable {
-
-	private static final long serialVersionUID = 1L;
-
-	@Inject
-	private Logger logger;
-
-	@Inject
-	private AttributeAuthorityService aaService;
-	
-	@Inject
-	private Saml2DecoderService saml2DecoderService;
-
-	@Inject
-	private SamlHelper samlHelper;
-
-	@RetryTransaction
-	public void consumeAttributeQuery(HttpServletRequest request, HttpServletResponse response,
-			SamlAAConfigurationEntity aaConfig) throws IOException {
-		logger.debug("Consuming SAML AttributeQuery");
-
-		try {
-			AttributeQuery query = saml2DecoderService.decodeAttributeQuery(request);
-			logger.debug("SAML AttributeQuery decoded");
 
-			Envelope envelope = aaService.processAttributeQuery(aaConfig, query);
 
-			response.getWriter().print(samlHelper.marshal(envelope));
+public interface SamlAttributeQueryService extends Serializable {
 
-		} catch (MessageDecodingException e) {
-			logger.info("Could not execute AttributeQuery: {}", e.getMessage());
-			sendErrorResponse(response, StatusCode.REQUEST_DENIED, e.getMessage());
-		} catch (SecurityException e) {
-			logger.info("Could not execute AttributeQuery: {}", e.getMessage());
-			sendErrorResponse(response, StatusCode.REQUEST_DENIED, e.getMessage());
-		} catch (SamlAuthenticationException e) {
-			logger.info("Could not execute AttributeQuery: {}", e.getMessage());
-			sendErrorResponse(response, StatusCode.REQUEST_DENIED, e.getMessage());
-		} catch (ComponentInitializationException e) {
-			logger.info("Could not execute AttributeQuery: {}", e.getMessage());
-			sendErrorResponse(response, StatusCode.REQUEST_DENIED, e.getMessage());
-		}
-	}
-	
-	private void sendErrorResponse(HttpServletResponse response, String statusCodeString, String messageString)
-			throws IOException {
-		Envelope envelope = aaService.buildErrorResponse(statusCodeString, messageString);
-		response.getWriter().print(samlHelper.marshal(envelope));
-	}
+	void consumeAttributeQuery(HttpServletRequest request, HttpServletResponse response,
+			SamlAAConfigurationEntity aaConfig) throws IOException;
 }
diff --git a/bwreg-service/src/main/java/edu/kit/scc/webreg/service/saml/SamlAttributeQueryServiceImpl.java b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/saml/SamlAttributeQueryServiceImpl.java
new file mode 100644
index 000000000..5786167b1
--- /dev/null
+++ b/bwreg-service/src/main/java/edu/kit/scc/webreg/service/saml/SamlAttributeQueryServiceImpl.java
@@ -0,0 +1,75 @@
+package edu.kit.scc.webreg.service.saml;
+
+import java.io.IOException;
+
+import org.opensaml.messaging.decoder.MessageDecodingException;
+import org.opensaml.saml.saml2.core.AttributeQuery;
+import org.opensaml.saml.saml2.core.StatusCode;
+import org.opensaml.soap.soap11.Envelope;
+import org.slf4j.Logger;
+
+import edu.kit.scc.webreg.annotations.RetryTransaction;
+import edu.kit.scc.webreg.entity.SamlAAConfigurationEntity;
+import edu.kit.scc.webreg.saml.idp.AttributeAuthorityService;
+import edu.kit.scc.webreg.service.saml.exc.SamlAuthenticationException;
+import jakarta.ejb.Stateless;
+import jakarta.ejb.TransactionManagement;
+import jakarta.ejb.TransactionManagementType;
+import jakarta.inject.Inject;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+@Stateless
+@TransactionManagement(TransactionManagementType.BEAN)
+public class SamlAttributeQueryServiceImpl implements SamlAttributeQueryService {
+
+	private static final long serialVersionUID = 1L;
+
+	@Inject
+	private Logger logger;
+
+	@Inject
+	private AttributeAuthorityService aaService;
+	
+	@Inject
+	private Saml2DecoderService saml2DecoderService;
+
+	@Inject
+	private SamlHelper samlHelper;
+
+	@Override
+	@RetryTransaction
+	public void consumeAttributeQuery(HttpServletRequest request, HttpServletResponse response,
+			SamlAAConfigurationEntity aaConfig) throws IOException {
+		logger.debug("Consuming SAML AttributeQuery");
+
+		try {
+			AttributeQuery query = saml2DecoderService.decodeAttributeQuery(request);
+			logger.debug("SAML AttributeQuery decoded");
+
+			Envelope envelope = aaService.processAttributeQuery(aaConfig, query);
+
+			response.getWriter().print(samlHelper.marshal(envelope));
+
+		} catch (MessageDecodingException e) {
+			logger.info("Could not execute AttributeQuery: {}", e.getMessage());
+			sendErrorResponse(response, StatusCode.REQUEST_DENIED, e.getMessage());
+		} catch (SecurityException e) {
+			logger.info("Could not execute AttributeQuery: {}", e.getMessage());
+			sendErrorResponse(response, StatusCode.REQUEST_DENIED, e.getMessage());
+		} catch (SamlAuthenticationException e) {
+			logger.info("Could not execute AttributeQuery: {}", e.getMessage());
+			sendErrorResponse(response, StatusCode.REQUEST_DENIED, e.getMessage());
+		} catch (ComponentInitializationException e) {
+			logger.info("Could not execute AttributeQuery: {}", e.getMessage());
+			sendErrorResponse(response, StatusCode.REQUEST_DENIED, e.getMessage());
+		}
+	}
+	
+	private void sendErrorResponse(HttpServletResponse response, String statusCodeString, String messageString)
+			throws IOException {
+		Envelope envelope = aaService.buildErrorResponse(statusCodeString, messageString);
+		response.getWriter().print(samlHelper.marshal(envelope));
+	}
+}
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 ce8b88ac5..448fb979f 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
@@ -2,6 +2,11 @@ package edu.kit.scc.webreg.saml.idp;
 
 import java.time.Instant;
 
+import javax.script.Invocable;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+
 import org.opensaml.core.xml.XMLObject;
 import org.opensaml.core.xml.XMLObjectBuilderFactory;
 import org.opensaml.core.xml.schema.XSString;
@@ -22,10 +27,12 @@ import org.opensaml.soap.soap11.Envelope;
 import org.slf4j.Logger;
 
 import edu.kit.scc.webreg.dao.SamlSpMetadataDao;
-import edu.kit.scc.webreg.dao.UserDao;
+import edu.kit.scc.webreg.dao.ScriptDao;
 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.script.ScriptingEnv;
 import edu.kit.scc.webreg.service.saml.Saml2ResponseValidationService;
 import edu.kit.scc.webreg.service.saml.SamlHelper;
 import edu.kit.scc.webreg.service.saml.SsoHelper;
@@ -46,18 +53,22 @@ public class AttributeAuthorityService {
 	private SamlSpMetadataDao spMetadataDao;
 
 	@Inject
-	private UserDao userService;
-
+	private ScriptDao scriptDao;
+	
+	@Inject
+	private ScriptingEnv scriptingEnv;
+	
 	@Inject
 	private SamlHelper samlHelper;
 
 	@Inject
 	private SsoHelper ssoHelper;
 
-	public Envelope processAttributeQuery(SamlAAConfigurationEntity aaConfig, AttributeQuery query) throws SamlAuthenticationException {
-		
+	public Envelope processAttributeQuery(SamlAAConfigurationEntity aaConfig, AttributeQuery query)
+			throws SamlAuthenticationException {
+
 		logger.debug("Processing AttributeQuery");
-		
+
 		Issuer issuer = query.getIssuer();
 		if (issuer == null || issuer.getValue() == null) {
 			throw new SamlAuthenticationException("Issuer not set");
@@ -68,6 +79,12 @@ public class AttributeAuthorityService {
 		if (spEntity == null)
 			throw new SamlAuthenticationException("Issuer metadata not in database");
 
+		if (!spEntity.getGenericStore().containsKey("aq_allowed"))
+			throw new SamlAuthenticationException("Issuer not allowed for attribute query");
+		else if (!spEntity.getGenericStore().get("aq_allowed").equalsIgnoreCase("true")) {
+			throw new SamlAuthenticationException("Issuer not allowed for attribute query");
+		}
+
 		EntityDescriptor spEntityDescriptor = samlHelper.unmarshal(spEntity.getEntityDescriptor(),
 				EntityDescriptor.class);
 
@@ -79,29 +96,53 @@ public class AttributeAuthorityService {
 		samlResponse.setIssueInstant(Instant.now());
 
 		if (query.getSubject() != null && query.getSubject().getNameID() != null) {
-			String nameIdValue = query.getSubject().getNameID().getValue();
-			String nameIdFormat = query.getSubject().getNameID().getFormat();
-
-			UserEntity user = userService.fetch(Long.parseLong(nameIdValue));
-			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));
-				samlResponse.getAssertions().add(assertion);
+
+			if (!spEntity.getGenericStore().containsKey("aq_resolve_user_scipt"))
+				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");
+			ScriptEntity script = scriptDao.findByName(resolveUserScript);
+			if (script == null)
+				throw new SamlAuthenticationException("NameId Resolver not configured correctly. Script not found: " + resolveUserScript);
+
+			try {
+				ScriptEngine engine = (new ScriptEngineManager()).getEngineByName(script.getScriptEngine());
+				engine.eval(script.getScript());
+				Invocable invocable = (Invocable) engine;
+
+				String nameIdValue = query.getSubject().getNameID().getValue();
+				String nameIdFormat = query.getSubject().getNameID().getFormat();
+
+				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));
+					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);
+			} catch (ScriptException e) {
+				logger.warn("Script contains errors: {}", e.getMessage());
+				throw new SamlAuthenticationException("NameId Resolver "+ resolveUserScript + " contains errors");
 			}
+		} else {
+			throw new SamlAuthenticationException("Subject or Subject Name ID is missing in request");
 		}
 
 		return buildSoapEnvelope(samlResponse);
 	}
-	
+
 	public Envelope buildErrorResponse(String statusCodeString, String messageString) {
 		Response samlResponse = buildSamlRespone(statusCodeString, messageString);
 		return buildSoapEnvelope(samlResponse);
 	}
-	
+
 	private Response buildSamlRespone(String statusCodeString, String messageString) {
 		Response samlResponse = samlHelper.create(Response.class, Response.DEFAULT_ELEMENT_NAME);
 		samlResponse.setStatus(buildSamlStatus(statusCodeString, messageString));
diff --git a/regapp-saml-sp/src/main/java/edu/kit/scc/regapp/saml/sp/as/AttributeQueryAttributeSource.java b/regapp-saml-sp/src/main/java/edu/kit/scc/regapp/saml/sp/as/AttributeQueryAttributeSource.java
index 70715ec60..b15789c80 100644
--- a/regapp-saml-sp/src/main/java/edu/kit/scc/regapp/saml/sp/as/AttributeQueryAttributeSource.java
+++ b/regapp-saml-sp/src/main/java/edu/kit/scc/regapp/saml/sp/as/AttributeQueryAttributeSource.java
@@ -20,8 +20,6 @@ import edu.kit.scc.webreg.dao.SamlIdpMetadataDao;
 import edu.kit.scc.webreg.dao.SamlSpConfigurationDao;
 import edu.kit.scc.webreg.dao.ScriptDao;
 import edu.kit.scc.webreg.dao.as.ASUserAttrValueDao;
-import edu.kit.scc.webreg.entity.SamlAAMetadataEntity;
-import edu.kit.scc.webreg.entity.SamlIdpMetadataEntity;
 import edu.kit.scc.webreg.entity.SamlMetadataEntity;
 import edu.kit.scc.webreg.entity.SamlSpConfigurationEntity;
 import edu.kit.scc.webreg.entity.ScriptEntity;
diff --git a/regapp-saml/src/main/java/edu/kit/scc/webreg/service/saml/AttributeQueryHelper.java b/regapp-saml/src/main/java/edu/kit/scc/webreg/service/saml/AttributeQueryHelper.java
index 3db6a5d33..26ca0ab16 100644
--- a/regapp-saml/src/main/java/edu/kit/scc/webreg/service/saml/AttributeQueryHelper.java
+++ b/regapp-saml/src/main/java/edu/kit/scc/webreg/service/saml/AttributeQueryHelper.java
@@ -26,7 +26,6 @@ import java.util.Set;
 
 import javax.net.ssl.SSLContext;
 
-import org.apache.hc.client5.http.config.ConnectionConfig;
 import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
 import org.apache.hc.client5.http.impl.classic.HttpClients;
diff --git a/regapp-saml/src/main/java/edu/kit/scc/webreg/service/saml/Saml2AssertionService.java b/regapp-saml/src/main/java/edu/kit/scc/webreg/service/saml/Saml2AssertionService.java
index dd5667f50..ee8979599 100644
--- a/regapp-saml/src/main/java/edu/kit/scc/webreg/service/saml/Saml2AssertionService.java
+++ b/regapp-saml/src/main/java/edu/kit/scc/webreg/service/saml/Saml2AssertionService.java
@@ -22,8 +22,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.inject.Inject;
 import javax.script.Invocable;
 import javax.script.ScriptEngine;
 import javax.script.ScriptEngineManager;
@@ -62,6 +60,8 @@ import edu.kit.scc.webreg.entity.SamlUserEntity_;
 import edu.kit.scc.webreg.entity.ScriptEntity;
 import edu.kit.scc.webreg.service.saml.exc.NoAssertionException;
 import edu.kit.scc.webreg.service.saml.exc.SamlAuthenticationException;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
 
 @ApplicationScoped
 public class Saml2AssertionService {
@@ -100,7 +100,7 @@ public class Saml2AssertionService {
 		}
 
 		saml2ValidationService.verifyStatus(samlResponse);
-		saml2ValidationService.verifyIssuer((SamlIdpMetadataEntity) idpEntity, samlResponse);
+		saml2ValidationService.verifyIssuer(idpEntity, samlResponse);
 		saml2ValidationService.verifyExpiration(samlResponse, 1000L * 60L * 10L);
 
 		Boolean responseSignatureValid = false;
diff --git a/regapp-saml/src/main/java/edu/kit/scc/webreg/service/saml/Saml2ResponseValidationService.java b/regapp-saml/src/main/java/edu/kit/scc/webreg/service/saml/Saml2ResponseValidationService.java
index 1e2a25a24..a9ed2f115 100644
--- a/regapp-saml/src/main/java/edu/kit/scc/webreg/service/saml/Saml2ResponseValidationService.java
+++ b/regapp-saml/src/main/java/edu/kit/scc/webreg/service/saml/Saml2ResponseValidationService.java
@@ -39,7 +39,6 @@ import org.opensaml.xmlsec.signature.support.SignatureException;
 import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
 import org.slf4j.Logger;
 
-import edu.kit.scc.webreg.entity.SamlIdpMetadataEntity;
 import edu.kit.scc.webreg.entity.SamlMetadataEntity;
 import edu.kit.scc.webreg.entity.SamlSpMetadataEntity;
 import edu.kit.scc.webreg.service.saml.exc.SamlAuthenticationException;
@@ -86,12 +85,12 @@ public class Saml2ResponseValidationService {
 
 	}
 
-	public void verifyIssuer(SamlIdpMetadataEntity metadataEntity,
+	public void verifyIssuer(SamlMetadataEntity metadataEntity,
 			Response samlResponse) throws SamlAuthenticationException {
 		verifyIssuer(metadataEntity, samlResponse.getIssuer());
 	}
 
-	public void verifyIssuer(SamlIdpMetadataEntity metadataEntity,
+	public void verifyIssuer(SamlMetadataEntity metadataEntity,
 			Issuer issuer) throws SamlAuthenticationException {
 
 		if (issuer == null)
-- 
GitLab