From 74f79afa8a607504a7c1bee0cd93aee3d3755e62 Mon Sep 17 00:00:00 2001 From: AFaust Date: Fri, 4 Sep 2020 21:39:32 +0200 Subject: [PATCH] Add suport for user name correction in Share - effectively support login by alternative identity attributes, e.g. Keycloak login by email --- .../keycloak/effectiveUserName.get.desc.xml | 9 + .../keycloak/effectiveUserName.get.json.ftl | 3 + share/src/main/config/module-context.xml | 17 ++ ...ameCorrectingSlingshotLoginController.java | 244 ++++++++++++++++++ 4 files changed, 273 insertions(+) create mode 100644 repository/src/main/webscripts/de/acosix/keycloak/effectiveUserName.get.desc.xml create mode 100644 repository/src/main/webscripts/de/acosix/keycloak/effectiveUserName.get.json.ftl create mode 100644 share/src/main/java/de/acosix/alfresco/keycloak/share/web/UserNameCorrectingSlingshotLoginController.java diff --git a/repository/src/main/webscripts/de/acosix/keycloak/effectiveUserName.get.desc.xml b/repository/src/main/webscripts/de/acosix/keycloak/effectiveUserName.get.desc.xml new file mode 100644 index 0000000..c84ab22 --- /dev/null +++ b/repository/src/main/webscripts/de/acosix/keycloak/effectiveUserName.get.desc.xml @@ -0,0 +1,9 @@ + + + Get effective user name + Retrieves the effective name for the currently authenticated user + /acosix/api/keycloak/effectiveUserName + argument + user + required + diff --git a/repository/src/main/webscripts/de/acosix/keycloak/effectiveUserName.get.json.ftl b/repository/src/main/webscripts/de/acosix/keycloak/effectiveUserName.get.json.ftl new file mode 100644 index 0000000..fe2a0ca --- /dev/null +++ b/repository/src/main/webscripts/de/acosix/keycloak/effectiveUserName.get.json.ftl @@ -0,0 +1,3 @@ +<#compress><#escape x as jsonUtils.encodeJSONString(x)>{ + "userName": "${person.properties.userName}" +} \ No newline at end of file diff --git a/share/src/main/config/module-context.xml b/share/src/main/config/module-context.xml index 4e81fd7..097dcdc 100644 --- a/share/src/main/config/module-context.xml +++ b/share/src/main/config/module-context.xml @@ -61,6 +61,23 @@ + + + + + + + + + + + + + + + diff --git a/share/src/main/java/de/acosix/alfresco/keycloak/share/web/UserNameCorrectingSlingshotLoginController.java b/share/src/main/java/de/acosix/alfresco/keycloak/share/web/UserNameCorrectingSlingshotLoginController.java new file mode 100644 index 0000000..30c8813 --- /dev/null +++ b/share/src/main/java/de/acosix/alfresco/keycloak/share/web/UserNameCorrectingSlingshotLoginController.java @@ -0,0 +1,244 @@ +/* + * Copyright 2019 - 2020 Acosix GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.acosix.alfresco.keycloak.share.web; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.util.PropertyCheck; +import org.alfresco.web.site.servlet.SlingshotLoginController; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.extensions.surf.RequestContext; +import org.springframework.extensions.surf.RequestContextUtil; +import org.springframework.extensions.surf.UserFactory; +import org.springframework.extensions.surf.exception.ConnectorServiceException; +import org.springframework.extensions.surf.exception.RequestContextException; +import org.springframework.extensions.surf.support.AlfrescoUserFactory; +import org.springframework.extensions.surf.support.ThreadLocalRequestContext; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.connector.Connector; +import org.springframework.extensions.webscripts.connector.ConnectorContext; +import org.springframework.extensions.webscripts.connector.ConnectorService; +import org.springframework.extensions.webscripts.connector.CredentialVault; +import org.springframework.extensions.webscripts.connector.Credentials; +import org.springframework.extensions.webscripts.connector.HttpMethod; +import org.springframework.extensions.webscripts.connector.Response; + +/** + * This specialised variant of a login controller performs user name corrections upon successful authantication of a user, in case the + * Repository tier authentication has resulted in any deviation from the user-provided user name. This may be the case because of case + * matching or alternative login attributes (like email) being supported that resolve back to a preferred user name. + * + * @author Axel Faust + */ +public class UserNameCorrectingSlingshotLoginController extends SlingshotLoginController implements InitializingBean +{ + + private static final Logger LOGGER = LoggerFactory.getLogger(UserNameCorrectingSlingshotLoginController.class); + + protected ConnectorService connectorService; + + /** + * + * {@inheritDoc} + */ + @Override + public void afterPropertiesSet() + { + PropertyCheck.mandatory(this, "connectorService", this.connectorService); + } + + /** + * @param connectorService + * the connectorService to set + */ + public void setConnectorService(final ConnectorService connectorService) + { + this.connectorService = connectorService; + } + + /** + * + * {@inheritDoc} + */ + @Override + protected void onSuccess(final HttpServletRequest request, final HttpServletResponse response) throws Exception + { + final HttpSession session = request.getSession(); + String userName = request.getParameter(PARAM_USERNAME); + if (userName == null) + { + userName = (String) session.getAttribute(UserFactory.SESSION_ATTRIBUTE_KEY_USER_ID); + } + + final String effectiveUserName = this.loadEffectiveUserName(request, session, userName); + + HttpServletRequest effectiveRequest = request; + if (effectiveUserName != null && !effectiveUserName.equals(userName)) + { + // store the proper user ID in session and facade the servlet request for the remainder of the operation to expose only the + // effective user name + session.setAttribute(UserFactory.SESSION_ATTRIBUTE_KEY_USER_ID, effectiveUserName); + effectiveRequest = new HttpServletRequestWrapper(request) + { + + /** + * + * {@inheritDoc} + */ + @Override + public String getParameter(final String name) + { + if (PARAM_USERNAME.equals(name)) + { + return effectiveUserName; + } + return super.getParameter(name); + } + + /** + * + * {@inheritDoc} + */ + @Override + public String[] getParameterValues(final String name) + { + if (PARAM_USERNAME.equals(name)) + { + return new String[] { effectiveUserName }; + } + return super.getParameterValues(name); + } + + /** + * + * {@inheritDoc} + */ + @Override + public Map getParameterMap() + { + final Map map = new HashMap<>(super.getParameterMap()); + map.put(PARAM_USERNAME, this.getParameterValues(PARAM_USERNAME)); + return map; + } + }; + + // map existing credentials to the new user name + final CredentialVault credentialVault = this.connectorService.getCredentialVault(session, userName); + if (credentialVault != null) + { + final CredentialVault newCredentialVault = this.connectorService.getCredentialVault(session, effectiveUserName); + for (final String storeId : credentialVault.getStoredIds()) + { + final Credentials credentials = credentialVault.retrieve(storeId); + final Credentials newCredentials = newCredentialVault.newCredentials(storeId); + newCredentials.setProperty(Credentials.CREDENTIAL_USERNAME, effectiveUserName); + newCredentials.setProperty(Credentials.CREDENTIAL_PASSWORD, credentials.getProperty(Credentials.CREDENTIAL_PASSWORD)); + } + } + } + + super.onSuccess(effectiveRequest, response); + } + + /** + * Attempts to load the effective user name for the authenticated user from the backend. + * + * @param request + * the servlet request being processed + * @param session + * the currently active session + * @param userId + * the ID of the user as provided by the user themselves + * @return the effective user name or {@code null} if the effective user name could not be loaded for whatever reason (will be logged) + */ + protected String loadEffectiveUserName(final HttpServletRequest request, final HttpSession session, final String userId) + { + String effectiveUserName; + try + { + final Connector connector = this.connectorService.getConnector(AlfrescoUserFactory.ALFRESCO_ENDPOINT_ID, userId, session); + + // bug in default Alfresco RequestCachingConnector: with ConnectorContext having HttpMethod.GET, null check of + // ThreadLocalRequestContext.getRequestContext() is short-circuited, causing NPE on access + final RequestContext requestContext = ThreadLocalRequestContext.getRequestContext(); + if (requestContext == null) + { + try + { + RequestContextUtil.initRequestContext(this.getApplicationContext(), request, true); + } + catch (final RequestContextException e) + { + LOGGER.error("Failed to initialise request context", e); + throw new AlfrescoRuntimeException("Failed to initialise request context", e); + } + } + + final ConnectorContext c = new ConnectorContext(HttpMethod.GET); + c.setContentType("application/json"); + final Response res = connector.call("/acosix/api/keycloak/effectiveUserName", c); + + if (res.getStatus().getCode() == Status.STATUS_OK) + { + final String responseText = res.getResponse(); + final JSONParser jsonParser = new JSONParser(); + final Object userData = jsonParser.parse(responseText.toString()); + if (userData instanceof JSONObject) + { + effectiveUserName = (String) ((JSONObject) userData).get("userName"); + } + else + { + LOGGER.warn("Response in call to load effective user name for {} was not a proper JSON object", userId); + effectiveUserName = null; + } + } + else + { + if (res.getStatus().getCode() == 401) + { + LOGGER.debug("Failed to load effective user name for {} with backend call as authentication was not / no longer active", + userId); + } + else + { + LOGGER.warn("Failed to load effective user name for {} with backend call resulting in HTTP response with status {} {}", + userId, res.getStatus().getCode(), res.getStatus().getMessage()); + } + effectiveUserName = null; + } + } + catch (final ConnectorServiceException | ParseException ex) + { + LOGGER.error("Failed to load effective user name for {}", userId, ex); + effectiveUserName = null; + } + + return effectiveUserName; + } +}